Compare commits

..

861 Commits

Author SHA1 Message Date
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
Johannes Zellner fa193276c9 Require exactly one user in accessRestriction for singleUser app installations 2015-10-16 20:01:45 +02:00
Johannes Zellner 0ca09c384a Hide client secret field for simple auth 2015-10-16 19:41:50 +02:00
Johannes Zellner a6a39cc4e6 Adapt clients.getAllWithDetailsByUserId() to new client types 2015-10-16 19:36:12 +02:00
Johannes Zellner c9f84f6259 Show user selection for singleUser apps 2015-10-16 18:06:49 +02:00
Johannes Zellner 07063ca4f0 Adjust the webadmin to the accessRestriction changes 2015-10-16 16:14:23 +02:00
Johannes Zellner b5cfdcf875 Fixup the unit tests for accessRestriction format change 2015-10-16 16:06:13 +02:00
Johannes Zellner 373db25077 Make accessRestriction a JSON format to prepare for group access control 2015-10-16 15:32:19 +02:00
Johannes Zellner f8c2ebe61a Taks accessRestriction and oauthProxy into account for an update through the cli 2015-10-16 14:50:00 +02:00
Johannes Zellner ae23fade1e Show oauthProxy and accessRestriction values at app installation and configuration 2015-10-16 14:50:00 +02:00
Johannes Zellner 5386c05c0d Give developer tokens the correct scopes 2015-10-16 14:50:00 +02:00
Johannes Zellner aed94c8aaf roleDeveloper is no more 2015-10-16 14:50:00 +02:00
Johannes Zellner 37185fc4d5 Only allow simple auth clients through simple auth 2015-10-16 14:49:51 +02:00
Johannes Zellner cc64c6c9f7 Test using simple auth credentials in oauth 2015-10-16 11:48:12 +02:00
Johannes Zellner 0c0782ccd7 Fixup oauth to not allow simple auth clients 2015-10-16 11:27:42 +02:00
Johannes Zellner 5bc9f9e995 use clientdb types in authorization endpoint 2015-10-16 11:22:16 +02:00
Johannes Zellner 22402d1741 Remove legacy test auth client type 2015-10-16 10:05:58 +02:00
Johannes Zellner 8f203b07a1 Fix indentation 2015-10-16 09:19:05 +02:00
Girish Ramakrishnan 9c157246b7 add type field to clients table 2015-10-15 17:35:47 -07:00
Girish Ramakrishnan d0dfe1ef7f remove unused variable 2015-10-15 17:35:47 -07:00
Girish Ramakrishnan a9ccc7e2aa remove updating clients
clients are immutable
2015-10-15 16:08:17 -07:00
Girish Ramakrishnan 63edbae1be minor rename 2015-10-15 15:51:51 -07:00
Girish Ramakrishnan 8afe537497 fix typo 2015-10-15 15:32:14 -07:00
Girish Ramakrishnan f33844d8f1 fix debug tag 2015-10-15 15:19:28 -07:00
Girish Ramakrishnan c750d00355 ignore any tmp cleanup errors 2015-10-15 14:47:43 -07:00
Girish Ramakrishnan bb9b39e3c0 callback can be null 2015-10-15 14:25:38 -07:00
Girish Ramakrishnan 057b89ab8e Check error code of image removal 2015-10-15 14:06:05 -07:00
Girish Ramakrishnan 23fc4bec36 callback can be null 2015-10-15 12:06:38 -07:00
Girish Ramakrishnan 6b82fb9ddb Remove old addon images on infra update
Fixes #329
2015-10-15 12:01:31 -07:00
Girish Ramakrishnan a3ca5a36e8 update test image 2015-10-15 11:11:54 -07:00
Girish Ramakrishnan f57c91847d addons do not write to /var/log anymore 2015-10-15 11:00:51 -07:00
Johannes Zellner eda4dc83a3 Do not fail in container.sh when trying to remove non-existing directories 2015-10-15 18:06:57 +02:00
Johannes Zellner 5a0bf8071e Handle the various appId types we have by now 2015-10-15 17:57:07 +02:00
Johannes Zellner 09dfc6a34b Get the oauth2 debug()s in shape 2015-10-15 16:55:48 +02:00
Johannes Zellner 3b8ebe9a59 Fixup the oauth tests with accessRestriction support 2015-10-15 16:50:05 +02:00
Johannes Zellner 2ba1092809 Adhere to accessRestriction for oauth authorization endpoint 2015-10-15 16:49:13 +02:00
Johannes Zellner 7c97ab5408 Revert "Since we got fully rid of the decision dialog, no need to serialze the client anymore"
This is now again required, due to the accesRestriction check

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2. Merge into box server (this commit)

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

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

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

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

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

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

Fixes #483
2015-09-09 16:57:43 -07:00
Girish Ramakrishnan c88591489d make apps test work 2015-09-09 15:51:56 -07:00
Girish Ramakrishnan 719404b6cf lint 2015-09-09 15:03:43 -07:00
Girish Ramakrishnan f2c27489c8 test: make unregister subdomain test work 2015-09-09 14:36:09 -07:00
Girish Ramakrishnan d6a0c93f2f test: make register subdomain work 2015-09-09 14:32:05 -07:00
Girish Ramakrishnan c64d5fd2e3 error is already Error 2015-09-09 14:26:53 -07:00
Girish Ramakrishnan 5b62aeb73a make aws endpoint configurable for tests 2015-09-09 12:03:47 -07:00
Girish Ramakrishnan 7e83f2dd4a intercept delete calls to test image 2015-09-09 11:32:09 -07:00
Girish Ramakrishnan ed48f84355 give taskmanager couple of seconds to kill all processes 2015-09-09 10:39:38 -07:00
Girish Ramakrishnan f3d15cd4a5 fix initialization of apps-test 2015-09-09 10:22:17 -07:00
Girish Ramakrishnan 8c270269db remove dead code 2015-09-09 09:28:06 -07:00
Johannes Zellner bea605310a Use memoryLimit from manifest for graphs if specified 2015-09-09 17:11:54 +02:00
Johannes Zellner 8184894563 Remove upgrade view altogether 2015-09-09 16:47:13 +02:00
Johannes Zellner 47a87cc298 Remove upgrade link in the menu 2015-09-09 16:46:28 +02:00
Johannes Zellner 553a6347e6 Actually hand the backupKey over in an update 2015-09-09 12:37:09 +02:00
Girish Ramakrishnan a35ebd57f9 call iteratorDone when finished 2015-09-09 00:43:42 -07:00
Girish Ramakrishnan 97174d7af0 make cloudron-test pass 2015-09-08 22:13:50 -07:00
Girish Ramakrishnan 659268c04a provide default backupPrefix for tests 2015-09-08 21:16:50 -07:00
Girish Ramakrishnan 67d06c5efa better debug messages 2015-09-08 21:11:46 -07:00
Girish Ramakrishnan 6e6d8c0bc5 awscredentials is now POST 2015-09-08 21:02:21 -07:00
Girish Ramakrishnan 658af3edcf disable failing subdomains test
This needs aws mock
2015-09-08 20:38:52 -07:00
Girish Ramakrishnan 9753d9dc7e removeUser takes a userId and not username 2015-09-08 16:38:02 -07:00
Girish Ramakrishnan 4e331cfb35 retry registering and unregistering subdomain 2015-09-08 12:51:25 -07:00
Girish Ramakrishnan a1fa94707b Remove ununsed error codes 2015-09-08 11:28:29 -07:00
Girish Ramakrishnan 88f1107ed6 Remove unused AWSError 2015-09-08 11:26:35 -07:00
Girish Ramakrishnan e97b9fcc60 Do not start apptask for apps that are installed and running 2015-09-08 10:24:39 -07:00
Girish Ramakrishnan 71fe643099 Check if we have reached concurrency limit before locking 2015-09-08 10:20:34 -07:00
Johannes Zellner 74874a459d Remove ... for labels while showing the progress bar 2015-09-08 15:49:10 +02:00
Johannes Zellner 7c5fc17500 Cleanup linter issues in updatechecker.js 2015-09-08 10:03:37 +02:00
Girish Ramakrishnan 26aefadfba systemd: fix crashnotifier 2015-09-07 21:40:01 -07:00
Girish Ramakrishnan 51a28842cf systemd: pass the instance name as argument 2015-09-07 21:16:22 -07:00
Girish Ramakrishnan 210c2f3cc1 Output some logs in crashnotifier 2015-09-07 21:10:00 -07:00
Girish Ramakrishnan 773c326eb7 systemd: just wait for 5 seconds for box to die 2015-09-07 20:58:14 -07:00
Girish Ramakrishnan cb2fb026c5 systemd: do not restart crashnotifier 2015-09-07 20:54:58 -07:00
Girish Ramakrishnan a4731ad054 200m is a more sane memory limit 2015-09-07 20:48:29 -07:00
Girish Ramakrishnan aa33938fb5 systemd: fix config files 2015-09-07 20:46:32 -07:00
Girish Ramakrishnan edfe8f1ad0 disable pager when collecting logs 2015-09-07 20:27:27 -07:00
Girish Ramakrishnan 41399a2593 Make crashnotifier.js executable 2015-09-07 20:15:13 -07:00
Girish Ramakrishnan 2a4c467ab8 systemd: Fix crashnotifier 2015-09-07 20:14:37 -07:00
Girish Ramakrishnan 6be6092c0e Add memory limits on services 2015-09-07 19:16:34 -07:00
Girish Ramakrishnan e76584b0da Move from supervisor to systemd
This removes logrotate as well since we use systemd logging
2015-09-07 14:31:25 -07:00
Girish Ramakrishnan b3816615db run upto 5 apptasks in parallel
fixes #482
2015-09-05 09:17:46 -07:00
Johannes Zellner 212d0bd55a Revert "Add hack for broken app backup tarballs"
This reverts commit 9723951bfc.
2015-08-31 21:44:24 -07:00
Girish Ramakrishnan 712ada940e Add hack for broken app backup tarballs 2015-08-31 18:58:38 -07:00
Johannes Zellner ba690c6346 Add missing records argument 2015-08-30 23:00:01 -07:00
Johannes Zellner e910e19f57 Fix debug tag 2015-08-30 22:54:52 -07:00
Johannes Zellner 0c2532b0b5 Give default value to config.dnsInSync 2015-08-30 22:35:44 -07:00
Johannes Zellner 9c9b17a5f0 Remove cloudron.config prior to every test run 2015-08-30 22:35:44 -07:00
Johannes Zellner 816dea91ec Assert for dns record values 2015-08-30 22:35:44 -07:00
Johannes Zellner c228f8d4d5 Merge admin dns and mail dns setup
This now also checks if the mail records are in sync
2015-08-30 22:35:43 -07:00
Johannes Zellner 05bb99fad4 give dns record changeIds as a result for addMany() 2015-08-30 22:35:43 -07:00
Johannes Zellner 51b2457b3d Setup webadmin domain on the box side 2015-08-30 22:35:43 -07:00
Girish Ramakrishnan ed71fca23e Fix css 2015-08-30 22:25:18 -07:00
Girish Ramakrishnan 20e8e72ac2 reserved blocks are used 2015-08-30 22:24:57 -07:00
Girish Ramakrishnan 13fe0eb882 Only display one donut for memory usage 2015-08-30 22:13:01 -07:00
Girish Ramakrishnan e0476c9030 Reboot is a post route 2015-08-30 21:38:54 -07:00
Girish Ramakrishnan fca82fd775 Display upto 600mb for apps 2015-08-30 17:21:44 -07:00
Johannes Zellner 37c8ba8ddd Reduce logging for aws credentials 2015-08-30 17:03:10 -07:00
Johannes Zellner f87011b5c2 Also always check for dns propagation 2015-08-30 17:00:23 -07:00
Johannes Zellner 7f149700f8 Remove wrong optimization for subdomain records 2015-08-30 16:54:33 -07:00
Johannes Zellner 78ba9070fc use config.appFqdn() to handle custom domains 2015-08-30 16:29:09 -07:00
Johannes Zellner e31e5e1f69 Reuse dnsRecordId for record status id 2015-08-30 15:58:54 -07:00
Johannes Zellner 31d9027677 Query dns status with aws statusId 2015-08-30 15:51:33 -07:00
Johannes Zellner debcd6f353 aws provides uppercase properties 2015-08-30 15:47:08 -07:00
Johannes Zellner 5cb1681922 Fixup the zonename comparison 2015-08-30 15:37:18 -07:00
Johannes Zellner 9074bccea0 Move subdomain management from appstore to box 2015-08-30 15:29:14 -07:00
Girish Ramakrishnan 291798f574 Pass along aws config for updates 2015-08-27 22:45:04 -07:00
Girish Ramakrishnan b104843ae1 Add missing quotes to cloudron.conf 2015-08-27 20:15:04 -07:00
Girish Ramakrishnan dd062c656f Fix failing test 2015-08-27 11:43:36 -07:00
Girish Ramakrishnan ae2eb718c6 check if response has credentials object 2015-08-27 11:43:02 -07:00
Girish Ramakrishnan 7ac26bb653 Fix backup response 2015-08-27 11:19:40 -07:00
Girish Ramakrishnan 41a726e8a7 Fix backup test 2015-08-27 11:17:36 -07:00
Girish Ramakrishnan 4b69216548 bash: quote the array expansion 2015-08-27 10:13:05 -07:00
Girish Ramakrishnan 99395ddf5a bash: quoting array expansion because thats how it is 2015-08-27 09:49:44 -07:00
Girish Ramakrishnan 5f9fa5c352 bash: empty array expansion barfs with set -u 2015-08-27 09:33:40 -07:00
Girish Ramakrishnan 9013331917 Fix coding style 2015-08-27 09:30:32 -07:00
Girish Ramakrishnan 3a8f80477b getSignedDownloadUrl must return an object with url and sessionToken 2015-08-27 09:26:19 -07:00
Johannes Zellner 813c680ed0 pass full box data to the update 2015-08-26 10:59:17 -07:00
Johannes Zellner a0eccd615f Send new version to update to to the installer 2015-08-26 09:42:48 -07:00
Johannes Zellner 59be539ecd make restoreapp.sh support aws session tokens 2015-08-26 09:14:15 -07:00
Johannes Zellner a04740114c Generate app restore urls locally 2015-08-26 09:11:28 -07:00
Johannes Zellner 60b5d71c74 appBackupIds are not needed for backup url generation 2015-08-26 09:06:45 -07:00
Johannes Zellner 0a8b4b0c43 Load our style sheet as early as possible 2015-08-25 21:59:01 -07:00
Johannes Zellner ec21105c47 use backupKey from userData 2015-08-25 18:44:52 -07:00
Girish Ramakrishnan 444258e7ee backupKey is a function 2015-08-25 18:37:51 -07:00
Johannes Zellner e6fd05c2bd Support optional aws related userData 2015-08-25 17:52:01 -07:00
Johannes Zellner 9fdcd452d0 Use locally generate signed urls for app backup 2015-08-25 17:52:01 -07:00
Johannes Zellner f39b9d5618 Support session tokens in backupapp.sh 2015-08-25 17:52:00 -07:00
Johannes Zellner 76e4c4919d Only federated tokens need session token 2015-08-25 17:52:00 -07:00
Johannes Zellner d1f159cdb4 Also send the restoreKey for the backup done webhook 2015-08-25 17:52:00 -07:00
Johannes Zellner c63065e460 Also send the sessionToken when using the pre-signed url 2015-08-25 17:52:00 -07:00
Johannes Zellner 124c1d94a4 Translate the federated credentials 2015-08-25 17:52:00 -07:00
Johannes Zellner e9161b726a AWS credential creation returns 201 2015-08-25 17:52:00 -07:00
Johannes Zellner fd0d27b192 AWS credentials are now dealt with a level down 2015-08-25 17:52:00 -07:00
Johannes Zellner 50064a40fe Use dev bucket for now as a default 2015-08-25 17:52:00 -07:00
Johannes Zellner c9bc5fc38e Use signed urls for upload on the box side 2015-08-25 17:52:00 -07:00
Johannes Zellner 58f533fe50 Add config.aws().backupPrefix 2015-08-25 17:52:00 -07:00
Johannes Zellner efcdffd8ff Add getSignedUploadUrl() to aws.js 2015-08-25 17:52:00 -07:00
Johannes Zellner 22793c3886 move aws-sdk from dev to normal dependencies 2015-08-25 17:52:00 -07:00
Johannes Zellner 797ddbacc0 Return aws credentials from config.js 2015-08-25 17:52:00 -07:00
Johannes Zellner e011962469 refactor backupBoxWithAppBackupIds() 2015-08-25 17:52:00 -07:00
Johannes Zellner b376ad9815 Add webhooks.js 2015-08-25 17:51:59 -07:00
Johannes Zellner 77248fe65c Construct backupUrl locally 2015-08-25 17:51:59 -07:00
Johannes Zellner 1dad115203 Add initial aws object to config.js 2015-08-25 17:51:59 -07:00
Johannes Zellner 8812d58031 Add backupKey to config 2015-08-25 17:51:59 -07:00
Johannes Zellner fff7568f7e Add aws.js 2015-08-25 17:51:59 -07:00
Johannes Zellner ff6662579d Fix typo in backupapp.sh help output 2015-08-25 17:51:59 -07:00
Girish Ramakrishnan 0cf9fbd909 Merge data into args 2015-08-25 15:55:52 -07:00
Girish Ramakrishnan 848b745fcb Fix boolean logic 2015-08-25 12:24:02 -07:00
Girish Ramakrishnan 9a35c40b24 Add force argument
This fixes crash when auto-updating apps
2015-08-25 10:01:20 -07:00
Girish Ramakrishnan 1f1e6124cd oldConfig can be null during a restore/upgrade 2015-08-25 09:59:44 -07:00
Girish Ramakrishnan 033df970ad Update manifestformat@1.7.0 2015-08-24 22:56:02 -07:00
Girish Ramakrishnan dd80a795a0 Read memoryLimit from manifest 2015-08-24 22:44:35 -07:00
Girish Ramakrishnan 1eec6a39c6 Show upto 200mb 2015-08-24 22:39:06 -07:00
Girish Ramakrishnan dd6b8face9 Set app memory limit to 200MB (includes 100 MB swap) 2015-08-24 21:58:19 -07:00
Girish Ramakrishnan 288de7e03a Add RSTATE_ERROR 2015-08-24 21:58:19 -07:00
Girish Ramakrishnan a760ef4d22 Rebase addons to use base image 0.3.3 2015-08-24 10:19:18 -07:00
Johannes Zellner 0dd745bce4 Fix form submit with enter for update form 2015-08-22 17:21:25 -07:00
Johannes Zellner d4d5d371ac Use POST heartbeat route instead of GET 2015-08-22 16:51:56 -07:00
Johannes Zellner 205bf4ddbd Offset the footer in apps view 2015-08-20 23:50:52 -07:00
Girish Ramakrishnan 4ab84d42c6 Delete image only if it changed
This optimization won't work if we have two dockerImage with same
image id....
2015-08-19 14:24:32 -07:00
Girish Ramakrishnan ee74badf3a Check for dockerImage in manifest in install/update/restore routes 2015-08-19 11:08:45 -07:00
Girish Ramakrishnan aa173ff74c restore without a backup is the same as re-install 2015-08-19 11:00:00 -07:00
Girish Ramakrishnan b584fc33f5 CN of admin group is admins 2015-08-18 16:35:52 -07:00
Girish Ramakrishnan 15c9d8682e Base image is now 0.3.3 2015-08-18 15:43:50 -07:00
Girish Ramakrishnan 361be8c26b containerId can be null 2015-08-18 15:43:50 -07:00
Girish Ramakrishnan 4db9a5edd6 Clean up the old image and not the current one 2015-08-18 10:01:15 -07:00
Johannes Zellner bcc878da43 Hide update input fields and update button if it is blocked by apps 2015-08-18 16:59:36 +02:00
Johannes Zellner 79f179fed4 Add note, why sendError() is required 2015-08-18 16:53:29 +02:00
Johannes Zellner a924a9a627 Revert "remove obsolete sendError() function"
This reverts commit 5d9b122dd5.
2015-08-18 16:49:53 +02:00
Girish Ramakrishnan 45d444df0e leave a note about force_update 2015-08-17 21:30:56 -07:00
Girish Ramakrishnan 92461a3366 Remove ununsed require 2015-08-17 21:23:32 -07:00
Girish Ramakrishnan 032a430c51 Fix debug message 2015-08-17 21:23:27 -07:00
Girish Ramakrishnan a6a3855e79 Do not remove icon for non-appstore installs
Fixes #466
2015-08-17 19:37:51 -07:00
Girish Ramakrishnan 2386545814 Add a note why oldConfig can be null 2015-08-17 10:05:07 -07:00
Johannes Zellner 2059152dd3 remove obsolete sendError() function 2015-08-17 14:55:56 +02:00
Johannes Zellner 32d2c260ab Move appstore badges out of the way for the app titles 2015-08-17 11:50:31 +02:00
Johannes Zellner 384c7873aa Correctly mark apps pending for approval
Fixes #339
2015-08-17 11:50:08 +02:00
Girish Ramakrishnan 9266302c4c Print graphite container id 2015-08-13 15:57:36 -07:00
Girish Ramakrishnan 755dce7bc4 fix graph issue finally 2015-08-13 15:54:27 -07:00
Girish Ramakrishnan dd3e38ae55 Use latest graphite 2015-08-13 15:53:36 -07:00
Girish Ramakrishnan 9dfaa2d20f Create symlink in start.sh (and not container setup) 2015-08-13 15:36:21 -07:00
Girish Ramakrishnan d6a4ff23e2 restart mysql in start.sh and not container setup 2015-08-13 15:16:01 -07:00
Girish Ramakrishnan c2ab7e2c1f restart collectd 2015-08-13 15:04:57 -07:00
Girish Ramakrishnan b9e4662dbb fix graphs again 2015-08-13 15:03:44 -07:00
Girish Ramakrishnan 10df0a527f Fix typo
remove thead_cache_size. it's dynamic anyways
2015-08-13 14:53:05 -07:00
Girish Ramakrishnan 9aad3688e1 Revert "Add hack to make graphs work with latest collectd"
This reverts commit a959418544.
2015-08-13 14:42:47 -07:00
Girish Ramakrishnan e78dbcb5d4 limit threads and max connections 2015-08-13 14:42:36 -07:00
Girish Ramakrishnan 5e8cd09f51 Bump infra version 2015-08-13 14:22:39 -07:00
Girish Ramakrishnan 22f65a9364 Add hack to make graphs work with latest collectd
For some reason df-vda1 is not being collected by carbon. I have tried
all sorts of things and nothing works. This is a hack to get it working.
2015-08-13 13:47:44 -07:00
Girish Ramakrishnan 81b7432044 Turn off performance_schema in mysql 5.6 2015-08-13 13:47:44 -07:00
Girish Ramakrishnan d49b90d9f2 Remove unused nodejs-disks 2015-08-13 10:34:06 -07:00
Girish Ramakrishnan 9face9cf35 systemd has moved around the cgroup hierarchy
https://github.com/docker/docker/issues/9902

There is some rationale here:
https://libvirt.org/cgroups.html
2015-08-13 10:21:33 -07:00
Girish Ramakrishnan 33ac34296e CpuShares is part of HostConfig 2015-08-12 23:47:35 -07:00
Girish Ramakrishnan 670ffcd489 Add warning 2015-08-12 19:52:23 -07:00
Girish Ramakrishnan ec7b365c31 Use BASE_IMAGE as well 2015-08-12 19:51:44 -07:00
Girish Ramakrishnan 433d78c7ff Fix graphite version 2015-08-12 19:51:08 -07:00
Girish Ramakrishnan ed041fdca6 Put image names in one place 2015-08-12 19:38:44 -07:00
Girish Ramakrishnan b8e4ed2369 Use latest images 2015-08-12 19:19:58 -07:00
Johannes Zellner d12f260d12 Prevent accessing oldConfig if it does not exist 2015-08-12 21:17:52 +02:00
Johannes Zellner ba7989b57b Add ldap 'users' group 2015-08-12 17:38:31 +02:00
Johannes Zellner 88df410f5b Add ldap search unit tests 2015-08-12 15:31:54 +02:00
Johannes Zellner 2436db3b1f Add ldap memberof attribute 2015-08-12 15:31:44 +02:00
Johannes Zellner d15874df63 Add initial ldap unit tests 2015-08-12 15:00:38 +02:00
Johannes Zellner 8fb90254cd Ensure the focus is properly set when restoring 2015-08-12 14:35:51 +02:00
Johannes Zellner cbd712c20e Better integrate the progress bar 2015-08-12 14:32:20 +02:00
Johannes Zellner 8c004798f2 Improve login form layout 2015-08-12 14:23:13 +02:00
Johannes Zellner c1b0cbe78d Give appstore hover a different color 2015-08-12 14:07:40 +02:00
Johannes Zellner 5ee72c8e98 Make webadmin pages a bit more streamlined with padding 2015-08-12 13:48:55 +02:00
Girish Ramakrishnan c125cc17dc Apps must only get 50% less cpu than system processes when there is a contention for cpu 2015-08-11 17:00:48 -07:00
Johannes Zellner 18feff1bfb Increase installed app title 2015-08-11 15:22:30 +02:00
Johannes Zellner f74f713bbd Hide geeky toolbar in apps icons 2015-08-11 13:04:50 +02:00
Girish Ramakrishnan 0ea14db172 Fix redis installation on 1.7 2015-08-10 23:00:24 -07:00
Girish Ramakrishnan 74785a40d5 r -> ro (docker 1.7) 2015-08-10 21:14:28 -07:00
Girish Ramakrishnan dcfcd5be84 Create docker volume directories since docker 1.7 does not create them 2015-08-10 21:00:56 -07:00
Girish Ramakrishnan 814674eac5 addons can be null in apps.backupApp
addons.backup already takes care of null.

a future commit will give defaults for all non-default manifest fields
at some point and document them as so
2015-08-10 13:47:51 -07:00
Girish Ramakrishnan 1a7fff9867 Keep linter happy 2015-08-10 13:42:04 -07:00
Johannes Zellner 30b248a0f6 Allow non published versions to be shown if explicitly requested
Fixes #468
2015-08-10 16:16:40 +02:00
Johannes Zellner 7168455de3 Do not use table layout for login view
Fixes #458
2015-08-10 15:26:45 +02:00
Johannes Zellner 085f63e3c7 Show cloudron name in login screen 2015-08-10 15:04:12 +02:00
Johannes Zellner 015be64923 Show cloudron avatar in login screen 2015-08-10 15:01:58 +02:00
Johannes Zellner 2c2471811d Restructure the login page 2015-08-10 14:51:04 +02:00
Johannes Zellner 1025249e93 Since addons are optional, ensure we have a valid empty object in the db 2015-08-10 10:37:55 +02:00
Johannes Zellner 41ffc4bcf3 If we have an empty app search show modal dialog link 2015-08-09 15:19:21 +02:00
Johannes Zellner 2739d54cc1 Make appstore feedback form a modal dialog 2015-08-09 14:48:00 +02:00
Girish Ramakrishnan c4c463cbc2 collect logs using a sudo script
docker logs can only be read by root
2015-08-08 19:04:59 -07:00
Girish Ramakrishnan 8cd13bd43f Update safetydance 2015-08-08 18:53:16 -07:00
Girish Ramakrishnan e4ef279759 Update safetydance and lastmile 2015-08-06 13:54:15 -07:00
Girish Ramakrishnan cf7fecb57b bump cloudron-manifestformat 2015-08-06 13:50:27 -07:00
Girish Ramakrishnan 226041dcb1 Display settings path
Fixes #465
2015-08-06 13:44:09 -07:00
Johannes Zellner 7548025561 If an app search is empty, show hint to give feedback 2015-08-06 18:35:08 +02:00
Johannes Zellner fdbee427ee Show app feedback form in appstore
Fixes #461
2015-08-06 18:30:49 +02:00
Johannes Zellner d861d6d6e4 Properly offset the footer in support view 2015-08-06 18:30:25 +02:00
Johannes Zellner 0a648edcaa Add app feedback category 2015-08-06 17:34:40 +02:00
Johannes Zellner 18850c1fba Cloudron prices are in cents 2015-08-06 16:24:19 +02:00
Girish Ramakrishnan f6df4cab67 Remove ADMIN_ORIGIN 2015-08-05 17:27:55 -07:00
Johannes Zellner 019d29c5b7 Use assert.strictEqual() to see the values 2015-08-05 17:49:19 +02:00
Johannes Zellner 0b4256a992 Unify feedback and ticket forms 2015-08-05 14:27:04 +02:00
Johannes Zellner 7d58d69389 Fix setup step on ng-enter 2015-08-04 22:17:58 +02:00
Johannes Zellner 864dd5bf26 New shrinkwrap for ldapjs without dtrace-provider
We have to install ldapjs with --no-optional

Fixes #460
2015-08-04 20:43:36 +02:00
Johannes Zellner abdde7a950 Put the correct faq and docs links 2015-08-04 19:36:05 +02:00
Johannes Zellner 8bcbd860be Add unit tests for feedback route and fix the route 2015-08-04 16:59:35 +02:00
Johannes Zellner be61c42fe8 Send feedback and tickets to support@cloudron.io 2015-08-04 16:05:20 +02:00
Johannes Zellner 6d5afc2d75 Give support form headers more space 2015-08-04 16:04:44 +02:00
Johannes Zellner 88d905e8cc Add support form feedback 2015-08-04 16:01:50 +02:00
Johannes Zellner d8ccc766b9 Add text-bold class 2015-08-04 16:01:33 +02:00
Johannes Zellner d22e0f0483 mailer functions only enqueue, respond immediately 2015-08-04 15:39:14 +02:00
Johannes Zellner c8f6973312 Do not send adminEmail for feedback mails 2015-08-04 14:56:43 +02:00
Johannes Zellner 3f0f0048bc add missing email format 2015-08-04 14:52:40 +02:00
Johannes Zellner 88643f0875 Add missing %> 2015-08-04 14:49:43 +02:00
Johannes Zellner e11bb10bb8 The requested function is in mailer 2015-08-04 14:45:42 +02:00
Johannes Zellner 7b9930c7f0 Do the feedback and ticket form plumbing 2015-08-04 14:44:39 +02:00
Johannes Zellner da48e32bcc Add feedback route 2015-08-04 14:31:40 +02:00
Johannes Zellner 57e2803bd2 Add feedback email template 2015-08-04 14:31:33 +02:00
Johannes Zellner 0d1ba01d65 Add initial support view 2015-08-04 11:33:36 +02:00
Girish Ramakrishnan 95cbec19af Copy the manifest because changes are made to it
Because of that, manifest verification fails (isNew property appears in manfiest)
2015-08-03 21:31:15 -07:00
Girish Ramakrishnan cc97654b23 Fix text 2015-08-02 19:02:45 -07:00
Girish Ramakrishnan 5bb983f175 Send docker log in crash email 2015-08-01 21:42:34 -07:00
Johannes Zellner 7cb6434de1 Move avatar name below the selected avatar preview 2015-07-30 16:38:10 +02:00
Johannes Zellner cb1b495da2 Revert "Actually remove dtrace dep"
This reverts commit 2b9bf6d019.
2015-07-30 14:53:53 +02:00
Girish Ramakrishnan e134136d59 previewAvatar seems to be defined in step1 and step2 2015-07-29 18:10:25 -07:00
Girish Ramakrishnan 85a681e330 There is no step4 2015-07-29 17:09:05 -07:00
Girish Ramakrishnan dc5c0fd830 setPreviewAvatar only in avatar selection step 2015-07-29 16:30:32 -07:00
Girish Ramakrishnan e7bf8452ab randomize default avatar 2015-07-29 16:11:37 -07:00
Girish Ramakrishnan 157f972b20 Decrease size of image preview 2015-07-29 16:11:20 -07:00
Girish Ramakrishnan b36028dc11 Pick -> Choose 2015-07-29 15:55:41 -07:00
Girish Ramakrishnan 70092ec559 Ensure image got loaded before setting the preview 2015-07-29 15:53:58 -07:00
Girish Ramakrishnan 56d740d597 Merge welcome step and step2 2015-07-29 15:11:34 -07:00
Girish Ramakrishnan ed55e52363 Actually remove dtrace dep
Use --no-optional when installing dtrace
2015-07-29 10:15:25 -07:00
Johannes Zellner 89c36ae6a9 Do not show the update page if update failed 2015-07-29 14:19:15 +02:00
Johannes Zellner 3027c119fe Use angular in update dialog and show errors 2015-07-29 14:02:31 +02:00
Johannes Zellner 4f129102a8 Use -1 for progress to indicate an error 2015-07-29 13:53:36 +02:00
Johannes Zellner 2dd6bb0c67 Rename upgradeError to updateError in update 2015-07-29 13:52:59 +02:00
Johannes Zellner b928b08a4c Reset update progress on update failure 2015-07-29 12:41:19 +02:00
Johannes Zellner 9dcc6e68a4 Use new avatar set
Fixes #456
2015-07-29 11:13:59 +02:00
Girish Ramakrishnan 452e67be54 This is probably obvious 2015-07-28 23:12:53 -07:00
Girish Ramakrishnan 9e0611f6d8 Improve wording of wizard 2015-07-28 23:09:06 -07:00
Girish Ramakrishnan ad3392ef2e model is queried from appstore 2015-07-28 17:08:32 -07:00
Girish Ramakrishnan 71e8abf081 define adminOrigin in splashpage.sh 2015-07-28 16:52:27 -07:00
Girish Ramakrishnan 46172e76c6 Keep updater arguments sorted for readability 2015-07-28 16:03:32 -07:00
Girish Ramakrishnan 7e639bd0e2 Release update/upgrade lock only on error 2015-07-28 15:28:10 -07:00
Girish Ramakrishnan 7a9af5373b Check percent value before redirecting to update.html 2015-07-28 14:43:49 -07:00
Girish Ramakrishnan 3ea7a11d97 Set progress completion error messages 2015-07-28 14:40:22 -07:00
Girish Ramakrishnan f582ba1ba7 console.error any backup error message for now 2015-07-28 14:30:40 -07:00
Girish Ramakrishnan b96fc2bc56 initialize percent 2015-07-28 14:28:53 -07:00
Girish Ramakrishnan 48c16277f0 Create error object properly 2015-07-28 14:22:34 -07:00
Girish Ramakrishnan 4ad4ff0b10 Use progress.set in upgrade/update code paths 2015-07-28 14:22:08 -07:00
Girish Ramakrishnan 25f05e5abd Add missing ; 2015-07-28 13:09:24 -07:00
Girish Ramakrishnan 7c214a9181 log update and upgrade errors 2015-07-28 10:03:52 -07:00
Johannes Zellner d66b1eef59 Better support for active directory clients 2015-07-28 18:39:16 +02:00
Girish Ramakrishnan 58f52b90f8 better debug on what is being autoupdated 2015-07-28 09:37:46 -07:00
Girish Ramakrishnan edb67db4ea Remove unnecessary debug making logs very verbose 2015-07-28 09:32:19 -07:00
Johannes Zellner 733014d8d9 No need to guess the apiOrigin anymore, we redirect now
Fixes #436
2015-07-28 14:03:48 +02:00
Johannes Zellner 4980f79688 Show link to referrer in appstatus 2015-07-28 14:01:51 +02:00
Johannes Zellner 3d8b90f5c8 Redirect on app error to webadmin appstatus page
Part of #436
2015-07-28 13:46:58 +02:00
Johannes Zellner eea547411b Show testing badges in appstore view 2015-07-28 13:21:23 +02:00
Johannes Zellner af682e5bb1 Fix the app icons in the install app grid 2015-07-28 13:06:55 +02:00
Johannes Zellner 739dcfde8b Show version and author in install dialog 2015-07-28 12:53:33 +02:00
Johannes Zellner 1db58dd78d Support ?version in direct appstore URLs
Fixes #454
2015-07-28 11:49:04 +02:00
Johannes Zellner 947137b3f9 Ensure we have a fallback avatar 2015-07-28 11:28:06 +02:00
Johannes Zellner 509e2caa83 Also show avatar in nakeddomain error page 2015-07-28 11:19:13 +02:00
Johannes Zellner a0e67daa52 Use avatar in error page 2015-07-28 11:18:55 +02:00
Johannes Zellner 32584f3a90 Fix long lasting navbar padding issue 2015-07-28 10:57:48 +02:00
Johannes Zellner 3513f321fb Reload webadmin in case the avatar changes
Fixes #452
2015-07-28 10:50:33 +02:00
Johannes Zellner 8aaccbba55 Show avatar in navbar 2015-07-28 10:49:56 +02:00
Johannes Zellner 31ab86a97f Show avatar as favicon 2015-07-28 10:40:10 +02:00
Girish Ramakrishnan 2c0786eb37 Use ldapjs from github directly
The 0.7.x ldapjs is over a year old and uses dtrace as a dep which
causes issues when rebuilding.
2015-07-27 13:06:30 -07:00
Johannes Zellner 3db8ebf97f Ensure the appstore ui can operate always on manifest.tags 2015-07-27 19:29:25 +02:00
Johannes Zellner 804105ce2b Add testing section in appstore and mark testing apps
This is not some final design to indicate which app is in testing
but the logistics are there, mainly css from now

Fixes #451
2015-07-27 17:09:59 +02:00
Johannes Zellner c4bb56dc95 Show non published apps in webadmin 2015-07-27 16:34:37 +02:00
Johannes Zellner 87c76a3eb3 Read apps from actual response body 2015-07-27 16:27:50 +02:00
Johannes Zellner 6bceff14ec Add proxy api to get non approved app listings 2015-07-27 14:00:44 +02:00
Girish Ramakrishnan 6b62561706 Add mandatory addons object 2015-07-24 06:59:34 -07:00
Girish Ramakrishnan d558c06803 Add missing semicolon 2015-07-24 06:53:07 -07:00
Girish Ramakrishnan ef9508ccc5 Use BOX_ENV instead of NODE_ENV
Let NODE_ENV be used by node modules and always be set to production

Fixes #453
2015-07-24 01:42:28 -07:00
Girish Ramakrishnan ec8342c2ce Better progress messages 2015-07-23 22:50:58 -07:00
Girish Ramakrishnan 6839f47f99 Fix typo 2015-07-23 14:30:15 -07:00
Girish Ramakrishnan d32990d0e5 Set server_names_hash_bucket_size
e2e tests fail like so when the hostnames are long

Thu, 23 Jul 2015 20:40:23 GMT box:apptask test8629 writing config to /home/yellowtent/data/nginx/applications/a3822f18-2f95-4b73-b8e9-2983dfcaae31.conf
Thu, 23 Jul 2015 20:40:23 GMT box:shell.js reloadNginx execFile: /usr/bin/sudo -S /home/yellowtent/box/src/scripts/reloadnginx.sh
Thu, 23 Jul 2015 20:40:24 GMT box:shell.js reloadNginx (stderr): nginx: [emerg] could not build the server_names_hash, you should increase server_names_hash_bucket_size: 64

Thu, 23 Jul 2015 20:40:24 GMT box:shell.js reloadNginx code: 1, signal: null
Thu, 23 Jul 2015 20:40:24 GMT box:apptask test8629 error installing app: Error: Exited with error 1 signal null
Thu, 23 Jul 2015 20:40:24 GMT box:apptask test8629 installationState: pending_install progress: 15, Configure nginx
^[[1m^[[31mERROR^[[39m^[[22m Exited with error 1 signal null ^[[1m[ /home/yellowtent/box/src/apptask.js:909:32 ]^[[22m
^[[32mstack: ^[[39m
  """
    Error: Exited with error 1 signal null
        at ChildProcess.<anonymous> (/home/yellowtent/box/src/shell.js:38:53)
        at ChildProcess.emit (events.js:110:17)
        at Process.ChildProcess._handle.onexit (child_process.js:1074:12)
  """
^[[32mmessage: ^[[39mExited with error 1 signal null
2015-07-23 13:55:46 -07:00
Johannes Zellner 71dbe21fc3 Set no-cache for the avatar 2015-07-23 16:34:44 +02:00
Johannes Zellner f36616abbb Remove developerMode from update provisioning data
Finally fixes #442
2015-07-23 13:31:39 +02:00
Johannes Zellner db6d6d565f Remove developerMode from config.js 2015-07-23 13:26:30 +02:00
Johannes Zellner 5f3fc68b5e Fixup developers test with new developer mode setting 2015-07-23 13:19:51 +02:00
Johannes Zellner bdca5e343b Fixup clients test with new developer mode setting 2015-07-23 13:16:36 +02:00
Johannes Zellner 58cf712e71 Fix apps-test to use settings developerMode 2015-07-23 12:59:47 +02:00
Johannes Zellner ca7e67ea4f Use developerMode from settings instead of config 2015-07-23 12:52:04 +02:00
Johannes Zellner b202043019 Add developerMode to settings
Part of #442
2015-07-23 12:42:56 +02:00
Johannes Zellner 19fef4c337 Add missing appId key to access app updateInfo 2015-07-23 07:21:05 +02:00
Johannes Zellner 7b864fed04 Only log error if NOOP_CALLBACK got an error 2015-07-23 07:17:30 +02:00
Johannes Zellner 553667557c Add polyfill for chrome for canvas.toBlob()
Fixes #448
2015-07-21 15:29:43 +02:00
Girish Ramakrishnan 3f732abbb3 Add debugs 2015-07-20 11:05:30 -07:00
Girish Ramakrishnan 1af3397898 Disable removeIcon is apptask for now 2015-07-20 11:01:52 -07:00
Girish Ramakrishnan 0d89612769 unusedAddons must be an object, not an array 2015-07-20 10:50:44 -07:00
Girish Ramakrishnan d71073ca6a Add text for force update 2015-07-20 10:43:19 -07:00
Girish Ramakrishnan 38c2c78633 Restoring app will lose all content if no backup
Maybe we should allow the user to force update if there is no backup?
We can add that as the need arises...
2015-07-20 10:39:30 -07:00
Girish Ramakrishnan 17b1f469d7 Handle forced updates 2015-07-20 10:09:02 -07:00
Girish Ramakrishnan 1e67241049 Return error on unknown installation command 2015-07-20 10:03:55 -07:00
Girish Ramakrishnan 173efa6920 Leave note on when lastBackupId can be null 2015-07-20 09:54:17 -07:00
Girish Ramakrishnan 0285562133 Revert the manifest and portBindings on a failed update
Fixes #443
2015-07-20 09:48:31 -07:00
Girish Ramakrishnan 26fbace897 During an update backup the old addons
Fixes #444
2015-07-20 00:50:36 -07:00
Girish Ramakrishnan df9d321ac3 app.portBindings and newManifest.tcpPorts may be null 2015-07-20 00:10:36 -07:00
79 changed files with 4451 additions and 4406 deletions
+3 -2
View File
@@ -14,9 +14,9 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
simpleauth = require('./src/simpleauth.js'),
oauthproxy = require('./src/oauthproxy.js'),
server = require('./src/server.js'),
simpleauth = require('./src/simpleauth.js');
server = require('./src/server.js');
console.log();
console.log('==========================================');
@@ -26,6 +26,7 @@ console.log();
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore token: ', config.token());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log();
+6 -1
View File
@@ -36,7 +36,12 @@ function main() {
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
sendCrashNotification(processName);
mailer.initialize(function (error) {
if (error) return console.error(error);
sendCrashNotification(processName);
});
}
main();
@@ -1,16 +0,0 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY manifestJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(2048)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -1,19 +0,0 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps MODIFY accessRestrictionJson TEXT'),
db.runSql.bind(db, 'ALTER TABLE apps MODIFY lastBackupConfigJson TEXT'),
db.runSql.bind(db, 'ALTER TABLE apps MODIFY oldConfigJson TEXT')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps MODIFY accessRestrictionJson VARCHAR(2048)'),
db.runSql.bind(db, 'ALTER TABLE apps MODIFY lastBackupConfigJson VARCHAR(2048)'),
db.runSql.bind(db, 'ALTER TABLE apps MODIFY oldConfigJson VARCHAR(2048)')
], callback);
};
+4 -4
View File
@@ -45,18 +45,18 @@ CREATE TABLE IF NOT EXISTS apps(
runState VARCHAR(512),
health VARCHAR(128),
containerId VARCHAR(128),
manifestJson TEXT,
manifestJson VARCHAR(2048),
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512),
accessRestrictionJson TEXT,
accessRestrictionJson VARCHAR(2048),
oauthProxy BOOLEAN DEFAULT 0,
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
lastBackupId VARCHAR(128),
lastBackupConfigJson TEXT, // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
lastBackupConfigJson VARCHAR(2048), // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
oldConfigJson TEXT, // used to pass old config for apptask
oldConfigJson VARCHAR(2048), // used to pass old config for apptask
PRIMARY KEY(id));
+3309 -2110
View File
File diff suppressed because it is too large Load Diff
+8 -10
View File
@@ -10,11 +10,10 @@
"type": "git"
},
"engines": [
"node >=4.0.0 <=4.1.1"
"node >= 0.12.0"
],
"dependencies": {
"async": "^1.2.1",
"attempt": "^1.0.1",
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"bytes": "^2.1.0",
@@ -52,19 +51,18 @@
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.0.2",
"password-generator": "^1.0.0",
"proxy-middleware": "^0.13.0",
"safetydance": "^0.1.0",
"safetydance": "0.0.19",
"semver": "^4.3.6",
"serve-favicon": "^2.2.0",
"split": "^1.0.0",
"superagent": "^1.5.0",
"supererror": "^0.7.1",
"superagent": "~0.21.0",
"supererror": "^0.7.0",
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
"underscore": "^1.7.0",
"ursa": "^0.9.1",
"valid-url": "^1.0.9",
"validator": "^4.4.0",
"validator": "^3.30.0",
"x509": "^0.2.2"
},
"devDependencies": {
@@ -85,9 +83,9 @@
"istanbul": "*",
"js2xmlparser": "^1.0.0",
"mocha": "*",
"nock": "^3.4.0",
"nock": "^2.6.0",
"node-sass": "^3.0.0-alpha.0",
"redis": "^2.4.2",
"redis": "^0.12.1",
"request": "^2.65.0",
"sinon": "^1.12.2",
"yargs": "^3.15.0"
+8 -8
View File
@@ -3,17 +3,17 @@
# If you change the infra version, be sure to put a warning
# in the change log
INFRA_VERSION=21
INFRA_VERSION=20
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
# These constants are used in the installer script as well
BASE_IMAGE=cloudron/base:0.8.0
MYSQL_IMAGE=cloudron/mysql:0.8.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.8.0
MONGODB_IMAGE=cloudron/mongodb:0.8.0
REDIS_IMAGE=cloudron/redis:0.8.0 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.9.0
GRAPHITE_IMAGE=cloudron/graphite:0.8.0
BASE_IMAGE=cloudron/base:0.7.0
MYSQL_IMAGE=cloudron/mysql:0.7.0
POSTGRESQL_IMAGE=cloudron/postgresql:0.7.0
MONGODB_IMAGE=cloudron/mongodb:0.7.0
REDIS_IMAGE=cloudron/redis:0.7.0 # if you change this, fix src/addons.js as well
MAIL_IMAGE=cloudron/mail:0.8.0
GRAPHITE_IMAGE=cloudron/graphite:0.7.0
MYSQL_REPO=cloudron/mysql
POSTGRESQL_REPO=cloudron/postgresql
+2 -11
View File
@@ -11,7 +11,6 @@ arg_is_custom_domain="false"
arg_restore_key=""
arg_restore_url=""
arg_retire="false"
arg_tls_config=""
arg_tls_cert=""
arg_tls_key=""
arg_token=""
@@ -19,7 +18,6 @@ arg_version=""
arg_web_server_origin=""
arg_backup_config=""
arg_dns_config=""
arg_provider=""
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
eval set -- "${args}"
@@ -32,17 +30,12 @@ while true; do
;;
--data)
# only read mandatory non-empty parameters here
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_is_custom_domain arg_box_versions_url arg_version <<EOF
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn isCustomDomain boxVersionsUrl version | tr '\n' ' ')
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_token arg_is_custom_domain arg_box_versions_url arg_version <<EOF
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn token isCustomDomain boxVersionsUrl version | tr '\n' ' ')
EOF
# read possibly empty parameters here
arg_tls_cert=$(echo "$2" | $json tlsCert)
arg_tls_key=$(echo "$2" | $json tlsKey)
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
arg_tls_config=$(echo "$2" | $json tlsConfig)
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
arg_restore_url=$(echo "$2" | $json restore.url)
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
@@ -73,7 +66,5 @@ echo "restore url: ${arg_restore_url}"
echo "tls cert: ${arg_tls_cert}"
echo "tls key: ${arg_tls_key}"
echo "token: ${arg_token}"
echo "tlsConfig: ${arg_tls_config}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
+1 -1
View File
@@ -20,7 +20,7 @@ systemctl daemon-reload
systemctl enable cloudron.target
########## sudoers
rm -f /etc/sudoers.d/yellowtent
rm -f /etc/sudoers.d/*
cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
########## collectd
-3
View File
@@ -1,6 +1,3 @@
# sudo logging breaks journalctl output with very long urls (systemd bug)
Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
-2
View File
@@ -14,6 +14,4 @@ User=yellowtent
Group=yellowtent
MemoryLimit=200M
TimeoutStopSec=5s
StartLimitInterval=1
StartLimitBurst=60
+13 -34
View File
@@ -40,7 +40,6 @@ set_progress "10" "Ensuring directories"
mkdir -p "${DATA_DIR}/box/appicons"
mkdir -p "${DATA_DIR}/box/certs"
mkdir -p "${DATA_DIR}/box/mail"
mkdir -p "${DATA_DIR}/box/acme" # acme keys
mkdir -p "${DATA_DIR}/graphite"
mkdir -p "${DATA_DIR}/mysql"
@@ -49,7 +48,6 @@ mkdir -p "${DATA_DIR}/mongodb"
mkdir -p "${DATA_DIR}/snapshots"
mkdir -p "${DATA_DIR}/addons"
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${DATA_DIR}/acme" # acme challenges
# bookkeep the version as part of data
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${DATA_DIR}/box/version"
@@ -92,41 +90,31 @@ EOF
set_progress "28" "Setup collectd"
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
## collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
#mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
## detect device, let it fail if non exists
#[[ -b "/dev/vda1" ]] && disk_device="/dev/vda1"
#[[ -b "/dev/xvda1" ]] && disk_device="/dev/xvda1"
#vda1_id=$(blkid -s UUID -o value ${disk_device})
#ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
# collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
vda1_id=$(blkid -s UUID -o value /dev/vda1)
ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
service collectd restart
set_progress "30" "Setup nginx"
# setup naked domain to use admin by default. app restoration will overwrite this config
mkdir -p "${DATA_DIR}/nginx/applications"
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
# generate the main nginx config file
${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"
# generate these for update code paths as well to overwrite splash
admin_cert_file="${DATA_DIR}/nginx/cert/host.cert"
admin_key_file="${DATA_DIR}/nginx/cert/host.key"
if [[ -f "${DATA_DIR}/box/certs/${admin_fqdn}.cert" && -f "${DATA_DIR}/box/certs/${admin_fqdn}.key" ]]; then
admin_cert_file="${DATA_DIR}/box/certs/${admin_fqdn}.cert"
admin_key_file="${DATA_DIR}/box/certs/${admin_fqdn}.key"
fi
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
mkdir -p "${DATA_DIR}/nginx/cert"
if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
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
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert
echo "${arg_tls_key}" > ${DATA_DIR}/nginx/cert/host.key
set_progress "33" "Changing ownership"
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons"
chown "${USER}:${USER}" "${DATA_DIR}"
set_progress "40" "Setting up infra"
@@ -146,7 +134,6 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"isCustomDomain": ${arg_is_custom_domain},
"boxVersionsUrl": "${arg_box_versions_url}",
"adminEmail": "admin@${arg_fqdn}",
"provider": "${arg_provider}",
"database": {
"hostname": "localhost",
"username": "root",
@@ -181,14 +168,6 @@ if [[ ! -z "${arg_dns_config}" ]]; then
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
fi
# Add TLS Configuration
if [[ ! -z "${arg_tls_config}" ]]; then
echo "Add TLS Config"
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
# Add webadmin oauth client
# The domain might have changed, therefor we have to update the record
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
+1 -2
View File
@@ -37,8 +37,7 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# only serve up the status page if we get proxy gateway errors
error_page 502 503 504 @appstatus;
error_page 500 502 503 504 @appstatus;
location @appstatus {
return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
}
@@ -38,12 +38,6 @@ http {
deny all;
}
# acme challenges
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/data/acme/;
}
location / {
# redirect everything to HTTPS
return 301 https://$host$request_uri;
@@ -61,7 +55,7 @@ http {
error_page 404 = @fallback;
location @fallback {
internal;
root /home/yellowtent/box/webadmin/dist;
root <%= sourceDir %>/webadmin/dist;
rewrite ^/$ /nakeddomain.html break;
}
+7 -7
View File
@@ -325,10 +325,10 @@ function setupSimpleAuth(app, options, callback) {
if (error) return callback(error);
var env = [
'SIMPLE_AUTH_SERVER=172.17.0.1',
'SIMPLE_AUTH_SERVER=172.17.42.1',
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.17.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.17.0.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.17.42.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.17.42.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + id
];
@@ -359,9 +359,9 @@ function setupLdap(app, options, callback) {
assert.strictEqual(typeof callback, 'function');
var env = [
'LDAP_SERVER=172.17.0.1',
'LDAP_SERVER=172.17.42.1',
'LDAP_PORT=' + config.get('ldapPort'),
'LDAP_URL=ldap://172.17.0.1:' + config.get('ldapPort'),
'LDAP_URL=ldap://172.17.42.1:' + config.get('ldapPort'),
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
@@ -391,7 +391,7 @@ function setupSendMail(app, options, callback) {
var env = [
'MAIL_SMTP_SERVER=mail',
'MAIL_SMTP_PORT=2500', // if you change this, change the mail container
'MAIL_SMTP_USERNAME=' + (app.location || app.id) + '-app', // use app.id for bare domains
'MAIL_SMTP_USERNAME=' + (app.location || app.id), // use app.id for bare domains
'MAIL_DOMAIN=' + config.fqdn()
];
@@ -778,7 +778,7 @@ function setupRedis(app, options, callback) {
name: 'redis-' + app.id,
Hostname: 'redis-' + app.location,
Tty: true,
Image: 'cloudron/redis:0.8.0', // if you change this, fix setup/INFRA_VERSION as well
Image: 'cloudron/redis:0.7.0', // if you change this, fix setup/INFRA_VERSION as well
Cmd: null,
Volumes: {
'/tmp': {},
+4 -10
View File
@@ -92,10 +92,8 @@ function checkAppHealth(app, callback) {
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
if (error || res.status >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
} else {
@@ -112,11 +110,7 @@ function processApps(callback) {
async.each(apps, checkAppHealth, function (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);
debug('apps alive: [%s]', apps.map(function (a) { return a.location; }).join(', '));
callback(null);
});
@@ -157,7 +151,7 @@ function processDockerEvents() {
debug('OOM Context: %s', context);
// do not send mails for dev apps
if (error || app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
if (app.appStoreId !== '') mailer.sendCrashNotification(program, context); // app can be null if it's an addon crash
});
});
+27 -43
View File
@@ -48,18 +48,17 @@ var addons = require('./addons.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
docker = require('./docker.js').connection,
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
semver = require('semver'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
@@ -73,8 +72,6 @@ var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'),
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function debugApp(app, args) {
assert(!app || typeof app === 'object');
@@ -176,7 +173,7 @@ function validatePortBindings(portBindings, tcpPorts) {
if (!Number.isInteger(portBindings[env])) return new Error(portBindings[env] + ' is not an integer');
if (portBindings[env] <= 0 || portBindings[env] > 65535) return new Error(portBindings[env] + ' is out of range');
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, + portBindings[env]);
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
@@ -288,16 +285,12 @@ function purchase(appStoreId, callback) {
// Skip purchase if appStoreId is empty
if (appStoreId === '') return callback(null);
// Skip if we don't have an appstore token
if (config.token() === '') return callback(null);
var url = config.apiServerOrigin() + '/api/v1/apps/' + appStoreId + '/purchase';
superagent.post(url).query({ token: config.token() }).end(function (error, res) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error));
if (res.statusCode === 402) return callback(new AppsError(AppsError.BILLING_REQUIRED));
if (res.statusCode === 404) return callback(new AppsError(AppsError.NOT_FOUND));
if (res.statusCode !== 201 && res.statusCode !== 200) return callback(new Error(util.format('App purchase failed. %s %j', res.status, res.body)));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (res.status === 402) return callback(new AppsError(AppsError.BILLING_REQUIRED));
if (res.status !== 201 && res.status !== 200) return callback(new Error(util.format('App purchase failed. %s %j', res.status, res.body)));
callback(null);
});
@@ -343,7 +336,7 @@ function install(appId, appStoreId, manifest, location, portBindings, accessRest
}
}
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
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);
@@ -384,7 +377,7 @@ function configure(appId, location, portBindings, accessRestriction, oauthProxy,
error = validateAccessRestriction(accessRestriction);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, error.message));
error = certificates.validateCertificate(cert, key, config.appFqdn(location));
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) {
@@ -500,6 +493,8 @@ function getLogs(appId, lines, follow, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback(new AppsError(AppsError.BAD_STATE, util.format('App is in %s state.', app.installationState)));
var args = [ '--output=json', '--no-pager', '--lines=' + lines ];
if (follow) args.push('--follow');
args = args.concat(appLogFilter(app));
@@ -516,7 +511,7 @@ function getLogs(appId, lines, follow, callback) {
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: source || 'main'
}) + '\n';
});
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
@@ -648,42 +643,31 @@ function exec(appId, options, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
var createOptions = {
var container = docker.getContainer(app.containerId);
var execOptions = {
AttachStdin: true,
AttachStdout: true,
AttachStderr: true,
OpenStdin: true,
StdinOnce: false,
Tty: true
Tty: true,
Cmd: cmd
};
docker.createSubcontainer(app, app.id + '-exec-' + Date.now(), cmd, createOptions, function (error, container) {
container.exec(execOptions, function (error, exec) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
container.attach({ stream: true, stdin: true, stdout: true, stderr: true }, function (error, stream) {
var startOptions = {
Detach: false,
Tty: true,
stdin: true // this is a dockerode option that enabled openStdin in the modem
};
exec.start(startOptions, function(error, stream) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
docker.startContainer(container.id, function (error) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (options.rows && options.columns) {
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
}
if (options.rows && options.columns) {
container.resize({ h: options.rows, w: options.columns }, NOOP_CALLBACK);
}
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);
});
return callback(null, stream);
});
});
});
+85 -146
View File
@@ -9,7 +9,7 @@ exports = module.exports = {
startTask: startTask,
// exported for testing
_reserveHttpPort: reserveHttpPort,
_getFreePort: getFreePort,
_configureNginx: configureNginx,
_unconfigureNginx: unconfigureNginx,
_createVolume: createVolume,
@@ -19,6 +19,7 @@ exports = module.exports = {
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_reloadNginx: reloadNginx,
_waitForDnsPropagation: waitForDnsPropagation
};
@@ -35,7 +36,6 @@ var addons = require('./addons.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
certificates = require('./certificates.js'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
database = require('./database.js'),
@@ -47,7 +47,6 @@ var addons = require('./addons.js'),
hat = require('hat'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -60,66 +59,87 @@ var addons = require('./addons.js'),
uuid = require('node-uuid'),
_ = require('underscore');
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
RELOAD_COLLECTD_CMD = path.join(__dirname, 'scripts/reloadcollectd.sh'),
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
database.initialize(callback);
}
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
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 reserveHttpPort(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// We expect conflicts to not happen despite closing the port (parallel app installs, app update does not reconfigure nginx etc)
// https://tools.ietf.org/html/rfc6056#section-3.5 says linux uses random ephemeral port allocation
function getFreePort(callback) {
var server = net.createServer();
server.listen(0, function () {
var port = server.address().port;
updateApp(app, { httpPort: port }, function (error) {
if (error) {
server.close();
return callback(error);
}
server.close(callback);
server.close(function () {
return callback(null, port);
});
});
}
function reloadNginx(callback) {
shell.sudo('reloadNginx', [ RELOAD_NGINX_CMD ], callback);
}
function configureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = config.appFqdn(app.location);
certificates.ensureCertificate(vhost, function (error, certFilePath, keyFilePath) {
getFreePort(function (error, freePort) {
if (error) return callback(error);
nginx.configureApp(app, certFilePath, keyFilePath, callback);
var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
var vhost = config.appFqdn(app.location);
var certFilePath = safe.fs.statSync(path.join(paths.APP_CERTS_DIR, vhost + '.cert')) ? path.join(paths.APP_CERTS_DIR, vhost + '.cert') : 'cert/host.cert';
var keyFilePath = safe.fs.statSync(path.join(paths.APP_CERTS_DIR, vhost + '.key')) ? path.join(paths.APP_CERTS_DIR, vhost + '.key') : 'cert/host.key';
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
port: freePort,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debugApp(app, 'writing config to %s', nginxConfigFilename);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debugApp(app, 'Error creating nginx config : %s', safe.error.message);
return callback(safe.error);
}
async.series([
exports._reloadNginx,
updateApp.bind(null, app, { httpPort: freePort })
], callback);
});
}
function unconfigureNginx(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
debugApp(app, 'Error removing nginx configuration : %s', safe.error.message);
return callback(null);
}
// TODO: maybe revoke the cert
nginx.unconfigureApp(app, callback);
exports._reloadNginx(callback);
}
function createContainer(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
assert(!app.containerId); // otherwise, it will trigger volumeFrom
debugApp(app, 'creating container');
@@ -132,9 +152,6 @@ function createContainer(app, callback) {
}
function deleteContainers(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting containers');
docker.deleteContainers(app.id, function (error) {
@@ -145,16 +162,10 @@ function deleteContainers(app, callback) {
}
function createVolume(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
}
function deleteVolume(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id ], callback);
}
@@ -187,9 +198,6 @@ function removeOAuthProxyCredentials(app, callback) {
}
function addCollectdProfile(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(error);
@@ -198,9 +206,6 @@ function addCollectdProfile(app, callback) {
}
function removeCollectdProfile(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
shell.sudo('removeCollectdProfile', [ RELOAD_COLLECTD_CMD ], callback);
@@ -208,9 +213,6 @@ function removeCollectdProfile(app, callback) {
}
function verifyManifest(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Verifying manifest');
var manifest = app.manifest;
@@ -224,9 +226,6 @@ function verifyManifest(app, callback) {
}
function downloadIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Downloading icon of %s@%s', app.appStoreId, app.manifest.version);
var iconUrl = config.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
@@ -235,8 +234,8 @@ function downloadIcon(app, callback) {
.get(iconUrl)
.buffer(true)
.end(function (error, res) {
if (error && !error.response) return callback(new Error('Network error downloading icon:' + error.message));
if (res.statusCode !== 200) return callback(null); // ignore error. this can also happen for apps installed with cloudron-cli
if (error) return callback(new Error('Error downloading icon:' + error.message));
if (res.status !== 200) return callback(null); // ignore error. this can also happen for apps installed with cloudron-cli
if (!safe.fs.writeFileSync(path.join(paths.APPICONS_DIR, app.id + '.png'), res.body)) return callback(new Error('Error saving icon:' + safe.error.message));
@@ -245,64 +244,46 @@ function downloadIcon(app, callback) {
}
function registerSubdomain(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// 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
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
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
// 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
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
subdomains.add(app.location, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: result }, callback);
retryCallback(null, error || changeId);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: result }, callback);
});
}
function unregisterSubdomain(app, location, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof callback, 'function');
// do not unregister bare domain because we show a error/cloudron info page there
if (location === '') {
debugApp(app, 'Skip unregister of empty subdomain');
return callback(null);
}
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', location);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', location);
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
subdomains.remove(location, 'A', [ ip ], function (error) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: null }, callback);
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: null }, callback);
});
}
function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
fs.unlink(path.join(paths.APPICONS_DIR, app.id + '.png'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', error);
callback(null);
@@ -310,9 +291,6 @@ function removeIcon(app, callback) {
}
function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!config.CLOUDRON) {
debugApp(app, 'Skipping dns propagation check for development');
return callback(null);
@@ -336,10 +314,6 @@ function waitForDnsPropagation(app, callback) {
// updates the app object and the database
function updateApp(app, values, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
@@ -364,15 +338,11 @@ function updateApp(app, values, callback) {
// - setup the container (requires image, volumes, addons)
// - setup collectd (requires container id)
function install(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.series([
verifyManifest.bind(null, app),
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
@@ -381,8 +351,10 @@ function install(app, callback) {
unregisterSubdomain.bind(null, app, app.location),
removeOAuthProxyCredentials.bind(null, app),
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs
unconfigureNginx.bind(null, app),
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
downloadIcon.bind(null, app),
@@ -413,9 +385,6 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
debugApp(app, 'installed');
@@ -431,9 +400,6 @@ function install(app, callback) {
}
function backup(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
apps.backupApp.bind(null, app, app.manifest.addons),
@@ -454,9 +420,6 @@ function backup(app, callback) {
// restore is also called for upgrades and infra updates. note that in those cases it is possible there is no backup
function restore(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// we don't have a backup, same as re-install. this allows us to install from install failures (update failures always
// have a backupId)
if (!app.lastBackupId) {
@@ -466,7 +429,6 @@ function restore(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
@@ -480,8 +442,10 @@ function restore(app, callback) {
},
removeOAuthProxyCredentials.bind(null, app),
removeIcon.bind(null, app),
unconfigureNginx.bind(null, app),
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Configuring Nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '40, Downloading icon' }),
downloadIcon.bind(null, app),
@@ -512,9 +476,6 @@ function restore(app, callback) {
updateApp.bind(null, app, { installationProgress: '90, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring Nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
debugApp(app, 'restored');
@@ -532,12 +493,8 @@ function restore(app, callback) {
// note that configure is called after an infra update as well
function configure(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
removeCollectdProfile.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
@@ -547,8 +504,10 @@ function configure(app, callback) {
unregisterSubdomain(app, app.oldConfig.location, next);
},
removeOAuthProxyCredentials.bind(null, app),
unconfigureNginx.bind(null, app),
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '25, Configuring Nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Create OAuth proxy credentials' }),
allocateOAuthProxyCredentials.bind(null, app),
@@ -571,9 +530,6 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
configureNginx.bind(null, app),
// done!
function (callback) {
debugApp(app, 'configured');
@@ -590,9 +546,6 @@ function configure(app, callback) {
// nginx configuration is skipped because app.httpPort is expected to be available
function update(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
// app does not want these addons anymore
@@ -657,9 +610,6 @@ function update(app, callback) {
}
function uninstall(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'uninstalling');
async.series([
@@ -699,9 +649,6 @@ function uninstall(app, callback) {
}
function runApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
docker.startContainer(app.containerId, function (error) {
if (error) return callback(error);
@@ -710,20 +657,14 @@ function runApp(app, callback) {
}
function stopApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
docker.stopContainers(app.id, function (error) {
if (error) return callback(error);
updateApp(app, { runState: appdb.RSTATE_STOPPED, health: null }, callback);
updateApp(app, { runState: appdb.RSTATE_STOPPED }, callback);
});
}
function handleRunCommand(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.runState === appdb.RSTATE_PENDING_STOP) {
return stopApp(app, callback);
}
@@ -739,9 +680,6 @@ function handleRunCommand(app, callback) {
}
function startTask(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
// determine what to do
appdb.get(appId, function (error, app) {
if (error) return callback(error);
@@ -785,3 +723,4 @@ if (require.main === module) {
});
});
}
+1 -1
View File
@@ -62,7 +62,7 @@ function getAllPaged(page, perPage, callback) {
api(backupConfig.provider).getAllPaged(backupConfig, page, perPage, function (error, backups) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
return callback(null, backups); // [ { creationTime, restoreKey } ] sorted by time (latest first
return callback(null, backups); // [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first
});
});
}
-425
View File
@@ -1,425 +0,0 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme'),
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
ursa = require('ursa'),
util = require('util'),
_ = require('underscore');
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf';
exports = module.exports = {
getCertificate: getCertificate
};
function AcmeError(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(AcmeError, Error);
AcmeError.INTERNAL_ERROR = 'Internal Error';
AcmeError.EXTERNAL_ERROR = 'External Error';
AcmeError.ALREADY_EXISTS = 'Already Exists';
AcmeError.NOT_COMPLETED = 'Not Completed';
AcmeError.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.chainPem = options.prod ? safe.fs.readFileSync(__dirname + '/lets-encrypt-x1-cross-signed.pem.txt') : new Buffer('');
}
Acme.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory', function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = util.isBuffer(str) ? str : new Buffer(str);
return urlBase64Encode(buf.toString('base64'));
}
Acme.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
var privateKey = ursa.createPrivateKey(this.accountKeyPem);
var header = {
alg: 'RS256',
jwk: {
e: b64(privateKey.getExponent()),
kty: 'RSA',
n: b64(privateKey.getModulus())
}
};
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = ursa.createSigner('sha256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(privateKey, 'base64'));
var data = {
header: header,
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme.prototype.registerUser = function (email, callback) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-reg',
contact: [ 'mailto:' + email ],
agreement: LE_AGREEMENT
};
debug('registerUser: %s', email);
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return callback(new AcmeError(AcmeError.ALREADY_EXISTS, result.body.detail));
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerUser: registered user %s', email);
callback();
});
};
Acme.prototype.registerDomain = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-authz',
identifier: {
type: 'dns',
value: domain
}
};
debug('registerDomain: %s', domain);
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerDomain: registered %s', domain);
callback(null, result.body);
});
};
Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
var token = challenge.token;
assert(util.isBuffer(this.accountKeyPem));
var privateKey = ursa.createPrivateKey(this.accountKeyPem);
var jwk = {
e: b64(privateKey.getExponent()),
kty: 'RSA',
n: b64(privateKey.getModulus())
};
var shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
var thumbprint = urlBase64Encode(shasum.digest('base64'));
var keyAuthorization = token + '.' + thumbprint;
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
if (error) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, error));
callback();
});
};
Acme.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.uri);
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.uri).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.uri);
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 202) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s"', result.body.status);
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme.prototype.signCertificate = function (domain, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var payload = {
resource: 'new-cert',
csr: b64(csrDer)
};
debug('signCertificate: sending new-cert request');
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
if (error) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
var certUrl = result.headers.location;
if (!certUrl) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // for renewal
return callback(null, result.headers.location);
});
};
Acme.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var execSync = safe.child_process.execSync;
var privateKeyFile = path.join(outdir, domain + '.key');
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var csrFile = path.join(outdir, domain + '.csr');
if (!safe.fs.writeFileSync(csrFile, csrFile)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var that = this;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).end(function (error, result) {
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var certificateDer = result.text;
var execSync = safe.child_process.execSync;
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
debug('downloadCertificate: cert der file saved');
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, that.chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file saved at %s', certificateFile);
callback();
});
};
Acme.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we cannot use owner email because we don't have it yet (the admin cert is fetched before activation)
// one option is to update the owner email when a second cert is requested (https://github.com/ietf-wg-acme/acme/issues/30)
var email = 'admin@cloudron.io';
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
that.registerUser(email, function (error) {
if (error && error.reason !== AcmeError.ALREADY_EXISTS) return callback(error);
that.registerDomain(domain, function (error, result) {
if (error) return callback(error);
debug('acmeFlow: challenges: %j', result);
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'no http challenges'));
var challenge = httpChallenges[0];
async.waterfall([
that.prepareHttpChallenge.bind(that, challenge),
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, domain),
that.signCertificate.bind(that, domain),
that.downloadCertificate.bind(that, domain)
], callback);
});
});
};
Acme.prototype.getCertificate = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var certUrl = safe.fs.readFileSync(path.join(outdir, domain + '.url'), 'utf8');
var certificateGetter;
if (certUrl) {
debug('getCertificate: renewing existing cert for %s from %s', domain, certUrl);
certificateGetter = this.downloadCertificate.bind(this, domain, certUrl);
} else {
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
certificateGetter = this.acmeFlow.bind(this, domain);
}
certificateGetter(function (error) {
if (error) return callback(error);
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
});
};
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme(options || { });
acme.getCertificate(domain, callback);
}
-18
View File
@@ -1,18 +0,0 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate
};
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', domain);
return callback(null, 'cert/host.cert', 'cert/host.key');
}
@@ -1,27 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEqDCCA5CgAwIBAgIRAJgT9HUT5XULQ+dDHpceRL0wDQYJKoZIhvcNAQELBQAw
PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
Ew5EU1QgUm9vdCBDQSBYMzAeFw0xNTEwMTkyMjMzMzZaFw0yMDEwMTkyMjMzMzZa
MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAJzTDPBa5S5Ht3JdN4OzaGMw6tc1Jhkl4b2+NfFwki+3uEtB
BaupnjUIWOyxKsRohwuj43Xk5vOnYnG6eYFgH9eRmp/z0HhncchpDpWRz/7mmelg
PEjMfspNdxIknUcbWuu57B43ABycrHunBerOSuu9QeU2mLnL/W08lmjfIypCkAyG
dGfIf6WauFJhFBM/ZemCh8vb+g5W9oaJ84U/l4avsNwa72sNlRZ9xCugZbKZBDZ1
gGusSvMbkEl4L6KWTyogJSkExnTA0DHNjzE4lRa6qDO4Q/GxH8Mwf6J5MRM9LTb4
4/zyM2q5OTHFr8SNDR1kFjOq+oQpttQLwNh9w5MCAwEAAaOCAZIwggGOMBIGA1Ud
EwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQDAgGGMH8GCCsGAQUFBwEBBHMwcTAy
BggrBgEFBQcwAYYmaHR0cDovL2lzcmcudHJ1c3RpZC5vY3NwLmlkZW50cnVzdC5j
b20wOwYIKwYBBQUHMAKGL2h0dHA6Ly9hcHBzLmlkZW50cnVzdC5jb20vcm9vdHMv
ZHN0cm9vdGNheDMucDdjMB8GA1UdIwQYMBaAFMSnsaR7LHH62+FLkHX/xBVghYkQ
MFQGA1UdIARNMEswCAYGZ4EMAQIBMD8GCysGAQQBgt8TAQEBMDAwLgYIKwYBBQUH
AgEWImh0dHA6Ly9jcHMucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcwPAYDVR0fBDUw
MzAxoC+gLYYraHR0cDovL2NybC5pZGVudHJ1c3QuY29tL0RTVFJPT1RDQVgzQ1JM
LmNybDATBgNVHR4EDDAKoQgwBoIELm1pbDAdBgNVHQ4EFgQUqEpqYwR93brm0Tm3
pkVl7/Oo7KEwDQYJKoZIhvcNAQELBQADggEBANHIIkus7+MJiZZQsY14cCoBG1hd
v0J20/FyWo5ppnfjL78S2k4s2GLRJ7iD9ZDKErndvbNFGcsW+9kKK/TnY21hp4Dd
ITv8S9ZYQ7oaoqs7HwhEMY9sibED4aXw09xrJZTC9zK1uIfW6t5dHQjuOWv+HHoW
ZnupyxpsEUlEaFb+/SCI4KCSBdAsYxAcsHYI5xxEI4LutHp6s3OT2FuO90WfdsIk
6q78OMSdn875bNjdBYAqxUp2/LEIHfDBkLoQz0hFJmwAbYahqKaLn73PAAm1X2kj
f1w8DdnkabOLGeOVcj9LQ+s67vBykx4anTjURkbqZslUEUsn2k5xeua2zUk=
-----END CERTIFICATE-----
-250
View File
@@ -1,250 +0,0 @@
/* jslint node:true */
'use strict';
var acme = require('./cert/acme.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:src/certificates'),
fs = require('fs'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
x509 = require('x509');
exports = module.exports = {
installAdminCertificate: installAdminCertificate,
autoRenew: autoRenew,
setFallbackCertificate: setFallbackCertificate,
setAdminCertificate: setAdminCertificate,
CertificatesError: CertificatesError,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate
};
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function CertificatesError(reason, errorOrMessage) {
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(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
function getApi(callback) {
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
var api = tlsConfig.provider === 'caas' ? caas : acme;
var options = { };
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
callback(null, api, options);
});
}
function installAdminCertificate(callback) {
if (cloudron.isConfiguredSync()) return callback();
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
if (tlsConfig.provider === 'caas') return callback();
sysinfo.getIp(function (error, ip) {
if (error) return callback(error);
waitForDns(config.adminFqdn(), ip, config.fqdn(), function (error) {
if (error) return callback(error); // this cannot happen because we retry forever
ensureCertificate(config.adminFqdn(), function (error, certFilePath, keyFilePath) {
if (error) { // currently, this can never happen
debug('Error obtaining certificate. Proceed anyway', error);
return callback();
}
nginx.configureAdmin(certFilePath, keyFilePath, callback);
});
});
});
});
}
function needsRenewalSync(certFilePath) {
var result = safe.child_process.execSync('openssl x509 -checkend %s -in %s', 60 * 60 * 24 * 5, certFilePath);
return result === null; // command errored
}
function autoRenew(callback) {
debug('autoRenew: Checking certificates for renewal');
callback = callback || NOOP_CALLBACK;
var filenames = safe.fs.readdirSync(paths.APP_CERTS_DIR);
if (!filenames) {
debug('autoRenew: Error getting filenames: %s', safe.error.message);
return;
}
var certs = filenames.filter(function (f) {
return f.match(/\.cert$/) !== null && needsRenewalSync(path.join(paths.APP_CERTS_DIR, f));
});
debug('autoRenew: %j needs to be renewed', certs);
getApi(function (error, api, apiOptions) {
if (error) return callback(error);
async.eachSeries(certs, function iterator(cert, iteratorCallback) {
var domain = cert.match(/^(.*)\.cert$/)[1];
if (domain === 'host') return iteratorCallback(); // cannot renew fallback cert
debug('autoRenew: renewing cert for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error) {
if (error) debug('autoRenew: could not renew cert for %s', domain, error);
iteratorCallback(); // move on to next cert
});
});
});
}
// 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 setFallbackCertificate(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 CertificatesError(CertificatesError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.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 CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
nginx.reload(function (error) {
if (error) return callback(new CertificatesError(CertificatesError.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 vhost = config.appFqdn(constants.ADMIN_LOCATION);
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
var error = validateCertificate(cert, key, vhost);
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
nginx.configureAdmin(certFilePath, keyFilePath, callback);
}
function ensureCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// check if user uploaded a specific cert. ideally, we should not mix user certs and automatic certs as we do here...
var userCertFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var userKeyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
if (fs.existsSync(userCertFilePath) && fs.existsSync(userKeyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', domain, userKeyFilePath);
if (!needsRenewalSync(userCertFilePath)) return callback(null, userCertFilePath, userKeyFilePath);
debug('ensureCertificate: %s cert require renewal', domain);
}
getApi(function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs
}
callback(null, certFilePath, keyFilePath);
});
});
}
+74 -102
View File
@@ -33,12 +33,12 @@ var apps = require('./apps.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = require('./backups.js').BackupsError,
bytes = require('bytes'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
debug = require('debug')('box:cloudron'),
fs = require('fs'),
locker = require('./locker.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
progress = require('./progress.js'),
@@ -178,8 +178,8 @@ function setTimeZone(ip, callback) {
debug('setTimeZone ip:%s', ip);
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
if (error || result.statusCode !== 200) {
debug('Failed to get geo location', error);
return callback(null);
}
@@ -243,8 +243,6 @@ function getStatus(callback) {
callback(null, {
activated: count !== 0,
version: config.version(),
boxVersionsUrl: config.get('boxVersionsUrl'),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
cloudronName: cloudronName
});
});
@@ -256,21 +254,12 @@ function getCloudronDetails(callback) {
if (gCloudronDetails) return callback(null, gCloudronDetails);
if (!config.token()) {
gCloudronDetails = {
region: null,
size: null
};
return callback(null, gCloudronDetails);
}
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
if (error) return callback(error);
if (result.status !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
gCloudronDetails = result.body.box;
@@ -283,7 +272,7 @@ function getConfig(callback) {
getCloudronDetails(function (error, result) {
if (error) {
debug('Failed to fetch cloudron details.', error);
console.error('Failed to fetch cloudron details.', error);
// set fallback values to avoid dependency on appstore
result = {
@@ -292,32 +281,31 @@ 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) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getDeveloperMode(function (error, developerMode) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
isDev: config.isDev(),
fqdn: config.fqdn(),
ip: ip,
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
developerMode: developerMode,
region: result.region,
size: result.size,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
});
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
isDev: config.isDev(),
fqdn: config.fqdn(),
ip: sysinfo.getIp(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.get(),
isCustomDomain: config.isCustomDomain(),
developerMode: developerMode,
region: result.region,
size: result.size,
memory: memory,
cloudronName: cloudronName
});
});
});
@@ -325,11 +313,10 @@ function getConfig(callback) {
}
function sendHeartbeat() {
if (!config.token()) return;
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
if (error) debug('Error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
@@ -396,53 +383,48 @@ function addDnsRecords() {
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('internal error failed to read dkim public key')));
sysinfo.getIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
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 nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
var webadminRecord = { subdomain: 'my', type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
// DMARC requires special setup if report email id is in different domain
var dmarcRecord = { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' ] };
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
records.push(dmarcRecord);
}
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);
debug('addDnsRecords: %j', records);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
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 });
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
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);
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
retryCallback(error);
});
retryCallback(error);
});
}, function (error) {
gUpdatingDns = false;
debug('addDnsRecords: done updating records with error:', error);
callback(error);
});
}, function (error) {
gUpdatingDns = false;
debug('addDnsRecords: done updating records with error:', error);
callback(error);
});
}
@@ -481,10 +463,10 @@ function migrate(size, region, callback) {
.query({ token: config.token() })
.send({ size: size, region: region, restoreKey: restoreKey })
.end(function (error, result) {
if (error && !error.response) return unlock(error);
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
if (error) return unlock(error);
if (result.status === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.status === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.status !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
return unlock(null);
});
@@ -507,7 +489,7 @@ function update(boxUpdateInfo, callback) {
debug('Starting upgrade');
doUpgrade(boxUpdateInfo, function (error) {
if (error) {
console.error('Upgrade failed with error:', error);
debug('Upgrade failed with error: %s', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
@@ -515,7 +497,7 @@ function update(boxUpdateInfo, callback) {
debug('Starting update');
doUpdate(boxUpdateInfo, function (error) {
if (error) {
console.error('Update failed with error:', error);
debug('Update failed with error: %s', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
@@ -541,8 +523,8 @@ function doUpgrade(boxUpdateInfo, callback) {
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
if (error) return upgradeError(new Error('Error making upgrade request: ' + error));
if (result.status !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
@@ -570,8 +552,8 @@ function doUpdate(boxUpdateInfo, callback) {
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl')
.query({ token: config.token(), boxVersion: boxUpdateInfo.version })
.end(function (error, result) {
if (error && !error.response) return updateError(new Error('Network error fetching sourceTarballUrl: ' + error));
if (result.statusCode !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.statusCode));
if (error) return updateError(new Error('Error fetching sourceTarballUrl: ' + error));
if (result.status !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.status));
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + JSON.stringify(result.body)));
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
@@ -588,16 +570,6 @@ function doUpdate(boxUpdateInfo, callback) {
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
isCustomDomain: config.isCustomDomain(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
version: boxUpdateInfo.version,
boxVersionsUrl: config.get('boxVersionsUrl')
}
@@ -606,8 +578,8 @@ function doUpdate(boxUpdateInfo, callback) {
debug('updating box %j', args);
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
if (error && !error.response) return updateError(error);
if (result.statusCode !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
if (error) return updateError(error);
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
@@ -710,13 +682,13 @@ function backupBoxAndApps(callback) {
++processed;
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
if (error && error.reason !== AppsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
}, function appsBackedUp(error, backupIds) {
+3 -3
View File
@@ -1,6 +1,6 @@
LoadPlugin "table"
<Plugin table>
<Table "/sys/fs/cgroup/memory/system.slice/docker.service/docker/<%= containerId %>/memory.stat">
<Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.stat">
Instance "<%= appId %>-memory"
Separator " \\n"
<Result>
@@ -10,7 +10,7 @@ LoadPlugin "table"
</Result>
</Table>
<Table "/sys/fs/cgroup/memory/system.slice/docker.service/docker/<%= containerId %>/memory.max_usage_in_bytes">
<Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.max_usage_in_bytes">
Instance "<%= appId %>-memory"
Separator "\\n"
<Result>
@@ -20,7 +20,7 @@ LoadPlugin "table"
</Result>
</Table>
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker/<%= containerId %>/cpuacct.stat">
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker-<%= containerId %>.scope/cpuacct.stat">
Instance "<%= appId %>-cpu"
Separator " \\n"
<Result>
-11
View File
@@ -17,7 +17,6 @@ exports = module.exports = {
TEST: process.env.BOX_ENV === 'test',
// convenience getters
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
fqdn: fqdn,
@@ -29,7 +28,6 @@ exports = module.exports = {
// these values are derived
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
adminFqdn: adminFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
@@ -83,7 +81,6 @@ function initConfig() {
data.ldapPort = 3002;
data.oauthProxyPort = 3003;
data.simpleAuthPort = 3004;
data.provider = 'caas';
if (exports.CLOUDRON) {
data.port = 3000;
@@ -158,10 +155,6 @@ function appFqdn(location) {
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
}
function adminFqdn() {
return appFqdn(constants.ADMIN_LOCATION);
}
function adminOrigin() {
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
}
@@ -196,7 +189,3 @@ function database() {
function isDev() {
return /dev/i.test(get('boxVersionsUrl'));
}
function provider() {
return get('provider');
}
+1 -14
View File
@@ -7,7 +7,6 @@ exports = module.exports = {
var apps = require('./apps.js'),
assert = require('assert'),
certificates = require('./certificates.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
CronJob = require('cron').CronJob,
@@ -24,8 +23,7 @@ var gAutoupdaterJob = null,
gBackupJob = null,
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gSchedulerSyncJob = null,
gCertificateRenewJob = null;
gSchedulerSyncJob = null;
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
@@ -109,14 +107,6 @@ function recreateJobs(unusedTimeZone, callback) {
timeZone: allSettings[settings.TIME_ZONE_KEY]
});
if (gCertificateRenewJob) gCertificateRenewJob.stop();
gCertificateRenewJob = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: certificates.autoRenew,
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]);
@@ -189,8 +179,5 @@ function uninitialize(callback) {
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
gSchedulerSyncJob = null;
if (gCertificateRenewJob) gCertificateRenewJob.stop();
gCertificateRenewJob = null;
callback();
}
-2
View File
@@ -126,8 +126,6 @@ function clear(callback) {
function beginTransaction(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool === null) return callback(new Error('No database connection pool.'));
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback(error);
+2 -8
View File
@@ -13,7 +13,6 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:developer'),
tokendb = require('./tokendb.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
@@ -39,7 +38,6 @@ function DeveloperError(reason, errorOrMessage) {
}
util.inherits(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function enabled(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -79,12 +77,8 @@ function getNonApprovedApps(callback) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
if (result.statusCode === 401) {
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
return callback(null, []);
}
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
if (result.status !== 200) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
callback(null, result.body.apps || []);
});
+11 -11
View File
@@ -40,9 +40,9 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
.query({ token: dnsConfig.token })
.send(data)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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);
});
@@ -63,8 +63,8 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
.query({ token: dnsConfig.token, type: type })
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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);
});
@@ -107,10 +107,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
.query({ token: dnsConfig.token })
.send(data)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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);
});
@@ -127,8 +127,8 @@ function getChangeStatus(dnsConfig, changeId, callback) {
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
.query({ token: dnsConfig.token })
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
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);
});
+5 -5
View File
@@ -176,19 +176,19 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
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('del: resource record set not found.', error);
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('del: hosted zone not found.', error);
debug('delSubdomain: hosted zone not found.', error);
return callback(new SubdomainError(SubdomainError.NOT_FOUND, new Error(error)));
} else if (error && error.code === 'PriorRequestNotComplete') {
debug('del: resource is still busy', error);
debug('delSubdomain: resource is still busy', error);
return callback(new SubdomainError(SubdomainError.STILL_BUSY, new Error(error)));
} else if (error && error.code === 'InvalidChangeBatch') {
debug('del: invalid change batch. No such record to be deleted.');
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('del: error', error);
debug('delSubdomain: error', error);
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, new Error(error)));
}
+16 -25
View File
@@ -8,8 +8,7 @@ var addons = require('./addons.js'),
Docker = require('dockerode'),
safe = require('safetydance'),
semver = require('semver'),
util = require('util'),
_ = require('underscore');
util = require('util');
exports = module.exports = {
connection: connectionInstance(),
@@ -17,10 +16,8 @@ exports = module.exports = {
createContainer: createContainer,
startContainer: startContainer,
stopContainer: stopContainer,
stopContainerByName: stopContainer,
stopContainers: stopContainers,
deleteContainer: deleteContainer,
deleteContainerByName: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer
@@ -54,7 +51,7 @@ function targetBoxVersion(manifest) {
if ('minBoxVersion' in manifest) return manifest.minBoxVersion;
return '99999.99999.99999'; // compatible with the latest version
return '0.0.1';
}
function pullImage(manifest, callback) {
@@ -67,17 +64,18 @@ function pullImage(manifest, callback) {
// is emitted as a chunk
stream.on('data', function (chunk) {
var data = safe.JSON.parse(chunk) || { };
debug('pullImage %s: %j', manifest.id, data);
debug('pullImage data: %j', data);
// The information here is useless because this is per layer as opposed to per image
if (data.status) {
// debugApp(app, 'progress: %s', data.status); // progressDetail { current, total }
} else if (data.error) {
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
debug('pullImage error detail: %s', data.errorDetail.message);
}
});
stream.on('end', function () {
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
debug('downloaded image %s successfully', manifest.dockerImage);
var image = docker.getImage(manifest.dockerImage);
@@ -86,14 +84,14 @@ function pullImage(manifest, callback) {
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);
debug('This image exposes ports: %j', data.Config.ExposedPorts);
callback(null);
});
});
stream.on('error', function (error) {
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
debug('error pulling image %s : %j', manifest.dockerImage, error);
callback(error);
});
@@ -104,12 +102,12 @@ function downloadImage(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
debug('downloadImage %s %s', manifest.id, manifest.dockerImage);
debug('downloadImage %s', 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++);
async.retry({ times: 5, interval: 15000 }, function (retryCallback) {
debug('Downloading image. attempt: %s', attempt++);
pullImage(manifest, function (error) {
if (error) console.error(error);
@@ -119,11 +117,10 @@ function downloadImage(manifest, callback) {
}, callback);
}
function createSubcontainer(app, name, cmd, options, callback) {
function createSubcontainer(app, name, cmd, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert(!cmd || util.isArray(cmd));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection,
@@ -156,9 +153,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
}
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
// for subcontainers, this should ideally be false. but docker does not allow network sharing if the app container is not running
// this means cloudron exec does not work
var isolatedNetworkNs = true;
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
@@ -166,8 +160,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var containerOptions = {
name: name, // used for filtering logs
// do _not_ set hostname to app fqdn. doing so sets up the dns name to look up the internal docker ip. this makes curl from within container fail
// for subcontainers, this should not be set because we already share the network namespace with app container
Hostname: isolatedNetworkNs ? (semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location)) : null,
Hostname: semver.gte(targetBoxVersion(app.manifest), '0.0.77') ? app.location : config.appFqdn(app.location),
Tty: isAppContainer,
Image: app.manifest.dockerImage,
Cmd: cmd,
@@ -189,30 +182,28 @@ function createSubcontainer(app, name, cmd, options, callback) {
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" ],
NetworkMode: isolatedNetworkNs ? 'default' : ('container:' + app.containerId), // share network namespace with parent
Links: isolatedNetworkNs ? addons.getLinksSync(app, app.manifest.addons) : null, // links is redundant with --net=container
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);
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
docker.createContainer(containerOptions, callback);
});
}
function createContainer(app, callback) {
createSubcontainer(app, app.id /* name */, null /* cmd */, { } /* options */, callback);
createSubcontainer(app, app.id /* name */, null /* cmd */, callback);
}
function startContainer(containerId, callback) {
+2 -1
View File
@@ -5,7 +5,8 @@ Dear Admin,
The application titled '<%= title %>' that you installed at <%= appFqdn %>
is not responding.
This is most likely a problem in the application.
This is most likely a problem in the application. Please report this issue to
support@cloudron.io (by forwarding this email).
You are receiving this email because you are an Admin of the Cloudron at <%= fqdn %>.
+7 -15
View File
@@ -133,15 +133,6 @@ function checkDns() {
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) {
if (error) return console.error(error);
@@ -153,9 +144,12 @@ function sendMails(queue) {
port: 2500 // this value comes from mail container
}));
debug('Processing mail queue of size %d (through %s:2500)', queue.length, mailServerIp);
var mailQueueCopy = gMailQueue;
gMailQueue = [ ];
async.mapSeries(queue, function iterator(mailOptions, callback) {
debug('Processing mail queue of size %d (through %s:2500)', mailQueueCopy.length, mailServerIp);
async.mapSeries(mailQueueCopy, function iterator(mailOptions, callback) {
transport.sendMail(mailOptions, function (error) {
if (error) return console.error(error); // TODO: requeue?
debug('Email sent to ' + mailOptions.to);
@@ -284,7 +278,7 @@ function appDied(app) {
var mailOptions = {
from: config.get('adminEmail'),
to: adminEmails.concat('support@cloudron.io').join(', '),
to: adminEmails.join(', '),
subject: util.format('App %s is down', app.location),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: config.appFqdn(app.location), format: 'text' })
};
@@ -329,8 +323,6 @@ 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) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
@@ -342,7 +334,7 @@ function sendCrashNotification(program, context) {
text: render('crash_notification.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
sendMails([ mailOptions ]);
enqueue(mailOptions);
}
function sendFeedback(user, type, subject, description) {
-93
View File
@@ -1,93 +0,0 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:src/nginx'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js');
exports = module.exports = {
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload
};
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function configureAdmin(certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: config.adminFqdn(),
endpoint: 'admin',
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);
reload(callback);
}
function configureApp(app, certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = app.oauthProxy ? 'oauthproxy' : 'app';
var vhost = config.appFqdn(app.location);
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
port: app.httpPort,
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');
debug('writing config for "%s" to %s', app.location, nginxConfigFilename);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', app.location, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
debug('Error removing nginx configuration of "%s": %s', app.location, safe.error.message);
return callback(null);
}
reload(callback);
}
function reload(callback) {
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
+2 -2
View File
@@ -35,9 +35,9 @@ args.forEach(function (arg) {
});
if (code && redirectURI) {
window.location.href = redirectURI + (redirectURI.indexOf('?') !== -1 ? '&' : '?') + 'code=' + code + (state ? '&state=' + state : '');
window.location.href = redirectURI + '?code=' + code + (state ? '&state=' + state : '');
} else if (redirectURI && accessToken) {
window.location.href = redirectURI + (redirectURI.indexOf('?') !== -1 ? '&' : '?') + 'token=' + accessToken + (state ? '&state=' + state : '');
window.location.href = redirectURI + '?token=' + accessToken + (state ? '&state=' + state : '');
} else {
window.location.href = '/api/v1/session/login';
}
+1 -1
View File
@@ -92,7 +92,7 @@ function authenticate(req, res, next) {
.post(config.internalAdminOrigin() + '/api/v1/oauth/token')
.query(query).send(data)
.end(function (error, result) {
if (error && !error.response) {
if (error) {
console.error(error);
return res.send(500, 'Unable to contact the oauth server.');
}
+2 -4
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
NGINX_CERT_DIR: path.join(config.baseDir(), 'data/nginx/cert'),
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
SCHEDULER_FILE: path.join(config.baseDir(), 'data/addons/scheduler.json'),
DNS_IN_SYNC_FILE: path.join(config.baseDir(), 'data/dns_in_sync'),
@@ -27,8 +28,5 @@ exports = module.exports = {
CLOUDRON_AVATAR_FILE: path.join(config.baseDir(), 'data/box/avatar.png'),
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json'),
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'data/acme'),
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'data/box/acme/acme.key')
UPDATE_CHECKER_FILE: path.join(config.baseDir(), 'data/box/updatechecker.json')
};
-3
View File
@@ -133,7 +133,6 @@ function installApp(req, res, next) {
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.USER_REQUIRED) return next(new HttpError(400, 'accessRestriction must specify one user'));
if (error) return next(new HttpError(500, error));
@@ -369,8 +368,6 @@ function exec(req, res, next) {
duplexStream.pipe(res.socket);
res.socket.pipe(duplexStream);
res.on('close', duplexStream.close);
});
}
+5 -11
View File
@@ -57,15 +57,12 @@ function activate(req, res, next) {
if (error && error.reason === CloudronError.BAD_EMAIL) return next(new HttpError(400, 'Bad email'));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken }).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken:req.query.setupToken }).end(function (error, result) {
if (error) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
if (result.statusCode !== 201) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
next(new HttpSuccess(201, info));
});
@@ -75,16 +72,13 @@ function activate(req, res, next) {
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
// skip setupToken auth for non caas case
if (config.provider() !== 'caas') return next();
if (typeof req.query.setupToken !== 'string') return next(new HttpError(400, 'no setupToken provided'));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken }).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (error) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text ? result.text.message : 'Internal error'));
next();
});
+1 -1
View File
@@ -359,7 +359,7 @@ var authorization = [
var redirectPath = url.parse(redirectURI).path;
var redirectOrigin = client.redirectURI;
callback(null, client, '/api/v1/session/callback?redirectURI=' + encodeURIComponent(url.resolve(redirectOrigin, redirectPath)));
callback(null, client, '/api/v1/session/callback?redirectURI=' + url.resolve(redirectOrigin, redirectPath));
});
}),
function (req, res, next) {
+4 -30
View File
@@ -15,16 +15,11 @@ exports = module.exports = {
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate
};
var assert = require('assert'),
certificates = require('../certificates.js'),
CertificatesError = require('../certificates.js').CertificatesError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
@@ -116,27 +111,6 @@ function setDnsConfig(req, res, next) {
});
}
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');
@@ -144,8 +118,8 @@ function setCertificate(req, res, next) {
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'));
certificates.setFallbackCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
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, {}));
@@ -159,8 +133,8 @@ function setAdminCertificate(req, res, next) {
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'));
certificates.setAdminCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
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, {}));
+97 -99
View File
@@ -27,7 +27,7 @@ var appdb = require('../../appdb.js'),
nock = require('nock'),
paths = require('../../paths.js'),
redis = require('redis'),
superagent = require('superagent'),
request = require('superagent'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
@@ -114,10 +114,11 @@ function setup(done) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
@@ -136,10 +137,11 @@ function setup(done) {
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
request.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1 })
.end(function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(201);
callback(null);
@@ -154,8 +156,6 @@ function setup(done) {
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], done);
}
@@ -196,174 +196,174 @@ describe('App API', function () {
});
it('app install fails - missing manifest', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('manifest is required');
done();
done(err);
});
});
it('app install fails - missing appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('appStoreId is required');
done();
done(err);
});
});
it('app install fails - invalid json', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send('garbage')
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
done(err);
});
});
it('app install fails - invalid location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: '!awesome', accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
done();
done(err);
});
});
it('app install fails - invalid location type', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: 42, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
done();
done(err);
});
});
it('app install fails - reserved admin location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.ADMIN_LOCATION, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.ADMIN_LOCATION + ' is reserved');
done();
done(err);
});
});
it('app install fails - reserved api location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: constants.API_LOCATION, accessRestriction: null, oauthProxy: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
done();
done(err);
});
});
it('app install fails - portBindings must be object', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: 23, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
done();
done(err);
});
});
it('app install fails - accessRestriction is required', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
done();
done(err);
});
});
it('app install fails - accessRestriction type is wrong', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: '', oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
done();
done(err);
});
});
it('app install fails - accessRestriction no users not allowed', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction must specify one user');
done();
done(err);
});
});
it('app install fails - accessRestriction too many users not allowed', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST_1, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: { users: [ 'one', 'two' ] }, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction must specify one user');
done();
done(err);
});
});
it('app install fails - oauthProxy is required', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: {}, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('oauthProxy must be a boolean');
done();
done(err);
});
});
it('app install fails for non admin', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token_1 })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('app install fails due to purchase failure', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(402, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(402);
expect(fake.isDone()).to.be.ok();
done();
done(err);
});
});
it('app install succeeds with purchase', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
@@ -371,14 +371,14 @@ describe('App API', function () {
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake.isDone()).to.be.ok();
done();
done(err);
});
});
it('app install fails because of conflicting location', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
@@ -389,120 +389,120 @@ describe('App API', function () {
});
it('can get app status', function (done) {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.id).to.eql(APP_ID);
expect(res.body.installationState).to.be.ok();
done();
done(err);
});
});
it('cannot get invalid app status', function (done) {
superagent.get(SERVER_URL + '/api/v1/apps/kubachi')
request.get(SERVER_URL + '/api/v1/apps/kubachi')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
done(err);
});
});
it('can get all apps', function (done) {
superagent.get(SERVER_URL + '/api/v1/apps')
request.get(SERVER_URL + '/api/v1/apps')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.apps).to.be.an('array');
expect(res.body.apps[0].id).to.eql(APP_ID);
expect(res.body.apps[0].installationState).to.be.ok();
done();
done(err);
});
});
it('non admin can get all apps', function (done) {
superagent.get(SERVER_URL + '/api/v1/apps')
request.get(SERVER_URL + '/api/v1/apps')
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.apps).to.be.an('array');
expect(res.body.apps[0].id).to.eql(APP_ID);
expect(res.body.apps[0].installationState).to.be.ok();
done();
done(err);
});
});
it('can get appBySubdomain', function (done) {
superagent.get(SERVER_URL + '/api/v1/subdomains/' + APP_LOCATION)
request.get(SERVER_URL + '/api/v1/subdomains/' + APP_LOCATION)
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.id).to.eql(APP_ID);
expect(res.body.installationState).to.be.ok();
done();
done(err);
});
});
it('cannot get invalid app by Subdomain', function (done) {
superagent.get(SERVER_URL + '/api/v1/subdomains/tikaloma')
request.get(SERVER_URL + '/api/v1/subdomains/tikaloma')
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
done(err);
});
});
it('cannot uninstall invalid app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
request.post(SERVER_URL + '/api/v1/apps/whatever/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
done(err);
});
});
it('cannot uninstall app without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
done(err);
});
});
it('cannot uninstall app with wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD+PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('non admin cannot uninstall app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('can uninstall app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
done(err);
});
});
it('app install succeeds already purchased', function (done) {
var fake = nock(config.apiServerOrigin()).post('/api/v1/apps/test/purchase?token=APPSTORE_TOKEN').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION_2, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
@@ -510,7 +510,7 @@ describe('App API', function () {
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake.isDone()).to.be.ok();
done();
done(err);
});
});
@@ -520,7 +520,7 @@ describe('App API', function () {
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
@@ -531,7 +531,7 @@ describe('App API', function () {
// overwrite non dev token
token = result.body.token;
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
@@ -539,18 +539,18 @@ describe('App API', function () {
expect(res.body.id).to.be.a('string');
expect(fake.isDone()).to.be.ok();
APP_ID = res.body.id;
done();
done(err);
});
});
});
});
it('can uninstall app without password but developer token', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
done(err);
});
});
});
@@ -627,7 +627,7 @@ describe('App installation', function () {
var count = 0;
function checkInstallStatus() {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -638,7 +638,7 @@ describe('App installation', function () {
});
}
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: null, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
@@ -704,7 +704,7 @@ describe('App installation', function () {
it('installation - is up and running', function (done) {
expect(appResult.httpPort).to.be(undefined);
setTimeout(function () {
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
request.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
.end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
@@ -841,7 +841,7 @@ describe('App installation', function () {
});
xit('logs - stdout and stderr', function (done) {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID + '/logs')
.query({ access_token: token })
.end(function (err, res) {
var data = '';
@@ -855,7 +855,7 @@ describe('App installation', function () {
});
xit('logStream - requires event-stream accept header', function (done) {
superagent.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 })
.end(function (err, res) {
expect(res.statusCode).to.be(400);
@@ -894,7 +894,7 @@ describe('App installation', function () {
});
it('non admin cannot stop app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
@@ -903,7 +903,7 @@ describe('App installation', function () {
});
it('can stop app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
@@ -914,7 +914,7 @@ describe('App installation', function () {
it('did stop the app', function (done) {
// give the app a couple of seconds to die
setTimeout(function () {
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
request.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
.end(function (err, res) {
expect(err).to.be.ok();
done();
@@ -923,7 +923,7 @@ describe('App installation', function () {
});
it('nonadmin cannot start app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
.query({ access_token: token_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
@@ -932,7 +932,7 @@ describe('App installation', function () {
});
it('can start app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/start')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
@@ -942,7 +942,7 @@ describe('App installation', function () {
it('did start the app', function (done) {
setTimeout(function () {
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
request.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
.end(function (err, res) {
expect(!err).to.be.ok();
expect(res.statusCode).to.equal(200);
@@ -954,7 +954,7 @@ describe('App installation', function () {
it('can uninstall app', function (done) {
var count = 0;
function checkUninstallStatus() {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
.query({ access_token: token })
.end(function (err, res) {
if (res.statusCode === 404) return done(null);
@@ -963,7 +963,7 @@ describe('App installation', function () {
});
}
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
@@ -1063,8 +1063,6 @@ describe('App installation - port bindings', function () {
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
function (callback) {
awsHockInstance
.get('/2013-04-01/hostedzone')
@@ -1097,7 +1095,7 @@ describe('App installation - port bindings', function () {
var count = 0;
function checkInstallStatus() {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -1108,7 +1106,7 @@ describe('App installation - port bindings', function () {
});
}
superagent.post(SERVER_URL + '/api/v1/apps/install')
request.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appId: APP_ID, appStoreId: APP_STORE_ID, manifest: APP_MANIFEST, password: PASSWORD, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null, oauthProxy: false })
.end(function (err, res) {
@@ -1164,7 +1162,7 @@ describe('App installation - port bindings', function () {
var tryCount = 20;
expect(appResult.httpPort).to.be(undefined);
(function healthCheck() {
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
request.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
.end(function (err, res) {
if (err || res.statusCode !== 200) {
if (--tryCount === 0) return done(new Error('Timedout'));
@@ -1254,7 +1252,7 @@ describe('App installation - port bindings', function () {
assert.strictEqual(typeof count, 'number');
assert.strictEqual(typeof done, 'function');
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -1266,7 +1264,7 @@ describe('App installation - port bindings', function () {
}
it('cannot reconfigure app with missing location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true })
.end(function (err, res) {
@@ -1276,7 +1274,7 @@ describe('App installation - port bindings', function () {
});
it('cannot reconfigure app with missing accessRestriction', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
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 }, oauthProxy: false })
.end(function (err, res) {
@@ -1286,7 +1284,7 @@ describe('App installation - port bindings', function () {
});
it('cannot reconfigure app with missing oauthProxy', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null })
.end(function (err, res) {
@@ -1296,7 +1294,7 @@ describe('App installation - port bindings', function () {
});
it('cannot reconfigure app with only the cert, no key', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
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) {
@@ -1306,7 +1304,7 @@ describe('App installation - port bindings', function () {
});
it('cannot reconfigure app with only the key, no cert', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
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) {
@@ -1316,7 +1314,7 @@ describe('App installation - port bindings', function () {
});
it('cannot reconfigure app with cert not bein a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
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) {
@@ -1326,7 +1324,7 @@ describe('App installation - port bindings', function () {
});
it('cannot reconfigure app with key not bein a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
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) {
@@ -1336,7 +1334,7 @@ describe('App installation - port bindings', function () {
});
it('non admin cannot reconfigure app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
.query({ access_token: token_1 })
.send({ appId: APP_ID, password: PASSWORD, location: APP_LOCATION_NEW, portBindings: { ECHO_SERVER_PORT: 7172 }, accessRestriction: null, oauthProxy: true })
.end(function (err, res) {
@@ -1346,7 +1344,7 @@ describe('App installation - port bindings', function () {
});
it('can reconfigure app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
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 })
.end(function (err, res) {
@@ -1430,7 +1428,7 @@ describe('App installation - port bindings', function () {
});
it('can reconfigure app with custom certificate', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/configure')
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) {
@@ -1440,7 +1438,7 @@ describe('App installation - port bindings', function () {
});
it('can stop app', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/stop')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
@@ -1465,7 +1463,7 @@ describe('App installation - port bindings', function () {
it('can uninstall app', function (done) {
var count = 0;
function checkUninstallStatus() {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
request.get(SERVER_URL + '/api/v1/apps/' + APP_ID)
.query({ access_token: token })
.end(function (err, res) {
if (res.statusCode === 404) return done(null);
@@ -1474,7 +1472,7 @@ describe('App installation - port bindings', function () {
});
}
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
request.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
+16 -11
View File
@@ -11,7 +11,7 @@ var appdb = require('../../appdb.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
request = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
nock = require('nock'),
@@ -33,10 +33,11 @@ function setup(done) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
@@ -55,7 +56,7 @@ function setup(done) {
},
function createSettings(callback) {
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }, callback);
settings.setBackupConfig({ provider: 'caas', token: 'BACKUP_TOKEN' }, callback);
}
], done);
}
@@ -73,22 +74,22 @@ describe('Backups API', function () {
after(cleanup);
describe('get', function () {
it('cannot get backups with appstore superagent 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=BACKUP_TOKEN').reply(401, {});
superagent.get(SERVER_URL + '/api/v1/backups')
request.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(503);
expect(req.isDone()).to.be.ok();
done();
done(err);
});
});
it('can get backups', function (done) {
var req = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/backups?token=BACKUP_TOKEN').reply(200, { backups: ['foo', 'bar']});
superagent.get(SERVER_URL + '/api/v1/backups')
request.get(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -96,24 +97,26 @@ describe('Backups API', function () {
expect(res.body.backups).to.be.an(Array);
expect(res.body.backups[0]).to.eql('foo');
expect(res.body.backups[1]).to.eql('bar');
done();
done(err);
});
});
});
describe('create', function () {
it('fails due to mising token', function (done) {
superagent.post(SERVER_URL + '/api/v1/backups')
request.post(SERVER_URL + '/api/v1/backups')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.post(SERVER_URL + '/api/v1/backups')
request.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -124,9 +127,10 @@ describe('Backups API', function () {
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
superagent.post(SERVER_URL + '/api/v1/backups')
request.post(SERVER_URL + '/api/v1/backups')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
@@ -142,3 +146,4 @@ describe('Backups API', function () {
});
});
});
+41
View File
@@ -46,6 +46,7 @@ describe('OAuth Clients API', function () {
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
@@ -72,6 +73,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
@@ -87,6 +89,7 @@ describe('OAuth Clients API', function () {
superagent.post(SERVER_URL + '/api/v1/oauth/clients')
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -97,6 +100,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -107,6 +111,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -117,6 +122,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -127,6 +133,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -137,6 +144,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: 'someApp', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -147,6 +155,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: '', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -157,6 +166,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -167,6 +177,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: 'profile' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
expect(result.body.id).to.be.a('string');
expect(result.body.appId).to.be.a('string');
@@ -200,6 +211,7 @@ describe('OAuth Clients API', function () {
.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();
@@ -218,6 +230,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
CLIENT_0 = result.body;
@@ -239,6 +252,7 @@ describe('OAuth Clients API', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
@@ -253,6 +267,7 @@ describe('OAuth Clients API', function () {
it('fails without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -263,6 +278,7 @@ describe('OAuth Clients API', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
@@ -272,6 +288,7 @@ describe('OAuth Clients API', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body).to.eql(CLIENT_0);
done();
@@ -301,6 +318,7 @@ describe('OAuth Clients API', function () {
.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();
@@ -319,6 +337,7 @@ describe('OAuth Clients API', function () {
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
CLIENT_0 = result.body;
@@ -340,6 +359,7 @@ describe('OAuth Clients API', function () {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
@@ -354,6 +374,7 @@ describe('OAuth Clients API', function () {
it('fails without token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -364,6 +385,7 @@ describe('OAuth Clients API', function () {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
@@ -373,11 +395,13 @@ describe('OAuth Clients API', function () {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/oauth/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(404);
done();
@@ -419,6 +443,7 @@ describe('Clients', function () {
.query({ setupToken: 'somesetuptoken' })
.send({ username: USER_0.username, password: USER_0.password, email: USER_0.email })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
@@ -448,6 +473,7 @@ describe('Clients', function () {
it('fails due to missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -457,6 +483,7 @@ describe('Clients', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -466,6 +493,7 @@ describe('Clients', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -475,6 +503,7 @@ describe('Clients', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.clients.length).to.eql(1);
@@ -492,6 +521,7 @@ describe('Clients', function () {
it('fails due to missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -501,6 +531,7 @@ describe('Clients', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -510,6 +541,7 @@ describe('Clients', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -519,6 +551,7 @@ describe('Clients', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
@@ -528,6 +561,7 @@ describe('Clients', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
@@ -545,6 +579,7 @@ describe('Clients', function () {
it('fails due to missing token', function (done) {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -554,6 +589,7 @@ describe('Clients', function () {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -563,6 +599,7 @@ describe('Clients', function () {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -572,6 +609,7 @@ describe('Clients', function () {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/CID-WEBADMIN/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(404);
done();
});
@@ -581,6 +619,7 @@ describe('Clients', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
@@ -589,12 +628,14 @@ describe('Clients', function () {
superagent.del(SERVER_URL + '/api/v1/oauth/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(204);
// further calls with this token should not work
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
+75 -64
View File
@@ -11,8 +11,7 @@ var async = require('async'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
os = require('os'),
superagent = require('superagent'),
request = require('superagent'),
server = require('../../server.js'),
shell = require('../../shell.js');
@@ -55,34 +54,23 @@ describe('Cloudron', function () {
after(cleanup);
it('fails due to missing setupToken', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails due to internal server error on appstore side', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(500, { message: 'this is wrong' });
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(500);
expect(scope.isDone()).to.be.ok();
done();
});
});
it('fails due to empty username', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: '', password: 'somepassword', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
@@ -92,10 +80,11 @@ describe('Cloudron', function () {
it('fails due to empty password', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: '', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
@@ -105,10 +94,11 @@ describe('Cloudron', function () {
it('fails due to empty email', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
@@ -118,10 +108,11 @@ describe('Cloudron', function () {
it('fails due to empty name', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: '', email: 'admin@foo.bar', name: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
@@ -131,10 +122,11 @@ describe('Cloudron', function () {
it('fails due to invalid email', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: 'invalidemail' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
@@ -145,10 +137,11 @@ describe('Cloudron', function () {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar', name: 'tester' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
@@ -159,10 +152,11 @@ describe('Cloudron', function () {
it('fails the second time', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'somepassword', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(409);
expect(scope.isDone()).to.be.ok();
done();
@@ -181,10 +175,11 @@ describe('Cloudron', function () {
config._reset();
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
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();
@@ -201,17 +196,19 @@ describe('Cloudron', function () {
after(cleanup);
it('cannot get without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
request.get(SERVER_URL + '/api/v1/cloudron/config')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds without appstore', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
request.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
@@ -223,7 +220,7 @@ describe('Cloudron', function () {
expect(result.body.developerMode).to.be.a('boolean');
expect(result.body.size).to.eql(null);
expect(result.body.region).to.eql(null);
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.memory).to.eql(0);
expect(result.body.cloudronName).to.be.a('string');
done();
@@ -233,9 +230,10 @@ describe('Cloudron', function () {
it('succeeds', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/localhost?token=' + config.token()).reply(200, { box: { region: 'sfo', size: '1gb' }});
superagent.get(SERVER_URL + '/api/v1/cloudron/config')
request.get(SERVER_URL + '/api/v1/cloudron/config')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql(null);
@@ -247,7 +245,7 @@ describe('Cloudron', function () {
expect(result.body.developerMode).to.be.a('boolean');
expect(result.body.size).to.eql('1gb');
expect(result.body.region).to.eql('sfo');
expect(result.body.memory).to.eql(os.totalmem());
expect(result.body.memory).to.eql(1073741824);
expect(result.body.cloudronName).to.be.a('string');
expect(scope.isDone()).to.be.ok();
@@ -269,10 +267,11 @@ describe('Cloudron', function () {
config._reset();
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
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();
@@ -283,77 +282,72 @@ describe('Cloudron', function () {
callback();
});
},
function setupBackupConfig(callback) {
superagent.post(SERVER_URL + '/api/v1/settings/backup_config')
.send({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
callback();
});
}
], done);
});
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo'})
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with missing size', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong size type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 4, region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with missing region', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with wrong region type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 4, password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -361,7 +355,7 @@ describe('Cloudron', function () {
it('fails when in wrong state', function (done) {
var scope2 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
var scope3 = nock(config.apiServerOrigin())
@@ -377,10 +371,11 @@ describe('Cloudron', function () {
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
@@ -396,6 +391,7 @@ describe('Cloudron', function () {
});
});
it('succeeds', function (done) {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/migrate?token=APPSTORE_TOKEN', function (body) {
return body.size && body.region && body.restoreKey;
@@ -408,15 +404,16 @@ describe('Cloudron', function () {
.reply(200, { id: 'someid' });
var scope3 = nock(config.apiServerOrigin())
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=APPSTORE_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey', SessionToken: 'sessionToken' } });
injectShellMock();
superagent.post(SERVER_URL + '/api/v1/cloudron/migrate')
request.post(SERVER_URL + '/api/v1/cloudron/migrate')
.send({ size: 'small', region: 'sfo', password: PASSWORD })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
@@ -444,10 +441,11 @@ describe('Cloudron', function () {
config._reset();
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
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();
@@ -464,112 +462,125 @@ describe('Cloudron', function () {
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: '', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unknown type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with ticket type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
done();
});
});
it('succeeds with app type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'app', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
done();
});
});
it('fails without description', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: '', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty description', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', subject: 'some subject', description: '' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with feedback type', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'feedback', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
done();
});
});
it('fails without subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/feedback')
request.post(SERVER_URL + '/api/v1/cloudron/feedback')
.send({ type: 'ticket', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
});
});
+51 -26
View File
@@ -11,7 +11,7 @@ var async = require('async'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
superagent = require('superagent'),
request = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js');
@@ -43,10 +43,11 @@ describe('Developer API', function () {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
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();
@@ -66,8 +67,9 @@ describe('Developer API', function () {
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
superagent.get(SERVER_URL + '/api/v1/developer')
request.get(SERVER_URL + '/api/v1/developer')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -78,9 +80,10 @@ describe('Developer API', function () {
settings.setDeveloperMode(true, function (error) {
expect(error).to.be(null);
superagent.get(SERVER_URL + '/api/v1/developer')
request.get(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
done();
});
@@ -91,9 +94,10 @@ describe('Developer API', function () {
settings.setDeveloperMode(false, function (error) {
expect(error).to.be(null);
superagent.get(SERVER_URL + '/api/v1/developer')
request.get(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
@@ -110,10 +114,11 @@ describe('Developer API', function () {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
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();
@@ -130,74 +135,82 @@ describe('Developer API', function () {
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer')
request.post(SERVER_URL + '/api/v1/developer')
.send({ enabled: true })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to missing password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer')
request.post(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.send({ enabled: true })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails due to empty password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer')
request.post(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.send({ password: '', enabled: true })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(403);
done();
});
});
it('fails due to wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer')
request.post(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.send({ password: PASSWORD.toUpperCase(), enabled: true })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(403);
done();
});
});
it('fails due to missing enabled property', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer')
request.post(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails due to wrong enabled property type', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer')
request.post(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.send({ password: PASSWORD, enabled: 'true' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds enabling', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer')
request.post(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.send({ password: PASSWORD, enabled: true })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
superagent.get(SERVER_URL + '/api/v1/developer')
request.get(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
done();
});
@@ -205,15 +218,17 @@ describe('Developer API', function () {
});
it('succeeds disabling', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer')
request.post(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.send({ password: PASSWORD, enabled: false })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
superagent.get(SERVER_URL + '/api/v1/developer')
request.get(SERVER_URL + '/api/v1/developer')
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(412);
done();
});
@@ -232,10 +247,11 @@ describe('Developer API', function () {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
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();
@@ -252,71 +268,79 @@ describe('Developer API', function () {
after(cleanup);
it('fails without body', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with empty username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: '', password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with empty password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD.toUpperCase() })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
});
it('with username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(result.body.token).to.be.a('string');
@@ -325,9 +349,10 @@ describe('Developer API', function () {
});
it('with email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
request.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.expiresAt).to.be.a('number');
expect(result.body.token).to.be.a('string');
+23
View File
@@ -319,6 +319,7 @@ describe('OAuth2', function () {
it('fails due to missing redirect_uri param', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(result.text.indexOf('Invalid request. redirect_uri query param is not set.')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
@@ -329,6 +330,7 @@ describe('OAuth2', function () {
it('fails due to missing client_id param', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(result.text.indexOf('Invalid request. client_id query param is not set.')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
@@ -339,6 +341,7 @@ describe('OAuth2', function () {
it('fails due to missing response_type param', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&client_id=someclientid')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(result.text.indexOf('Invalid request. response_type query param is not set.')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
@@ -349,6 +352,7 @@ describe('OAuth2', function () {
it('fails for unkown grant type', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&client_id=someclientid&response_type=foobar')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(result.text.indexOf('Invalid request. Only token and code response types are supported.')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
@@ -359,6 +363,7 @@ describe('OAuth2', function () {
it('succeeds for grant type code', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&client_id=someclientid&response_type=code')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=http://someredirect";</script>');
expect(result.statusCode).to.equal(200);
done();
@@ -368,6 +373,7 @@ describe('OAuth2', function () {
it('succeeds for grant type token', function (done) {
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&client_id=someclientid&response_type=token')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=http://someredirect";</script>');
expect(result.statusCode).to.equal(200);
done();
@@ -382,6 +388,7 @@ describe('OAuth2', function () {
it('fails without prior authentication call and not returnTo query', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/login')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(result.text.indexOf('Invalid login request. No returnTo provided.')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
@@ -394,6 +401,7 @@ describe('OAuth2', function () {
superagent.get(SERVER_URL + '/api/v1/session/login?returnTo=http://someredirect')
.redirects(0)
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(302);
expect(result.headers.location).to.eql('http://someredirect');
@@ -405,6 +413,7 @@ describe('OAuth2', function () {
superagent.get(SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=http://someredirect&response_type=code')
.redirects(0)
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(result.text.indexOf('Invalid request. client_id query param is not set.')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
@@ -1280,6 +1289,7 @@ describe('Password', function () {
it('reset request succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/resetRequest.html')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
done();
@@ -1289,6 +1299,7 @@ describe('Password', function () {
it('setup fails due to missing reset_token', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -1298,6 +1309,7 @@ describe('Password', function () {
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
.query({ reset_token: hat(256) })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -1307,6 +1319,7 @@ describe('Password', function () {
superagent.get(SERVER_URL + '/api/v1/session/password/setup.html')
.query({ reset_token: USER_0.resetToken })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
done();
@@ -1316,6 +1329,7 @@ describe('Password', function () {
it('reset fails due to missing reset_token', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -1325,6 +1339,7 @@ describe('Password', function () {
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
.query({ reset_token: hat(256) })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -1334,6 +1349,7 @@ describe('Password', function () {
superagent.get(SERVER_URL + '/api/v1/session/password/reset.html')
.query({ reset_token: USER_0.resetToken })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
done();
@@ -1343,6 +1359,7 @@ describe('Password', function () {
it('sent succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/session/password/sent.html')
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
done();
@@ -1358,6 +1375,7 @@ describe('Password', function () {
superagent.post(SERVER_URL + '/api/v1/session/password/resetRequest')
.send({ identifier: USER_0.email })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.text.indexOf('<!-- tester -->')).to.not.equal(-1);
expect(result.statusCode).to.equal(200);
done();
@@ -1373,6 +1391,7 @@ describe('Password', function () {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: 'somepassword' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -1382,6 +1401,7 @@ describe('Password', function () {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ resetToken: hat(256) })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
@@ -1391,6 +1411,7 @@ describe('Password', function () {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: '', resetToken: hat(256) })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -1400,6 +1421,7 @@ describe('Password', function () {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: '', resetToken: '' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -1417,6 +1439,7 @@ describe('Password', function () {
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: 'somepassword', resetToken: USER_0.resetToken })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(scope.isDone()).to.be.ok();
expect(result.statusCode).to.equal(200);
done();
+41 -33
View File
@@ -13,7 +13,7 @@ var appdb = require('../../appdb.js'),
expect = require('expect.js'),
path = require('path'),
paths = require('../../paths.js'),
superagent = require('superagent'),
request = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js'),
fs = require('fs'),
@@ -38,10 +38,11 @@ function setup(done) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
@@ -77,17 +78,17 @@ describe('Settings API', function () {
describe('autoupdate_pattern', function () {
it('can get auto update pattern (default)', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
request.get(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.pattern).to.be.ok();
done();
done(err);
});
});
it('cannot set autoupdate_pattern without pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -101,7 +102,7 @@ describe('Settings API', function () {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '00 30 11 * * 1-5' })
.end(function (err, res) {
@@ -117,7 +118,7 @@ describe('Settings API', function () {
eventPattern = pattern;
});
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: 'never' })
.end(function (err, res) {
@@ -128,7 +129,7 @@ describe('Settings API', function () {
});
it('cannot set invalid autoupdate_pattern', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
request.post(SERVER_URL + '/api/v1/settings/autoupdate_pattern')
.query({ access_token: token })
.send({ pattern: '1 3 x 5 6' })
.end(function (err, res) {
@@ -142,17 +143,17 @@ describe('Settings API', function () {
var name = 'foobar';
it('get default succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_name')
request.get(SERVER_URL + '/api/v1/settings/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.name).to.be.ok();
done();
done(err);
});
});
it('cannot set without name', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -161,7 +162,7 @@ describe('Settings API', function () {
});
it('cannot set empty name', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
.query({ access_token: token })
.send({ name: '' })
.end(function (err, res) {
@@ -171,7 +172,7 @@ describe('Settings API', function () {
});
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_name')
request.post(SERVER_URL + '/api/v1/settings/cloudron_name')
.query({ access_token: token })
.send({ name: name })
.end(function (err, res) {
@@ -181,29 +182,29 @@ describe('Settings API', function () {
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_name')
request.get(SERVER_URL + '/api/v1/settings/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.name).to.eql(name);
done();
done(err);
});
});
});
describe('cloudron_avatar', function () {
it('get default succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
request.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.a(Buffer);
done();
done(err);
});
});
it('cannot set without data', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -212,7 +213,7 @@ describe('Settings API', function () {
});
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
request.post(SERVER_URL + '/api/v1/settings/cloudron_avatar')
.query({ access_token: token })
.attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
.end(function (err, res) {
@@ -222,7 +223,7 @@ describe('Settings API', function () {
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
request.get(SERVER_URL + '/api/v1/settings/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -234,17 +235,17 @@ describe('Settings API', function () {
describe('dns_config', function () {
it('get dns_config fails', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/dns_config')
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();
done(err);
});
});
it('cannot set without data', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/dns_config')
request.post(SERVER_URL + '/api/v1/settings/dns_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
@@ -253,7 +254,7 @@ describe('Settings API', function () {
});
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/settings/dns_config')
request.post(SERVER_URL + '/api/v1/settings/dns_config')
.query({ access_token: token })
.send({ provider: 'route53', accessKeyId: 'accessKey', secretAccessKey: 'secretAccessKey' })
.end(function (err, res) {
@@ -263,12 +264,12 @@ describe('Settings API', function () {
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/settings/dns_config')
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();
done(err);
});
});
});
@@ -283,68 +284,75 @@ describe('Settings API', function () {
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) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
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) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
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) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
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) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
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) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
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) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
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) {
superagent.post(SERVER_URL + '/api/v1/settings/certificate')
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();
});
+34 -21
View File
@@ -12,7 +12,7 @@ var clientdb = require('../../clientdb.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
request = require('superagent'),
server = require('../../server.js'),
simpleauth = require('../../simpleauth.js'),
nock = require('nock');
@@ -109,7 +109,7 @@ describe('SimpleAuth API', function () {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
@@ -146,9 +146,10 @@ describe('SimpleAuth API', function () {
it('cannot login without clientId', function (done) {
var body = {};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
@@ -159,9 +160,10 @@ describe('SimpleAuth API', function () {
clientId: 'someclientid'
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
@@ -173,9 +175,10 @@ describe('SimpleAuth API', function () {
username: USERNAME
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
@@ -188,9 +191,10 @@ describe('SimpleAuth API', function () {
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
@@ -203,9 +207,10 @@ describe('SimpleAuth API', function () {
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
@@ -218,9 +223,10 @@ describe('SimpleAuth API', function () {
password: ''
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
@@ -233,9 +239,10 @@ describe('SimpleAuth API', function () {
password: PASSWORD+PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
@@ -248,9 +255,10 @@ describe('SimpleAuth API', function () {
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
@@ -263,9 +271,10 @@ describe('SimpleAuth API', function () {
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
@@ -278,7 +287,7 @@ describe('SimpleAuth API', function () {
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
@@ -290,7 +299,7 @@ describe('SimpleAuth API', function () {
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
request.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
@@ -309,7 +318,7 @@ describe('SimpleAuth API', function () {
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
@@ -321,7 +330,7 @@ describe('SimpleAuth API', function () {
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
request.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
@@ -340,9 +349,10 @@ describe('SimpleAuth API', function () {
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
@@ -359,7 +369,7 @@ describe('SimpleAuth API', function () {
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
request.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
@@ -372,32 +382,35 @@ describe('SimpleAuth API', function () {
});
it('fails without access_token', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unkonwn access_token', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken+accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
request.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
superagent.get(SERVER_URL + '/api/v1/profile')
request.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(401);
done();
+82 -76
View File
@@ -10,7 +10,7 @@ var config = require('../../config.js'),
database = require('../../database.js'),
tokendb = require('../../tokendb.js'),
expect = require('expect.js'),
superagent = require('superagent'),
request = require('superagent'),
nock = require('nock'),
server = require('../../server.js'),
userdb = require('../../userdb.js');
@@ -50,7 +50,7 @@ describe('User API', function () {
after(cleanup);
it('device is in first time mode', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
request.get(SERVER_URL + '/api/v1/cloudron/status')
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.activated).to.not.be.ok();
@@ -61,21 +61,21 @@ describe('User API', function () {
it('create admin fails due to missing parameters', function (done) {
var scope = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME_0 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(scope.isDone()).to.be.ok();
done();
done(err);
});
});
it('create admin fails because only POST is allowed', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/activate')
request.get(SERVER_URL + '/api/v1/cloudron/activate')
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
done(err);
});
});
@@ -83,7 +83,7 @@ describe('User API', function () {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
request.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME_0, password: PASSWORD, email: EMAIL })
.end(function (err, res) {
@@ -99,16 +99,16 @@ describe('User API', function () {
});
it('device left first time mode', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
request.get(SERVER_URL + '/api/v1/cloudron/status')
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.activated).to.be.ok();
done();
done(err);
});
});
it('can get userInfo with token', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -119,7 +119,7 @@ describe('User API', function () {
// stash for further use
user_0 = res.body;
done();
done(err);
});
});
@@ -131,9 +131,10 @@ describe('User API', function () {
expect(error).to.not.be.ok();
setTimeout(function () {
superagent.get(SERVER_URL + '/api/v1/users/' + user_0.username)
request.get(SERVER_URL + '/api/v1/users/' + user_0.username)
.query({ access_token: token })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(401);
done();
});
@@ -142,46 +143,46 @@ describe('User API', function () {
});
it('can get userInfo with token', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.username).to.equal(USERNAME_0);
expect(res.body.email).to.equal(EMAIL);
expect(res.body.admin).to.be.ok();
done();
done(err);
});
});
it('cannot get userInfo only with basic auth', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.auth(USERNAME_0, PASSWORD)
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
done(err);
});
});
it('cannot get userInfo with invalid token (token length)', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: 'x' + token })
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
done(err);
});
});
it('cannot get userInfo with invalid token (wrong token)', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token.toUpperCase() })
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
done(err);
});
});
it('can get userInfo with token in auth header', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.set('Authorization', 'Bearer ' + token)
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -190,30 +191,30 @@ describe('User API', function () {
expect(res.body.admin).to.be.ok();
expect(res.body.password).to.not.be.ok();
expect(res.body.salt).to.not.be.ok();
done();
done(err);
});
});
it('cannot get userInfo with invalid token in auth header', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.set('Authorization', 'Bearer ' + 'x' + token)
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
done(err);
});
});
it('cannot get userInfo with invalid token (wrong token)', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.set('Authorization', 'Bearer ' + 'x' + token.toUpperCase())
.end(function (err, res) {
expect(res.statusCode).to.equal(401);
done();
done(err);
});
});
it('create second user succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
request.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1 })
.end(function (err, res) {
@@ -227,86 +228,90 @@ describe('User API', function () {
it('set second user as admin succeeds', function (done) {
// TODO is USERNAME_1 in body and url redundant?
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
.query({ access_token: token })
.send({ username: USERNAME_1, admin: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
done(err);
});
});
it('remove first user from admins succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_0, admin: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
done(err);
});
});
it('remove second user by first, now normal, user fails', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_1)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('remove second user from admins and thus last admin fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_1 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_1, admin: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('reset first user as admin succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/admin')
.query({ access_token: token_1 })
.send({ username: USERNAME_0, admin: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
done(err);
});
});
it('create user missing username fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
request.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ email: EMAIL_2 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('create user missing email fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
request.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(400);
done();
});
});
it('create second and third user', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
request.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2 })
.end(function (error, res) {
expect(error).to.not.be.ok();
expect(res.statusCode).to.equal(201);
superagent.post(SERVER_URL + '/api/v1/users')
request.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_3, email: EMAIL_3 })
.end(function (error, res) {
expect(error).to.not.be.ok();
expect(res.statusCode).to.equal(201);
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
@@ -316,9 +321,10 @@ describe('User API', function () {
});
it('second user userInfo', function (done) {
superagent.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
request.get(SERVER_URL + '/api/v1/users/' + USERNAME_2)
.query({ access_token: token_1 })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.username).to.equal(USERNAME_2);
expect(result.body.email).to.equal(EMAIL_2);
@@ -329,17 +335,17 @@ describe('User API', function () {
});
it('create user with same username should fail', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
request.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
done(err);
});
});
it('list users', function (done) {
superagent.get(SERVER_URL + '/api/v1/users')
request.get(SERVER_URL + '/api/v1/users')
.query({ access_token: token_2 })
.end(function (error, res) {
expect(error).to.be(null);
@@ -361,106 +367,106 @@ describe('User API', function () {
});
it('user removes himself is not allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('admin cannot remove normal user without giving a password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
done(err);
});
});
it('admin cannot remove normal user with empty password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
.query({ access_token: token })
.send({ password: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('admin cannot remove normal user with giving wrong password', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('admin removes normal user', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_3)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
done(err);
});
});
it('admin removes himself should not be allowed', function (done) {
superagent.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.del(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
// Change email
it('change email fails due to missing token', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.send({ password: PASSWORD, email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
done(error);
});
});
it('change email fails due to missing password', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
done(error);
});
});
it('change email fails due to wrong password', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD+PASSWORD, email: EMAIL_0_NEW })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
done(error);
});
});
it('change email fails due to invalid email', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD, email: 'foo@bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
done(error);
});
});
it('change email succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
request.put(SERVER_URL + '/api/v1/users/' + USERNAME_0)
.query({ access_token: token })
.send({ password: PASSWORD, email: EMAIL_0_NEW })
.end(function (error, result) {
@@ -471,52 +477,52 @@ describe('User API', function () {
// Change password
it('change password fails due to missing current password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ newPassword: 'some wrong password' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
done(err);
});
});
it('change password fails due to missing new password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
done(err);
});
});
it('change password fails due to wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: 'some wrong password', newPassword: 'newpassword' })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
done(err);
});
});
it('change password fails due to invalid password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: PASSWORD, newPassword: 'five' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
done(err);
});
});
it('change password succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
request.post(SERVER_URL + '/api/v1/users/' + USERNAME_0 + '/password')
.query({ access_token: token })
.send({ password: PASSWORD, newPassword: 'new_password' })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
done(err);
});
});
});
+48 -23
View File
@@ -12,12 +12,29 @@ var appdb = require('./appdb.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 }
var gState = { };
// 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');
@@ -29,18 +46,17 @@ function sync(callback) {
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);
if (removedAppIds.length !== 0) debug('sync: stopping jobs of removed apps %j', removedAppIds);
async.eachSeries(removedAppIds, function (appId, iteratorDone) {
stopJobs(appId, gState[appId], iteratorDone);
stopJobs(appId, gState[appId], true /* killContainers */, iteratorDone);
}, function (error) {
if (error) debug('Error stopping jobs of removed apps', error);
if (error) debug('Error stopping jobs : %j', error);
gState = _.omit(gState, removedAppIds);
debug('sync: checking apps %j', allAppIds);
// start tasks of new apps
async.eachSeries(allApps, function (app, iteratorDone) {
var appState = gState[app.id] || null;
var schedulerConfig = app.manifest.addons.scheduler || null;
@@ -51,8 +67,8 @@ function sync(callback) {
return iteratorDone(); // nothing changed
}
debug('sync: app %s changed', app.id);
stopJobs(app.id, appState, function (error) {
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) {
@@ -62,9 +78,12 @@ function sync(callback) {
gState[app.id] = {
schedulerConfig: schedulerConfig,
cronJobs: createCronJobs(app.id, schedulerConfig)
cronJobs: createCronJobs(app.id, schedulerConfig),
containerIds: { }
};
saveState(gState);
iteratorDone();
});
});
@@ -74,22 +93,23 @@ function sync(callback) {
});
}
function killContainer(containerName, callback) {
if (!containerName) return callback();
function killContainer(containerId, callback) {
if (!containerId) return callback();
async.series([
docker.stopContainerByName.bind(null, containerName),
docker.deleteContainerByName.bind(null, containerName)
docker.stopContainer.bind(null, containerId),
docker.deleteContainer.bind(null, containerId)
], function (error) {
if (error) debug('Failed to kill task with name %s : %s', containerName, error.message);
if (error) debug('Failed to kill task with containerId %s : %s', containerId, error.message);
callback(error);
});
}
function stopJobs(appId, appState, callback) {
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);
@@ -101,8 +121,9 @@ function stopJobs(appId, appState, callback) {
appState.cronJobs[taskName].stop();
}
var containerName = appId + '-' + taskName;
killContainer(containerName, iteratorDone);
if (!killContainers) return iteratorDone();
killContainer(appState.containerIds[taskName], iteratorDone);
}, callback);
}
@@ -140,6 +161,8 @@ function doTask(appId, taskName, callback) {
callback = callback || NOOP_CALLBACK;
var appState = gState[appId];
debug('Executing task %s/%s', appId, taskName);
apps.get(appId, function (error, app) {
@@ -150,16 +173,18 @@ function doTask(appId, taskName, callback) {
return callback();
}
var containerName = app.id + '-' + taskName;
if (appState.containerIds[taskName]) debug('task %s/%s has existing container %s. killing it', appId, taskName, appState.containerIds[taskName]);
killContainer(containerName, function (error) {
killContainer(appState.containerIds[taskName], function (error) {
if (error) return callback(error);
debug('Creating subcontainer for %s/%s : %s', app.id, taskName, gState[appId].schedulerConfig[taskName].command);
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, containerName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], { } /* options */, function (error, container) {
if (error) return callback(error);
docker.createSubcontainer(app, app.id + '-' + taskName, [ '/bin/sh', '-c', gState[appId].schedulerConfig[taskName].command ], function (error, container) {
appState.containerIds[taskName] = container.id;
saveState(gState);
docker.startContainer(container.id, callback);
});
+1 -1
View File
@@ -42,7 +42,7 @@ for try in `seq 1 5`; do
if tar -cvzf - -C "${app_data_snapshot}" . \
| openssl aes-256-cbc -e -pass "pass:${backup_key}" \
| curl --fail -X PUT ${headers[@]} --data-binary @- "${backup_url}" 2>"${error_log}"; then
| curl --fail -X PUT "${headers[@]}" --data-binary @- "${backup_url}" 2>"${error_log}"; then
break
fi
cat "${error_log}" && rm "${error_log}"
+2 -2
View File
@@ -21,7 +21,7 @@ readonly program_name=$1
echo "${program_name}.log"
echo "-------------------"
journalctl --all --no-pager -u ${program_name} -n 100
journalctl --no-pager -u ${program_name} -n 100
echo
echo
echo "dmesg"
@@ -31,7 +31,7 @@ echo
echo
echo "docker"
echo "------"
journalctl --all --no-pager -u docker -n 50
journalctl --no-pager -u docker -n 50
echo
echo
+1 -4
View File
@@ -10,7 +10,6 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
auth = require('./auth.js'),
certificates = require('./certificates.js'),
cloudron = require('./cloudron.js'),
cron = require('./cron.js'),
config = require('./config.js'),
@@ -21,6 +20,7 @@ var assert = require('assert'),
middleware = require('./middleware'),
passport = require('passport'),
path = require('path'),
paths = require('./paths.js'),
routes = require('./routes/index.js'),
taskmanager = require('./taskmanager.js');
@@ -161,8 +161,6 @@ function initializeExpressSync() {
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);
@@ -236,7 +234,6 @@ function start(callback) {
auth.initialize,
database.initialize,
cloudron.initialize, // keep this here because it reads activation state that others depend on
certificates.installAdminCertificate, // keep this before cron to block heartbeats until cert is ready
taskmanager.initialize,
mailer.initialize,
cron.initialize,
+114 -33
View File
@@ -26,31 +26,37 @@ exports = module.exports = {
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getTlsConfig: getTlsConfig,
setTlsConfig: setTlsConfig,
getDefaultSync: getDefaultSync,
getAll: getAll,
validateCertificate: validateCertificate,
setCertificate: setCertificate,
setAdminCertificate: setAdminCertificate,
AUTOUPDATE_PATTERN_KEY: 'autoupdate_pattern',
TIME_ZONE_KEY: 'time_zone',
CLOUDRON_NAME_KEY: 'cloudron_name',
DEVELOPER_MODE_KEY: 'developer_mode',
DNS_CONFIG_KEY: 'dns_config',
BACKUP_CONFIG_KEY: 'backup_config',
TLS_CONFIG_KEY: 'tls_config',
events: new (require('events').EventEmitter)()
};
var assert = require('assert'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
shell = require('./shell.js'),
util = require('util'),
x509 = require('x509'),
_ = require('underscore');
var gDefaults = (function () {
@@ -61,11 +67,13 @@ var gDefaults = (function () {
result[exports.DEVELOPER_MODE_KEY] = false;
result[exports.DNS_CONFIG_KEY] = { };
result[exports.BACKUP_CONFIG_KEY] = { };
result[exports.TLS_CONFIG_KEY] = { provider: 'caas' };
return result;
})();
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
if (config.TEST) {
// avoid noisy warnings during npm test
exports.events.setMaxListeners(100);
@@ -93,6 +101,7 @@ util.inherits(SettingsError, Error);
SettingsError.INTERNAL_ERROR = 'Internal Error';
SettingsError.NOT_FOUND = 'Not Found';
SettingsError.BAD_FIELD = 'Bad Field';
SettingsError.INVALID_CERT = 'Invalid certificate';
function setAutoupdatePattern(pattern, callback) {
assert.strictEqual(typeof pattern, 'string');
@@ -267,34 +276,6 @@ function setDnsConfig(dnsConfig, callback) {
});
}
function getTlsConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.TLS_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.TLS_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value)); // provider
});
}
function setTlsConfig(tlsConfig, callback) {
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('le-') !== 0) {
return callback(new SettingsError(SettingsError.BAD_FIELD, 'provider must be caas or le-*'));
}
settingsdb.set(exports.TLS_CONFIG_KEY, JSON.stringify(tlsConfig), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.TLS_CONFIG_KEY, tlsConfig);
callback(null);
});
}
function getBackupConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -341,3 +322,103 @@ function getAll(callback) {
callback(null, result);
});
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
function validateCertificate(cert, key, fqdn) {
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof fqdn, 'string');
if (cert === null && key === null) return null;
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
var content;
try {
content = x509.parseCert(cert);
} catch (e) {
return new Error('invalid cert: ' + e.message);
}
// check expiration
if (content.notAfter < new Date()) return new Error('cert expired');
function matchesDomain(domain) {
if (domain === fqdn) return true;
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
return false;
}
// check domain
var domains = content.altNames.concat(content.subject.commonName);
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (certModulus !== keyModulus) return new Error('key does not match the cert');
return null;
}
function setCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateCertificate(cert, key, '*.' + config.fqdn());
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) {
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
}
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) {
return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
}
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
return callback(null);
});
}
function setAdminCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'admin';
var vhost = config.appFqdn(constants.ADMIN_LOCATION);
var certFilePath = path.join(paths.APP_CERTS_DIR, 'admin.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'admin.key');
var error = validateCertificate(cert, key, vhost);
if (error) return callback(new SettingsError(SettingsError.INVALID_CERT, error.message));
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, safe.error.message));
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf');
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
shell.sudo('setAdminCertificate', [ RELOAD_NGINX_CMD ], function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
return callback(null);
});
}
+3 -8
View File
@@ -24,7 +24,7 @@ function getBackupCredentials(backupConfig, callback) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/awscredentials';
superagent.post(url).query({ token: backupConfig.token }).end(function (error, result) {
if (error && !error.response) return callback(error);
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'));
@@ -49,10 +49,11 @@ function getAllPaged(backupConfig, page, perPage, callback) {
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups';
superagent.get(url).query({ token: backupConfig.token }).end(function (error, result) {
if (error && !error.response) return callback(error);
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);
});
}
@@ -62,8 +63,6 @@ function getSignedUploadUrl(backupConfig, filename, callback) {
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);
@@ -86,8 +85,6 @@ function getSignedDownloadUrl(backupConfig, filename, callback) {
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);
@@ -111,8 +108,6 @@ function copyObject(backupConfig, from, to, callback) {
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);
+1 -26
View File
@@ -36,32 +36,7 @@ function getAllPaged(backupConfig, page, perPage, callback) {
assert.strictEqual(typeof perPage, 'number');
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,
EncodingType: 'url',
Prefix: backupConfig.prefix + '/backup_'
};
s3.listObjects(params, function (error, data) {
if (error) return callback(error);
var results = data.Contents.map(function (backup) {
return {
creationTime: backup.LastModified,
restoreKey: backup.Key.slice(backupConfig.prefix.length + 1)
};
});
results.sort(function (a, b) { return a.creationTime < b.creationTime; });
return callback(null, results);
});
});
return callback(new Error('Not implemented yet'));
}
function getSignedUploadUrl(backupConfig, filename, callback) {
+14 -54
View File
@@ -1,68 +1,28 @@
/* jslint node:true */
'use strict';
var assert = require('assert'),
caas = require('./sysinfo/caas.js'),
config = require('./config.js'),
ec2 = require('./sysinfo/ec2.js'),
util = require('util');
exports = module.exports = {
SysInfoError: SysInfoError,
getIp: getIp
};
var os = require('os');
var gCachedIp = null;
function SysInfoError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
function getIp() {
if (gCachedIp) return gCachedIp;
Error.call(this);
Error.captureStackTrace(this, this.constructor);
var ifaces = os.networkInterfaces();
for (var dev in ifaces) {
if (dev.match(/^(en|eth|wlp).*/) === null) continue;
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;
for (var i = 0; i < ifaces[dev].length; i++) {
if (ifaces[dev][i].family === 'IPv4') {
gCachedIp = ifaces[dev][i].address;
return gCachedIp;
}
}
}
}
util.inherits(SysInfoError, Error);
SysInfoError.INTERNAL_ERROR = 'Internal Error';
function getApi(callback) {
assert.strictEqual(typeof callback, 'function');
switch (config.provider()) {
case '': return callback(null, caas); // current fallback for caas
case 'caas': return callback(null, caas);
case 'digitalocean': return callback(null, caas);
case 'ec2': return callback(null, ec2);
default: return callback(new Error('Unkown provider ' + config.provider()));
}
return null;
}
function getIp(callback) {
assert.strictEqual(typeof callback, 'function');
if (gCachedIp) return callback(null, gCachedIp);
getApi(function (error, api) {
if (error) return callback(error);
api.getIp(function (error, ip) {
if (error) return callback(error);
gCachedIp = ip;
callback(null, gCachedIp);
});
});
}
-26
View File
@@ -1,26 +0,0 @@
'use strict';
exports = module.exports = {
getIp: getIp
};
var assert = require('assert'),
os = require('os'),
SysInfoError = require('../sysinfo.js').SysInfoError;
function getIp(callback) {
assert.strictEqual(typeof callback, 'function');
var ifaces = os.networkInterfaces();
for (var dev in ifaces) {
if (dev.match(/^(en|eth|wlp).*/) === null) continue;
for (var i = 0; i < ifaces[dev].length; i++) {
if (ifaces[dev][i].family === 'IPv4') {
return callback(null, ifaces[dev][i].address);
}
}
}
callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'No IP found'));
}
-21
View File
@@ -1,21 +0,0 @@
'use strict';
exports = module.exports = {
getIp: getIp
};
var assert = require('assert'),
superagent = require('superagent'),
SysInfoError = require('../sysinfo.js').SysInfoError,
util = require('util');
function getIp(callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get('http://169.254.169.254/latest/meta-data/public-ipv4').end(function (error, result) {
if (error) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, error.status ? 'Request failed: ' + error.status : 'Network failure'));
if (result.statusCode !== 200) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
callback(null, result.text);
});
}
+17 -31
View File
@@ -9,7 +9,6 @@ exports = module.exports = {
var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
child_process = require('child_process'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:taskmanager'),
@@ -40,11 +39,14 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
gPendingTasks = [ ]; // clear this first, otherwise stopAppTask will resume them
for (var appId in gActiveTasks) {
stopAppTask(appId);
}
cloudron.events.removeListener(cloudron.EVENT_CONFIGURED, resumeTasks);
locker.removeListener('unlocked', startNextTask);
async.eachSeries(Object.keys(gActiveTasks), stopAppTask, callback);
callback(null);
}
@@ -93,47 +95,31 @@ function startAppTask(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, signal) {
debug('Task for %s pid %s completed with status %s', appId, pid, code);
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
debug('Apptask crashed with code %s and signal %s', code, signal);
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
gActiveTasks[appId].once('exit', function (code) {
debug('Task for %s completed with status %s', appId, code);
if (code && code !== 50) { // apptask crashed
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code }, NOOP_CALLBACK);
}
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
});
}
function stopAppTask(appId, callback) {
function stopAppTask(appId) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
if (gActiveTasks[appId]) {
debug('stopAppTask : Killing existing task of %s with pid %s', appId, gActiveTasks[appId].pid);
gActiveTasks[appId].once('exit', function () { callback(); });
debug('stopAppTask : Killing existing task of %s with pid %s: ', appId, gActiveTasks[appId].pid);
gActiveTasks[appId].kill(); // this will end up calling the 'exit' handler
return;
}
if (gPendingTasks.indexOf(appId) !== -1) {
debug('stopAppTask: Removing pending task : %s', appId);
delete gActiveTasks[appId];
} else if (gPendingTasks.indexOf(appId) !== -1) {
debug('stopAppTask: Removing existing pending task : %s', appId);
gPendingTasks = _.without(gPendingTasks, appId);
} else {
debug('stopAppTask: no task for %s to be stopped', appId);
}
callback();
}
function restartAppTask(appId, callback) {
callback = callback || NOOP_CALLBACK;
async.series([
stopAppTask.bind(null, appId),
startAppTask.bind(null, appId)
], callback);
function restartAppTask(appId) {
stopAppTask(appId);
startAppTask(appId);
}
+7 -8
View File
@@ -85,8 +85,7 @@ describe('apptask', function () {
async.series([
database.initialize,
appdb.add.bind(null, APP.id, APP.appStoreId, APP.manifest, APP.location, APP.portBindings, APP.accessRestriction, APP.oauthProxy),
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }),
settings.setTlsConfig.bind(null, { provider: 'caas' })
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' })
], done);
});
@@ -98,12 +97,12 @@ describe('apptask', function () {
apptask.initialize(done);
});
it('reserve port', function (done) {
apptask._reserveHttpPort(APP, function (error) {
expect(error).to.not.be.ok();
expect(APP.httpPort).to.be.a('number');
var client = net.connect(APP.httpPort);
client.on('connect', function () { done(new Error('Port is not free:' + APP.httpPort)); });
it('free port', function (done) {
apptask._getFreePort(function (error, port) {
expect(error).to.be(null);
expect(port).to.be.a('number');
var client = net.connect(port);
client.on('connect', function () { done(new Error('Port is not free:' + port)); });
client.on('error', function (error) { done(); });
});
});
-90
View File
@@ -1,90 +0,0 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var certificates = require('../certificates.js'),
expect = require('expect.js');
describe('Certificates', function () {
describe('validateCertificate', function () {
/*
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(certificates.validateCertificate(null, null, 'foobar.com')).to.be(null);
});
it('does not allow only cert', function () {
expect(certificates.validateCertificate('cert', null, 'foobar.com')).to.be.an(Error);
});
it('does not allow only key', function () {
expect(certificates.validateCertificate(null, 'key', 'foobar.com')).to.be.an(Error);
});
it('does not allow empty string for cert', function () {
expect(certificates.validateCertificate('', 'key', 'foobar.com')).to.be.an(Error);
});
it('does not allow empty string for key', function () {
expect(certificates.validateCertificate('cert', '', 'foobar.com')).to.be.an(Error);
});
it('does not allow invalid cert', function () {
expect(certificates.validateCertificate('someinvalidcert', validKey0, 'foobar.com')).to.be.an(Error);
});
it('does not allow invalid key', function () {
expect(certificates.validateCertificate(validCert0, 'invalidkey', 'foobar.com')).to.be.an(Error);
});
it('does not allow cert without matching domain', function () {
expect(certificates.validateCertificate(validCert0, validKey0, 'cloudron.io')).to.be.an(Error);
});
it('allows valid cert with matching domain', function () {
expect(certificates.validateCertificate(validCert0, validKey0, 'foobar.com')).to.be(null);
});
it('allows valid cert with matching domain (wildcard)', function () {
expect(certificates.validateCertificate(validCert1, validKey1, 'abc.foobar.com')).to.be(null);
});
it('does now allow cert without matching domain (wildcard)', function () {
expect(certificates.validateCertificate(validCert1, validKey1, 'foobar.com')).to.be.an(Error);
expect(certificates.validateCertificate(validCert1, validKey1, 'bar.abc.foobar.com')).to.be.an(Error);
});
it('allows valid cert with matching domain (subdomain)', function () {
expect(certificates.validateCertificate(validCert2, validKey2, 'baz.foobar.com')).to.be(null);
});
it('does not allow cert without matching domain (subdomain)', function () {
expect(certificates.validateCertificate(validCert0, validKey0, 'baz.foobar.com')).to.be.an(Error);
});
it('does not allow invalid cert/key tuple', function () {
expect(certificates.validateCertificate(validCert0, validKey1, 'foobar.com')).to.be.an(Error);
});
});
});
+21 -33
View File
@@ -35,40 +35,28 @@ for script in "${scripts[@]}"; do
fi
done
image_missing=""
if ! docker inspect "${TEST_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${TEST_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${REDIS_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MYSQL_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${POSTGRESQL_IMAGE}"
image_missing="true"
fi
if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
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"
echo "docker pull "${TEST_IMAGE}" for tests to run"
exit 1
fi
if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${REDIS_IMAGE} for tests to run"
exit 1
fi
if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MYSQL_IMAGE} for tests to run"
exit 1
fi
if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${POSTGRESQL_IMAGE} for tests to run"
exit 1
fi
if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
echo "docker pull ${MONGODB_IMAGE} for tests to run"
exit 1
fi
+29 -25
View File
@@ -11,7 +11,7 @@ var progress = require('../progress.js'),
database = require('../database.js'),
expect = require('expect.js'),
nock = require('nock'),
superagent = require('superagent'),
request = require('superagent'),
server = require('../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -46,7 +46,7 @@ describe('Server', function () {
});
it('is reachable', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
request.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
expect(res.statusCode).to.equal(200);
done(err);
});
@@ -79,32 +79,32 @@ describe('Server', function () {
});
});
it('random bad superagents', function (done) {
superagent.get(SERVER_URL + '/random', function (err, res) {
expect(err).to.be.ok();
it('random bad requests', function (done) {
request.get(SERVER_URL + '/random', function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(404);
done();
done(err);
});
});
it('version', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
request.get(SERVER_URL + '/api/v1/cloudron/status', function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(200);
expect(res.body.version).to.equal('0.5.0');
done();
done(err);
});
});
it('status route is GET', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/status')
request.post(SERVER_URL + '/api/v1/cloudron/status')
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
request.get(SERVER_URL + '/api/v1/cloudron/status')
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
done();
done(err);
});
});
});
@@ -122,16 +122,18 @@ describe('Server', function () {
});
it('config fails due missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config', function (err, res) {
request.get(SERVER_URL + '/api/v1/cloudron/config', function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(401);
done();
done(err);
});
});
it('config fails due wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/config').query({ access_token: 'somewrongtoken' }).end(function (err, res) {
request.get(SERVER_URL + '/api/v1/cloudron/config').query({ access_token: 'somewrongtoken' }).end(function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(401);
done();
done(err);
});
});
});
@@ -148,7 +150,8 @@ describe('Server', function () {
});
it('succeeds with no progress', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.update).to.be(null);
expect(result.body.backup).to.be(null);
@@ -159,7 +162,8 @@ describe('Server', function () {
it('succeeds with update progress', function (done) {
progress.set(progress.UPDATE, 13, 'This is some status string');
superagent.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.update).to.be.an('object');
expect(result.body.update.percent).to.be.a('number');
@@ -175,7 +179,8 @@ describe('Server', function () {
it('succeeds with no progress after clearing the update', function (done) {
progress.clear(progress.UPDATE);
superagent.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
request.get(SERVER_URL + '/api/v1/cloudron/progress', function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(result.body.update).to.be(null);
expect(result.body.backup).to.be(null);
@@ -206,9 +211,8 @@ describe('Server', function () {
});
it('is not reachable anymore', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (error, result) {
request.get(SERVER_URL + '/api/v1/cloudron/status', function (error, result) {
expect(error).to.not.be(null);
expect(!error.response).to.be.ok();
done();
});
});
@@ -222,15 +226,15 @@ describe('Server', function () {
});
it('responds to OPTIONS', function (done) {
superagent('OPTIONS', SERVER_URL + '/api/v1/cloudron/status')
request('OPTIONS', SERVER_URL + '/api/v1/cloudron/status')
.set('Access-Control-Request-Method', 'GET')
.set('Access-Control-Request-Headers', 'accept, origin, x-superagented-with')
.set('Access-Control-Request-Headers', 'accept, origin, x-requested-with')
.set('Origin', 'http://localhost')
.end(function (error, res) {
.end(function (res) {
expect(res.headers['access-control-allow-methods']).to.be('GET, PUT, DELETE, POST, OPTIONS');
expect(res.headers['access-control-allow-credentials']).to.be('true');
expect(res.headers['access-control-allow-headers']).to.be('accept, origin, x-superagented-with'); // mirrored from superagent
expect(res.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from superagent
expect(res.headers['access-control-allow-headers']).to.be('accept, origin, x-requested-with'); // mirrored from request
expect(res.headers['access-control-allow-origin']).to.be('http://localhost'); // mirrors from request
done();
});
});
+81 -15
View File
@@ -100,21 +100,6 @@ describe('Settings', function () {
});
});
it('can set tls config', function (done) {
settings.setTlsConfig({ provider: 'caas' }, function (error) {
expect(error).to.be(null);
done();
});
});
it('can get tls config', function (done) {
settings.getTlsConfig(function (error, dnsConfig) {
expect(error).to.be(null);
expect(dnsConfig.provider).to.be('caas');
done();
});
});
it('can set backup config', function (done) {
settings.setBackupConfig({ provider: 'caas', token: 'TOKEN' }, function (error) {
expect(error).to.be(null);
@@ -141,4 +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);
});
});
});
+3 -3
View File
@@ -53,7 +53,7 @@ function getAppUpdates(callback) {
.timeout(10 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (error) return callback(error);
if (result.statusCode !== 200 || !result.body.appVersions) {
return callback(new Error(util.format('Error checking app update: %s %s', result.statusCode, result.text)));
@@ -88,8 +88,8 @@ function getBoxUpdates(callback) {
.get(config.get('boxVersionsUrl'))
.timeout(10 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200) return callback(new Error(util.format('Bad status: %s %s', result.statusCode, result.text)));
if (error) return callback(error);
if (result.status !== 200) return callback(new Error(util.format('Bad status: %s %s', result.status, result.text)));
var versions = safe.JSON.parse(result.text);
-89
View File
@@ -1,89 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
attempt = require('attempt'),
debug = require('debug')('box:src/waitfordns'),
dns = require('native-dns');
// the first arg to callback is not an error argument; this is required for async.every
function isChangeSynced(domain, ip, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
// ns records cannot have cname
dns.resolve4(nameserver, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) return callback(false);
async.every(nsIps, function (nsIp, iteratorCallback) {
var req = dns.Request({
question: dns.Question({ name: domain, type: 'A' }),
server: { address: nsIp },
timeout: 5000
});
req.on('timeout', function () { return iteratorCallback(false); });
req.on('message', function (error, message) {
if (error || !message.answer || message.answer.length === 0) return iteratorCallback(false);
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, message.answer[0], ip);
if (message.answer[0].address !== ip) return iteratorCallback(false);
iteratorCallback(true); // done
});
req.send();
}, callback);
});
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, ip, zoneName, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof zoneName, 'string');
var defaultOptions = {
retryInterval: 5000,
retries: Infinity
};
if (typeof options === 'function') {
callback = options;
options = defaultOptions;
} else {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
}
debug('waitForDNS: domain %s to be %s in zone %s.', domain, ip, zoneName);
attempt(function (attempts) {
var callback = this; // gross
debug('waitForDNS: %s attempt %s.', domain, attempts);
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, ip), function (synced) {
debug('waitForDNS: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
callback(synced ? null : new Error('ETRYAGAIN'));
});
});
}, { interval: options.retryInterval, retries: options.retries }, function (error) {
if (error) return callback(error);
debug('waitForDNS: %s done.', domain);
callback(null);
});
}
+1 -1
View File
@@ -32,7 +32,7 @@ function backupDone(filename, app, appBackupIds, callback) {
};
superagent.post(url).send(data).query({ token: config.token() }).end(function (error, result) {
if (error && !error.response) return callback(error);
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new Error(result.text));
if (!result.body) return callback(new Error('Unexpected response'));
+2 -2
View File
@@ -307,10 +307,10 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.getStatus = function (callback) {
Client.prototype.isServerFirstTime = function (callback) {
$http.get(client.apiOrigin + '/api/v1/cloudron/status').success(function(data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data);
callback(null, !data.activated);
}).error(defaultErrorHandler(callback));
};
+4 -8
View File
@@ -23,12 +23,8 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
Client.logout();
};
$scope.setup = function (provider) {
if (provider === 'caas') {
window.location.href = '/error.html?errorCode=1';
} else {
window.location.href = '/setup.html';
}
$scope.setup = function () {
window.location.href = '/error.html?errorCode=1';
};
$scope.error = function (error) {
@@ -67,9 +63,9 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
});
};
Client.getStatus(function (error, status) {
Client.isServerFirstTime(function (error, isFirstTime) {
if (error) return $scope.error(error);
if (!status.activated) return $scope.setup();
if (isFirstTime) return $scope.setup();
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
+17 -31
View File
@@ -43,8 +43,6 @@ app.service('Wizard', [ function () {
this.username = '';
this.email = '';
this.password = '';
this.setupToken = null;
this.provider = null;
this.availableAvatars = [{
file: null,
data: null,
@@ -221,7 +219,7 @@ app.controller('StepController', ['$scope', '$route', '$location', 'Wizard', fun
app.controller('FinishController', ['$scope', '$location', 'Wizard', 'Client', function ($scope, $location, Wizard, Client) {
$scope.wizard = Wizard;
Client.createAdmin(Wizard.username, Wizard.password, Wizard.email, Wizard.setupToken, function (error) {
Client.createAdmin($scope.wizard.username, $scope.wizard.password, $scope.wizard.email, $scope.setupToken, function (error) {
if (error) {
console.error('Internal error', error);
window.location.href = '/error.html';
@@ -251,43 +249,31 @@ app.controller('SetupController', ['$scope', '$location', 'Client', 'Wizard', fu
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
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; }, {});
Client.getStatus(function (error, status) {
if (!search.setupToken) return window.location.href = '/error.html?errorCode=2';
$scope.setupToken = search.setupToken;
if (!search.email) return window.location.href = '/error.html?errorCode=3';
Wizard.email = search.email;
if (search.customDomain === 'true') {
Wizard.dnsConfig = {
provider: 'route53',
accessKeyId: null,
secretAccessKey: null
};
}
Client.isServerFirstTime(function (error, isFirstTime) {
if (error) {
window.location.href = '/error.html';
return;
}
if (status.activated) {
if (!isFirstTime) {
window.location.href = '/';
return;
}
if (status.provider === 'caas') {
if (!search.setupToken) {
window.location.href = '/error.html?errorCode=2';
return;
}
if (!search.email) {
window.location.href = '/error.html?errorCode=3';
return;
}
if (search.customDomain === 'true') {
Wizard.dnsConfig = {
provider: 'route53',
accessKeyId: null,
secretAccessKey: null
};
}
Wizard.setupToken = search.setupToken;
}
Wizard.email = search.email;
Wizard.requireEmail = !search.email;
Wizard.provider = status.provider;
$location.path('/step1');
$scope.initialized = true;
+2 -2
View File
@@ -38,13 +38,13 @@
<body class="setup" ng-app="Application" ng-controller="SetupController">
<center ng-show="wizard.provider === 'caas' && !setupToken">
<center ng-show="!setupToken">
<h1> <i class="fa fa-frown-o fa-fw text-danger"></i> No setup token provided. </h1>
Please use the setup link for this cloudron.
</center>
<div class="main-container">
<div class="row" ng-show="initialized && !busy && !(wizard.provider === 'caas' && !setupToken)">
<div class="row" ng-show="initialized && !busy && setupToken">
<div class="col-md-8 col-md-offset-2">
<div class="card" style="max-width: none; padding: 20px;">
<form role="form" name="setup_form" novalidate>
+1 -19
View File
@@ -119,24 +119,6 @@
</div>
</div>
<!-- Modal error app -->
<div class="modal fade" id="appErrorModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ appError.app.location }}</h4>
</div>
<div class="modal-body">
<p>There was an error installing this app</p>
<p>{{appError.app.installationProgress}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<!-- Modal uninstall app -->
<div class="modal fade" id="appUninstallModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
@@ -237,7 +219,7 @@
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
<div class="col-sm-1 grid-item" ng-repeat="app in installedApps">
<div style="background-color: white;" class="highlight grid-item-content">
<a ng-href="{{app | applicationLink}}" ng-click="(app | installError) === true && showError(app)" target="_blank">
<a ng-href="{{app | applicationLink}}" target="_blank">
<div class="grid-item-top">
<div class="row">
<div class="col-xs-12 text-center" style="padding-left: 5px; padding-right: 5px;">
-15
View File
@@ -1,5 +1,4 @@
/* global ISTATES:false */
/* global HSTATES:false */
'use strict';
@@ -41,10 +40,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
password: ''
};
$scope.appError = {
app: {}
};
$scope.appUpdate = {
busy: false,
error: {},
@@ -206,16 +201,6 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
});
};
$scope.showError = function (app) {
$scope.reset();
$scope.appError.app = app;
$('#appErrorModal').modal('show');
return false; // prevent propagation and default
};
$scope.showRestore = function (app) {
$scope.reset();
+1 -1
View File
@@ -65,7 +65,7 @@
<form name="defaultCertForm" ng-submit="setDefaultCert()">
<fieldset>
<label class="control-label" for="defaultCertInput">Fallback Certificate</label>
<p>A wildcard certificate that will be used for apps installed without a specific certificate.</p>
<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) }">
+5 -6
View File
@@ -11,18 +11,17 @@
<div class="row">
<div class="col-md-4 col-md-offset-4 text-center">
<div class="form-group" ng-class="{ 'has-error': setup_form.username.$dirty && setup_form.username.$invalid }">
<input type="text" class="form-control" ng-model="wizard.username" id="inputUsername" name="username" placeholder="Username" ng-enter="focusNext(wizard.requireEmail ? 'inputEmail' : 'inputPassword', setup_form.username.$invalid)" ng-maxlength="512" ng-minlength="3" autofocus required autocomplete="off">
</div>
<div ng-show="wizard.requireEmail" class="form-group" ng-class="{ 'has-error': setup_form.email.$dirty && setup_form.email.$invalid }">
<input type="email" class="form-control" ng-model="wizard.email" id="inputEmail" name="email" placeholder="Email" ng-enter="focusNext('inputPassword', setup_form.email.$invalid)" required autocomplete="off">
<!-- <label class="control-label" for="inputUsername">Username</label> -->
<input type="text" class="form-control" ng-model="wizard.username" id="inputUsername" name="username" placeholder="Username" ng-enter="focusNext('inputPassword', setup_form.username.$invalid)" ng-maxlength="512" ng-minlength="3" autofocus required autocomplete="off">
</div>
<div class="form-group" ng-class="{ 'has-error': setup_form.password.$dirty && setup_form.password.$invalid }">
<input type="password" class="form-control" ng-model="wizard.password" id="inputPassword" name="password" placeholder="Password" ng-enter="next(setup_form.username.$invalid || setup_form.password.$invalid || setup_form.email.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
<!-- <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(setup_form.password.$invalid)" ng-maxlength="512" ng-minlength="5" required autocomplete="off">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 text-center">
<button class="btn btn-primary" ng-click="next(setup_form.username.$invalid || setup_form.password.$invalid || setup_form.email.$invalid)" ng-disabled="setup_form.username.$invalid || setup_form.password.$invalid">Done</button>
<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>