Compare commits

..

176 Commits

Author SHA1 Message Date
Girish Ramakrishnan 7547f6f6a6 Use async unlink
(cherry picked from commit 8dd3c55ecf)
2018-09-28 17:06:05 -07:00
Girish Ramakrishnan f9ae208eaa typoe
(cherry picked from commit 1ee902a541)
2018-09-28 17:02:17 -07:00
Girish Ramakrishnan f9cd81d262 godaddy: empty the text record for del
(cherry picked from commit 55cbe46c7c)
2018-09-28 14:36:05 -07:00
Girish Ramakrishnan db2fda0b18 acme2: Display any errors when cleaning up challenge
(cherry picked from commit 5a8a4e7907)
2018-09-28 14:33:26 -07:00
Girish Ramakrishnan 32744d4fdf acme2: fix challenge subdomain calculation in cleanup
(cherry picked from commit 3b5be641f0)
2018-09-28 13:48:34 -07:00
Girish Ramakrishnan e79857b9cf TXT values must be quoted
(cherry picked from commit a34fe120fb)
2018-09-27 20:22:13 -07:00
Girish Ramakrishnan ffb02a3ba8 Use the local branch in hotfix 2018-09-26 22:26:46 -07:00
Girish Ramakrishnan 47f404cbca 3.2 changes 2018-09-26 16:16:52 -07:00
Girish Ramakrishnan a9c1af50f7 start most cron jobs only after activation
Importing the db might take some time. If a cron runs in the middle,
it crashes.

TypeError: Cannot read property 'domain' of undefined
    at Object.fqdn (/home/yellowtent/box/src/domains.js:126:111)
    at /home/yellowtent/box/src/apps.js:460:36
    at /home/yellowtent/box/node_modules/async/dist/async.js:3110:16
    at replenish (/home/yellowtent/box/node_modules/async/dist/async.js:1011:17)
    at /home/yellowtent/box/node_modules/async/dist/async.js:1016:9
    at eachLimit$1 (/home/yellowtent/box/node_modules/async/dist/async.js:3196:24)
    at Object.<anonymous> (/home/yellowtent/box/node_modules/async/dist/async.js:1046:16)
    at /home/yellowtent/box/src/apps.js:458:19
    at /home/yellowtent/box/src/appdb.js:232:13
    at Query.args.(anonymous function) [as _callback] (/home/yellowtent/box/src/database.js
2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 2967ad0ec8 more debugs and comments 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 1fd2c3381d Fix perms issue when restoring
Fixes #536
2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 9d2f729089 Fix case where mailbox already exists 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 2aecf4be08 reset mailboxname to .app when empty
fixes #587
2018-09-26 16:10:43 -07:00
Girish Ramakrishnan cde43c8a47 3.1.4 changes 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 60fff645c4 Use alternateDomain fqdn for ensuring certificate
this makes it work for hyphenated domains as well
2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 299bcf5574 list domains only once 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 29745c277f Attach fqdn to all the alternateDomains 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 7a43b742c0 allow hyphenated subdomains in caas 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan f85a7f4d5c waitForDnsRecord: use subdomain as argument
this allows to hyphenate the subdomain correctly in all places

the original issue was that altDomain in caas was not working
because waitForDnsRecord was not hyphenating.
2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 291bee7f6a register alt domains in install route 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 78365acc43 Fix new account return value
https://tools.ietf.org/html/draft-ietf-acme-acme-07#section-7.3
2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 976fe1c67d acme2: register new account returns 201 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan e5ecfeb43f calculate subdomain correctly for non-wildcard domains 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 6c9d093da0 Fix double callback 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 0725d40484 select app's cert based on domain's wildcard flag
this also removes the confusing type field in the bundle. we instead
check the current nginx config to see what cert is in use.
2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 27b655c62e rework args to ensureCertificate 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan 2a7a0f04e4 Allow wildcard only with programmable DNS backend 2018-09-26 16:10:43 -07:00
Girish Ramakrishnan b2f7eac629 make ensureCertificate check any wildcard cert 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 40d95a45a8 acme2: implement wildcard certs 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan ef89b09817 Move type validation to routes logic 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan cfa29fa82e refactor to validateTlsConfig 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 7a07e0220f consolidate hyphenatedSubdomains handling 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan d0eba4d3e3 acme2: wait for dns 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 5eccf7dc3b Enhance waitForDns to support TXT records 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 29bd5fdbc7 acme2: dns authorization 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 7c28fa1175 pass domain arg to getCertificate API 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 323a498f04 rename func 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 2ea0d57d95 lint 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 55093af2ae Fix callback use 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 8f4c1055c3 acme2 implementation 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 0bb81bf0ee Fix tests 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 9a6b095563 acme -> acme1 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan b4f756dceb storage: add access denied function (unused) 2018-09-26 16:06:13 -07:00
Girish Ramakrishnan 3554f39fdd cloudflare: Add the chain message 2018-09-26 16:05:54 -07:00
Girish Ramakrishnan bcd3a30579 cloudflare: priority is now an integer 2018-09-26 16:05:54 -07:00
Girish Ramakrishnan 2e756a9b25 Return 424 for external errors 2018-09-26 16:05:54 -07:00
Girish Ramakrishnan bd4c5883f5 dns: implement wildcard dns validation 2018-09-26 16:05:54 -07:00
Girish Ramakrishnan 004f00a97b Make wildcard a separate provider
this is required because the config object is not returned for
locked domains and the UI display for the provider field is then
wrong.
2018-09-26 16:05:54 -07:00
Girish Ramakrishnan 44ed650538 typo 2018-09-26 16:05:54 -07:00
Girish Ramakrishnan a3ae73d48f 3.1.4 changes 2018-09-12 20:29:55 -07:00
Girish Ramakrishnan df66d77a68 cloudflare: Fix crash when there is an external error updating dns records 2018-09-06 12:26:11 -07:00
Girish Ramakrishnan 5e919b90f5 Better fix for grub 2018-09-06 11:56:50 -07:00
Girish Ramakrishnan 428269f503 3.1.3 changes 2018-09-06 00:41:45 -07:00
Girish Ramakrishnan b03e26a510 Fix typo 2018-09-06 00:41:37 -07:00
Girish Ramakrishnan 1e15b63a4a prevent deletion of mail domain as well 2018-09-06 00:16:12 -07:00
Girish Ramakrishnan 8d5e70f6aa lock the admin domain based on the edition 2018-09-06 00:15:45 -07:00
Girish Ramakrishnan 91a1bc7a01 move verifyOperator to users routes 2018-09-06 00:10:09 -07:00
Girish Ramakrishnan 0e3f9c9569 Move verifyAppOwnership to app route 2018-09-06 00:09:42 -07:00
Girish Ramakrishnan 2ad0a57fc1 Typo 2018-09-05 23:59:40 -07:00
Girish Ramakrishnan def3521ee1 Do not allow admin domain to be deleted 2018-09-05 17:12:02 -07:00
Girish Ramakrishnan 3d004b3dcc Disable various server/operator routes based on edition
The initial idea was to put an owner flag but this means that the
owner will be visible inside apps.
2018-09-05 15:31:58 -07:00
Girish Ramakrishnan 0439bd8869 move addSpacesSuffix to model logic 2018-09-04 16:37:08 -07:00
Girish Ramakrishnan 10b4043358 Add alternateDomains to app install route 2018-09-04 16:27:35 -07:00
Girish Ramakrishnan ac3b0f0082 Add spaces suffix for alternate domains 2018-09-04 14:12:50 -07:00
Girish Ramakrishnan d49a1dea7a Fix usage of domains.fqdn 2018-09-04 11:35:01 -07:00
Girish Ramakrishnan ec9c96da6f Fix comments 2018-09-04 10:48:54 -07:00
Johannes Zellner 2de630e491 Put the app owner also into ldap groups
Fixes #585
2018-09-03 17:14:11 +02:00
Johannes Zellner 3af358b9bc List app owner as admins in ldap search 2018-09-03 16:08:05 +02:00
Johannes Zellner b61478edc9 Attach req.app for further use in ldap routes 2018-09-03 15:38:50 +02:00
Johannes Zellner b23afdd32d Fix tests to match the adjusted purchase logic 2018-09-01 11:53:05 +02:00
Girish Ramakrishnan 43055da614 Add route to let admin set user password 2018-08-31 14:35:01 -07:00
Girish Ramakrishnan 2c3f1ab720 Fix the error messages 2018-08-31 14:06:06 -07:00
Johannes Zellner 35a31922a5 Always run all tests 2018-08-31 22:25:06 +02:00
Girish Ramakrishnan bf432dc26f Revert "Disable memory accounting setup"
This reverts commit ad22df6f71.

This actually serves the purpose of enabling swap.

https://raw.githubusercontent.com/moby/moby/master/contrib/check-config.sh
2018-08-31 13:21:33 -07:00
Johannes Zellner 18cc93799e Fully rely on the appstore during 'purchase' call 2018-08-31 10:53:35 +02:00
Girish Ramakrishnan fc3bc48f47 Fix crash when location is not provided 2018-08-30 22:08:48 -07:00
Girish Ramakrishnan fc96f59ecc Add edition checks 2018-08-30 21:20:49 -07:00
Girish Ramakrishnan 534a00b3af Disallow dot in location in hyphenatedSubdomains 2018-08-30 21:03:54 -07:00
Girish Ramakrishnan 619d1e44e5 Move validateHostname to domains code 2018-08-30 20:54:15 -07:00
Girish Ramakrishnan 068113bd5d remove redundant domain arg to domains.fqdn 2018-08-30 19:57:12 -07:00
Girish Ramakrishnan ca16072d90 hypenatedSubdomains: do not hyphenate txt dns records 2018-08-30 16:17:56 -07:00
Girish Ramakrishnan 6fac59cf9d cloudron-setup: print before we shift all the args 2018-08-30 14:30:49 -07:00
Girish Ramakrishnan f953cfc4d5 cloudron-setup: print the args to the log file 2018-08-30 14:06:25 -07:00
Johannes Zellner 7a1723d173 Fix app tests 2018-08-30 00:32:38 +02:00
Johannes Zellner b6643518f6 Adjust apps test to how we check subscriptions 2018-08-29 23:57:59 +02:00
Johannes Zellner 91470156c9 Only remove all app container on uninstall
For update and configure, we do not want to purge potential docker addon
created ones. They are managed by the app itself.
2018-08-29 21:28:58 +02:00
Johannes Zellner 40c6ab5615 Remove unused require 2018-08-29 21:25:02 +02:00
Girish Ramakrishnan 6cc4e44f22 pass edition to update.sh 2018-08-28 22:14:27 -07:00
Girish Ramakrishnan 976cf1740e Put edition in status route 2018-08-28 21:43:25 -07:00
Girish Ramakrishnan 22cdd3f55e spaces: add username suffix to location 2018-08-28 20:27:17 -07:00
Girish Ramakrishnan e0cd7999eb Make spaces an edition instead of setting 2018-08-28 18:31:48 -07:00
Johannes Zellner 4f7242fa6a add --edition option to cloudron-setup 2018-08-28 18:06:25 -07:00
Johannes Zellner 964da5ee52 Send the edition in the config route 2018-08-28 18:05:45 -07:00
Johannes Zellner baa99d1a44 add edition property to cloudron.conf 2018-08-28 18:05:15 -07:00
Girish Ramakrishnan 6d1cb1bb14 Do not update grub packages
It looks on some VPS, the grub then asks the user to re-install
since the disk name has changed/moved (presumably because the
image was created on a server with a different disk name)
2018-08-28 14:33:07 -07:00
Girish Ramakrishnan f7e6c5cd40 Add galaxygate as a provider 2018-08-28 12:25:16 -07:00
Girish Ramakrishnan ad22df6f71 Disable memory accounting setup
Overall, this is outside the scope of Cloudron
2018-08-28 12:18:31 -07:00
Girish Ramakrishnan 8e572a7c23 Fix changelog 2018-08-27 16:05:01 -07:00
Girish Ramakrishnan e49b57294d Give optional name for tokens 2018-08-27 14:59:52 -07:00
Girish Ramakrishnan badb6e4672 add note on clients.appId 2018-08-27 14:30:39 -07:00
Girish Ramakrishnan d09ff985af remove all user tokens when user is deleted 2018-08-27 14:05:22 -07:00
Girish Ramakrishnan a3130c8aab inviteLink is not available during user create anymore 2018-08-27 10:40:58 -07:00
Girish Ramakrishnan 0843d51c98 Check for existing port bindings in udpPorts 2018-08-26 17:50:17 -07:00
Girish Ramakrishnan 9a1b5dd5cc Add 3.1.1 changelog 2018-08-25 18:32:02 -07:00
Girish Ramakrishnan 6f398144cb validate hyphenatedSubdomain 2018-08-25 18:29:55 -07:00
Girish Ramakrishnan d91df50b9f caas: add hyphenateSubdomain property 2018-08-25 17:49:31 -07:00
Johannes Zellner 99ead48fd5 Add more changes 2018-08-25 11:14:14 +02:00
Johannes Zellner 922b6d2b18 Also renew alternate domain certificates
Part of #583
2018-08-25 11:04:53 +02:00
Girish Ramakrishnan 6a3b45223a Better name for internal functions 2018-08-24 15:38:44 -07:00
Girish Ramakrishnan 93b0f39545 ensureCertificate: make it take appDomain object 2018-08-24 15:07:13 -07:00
Girish Ramakrishnan dbe86af31e Make getApi takes a string domain 2018-08-24 15:00:19 -07:00
Girish Ramakrishnan 3ede50a141 remove unused function 2018-08-24 14:11:02 -07:00
Girish Ramakrishnan e607fe9a41 Specify subdomain fields explicitly 2018-08-24 10:39:59 -07:00
Johannes Zellner 43d125b216 Send hyphenatedSubdomains as non restricted property 2018-08-22 17:19:18 +02:00
Johannes Zellner 9467a2922a Return hyphenated subdomain name in getName() 2018-08-22 14:13:48 +02:00
Johannes Zellner b35c81e546 Handle hyphenatedSubdomains in the backend verifyDnsConfig() 2018-08-22 12:16:19 +02:00
Johannes Zellner 59700e455e Use the full domain record for domains.fqdn() 2018-08-22 12:15:46 +02:00
Johannes Zellner e8fcfc4594 Allow to specify hyphenatedSubdomains for a domain 2018-08-22 11:59:42 +02:00
Johannes Zellner 9bac7e8124 Move the comment where it belongs 2018-08-22 11:02:06 +02:00
Girish Ramakrishnan 210c453508 More changes 2018-08-21 21:25:07 -07:00
Girish Ramakrishnan 442d4e5c6f Fix failing tests 2018-08-21 18:57:11 -07:00
Girish Ramakrishnan af63cb936d More changes 2018-08-21 18:47:12 -07:00
Girish Ramakrishnan b4c9f64721 Issue token on password reset and setup 2018-08-21 18:42:18 -07:00
Girish Ramakrishnan c64a29e6fc More 3.1.0 changes 2018-08-21 17:06:51 -07:00
Johannes Zellner f05df7cfef Allow set admin flag on user creation 2018-08-21 17:12:46 +02:00
Johannes Zellner f4a76a26af Remove left-over docker proxy start call in platform.js 2018-08-21 15:31:53 +02:00
Girish Ramakrishnan f338e015d5 Revert "Also allow docker in containers spawned by an authorized app"
This reverts commit 4f336a05fc.

This is not required by an app yet. Besides, it breaks tests
2018-08-20 20:10:16 -07:00
Girish Ramakrishnan 89cf8167e6 Make tests work 2018-08-20 20:10:14 -07:00
Johannes Zellner d5194cfdc9 Remove nativeLogging docker addon support
Was only required for eclipse che
2018-08-20 15:22:10 +02:00
Johannes Zellner eb07d3d543 Drop all custom network configs in docker proxy 2018-08-20 15:19:08 +02:00
Girish Ramakrishnan 6a1a697820 Split the invite route into two 2018-08-17 16:27:29 -07:00
Girish Ramakrishnan e5cc81d8fa Fix test name 2018-08-17 13:45:10 -07:00
Johannes Zellner 3640b0bd0e Allow to override the logging backend for app like che 2018-08-17 15:30:37 +02:00
Johannes Zellner 4d4ce9b86e Bare bones support of docker exec through the proxy 2018-08-17 15:30:23 +02:00
Johannes Zellner db385c6770 Ensure the docker proxy tests cleanup correctly 2018-08-17 13:44:12 +02:00
Johannes Zellner 2925e98d54 Make PUT requests through the docker proxy work 2018-08-17 12:33:46 +02:00
Girish Ramakrishnan 75ee40865e 3.0.2 changes 2018-08-16 23:30:20 -07:00
Girish Ramakrishnan af58e56732 Fix issue where normal users are shown all apps 2018-08-16 20:04:03 -07:00
Johannes Zellner dc3e3f5f4d Ensure we pipe the parsed body again upstream to docker 2018-08-16 14:28:51 +02:00
Johannes Zellner 83304ff66c Verify that docker logs is correctly reporting another logging driver 2018-08-16 12:07:15 +02:00
Johannes Zellner 575e0cea33 Use syslog for containers created in apps 2018-08-16 11:37:08 +02:00
Girish Ramakrishnan 0bf3b45ddc Fix bind mapping logic 2018-08-15 16:52:30 -07:00
Johannes Zellner 826a0e7708 Add test case for docker logs through the proxy 2018-08-15 18:16:03 +02:00
Johannes Zellner 0522d1e3c4 Simply prefix all docker volume mounts with the app data dir 2018-08-15 18:00:51 +02:00
Johannes Zellner 12970bf50a Add some debugs for volume rewriting 2018-08-15 16:51:10 +02:00
Johannes Zellner 4a739213bf When creating a container the Labels are toplevel 2018-08-15 12:51:52 +02:00
Johannes Zellner 4f336a05fc Also allow docker in containers spawned by an authorized app 2018-08-15 12:35:34 +02:00
Girish Ramakrishnan c3dacba894 dockerproxy: rewrite labels and binds 2018-08-14 20:49:41 -07:00
Girish Ramakrishnan f88c01eea6 dockerproxy: Add app authorization 2018-08-14 20:04:15 -07:00
Girish Ramakrishnan 15b0dfcb60 rename variable 2018-08-14 19:03:59 -07:00
Girish Ramakrishnan ebd27b444d dockerproxy: use express 2018-08-14 18:59:41 -07:00
Johannes Zellner ee1c587922 Overwrite the docker container network in the proxy 2018-08-14 22:54:02 +02:00
Johannes Zellner 4da91ec90d Make the docker proxy work 2018-08-14 22:54:02 +02:00
Girish Ramakrishnan 3cf3c36e86 Fix tests 2018-08-13 22:54:56 -07:00
Girish Ramakrishnan 8bd6c9933f Add interval secs
Part of #568
2018-08-13 22:35:38 -07:00
Girish Ramakrishnan 2e0a7dcd47 Fix migration filename 2018-08-13 22:31:58 -07:00
Johannes Zellner 714c205538 Set the correct debug label 2018-08-13 22:06:28 +02:00
Johannes Zellner 00041add55 No need to pull in underscore to build an object 2018-08-13 22:01:51 +02:00
Johannes Zellner 7f5fe12712 Use DOCKER_HOST to make the cli work out of the box 2018-08-13 21:59:02 +02:00
Johannes Zellner 441fdb81f8 Move docker proxy into its own file 2018-08-13 21:14:17 +02:00
Johannes Zellner fb02e8768c Remove unused require 2018-08-13 21:05:07 +02:00
Johannes Zellner 14f0f954b7 Use docker proxy over DOCKER_URL env var when docker addon is used 2018-08-13 20:47:15 +02:00
Johannes Zellner 10f0d48b2a Use docker proxy port from config 2018-08-13 20:41:02 +02:00
Johannes Zellner 6933184c2e Revert "Expose the host /app/data folder when localstorage and docker addons are used"
We will use the docker proxy for that

This reverts commit b3aa59de19.
2018-08-13 20:39:23 +02:00
Johannes Zellner a1b983de23 Initial code for docker addon proxy 2018-08-13 20:38:39 +02:00
Johannes Zellner b3aa59de19 Expose the host /app/data folder when localstorage and docker addons are used 2018-08-13 19:40:41 +02:00
Girish Ramakrishnan 796ced999f Add some 3.1 changes 2018-08-13 09:18:44 -07:00
Girish Ramakrishnan 353b5e07bf Save correct type of port in db
Part of #504
2018-08-13 08:33:17 -07:00
Girish Ramakrishnan c29eef4c14 Set the udp ports in docker configuration
Part of #504
2018-08-12 22:47:59 -07:00
Girish Ramakrishnan 8bc7dc9724 Pass the manifest to validatePortBindings 2018-08-12 22:37:36 -07:00
Girish Ramakrishnan 60984d18dd Add type field to port bindings table
Part of #504
2018-08-12 22:32:36 -07:00
Girish Ramakrishnan df1dc80fc1 Change the internal portBindings representation
Part of #504
2018-08-12 22:32:31 -07:00
Girish Ramakrishnan 8e2f0cdf73 Update cloudron-manifestformat (udpPorts) 2018-08-12 19:00:28 -07:00
Ian Fijolek bf1e19f8e6 Add more detailed checking of DMARC
Fixes #570
2018-08-12 13:47:24 -07:00
Girish Ramakrishnan 9a7214ea07 Update manifestformat for docker addon 2018-08-10 12:31:46 -07:00
Johannes Zellner 4499f08357 Revert "Remove _docker addon"
This reverts commit d6f49eb54f.
2018-08-09 11:54:46 +02:00
94 changed files with 2615 additions and 863 deletions
+41 -1
View File
@@ -1351,5 +1351,45 @@
[3.0.2]
* Fix issue where normal users are shown apps they don't have access to
* Re-configure mail apps when mail is enabled/disabled
* Re-configure email apps when email is enabled/disabled
[3.1.0]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
[3.1.1]
* Fix caas domain migration
[3.1.2]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
* API token can now have a name
[3.1.3]
* Prevent dashboard domain from being deleted
* Add alternateDomains to app install route
[3.1.4]
* Fix issue where support tab was redirecting
[3.1.4]
* Fix issue where support tab was redirecting
[3.2.0]
* Add acme2 support. This provides DNS based validation removing inbound port 80 requirement
* Add support for wildcard certificates
* Allow mailbox name to be reset to the buit-in '.app' name
* Fix permission issue when restoring a Cloudron
* Fix a crash when restoring Cloudron
* Allow alternate domains to be set in app installation REST API
* Add SFO2 region for DigitalOcean Spaces
* Show the title in port bindings instead of the long description
+6 -2
View File
@@ -14,8 +14,11 @@ function die {
export DEBIAN_FRONTEND=noninteractive
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
apt-mark unhold grub* >/dev/null
echo "==> Installing required packages"
@@ -72,8 +75,9 @@ if [[ "${storage_driver}" != "overlay2" ]]; then
exit 1
fi
# do not upgrade grub because it might prompt user and break this script
echo "==> Enable memory accounting"
apt-get -y install grub2
apt-get -y --no-upgrade install grub2-common
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
update-grub
+7
View File
@@ -13,6 +13,7 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
dockerProxy = require('./src/dockerproxy.js'),
server = require('./src/server.js');
console.log();
@@ -25,6 +26,9 @@ console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
console.log(' LDAP Server Port: ', config.get('ldapPort'));
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
console.log();
console.log('==========================================');
console.log();
@@ -32,6 +36,7 @@ console.log();
async.series([
server.start,
ldap.start,
dockerProxy.start,
appHealthMonitor.start,
], function (error) {
if (error) {
@@ -46,11 +51,13 @@ var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
@@ -4,7 +4,7 @@ var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from apps', [ ], function (error, results) {
if (error) return done(error);
if (error) return callback(error);
var queries = [
db.runSql.bind(db, 'START TRANSACTION;')
@@ -0,0 +1,18 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings ADD COLUMN type VARCHAR(8) NOT NULL DEFAULT "tcp"'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP INDEX hostPort'), // this drops the unique constraint
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort, type)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort)'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP COLUMN type')
], callback);
};
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
backupConfig.intervalSecs = 24 * 60 * 60;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,23 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
// first check precondtion of domain entry in settings
db.all('SELECT * FROM domains', [ ], function (error, domains) {
if (error) return callback(error);
let caasDomains = domains.filter(function (d) { return d.provider === 'caas'; });
async.eachSeries(caasDomains, function (domain, iteratorCallback) {
let config = JSON.parse(domain.configJson);
config.hyphenatedSubdomains = true;
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,12 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tokens ADD COLUMN name VARCHAR(64) DEFAULT ""', [], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tokens DROP COLUMN name', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,21 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from domains WHERE provider=?', [ 'manual' ], function (error, results) {
if (error) return callback(error);
async.eachSeries(results, function (result, iteratorDone) {
var config = JSON.parse(result.configJson || '{}');
if (!config.wildcard) return iteratorDone();
delete config.wildcard;
db.runSql('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ 'wildcard', JSON.stringify(config), result.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
+3 -1
View File
@@ -41,6 +41,7 @@ CREATE TABLE IF NOT EXISTS groupMembers(
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
name VARCHAR(64) DEFAULT "", // description
accessToken VARCHAR(128) NOT NULL UNIQUE,
identifier VARCHAR(128) NOT NULL,
clientId VARCHAR(128),
@@ -50,7 +51,7 @@ CREATE TABLE IF NOT EXISTS tokens(
CREATE TABLE IF NOT EXISTS clients(
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
appId VARCHAR(128) NOT NULL,
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
type VARCHAR(16) NOT NULL,
clientSecret VARCHAR(512) NOT NULL,
redirectURI VARCHAR(512) NOT NULL,
@@ -92,6 +93,7 @@ CREATE TABLE IF NOT EXISTS apps(
CREATE TABLE IF NOT EXISTS appPortBindings(
hostPort INTEGER NOT NULL UNIQUE,
type VARCHAR(8) NOT NULL DEFAULT "tcp",
environmentVariable VARCHAR(128) NOT NULL,
appId VARCHAR(128) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id),
+79 -9
View File
@@ -1632,15 +1632,15 @@
}
},
"cloudron-manifestformat": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.11.0.tgz",
"integrity": "sha512-t4KR2KmK1JDtxw1n6IVNg0+xxspk/Cpb6m1WimE9hJ0KJYsIgZNnkce47uYiG9/nWrgUSV4xcdzsS91OOvWgig==",
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-2.13.1.tgz",
"integrity": "sha512-KvWaUw0q2U+EL+y7LJ+Q8YZfERYgyGRwj48ZRfsREaIjckS8mG03Oa2UIf2x/uGLfw0frWvTNdQXe3ZpC94N9g==",
"requires": {
"cron": "1.3.0",
"java-packagename-regex": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"java-packagename-regex": "1.0.0",
"safetydance": "0.0.15",
"semver": "4.3.6",
"tv4": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"tv4": "1.3.0",
"validator": "3.43.0"
},
"dependencies": {
@@ -1708,6 +1708,41 @@
"typedarray": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
}
},
"connect": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
"integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
"requires": {
"debug": "2.6.9",
"finalhandler": "1.1.0",
"parseurl": "1.3.2",
"utils-merge": "1.0.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"finalhandler": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
"integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
"requires": {
"debug": "2.6.9",
"encodeurl": "1.0.2",
"escape-html": "1.0.3",
"on-finished": "2.3.0",
"parseurl": "1.3.2",
"statuses": "1.3.1",
"unpipe": "1.0.0"
}
}
}
},
"connect-ensure-login": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz",
@@ -2682,6 +2717,11 @@
"safe-buffer": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"ejs": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
@@ -3192,13 +3232,13 @@
"dependencies": {
"mkdirp": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.5.tgz",
"integrity": "sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc=",
"dev": true
},
"rimraf": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha1-5Dm+Kq7jJzIZUnMPmaiSnk/FBYI=",
"dev": true
}
@@ -4490,7 +4530,8 @@
}
},
"java-packagename-regex": {
"version": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ="
},
"js-base64": {
@@ -5255,6 +5296,11 @@
}
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multiparty": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.1.4.tgz",
@@ -5964,6 +6010,14 @@
}
}
},
"on-finished": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
"requires": {
"ee-first": "1.1.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -6113,6 +6167,11 @@
"resolved": "https://registry.npmjs.org/parse-links/-/parse-links-0.1.0.tgz",
"integrity": "sha1-afpighugBBX+c2MyNVIeRUe36CE="
},
"parseurl": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
"integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
},
"passport": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz",
@@ -7873,6 +7932,11 @@
}
}
},
"statuses": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
"integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4="
},
"stdout-stream": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
@@ -8479,7 +8543,8 @@
"dev": true
},
"tv4": {
"version": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz",
"integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM="
},
"tweetnacl": {
@@ -8536,6 +8601,11 @@
"version": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
+3 -3
View File
@@ -20,7 +20,8 @@
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.11.0",
"cloudron-manifestformat": "^2.13.1",
"connect": "^3.6.6",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
@@ -91,8 +92,7 @@
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test/[^a]*",
"test_all": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
+12 -5
View File
@@ -44,6 +44,7 @@ fi
initBaseImage="true"
# provisioning data
provider=""
edition=""
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
@@ -52,13 +53,16 @@ sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,edition:,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--edition) edition="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
@@ -94,7 +98,7 @@ fi
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
@@ -104,6 +108,8 @@ elif [[ \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
"${provider}" != "digitalocean" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
@@ -114,7 +120,7 @@ elif [[ \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -137,12 +143,12 @@ echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories"
echo "Could not update package repositories. See ${LOG_FILE}"
exit 1
fi
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl)"
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
exit 1
fi
fi
@@ -169,6 +175,7 @@ fi
data=$(cat <<EOF
{
"provider": "${provider}",
"edition": "${edition}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
+1 -1
View File
@@ -44,7 +44,7 @@ branch=$(git rev-parse --abbrev-ref HEAD)
if [[ "${branch}" == "master" ]]; then
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git rev-parse "${branch}")
else
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "origin/${branch}")
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "${branch}")
fi
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}.tar.gz"
+5
View File
@@ -14,6 +14,7 @@ arg_version=""
arg_web_server_origin=""
arg_provider=""
arg_is_demo="false"
arg_edition=""
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
eval set -- "${args}"
@@ -55,6 +56,9 @@ while true; do
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
arg_edition=$(echo "$2" | $json edition)
[[ "${arg_edition}" == "" ]] && arg_edition=""
shift 2
;;
--) break;;
@@ -69,3 +73,4 @@ echo "fqdn: ${arg_fqdn}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
echo "edition: ${arg_edition}"
+2 -1
View File
@@ -218,7 +218,8 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
"adminFqdn": "${arg_admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo}
"isDemo": ${arg_is_demo},
"edition": "${arg_edition}"
}
CONF_END
+1 -1
View File
@@ -90,8 +90,8 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
# CSP headers for the admin/dashboard resources
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
+2 -6
View File
@@ -26,9 +26,9 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
settings = require('./settings.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
@@ -114,11 +114,7 @@ function scopesForUser(user, callback) {
if (user.admin) return callback(null, exports.VALID_SCOPES);
settings.getSpacesConfig(function (error, spaces) {
if (error) return callback(error);
callback(null, spaces.enabled ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
});
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
}
function validateToken(accessToken, callback) {
+8
View File
@@ -109,6 +109,12 @@ var KNOWN_ADDONS = {
teardown: NOOP,
backup: NOOP,
restore: NOOP
},
docker: {
setup: NOOP,
teardown: NOOP,
backup: NOOP,
restore: NOOP
}
};
@@ -199,6 +205,8 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, function (error, result) {
if (error) return callback(error);
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${config.get('dockerProxyPort')}` });
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
});
}
+28 -23
View File
@@ -71,7 +71,9 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
@@ -96,14 +98,16 @@ function postProcess(result) {
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
result.portBindings = { };
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
delete result.hostPorts;
delete result.environmentVariables;
delete result.portTypes;
for (var i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
}
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
@@ -133,15 +137,15 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -158,15 +162,15 @@ function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -182,15 +186,15 @@ function getByContainerId(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
database.query('SELECT * FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
result[0].alternateDomains = alternateDomains;
@@ -205,14 +209,14 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND type = ?'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('SELECT * FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
alternateDomains.forEach(function (d) {
@@ -271,8 +275,8 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
Object.keys(portBindings).forEach(function (env) {
queries.push({
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
args: [ env, portBindings[env], id ]
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
});
});
@@ -322,18 +326,19 @@ function getPortBindings(id, callback) {
var portBindings = { };
for (var i = 0; i < results.length; i++) {
portBindings[results[i].environmentVariable] = results[i].hostPort;
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
}
callback(null, portBindings);
});
}
function delPortBinding(hostPort, callback) {
function delPortBinding(hostPort, type, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -394,8 +399,8 @@ function updateWithConstraints(id, app, constraints, callback) {
// replace entries by app id
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
Object.keys(portBindings).forEach(function (env) {
var values = [ portBindings[env], env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
});
}
+148 -103
View File
@@ -45,10 +45,13 @@ exports = module.exports = {
setOwner: setOwner,
transferOwnership: transferOwnership,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
// exported for testing
_validateHostname: validateHostname,
_validatePortBindings: validatePortBindings,
_validateAccessRestriction: validateAccessRestriction
_validateAccessRestriction: validateAccessRestriction,
_translatePortBindings: translatePortBindings
};
var appdb = require('./appdb.js'),
@@ -81,7 +84,6 @@ var appdb = require('./appdb.js'),
split = require('split'),
superagent = require('superagent'),
taskmanager = require('./taskmanager.js'),
tld = require('tldjs'),
TransformStream = require('stream').Transform,
updateChecker = require('./updatechecker.js'),
url = require('url'),
@@ -123,43 +125,10 @@ AppsError.BILLING_REQUIRED = 'Billing Required';
AppsError.ACCESS_DENIED = 'Access denied';
AppsError.BAD_CERTIFICATE = 'Invalid certificate';
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name (and not dns name)
function validateHostname(location, domain, hostname) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof hostname, 'string');
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
if (hostname === config.adminFqdn()) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
if (!tld.isValid(tmp)) return new AppsError(AppsError.BAD_FIELD, 'Hostname is not a valid domain name');
if (hostname.length > 253) return new AppsError(AppsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
if (location) {
// label validation
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new AppsError(AppsError.BAD_FIELD, 'Invalid subdomain length');
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new AppsError(AppsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
if (/^[-.]/.test(location)) return new AppsError(AppsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
}
return null;
}
// validate the port bindings
function validatePortBindings(portBindings, tcpPorts) {
function validatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
// keep the public ports in sync with firewall rules in setup/start/cloudron-firewall.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
@@ -190,26 +159,58 @@ function validatePortBindings(portBindings, tcpPorts) {
if (!portBindings) return null;
var env;
for (env in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(env)) return new AppsError(AppsError.BAD_FIELD, env + ' is not valid environment variable');
for (let portName in portBindings) {
if (!/^[a-zA-Z0-9_]+$/.test(portName)) return new AppsError(AppsError.BAD_FIELD, `${portName} is not a valid environment variable`);
if (!Number.isInteger(portBindings[env])) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not an integer');
if (RESERVED_PORTS.indexOf(portBindings[env]) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(portBindings[env]));
if (portBindings[env] <= 1023 || portBindings[env] > 65535) return new AppsError(AppsError.BAD_FIELD, portBindings[env] + ' is not in permitted range');
const hostPort = portBindings[portName];
if (!Number.isInteger(hostPort)) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not an integer`);
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new AppsError(AppsError.PORT_RESERVED, String(hostPort));
if (hostPort <= 1023 || hostPort > 65535) return new AppsError(AppsError.BAD_FIELD, `${hostPort} is not in permitted range`);
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
// that the user wants the service disabled
tcpPorts = tcpPorts || { };
for (env in portBindings) {
if (!(env in tcpPorts)) return new AppsError(AppsError.BAD_FIELD, 'Invalid portBindings ' + env);
const tcpPorts = manifest.tcpPorts || { };
const udpPorts = manifest.udpPorts || { };
for (let portName in portBindings) {
if (!(portName in tcpPorts) && !(portName in udpPorts)) return new AppsError(AppsError.BAD_FIELD, `Invalid portBindings ${portName}`);
}
return null;
}
function translatePortBindings(portBindings, manifest) {
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(typeof manifest, 'object');
if (!portBindings) return null;
let result = {};
const tcpPorts = manifest.tcpPorts || { };
for (let portName in portBindings) {
const portType = portName in tcpPorts ? exports.PORT_TYPE_TCP : exports.PORT_TYPE_UDP;
result[portName] = { hostPort: portBindings[portName], type: portType };
}
return result;
}
function postProcess(app) {
let result = {};
for (let portName in app.portBindings) {
result[portName] = app.portBindings[portName].hostPort;
}
app.portBindings = result;
}
function addSpacesSuffix(location, user) {
if (user.admin || !config.isSpacesEnabled()) return location;
const spacesSuffix = user.username.replace(/\./g, '-');
return location === '' ? spacesSuffix : `${location}-${spacesSuffix}`;
}
function validateAccessRestriction(accessRestriction) {
assert.strictEqual(typeof accessRestriction, 'object');
@@ -305,8 +306,8 @@ function getDuplicateErrorDetails(location, portBindings, error) {
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
// check if any of the port bindings conflict
for (var env in portBindings) {
if (portBindings[env] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
}
return new AppsError(AppsError.ALREADY_EXISTS);
@@ -371,15 +372,21 @@ function get(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
domaindb.get(app.domain, function (error, result) {
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
postProcess(app);
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -396,25 +403,38 @@ function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
docker.getContainerIdByIp(ip, function (error, containerId) {
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
domaindb.get(app.domain, function (error, result) {
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
postProcess(app);
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
callback(null, app);
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
});
@@ -424,28 +444,34 @@ function getByIpAddress(ip, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.eachSeries(apps, function (app, iteratorDone) {
domaindb.get(app.domain, function (error, result) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
apps.forEach(postProcess);
async.eachSeries(apps, function (app, iteratorDone) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (error && error.reason !== DatabaseError.NOT_FOUND) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
iteratorDone(null, app);
});
});
}, function (error) {
if (error) return callback(error);
}, function (error) {
if (error) return callback(error);
callback(null, apps);
callback(null, apps);
});
});
});
}
@@ -487,8 +513,9 @@ function mailboxNameForLocation(location, manifest) {
return (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
}
function install(data, auditSource, callback) {
function install(data, user, auditSource, callback) {
assert(data && typeof data === 'object');
assert(user && typeof user === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -507,7 +534,8 @@ function install(data, auditSource, callback) {
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz',
ownerId = data.ownerId;
ownerId = data.ownerId,
alternateDomains = data.alternateDomains || [];
assert(data.appStoreId || data.manifest); // atleast one of them is required
@@ -520,7 +548,7 @@ function install(data, auditSource, callback) {
error = checkManifestConstraints(manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, manifest.tcpPorts);
error = validatePortBindings(portBindings, manifest);
if (error) return callback(error);
error = validateAccessRestriction(accessRestriction);
@@ -559,12 +587,14 @@ function install(data, auditSource, callback) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var fqdn = domains.fqdn(location, domain, domainObject.provider);
location = addSpacesSuffix(location, user);
alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
if (cert && key) {
let fqdn = domains.fqdn(location, domain, domainObject);
error = reverseProxy.validateCertificate(fqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
@@ -580,10 +610,11 @@ function install(data, auditSource, callback) {
mailboxName: mailboxNameForLocation(location, manifest),
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt
robotsTxt: robotsTxt,
alternateDomains: alternateDomains
};
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, portBindings, data, function (error) {
appdb.add(appId, appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -606,6 +637,7 @@ function install(data, auditSource, callback) {
// save cert to boxdata/certs
if (cert && key) {
let fqdn = domains.fqdn(location, domainObject);
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
@@ -626,16 +658,17 @@ function install(data, auditSource, callback) {
});
}
function configure(appId, data, auditSource, callback) {
function configure(appId, data, user, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert(data && typeof data === 'object');
assert(user && typeof user === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
get(appId, function (error, app) {
if (error) return callback(error);
var domain, location, portBindings, values = { };
let domain, location, portBindings, values = { }, mailboxName;
if ('location' in data) location = values.location = data.location.toLowerCase();
else location = app.location;
@@ -649,9 +682,10 @@ function configure(appId, data, auditSource, callback) {
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
error = validatePortBindings(data.portBindings, app.manifest);
if (error) return callback(error);
values.portBindings = translatePortBindings(data.portBindings, app.manifest);
portBindings = data.portBindings;
} else {
portBindings = app.portBindings;
}
@@ -681,26 +715,36 @@ function configure(appId, data, auditSource, callback) {
}
if ('mailboxName' in data) {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
if (data.mailboxName === '') { // special case to reset back to .app
mailboxName = mailboxNameForLocation(location, app.manifest);
} else {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
mailboxName = data.mailboxName;
}
} else { // keep existing name or follow the new location
mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
}
if ('alternateDomains' in data) {
// TODO validate all subdomains [{ domain: '', subdomain: ''}]
values.alternateDomains = data.alternateDomains;
values.alternateDomains.forEach(function (ad) { ad.subdomain = addSpacesSuffix(ad.subdomain, user); }); // TODO: validate these
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var fqdn = domains.fqdn(location, domain, domainObject.provider);
location = addSpacesSuffix(location, user);
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
let fqdn = domains.fqdn(location, domainObject);
if (data.cert && data.key) {
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
@@ -720,10 +764,8 @@ function configure(appId, data, auditSource, callback) {
debug('Will configure app with id:%s values:%j', appId, values);
// make the mailbox name follow the apps new location, if the user did not set it explicitly
var oldName = app.mailboxName;
var newName = data.mailboxName || (app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName);
mailboxdb.updateName(oldName, values.oldConfig.domain, newName, domain, function (error) {
if (newName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
mailboxdb.updateName(app.mailboxName /* old */, values.oldConfig.domain, mailboxName, domain, function (error) {
if (mailboxName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
@@ -916,9 +958,10 @@ function restore(appId, data, auditSource, callback) {
});
}
function clone(appId, data, auditSource, callback) {
function clone(appId, data, user, auditSource, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(user && typeof user === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -950,15 +993,16 @@ function clone(appId, data, auditSource, callback) {
error = checkManifestConstraints(backupInfo.manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, backupInfo.manifest.tcpPorts);
error = validatePortBindings(portBindings, backupInfo.manifest);
if (error) return callback(error);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
if (error) return callback(error);
location = addSpacesSuffix(location, user);
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
@@ -974,7 +1018,7 @@ function clone(appId, data, auditSource, callback) {
robotsTxt: app.robotsTxt
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, portBindings, data, function (error) {
appdb.add(newAppId, app.appStoreId, manifest, location, domain, ownerId, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
@@ -1159,11 +1203,12 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
function canAutoupdateApp(app, newManifest) {
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
var newTcpPorts = newManifest.tcpPorts || { };
var portBindings = app.portBindings; // this is never null
const newTcpPorts = newManifest.tcpPorts || { };
const newUdpPorts = newManifest.udpPorts || { };
const portBindings = app.portBindings; // this is never null
for (var env in portBindings) {
if (!(env in newTcpPorts)) return new Error(env + ' was in use but new update removes it');
for (let portName in portBindings) {
if (!(portName in newTcpPorts) && !(portName in newUdpPorts)) return new Error(`${portName} was in use but new update removes it`);
}
// it's fine if one or more (unused) keys got removed
+11 -31
View File
@@ -95,6 +95,7 @@ function isFreePlan(subscription) {
return !subscription || subscription.plan.id === 'free';
}
// See app.js install it will create a db record first but remove it again if appstore purchase fails
function purchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appstoreId, 'string');
@@ -102,41 +103,20 @@ function purchase(appId, appstoreId, callback) {
if (appstoreId === '') return callback(null);
function doThePurchase() {
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
callback(null);
});
});
}
getSubscription(function (error, result) {
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
// only check for app install count if on the free plan
if (result.id !== 'free') return doThePurchase();
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
appdb.getAppStoreIds(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
var count = result.filter(function (a) { return !!a.appStoreId; }).length;
// we only allow max of 2 app installations without a subscription
// WARNING install and clone in apps.js will first add the db record and then call purchase() so we test for more than 2 here
if (count > 2) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, 'Too many apps installed'));
doThePurchase();
callback(null);
});
});
}
+29 -14
View File
@@ -46,7 +46,6 @@ var addons = require('./addons.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
@@ -135,6 +134,20 @@ function createContainer(app, callback) {
});
}
// Only delete the main container of the app, not destroy any docker addon created ones
function deleteMainContainer(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting main app container');
docker.deleteContainer(app.containerId, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
});
}
function deleteContainers(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -345,7 +358,7 @@ function unregisterAlternateDomains(app, all, callback) {
assert.strictEqual(typeof all, 'boolean');
assert.strictEqual(typeof callback, 'function');
var obsoleteDomains
var obsoleteDomains;
if (all) obsoleteDomains = app.alternateDomains;
else obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) { return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; }); });
@@ -404,13 +417,12 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
if (error) return callback(error);
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, callback) {
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
domains.waitForDnsRecord(fqdn, domain.domain, ip, { interval: 5000, times: 240 }, callback);
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, iteratorCallback);
}, callback);
});
});
@@ -444,7 +456,7 @@ function install(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
deleteMainContainer.bind(null, app),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring
@@ -470,6 +482,9 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
@@ -557,7 +572,7 @@ function configure(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
deleteMainContainer.bind(null, app),
unregisterAlternateDomains.bind(null, app, false /* all */),
function (next) {
if (!locationChanged) return next();
@@ -658,7 +673,7 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
deleteMainContainer.bind(null, app),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
@@ -670,14 +685,14 @@ function update(app, callback) {
// free unused ports
function (next) {
// make sure we always have objects
var currentPorts = app.portBindings || {};
var newPorts = app.updateConfig.manifest.tcpPorts || {};
const currentPorts = app.portBindings || {};
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
if (newPorts[portName]) return callback(); // port still in use
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
appdb.delPortBinding(currentPorts[portName], function (error) {
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
else if (error) return next(error);
+17 -6
View File
@@ -124,6 +124,9 @@ function testConfig(backupConfig, callback) {
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BackupsError(BackupsError.BAD_FIELD, 'unknown format'));
// remember to adjust the cron ensureBackup task interval accordingly
if (backupConfig.intervalSecs < 6 * 60 * 60) return callback(new BackupsError(BackupsError.BAD_FIELD, 'Interval must be atleast 6 hours'));
api(backupConfig.provider).testConfig(backupConfig, callback);
}
@@ -562,9 +565,13 @@ function restore(backupConfig, backupId, callback) {
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, function (error) {
if (error) return callback(error);
debug('restore: download completed, importing database');
database.importFromFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('restore: database imported');
callback();
});
});
@@ -981,15 +988,19 @@ function ensureBackup(auditSource, callback) {
getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) {
if (error) {
debug('Unable to list backups', error);
return callback(error); // no point trying to backup if appstore is down
return callback(error);
}
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
backup(auditSource, callback);
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < (backupConfig.intervalSecs - 3600) * 1000)) { // adjust 1 hour
debug('Previous backup was %j, no need to backup now', backups[0]);
return callback(null);
}
backup(auditSource, callback);
});
});
}
+64 -62
View File
@@ -3,7 +3,7 @@
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme'),
debug = require('debug')('box:cert/acme1'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
parseLinks = require('parse-links'),
@@ -25,7 +25,7 @@ exports = module.exports = {
_name: 'acme'
};
function AcmeError(reason, errorOrMessage) {
function Acme1Error(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -43,18 +43,18 @@ function AcmeError(reason, errorOrMessage) {
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';
util.inherits(Acme1Error, Error);
Acme1Error.INTERNAL_ERROR = 'Internal Error';
Acme1Error.EXTERNAL_ERROR = 'External Error';
Acme1Error.ALREADY_EXISTS = 'Already Exists';
Acme1Error.NOT_COMPLETED = 'Not Completed';
Acme1Error.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) {
function Acme1(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
@@ -62,7 +62,7 @@ function Acme(options) {
this.email = options.email;
}
Acme.prototype.getNonce = function (callback) {
Acme1.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
@@ -91,7 +91,7 @@ function getModulus(pem) {
return Buffer.from(match[1], 'hex');
}
Acme.prototype.sendSignedRequest = function (url, payload, callback) {
Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -136,7 +136,7 @@ Acme.prototype.sendSignedRequest = function (url, payload, callback) {
});
};
Acme.prototype.updateContact = function (registrationUri, callback) {
Acme1.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -151,8 +151,8 @@ Acme.prototype.updateContact = function (registrationUri, callback) {
var that = this;
this.sendSignedRequest(registrationUri, 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 !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
debug('updateContact: contact of user updated to %s', that.email);
@@ -160,7 +160,7 @@ Acme.prototype.updateContact = function (registrationUri, callback) {
});
};
Acme.prototype.registerUser = function (callback) {
Acme1.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
@@ -173,9 +173,9 @@ Acme.prototype.registerUser = function (callback) {
var that = this;
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 (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
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)));
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerUser: registered user %s', that.email);
@@ -183,7 +183,7 @@ Acme.prototype.registerUser = function (callback) {
});
};
Acme.prototype.registerDomain = function (domain, callback) {
Acme1.prototype.registerDomain = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -198,9 +198,9 @@ Acme.prototype.registerDomain = function (domain, callback) {
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)));
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerDomain: registered %s', domain);
@@ -208,7 +208,7 @@ Acme.prototype.registerDomain = function (domain, callback) {
});
};
Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -232,13 +232,13 @@ Acme.prototype.prepareHttpChallenge = function (challenge, callback) {
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));
if (error) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, error));
callback();
});
};
Acme.prototype.notifyChallengeReady = function (challenge, callback) {
Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -252,14 +252,14 @@ Acme.prototype.notifyChallengeReady = function (challenge, callback) {
};
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)));
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme.prototype.waitForChallenge = function (challenge, callback) {
Acme1.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -271,18 +271,18 @@ Acme.prototype.waitForChallenge = function (challenge, callback) {
superagent.get(challenge.uri).timeout(30 * 1000).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
return retryCallback(new Acme1Error(Acme1Error.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));
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
if (result.body.status === 'pending') return retryCallback(new Acme1Error(Acme1Error.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
else return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
@@ -291,7 +291,7 @@ Acme.prototype.waitForChallenge = function (challenge, callback) {
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme.prototype.signCertificate = function (domain, csrDer, callback) {
Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
@@ -306,13 +306,13 @@ Acme.prototype.signCertificate = function (domain, csrDer, callback) {
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));
if (error) return callback(new Acme1Error(Acme1Error.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)));
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.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'));
if (!certUrl) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
@@ -320,7 +320,7 @@ Acme.prototype.signCertificate = function (domain, csrDer, callback) {
});
};
Acme.prototype.createKeyAndCsr = function (domain, callback) {
Acme1.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -333,15 +333,15 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
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));
if (!key) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.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));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
if (!csrDer) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
@@ -349,13 +349,13 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
};
// TODO: download the chain in a loop following 'up' header
Acme.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
Acme1.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
debug('downloadChain: linkHeader %s', linkHeader);
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
@@ -366,18 +366,18 @@ Acme.prototype.downloadChain = function (linkHeader, callback) {
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
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)));
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!chainPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -390,9 +390,9 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).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)));
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var certificateDer = result.text;
@@ -400,14 +400,14 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
debug('downloadCertificate: cert der file for %s saved', domain);
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));
if (!certificatePem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
@@ -416,14 +416,14 @@ Acme.prototype.downloadCertificate = function (domain, certUrl, callback) {
});
};
Acme.prototype.acmeFlow = function (domain, callback) {
Acme1.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
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));
if (!this.accountKeyPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
@@ -441,7 +441,7 @@ Acme.prototype.acmeFlow = function (domain, callback) {
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'));
if (httpChallenges.length === 0) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'no http challenges'));
var challenge = httpChallenges[0];
async.waterfall([
@@ -456,24 +456,26 @@ Acme.prototype.acmeFlow = function (domain, callback) {
});
};
Acme.prototype.getCertificate = function (domain, callback) {
Acme1.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
this.acmeFlow(domain, function (error) {
debug('getCertificate: start acme flow for %s from %s', hostname, this.caOrigin);
this.acmeFlow(hostname, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.key'));
});
};
function getCertificate(domain, options, callback) {
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme(options || { });
acme.getCertificate(domain, callback);
var acme = new Acme1(options || { });
acme.getCertificate(hostname, domain, callback);
}
+633
View File
@@ -0,0 +1,633 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme'
};
function Acme2Error(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(Acme2Error, Error);
Acme2Error.INTERNAL_ERROR = 'Internal Error';
Acme2Error.EXTERNAL_ERROR = 'External Error';
Acme2Error.ALREADY_EXISTS = 'Already Exists';
Acme2Error.NOT_COMPLETED = 'Not Completed';
Acme2Error.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 Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = null; // Buffer
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
this.directory = {};
this.performHttpAuthorization = !!options.performHttpAuthorization;
this.wildcard = !!options.wildcard;
}
Acme2.prototype.getNonce = function (callback) {
superagent.get(this.directory.newNonce).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 204) 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'));
}
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme2.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));
const that = this;
let header = {
url: url,
alg: 'RS256'
};
// keyId is null when registering account
if (this.keyId) {
header.kid = this.keyId;
} else {
header.jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
}
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 = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme2.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
// https://github.com/ietf-wg-acme/acme/issues/30
const payload = {
contact: [ 'mailto:' + this.email ]
};
const that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
debug(`updateContact: contact of user updated to ${that.email}`);
callback();
});
};
Acme2.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
var that = this;
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
// 200 if already exists. 201 for new accounts
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
debug(`registerUser: user registered keyid: ${result.headers.location}`);
that.keyId = result.headers.location;
that.updateContact(result.headers.location, callback);
});
};
Acme2.prototype.newOrder = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
identifiers: [{
type: 'dns',
value: domain
}]
};
debug('newOrder: %s', domain);
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
callback(null, order, orderUrl);
});
};
Acme2.prototype.waitForOrder = function (orderUrl, callback) {
assert.strictEqual(typeof orderUrl, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`waitForOrder: ${orderUrl}`);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
debug('waitForOrder: getting status');
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForOrder: network error getting uri %s', orderUrl);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 200) {
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
});
}, callback);
};
Acme2.prototype.getKeyAuthorization = function (token) {
assert(util.isBuffer(this.accountKeyPem));
let jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
let shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
let thumbprint = urlBase64Encode(shasum.digest('base64'));
return token + '.' + thumbprint;
};
Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.url);
const keyAuthorization = this.getKeyAuthorization(challenge.token);
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme2.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.url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.url);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 200) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme2Error(Acme2Error.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
Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof finalizationUrl, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
const payload = {
csr: b64(csrDer)
};
debug('signCertificate: sending sign request');
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
return callback(null);
});
};
Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
var csrFile = path.join(outdir, `${certName}.csr`);
var privateKeyFile = path.join(outdir, `${certName}.key`);
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
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(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
const fullChainPem = result.text;
const certName = hostname.replace('*.', '_.');
var certificateFile = path.join(outdir, `${certName}.cert`);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
callback();
});
};
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
callback(null, challenge);
});
};
Acme2.prototype.cleanupHttpChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.unlink(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), callback);
};
function getChallengeSubdomain(hostname, domain) {
let challengeSubdomain;
if (hostname === domain) {
challengeSubdomain = '_acme-challenge';
} else if (hostname.includes('*')) { // wildcard
challengeSubdomain = hostname.replace('*', '_acme-challenge').slice(0, -domain.length - 1);
} else {
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
}
return challengeSubdomain;
}
Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
let challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
domains.waitForDnsRecord(`${challengeSubdomain}`, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
callback(null, challenge);
});
});
};
Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
callback(null);
});
};
Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof callback, 'function');
const that = this;
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
const authorization = response.body;
if (that.performHttpAuthorization) {
that.prepareHttpChallenge(hostname, domain, authorization, callback);
} else {
that.prepareDnsChallenge(hostname, domain, authorization, callback);
}
});
};
Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
if (this.performHttpAuthorization) {
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
} else {
this.cleanupDnsChallenge(hostname, domain, challenge, callback);
}
};
Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
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 Acme2Error(Acme2Error.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;
this.registerUser(function (error) {
if (error) return callback(error);
that.newOrder(hostname, function (error, order, orderUrl) {
if (error) return callback(error);
async.eachSeries(order.authorizations, function (authorizationUrl, iteratorCallback) {
debug(`acmeFlow: authorizing ${authorizationUrl}`);
that.prepareChallenge(hostname, domain, authorizationUrl, function (error, challenge) {
if (error) return iteratorCallback(error);
async.waterfall([
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, hostname),
that.signCertificate.bind(that, hostname, order.finalize),
that.waitForOrder.bind(that, orderUrl),
that.downloadCertificate.bind(that, hostname)
], function (error) {
that.cleanupChallenge(hostname, domain, challenge, function (cleanupError) {
if (cleanupError) debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
iteratorCallback(error);
});
});
});
}, callback);
});
});
};
Acme2.prototype.getDirectory = function (callback) {
const that = this;
superagent.get(this.caDirectory).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching directory : ' + response.statusCode));
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') return callback(new Error(`Invalid response body : ${response.body}`));
that.directory = response.body;
callback(null);
});
};
Acme2.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getCertificate: start acme flow for ${hostname} from ${this.caDirectory}`);
if (hostname !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
hostname = domains.makeWildcard(hostname);
debug(`getCertificate: will get wildcard cert for ${hostname}`);
}
const that = this;
this.getDirectory(function (error) {
if (error) return callback(error);
that.acmeFlow(hostname, domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
callback(null, path.join(outdir, `${certName}.cert`), path.join(outdir, `${certName}.key`));
});
});
};
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme2(options || { });
acme.getCertificate(hostname, domain, callback);
}
+4 -3
View File
@@ -10,12 +10,13 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', vhost);
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
+4 -3
View File
@@ -10,12 +10,13 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', vhost);
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
+2 -1
View File
@@ -12,7 +12,8 @@ exports = module.exports = {
var assert = require('assert');
function getCertificate(domain, options, callback) {
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
+21 -8
View File
@@ -68,7 +68,7 @@ ClientsError.NOT_FOUND = 'Not found';
ClientsError.INTERNAL_ERROR = 'Internal Error';
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
function validateName(name) {
function validateClientName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
@@ -79,6 +79,14 @@ function validateName(name) {
return null;
}
function validateTokenName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
return null;
}
function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
@@ -89,7 +97,7 @@ function add(appId, type, redirectURI, scope, callback) {
var error = accesscontrol.validateScopeString(scope);
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
error = validateName(appId);
error = validateClientName(appId);
if (error) return callback(error);
var id = 'cid-' + uuid.v4();
@@ -244,12 +252,17 @@ function delByAppIdAndType(appId, type, callback) {
});
}
function addTokenByUserId(clientId, userId, expiresAt, callback) {
function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const name = options.name || '';
let error = validateTokenName(name);
if (error) return callback(error);
get(clientId, function (error, result) {
if (error) return callback(error);
@@ -265,7 +278,7 @@ function addTokenByUserId(clientId, userId, expiresAt, callback) {
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), function (error) {
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
@@ -282,17 +295,17 @@ function addTokenByUserId(clientId, userId, expiresAt, callback) {
}
// this issues a cid-cli token that does not require a password in various routes
function issueDeveloperToken(userObject, ip, callback) {
function issueDeveloperToken(userObject, auditSource, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
addTokenByUserId('cid-cli', userObject.id, expiresAt, function (error, result) {
addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: users.removePrivateFields(userObject) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
callback(null, result);
});
+8 -5
View File
@@ -74,7 +74,7 @@ function initialize(callback) {
async.series([
settings.initialize,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
cron.startPreActivationJobs,
onActivated
], callback);
}
@@ -83,7 +83,7 @@ function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
cron.uninitialize,
cron.stopJobs,
platform.stop,
settings.uninitialize
], callback);
@@ -99,7 +99,10 @@ function onActivated(callback) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (!count) return callback(); // not activated
platform.start(callback);
async.series([
platform.start,
cron.startPostActivationJobs
], callback);
});
}
@@ -145,10 +148,10 @@ function getConfig(callback) {
version: config.version(),
progress: progress.getAll(),
isDemo: config.isDemo(),
edition: config.edition(),
memory: os.totalmem(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
spaces: allSettings[settings.SPACES_CONFIG_KEY] // here because settings route cannot be accessed by spaces users
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
});
});
}
+31 -1
View File
@@ -24,6 +24,7 @@ exports = module.exports = {
version: version,
setVersion: setVersion,
database: database,
edition: edition,
// these values are derived
adminOrigin: adminOrigin,
@@ -38,6 +39,12 @@ exports = module.exports = {
isDemo: isDemo,
// feature flags based on editions (these have a separate license from standard edition)
isSpacesEnabled: isSpacesEnabled,
allowHyphenatedSubdomains: allowHyphenatedSubdomains,
allowOperatorActions: allowOperatorActions,
isAdminDomainLocked: isAdminDomainLocked,
// for testing resets to defaults
_reset: _reset
};
@@ -76,7 +83,8 @@ function saveSync() {
adminFqdn: data.adminFqdn,
adminLocation: data.adminLocation,
provider: data.provider,
isDemo: data.isDemo
isDemo: data.isDemo,
edition: data.edition
};
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
@@ -103,6 +111,8 @@ function initConfig() {
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.dockerProxyPort = 3003;
data.edition = '';
// keep in sync with start.sh
data.database = {
@@ -219,6 +229,23 @@ function isDemo() {
return get('isDemo') === true;
}
function isSpacesEnabled() {
return get('edition') === 'education';
}
function allowHyphenatedSubdomains() {
// we should move caas also to hostingprovider edition at some point
return get('edition') === 'hostingprovider' || get('provider') === 'caas';
}
function allowOperatorActions() {
return get('edition') !== 'hostingprovider';
}
function isAdminDomainLocked() {
return get('edition') === 'hostingprovider';
}
function provider() {
return get('provider');
}
@@ -235,3 +262,6 @@ function dkimSelector() {
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
}
function edition() {
return get('edition');
}
-2
View File
@@ -19,8 +19,6 @@ exports = module.exports = {
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
+13 -7
View File
@@ -1,8 +1,10 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize
startPostActivationJobs: startPostActivationJobs,
startPreActivationJobs: startPreActivationJobs,
stopJobs: stopJobs
};
var apps = require('./apps.js'),
@@ -55,9 +57,7 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
// Months: 0-11
// Day of Week: 0-6
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
function startPreActivationJobs(callback) {
if (config.provider() === 'caas') {
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
@@ -71,6 +71,12 @@ function initialize(callback) {
});
}
callback();
}
function startPostActivationJobs(callback) {
assert.strictEqual(typeof callback, 'function');
var randomHourMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
@@ -102,7 +108,7 @@ function recreateJobs(tz) {
if (gJobs.backup) gJobs.backup.stop();
gJobs.backup = new CronJob({
cronTime: '00 00 */6 * * *', // every 6 hours. backups.ensureBackup() will only trigger a backup once per day
cronTime: '00 00 */6 * * *', // check every 6 hours
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
@@ -263,7 +269,7 @@ function dynamicDnsChanged(enabled) {
}
}
function uninitialize(callback) {
function stopJobs(callback) {
assert.strictEqual(typeof callback, 'function');
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
+2 -1
View File
@@ -130,7 +130,8 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
var credentials = {
token: dnsConfig.token,
fqdn: domain
fqdn: domain,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
};
const testSubdomain = 'cloudrontestdns';
+4 -2
View File
@@ -28,10 +28,12 @@ function translateRequestError(result, callback) {
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = error.message;
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
if (error.code === 6003) {
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
else message = 'Invalid credentials';
} else if (error.error_chain[0] && error.error_chain[0].message) {
message = message + ' ' + error.error_chain[0].message;
}
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
@@ -112,7 +114,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var priority = null;
if (type === 'MX') {
priority = value.split(' ')[0];
priority = parseInt(value.split(' ')[0], 10);
value = value.split(' ')[1];
}
+3 -2
View File
@@ -21,6 +21,7 @@ const GODADDY_API = 'https://api.godaddy.com/v1/domains';
// this is a workaround for godaddy not having a delete API
// https://stackoverflow.com/questions/39347464/delete-record-libcloud-godaddy-api
const GODADDY_INVALID_IP = '0.0.0.0';
const GODADDY_INVALID_TXT = '""';
function formatError(response) {
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
@@ -109,7 +110,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
if (type !== 'A') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Not supported by GoDaddy API'))); // can never happen
if (type !== 'A' && type !== 'TXT') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Record deletion is not supported by GoDaddy API')));
// check if the record exists at all so that we don't insert the "Dead" record for no reason
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
@@ -119,7 +120,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
// godaddy does not have a delete API. so fill it up with an invalid IP that we can ignore in future get()
var records = [{
ttl: 600,
data: GODADDY_INVALID_IP
data: type === 'A' ? GODADDY_INVALID_IP : GODADDY_INVALID_TXT
}];
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
+1 -1
View File
@@ -60,6 +60,6 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
callback(null, { wildcard: !!dnsConfig.wildcard });
callback(null, {});
});
}
+3
View File
@@ -208,6 +208,9 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
var credentials = {
username: dnsConfig.username,
token: dnsConfig.token
+2 -1
View File
@@ -46,9 +46,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, value, options, callback) {
function waitForDns(domain, zoneName, type, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
+2 -2
View File
@@ -114,7 +114,7 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error, result) {
route53.changeResourceRecordSets(params, function(error) {
if (error && error.code === 'AccessDenied') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 'PriorRequestNotComplete') return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
@@ -241,7 +241,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
secretAccessKey: dnsConfig.secretAccessKey,
region: dnsConfig.region || 'us-east-1',
endpoint: dnsConfig.endpoint || null,
listHostedZonesByName: true // new/updated creds require this perm
listHostedZonesByName: true, // new/updated creds require this perm
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
+20 -8
View File
@@ -30,8 +30,9 @@ function resolveIp(hostname, options, callback) {
});
}
function isChangeSynced(domain, value, nameserver, callback) {
function isChangeSynced(domain, type, value, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -44,20 +45,30 @@ function isChangeSynced(domain, value, nameserver, callback) {
}
async.every(nsIps, function (nsIp, iteratorCallback) {
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
const resolveOptions = { server: nsIp, timeout: 5000 };
const resolver = type === 'A' ? resolveIp.bind(null, domain) : dns.resolve.bind(null, domain, 'TXT');
resolver(resolveOptions, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain} (${type})`);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain} (${type}): ${error}`);
return iteratorCallback(null, false);
}
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
let match;
if (type === 'A') {
match = answer.length === 1 && answer[0] === value;
} else if (type === 'TXT') { // answer is a 2d array of strings
match = answer.some(function (a) { return value === a.join(''); });
}
iteratorCallback(null, answer.length === 1 && answer[0] === value);
debug(`isChangeSynced: ${domain} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
iteratorCallback(null, match);
});
}, callback);
@@ -65,9 +76,10 @@ function isChangeSynced(domain, value, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, value, options, callback) {
function waitForDns(domain, zoneName, type, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
@@ -82,7 +94,7 @@ function waitForDns(domain, zoneName, value, options, callback) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
async.every(nameservers, isChangeSynced.bind(null, domain, type, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
+79
View File
@@ -0,0 +1,79 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
DomainsError = require('../domains.js').DomainsError,
sysinfo = require('../sysinfo.js'),
util = require('util');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
return callback();
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
const separator = dnsConfig.hyphenatedSubdomains ? '-' : '.';
const fqdn = `cloudrontest${separator}${domain}`;
dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, `Unable to resolve ${fqdn}`));
if (error || !result) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`));
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error.message}`));
if (result.length !== 1 || ip !== result[0]) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`));
callback(null, {});
});
});
});
}
+9 -6
View File
@@ -127,14 +127,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
dockerPortBindings[manifest.httpPort + '/tcp'] = [ { HostIp: '127.0.0.1', HostPort: app.httpPort + '' } ];
var portEnv = [];
for (var e in app.portBindings) {
var hostPort = app.portBindings[e];
var containerPort = manifest.tcpPorts[e].containerPort || hostPort;
for (let portName in app.portBindings) {
const hostPort = app.portBindings[portName];
const portType = portName in manifest.tcpPorts ? 'tcp' : 'udp';
const ports = portType == 'tcp' ? manifest.tcpPorts : manifest.udpPorts;
exposedPorts[containerPort + '/tcp'] = {};
portEnv.push(e + '=' + hostPort);
var containerPort = ports[portName].containerPort || hostPort;
dockerPortBindings[containerPort + '/tcp'] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
exposedPorts[`${containerPort}/${portType}`] = {};
portEnv.push(`${portName}=${hostPort}`);
dockerPortBindings[`${containerPort}/${portType}`] = [ { HostIp: '0.0.0.0', HostPort: hostPort + '' } ];
}
// first check db record, then manifest
+178
View File
@@ -0,0 +1,178 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
};
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
config = require('./config.js'),
express = require('express'),
debug = require('debug')('box:dockerproxy'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
middleware = require('./middleware'),
net = require('net'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
_ = require('underscore');
var gHttpServer = null;
function authorizeApp(req, res, next) {
// TODO add here some authorization
// - block apps not using the docker addon
// - block calls regarding platform containers
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (config.TEST) {
req.app = { id: 'testappid' };
return next();
}
apps.getByIpAddress(req.connection.remoteAddress, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error));
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
req.app = app;
next();
});
}
function attachDockerRequest(req, res, next) {
var options = {
socketPath: '/var/run/docker.sock',
method: req.method,
path: req.url,
headers: req.headers
};
req.dockerRequest = http.request(options, function (dockerResponse) {
res.writeHead(dockerResponse.statusCode, dockerResponse.headers);
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
res.write(' ');
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
dockerResponse.pipe(res, { end: true });
});
next();
}
function containersCreate(req, res, next) {
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
debug('Original volume binds:', req.body.HostConfig.Binds);
let binds = [];
for (let bind of (req.body.HostConfig.Binds || [])) {
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
else binds.push(`${dockerDataDir}/${bind}`);
}
// cleanup the paths from potential double slashes
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
debug('Rewritten volume binds:', binds);
safe.set(req.body, 'HostConfig.Binds', binds);
let plainBody = JSON.stringify(req.body);
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
req.dockerRequest.end(plainBody);
}
function process(req, res, next) {
// we have to rebuild the body since we consumed in in the parser
if (Object.keys(req.body).length !== 0) {
let plainBody = JSON.stringify(req.body);
req.dockerRequest.setHeader('Content-Length', Buffer.byteLength(plainBody));
req.dockerRequest.end(plainBody);
} else if (!req.readable) {
req.dockerRequest.end();
} else {
req.pipe(req.dockerRequest, { end: true });
}
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
assert(gHttpServer === null, 'Already started');
let json = middleware.json({ strict: true });
let router = new express.Router();
router.post('/:version/containers/create', containersCreate);
let proxyServer = express();
if (config.TEST) {
proxyServer.use(function (req, res, next) {
console.log('Proxying: ' + req.method, req.url);
next();
});
}
proxyServer.use(authorizeApp)
.use(attachDockerRequest)
.use(json)
.use(router)
.use(process)
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(config.get('dockerProxyPort'), '0.0.0.0', callback);
debug(`startDockerProxy: started proxy on port ${config.get('dockerProxyPort')}`);
gHttpServer.on('upgrade', function (req, client, head) {
// Create a new tcp connection to the TCP server
var remote = net.connect('/var/run/docker.sock', function () {
var upgradeMessage = req.method + ' ' + req.url + ' HTTP/1.1\r\n' +
`Host: ${req.headers.host}\r\n` +
'Connection: Upgrade\r\n' +
'Upgrade: tcp\r\n';
if (req.headers['content-type'] === 'application/json') {
// TODO we have to parse the immediate upgrade request body, but I don't know how
let plainBody = '{"Detach":false,"Tty":false}\r\n';
upgradeMessage += `Content-Type: application/json\r\n`;
upgradeMessage += `Content-Length: ${Buffer.byteLength(plainBody)}\r\n`;
upgradeMessage += '\r\n';
upgradeMessage += plainBody;
}
upgradeMessage += '\r\n';
// resend the upgrade event to the docker daemon, so it responds with the proper message through the pipes
remote.write(upgradeMessage);
// two-way pipes between client and docker daemon
client.pipe(remote).pipe(client);
});
});
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
if (gHttpServer) gHttpServer.close();
gHttpServer = null;
callback();
}
+149 -47
View File
@@ -6,6 +6,7 @@ module.exports = exports = {
getAll: getAll,
update: update,
del: del,
isLocked: isLocked,
fqdn: fqdn,
setAdmin: setAdmin,
@@ -19,12 +20,17 @@ module.exports = exports = {
removePrivateFields: removePrivateFields,
removeRestrictedFields: removeRestrictedFields,
validateHostname: validateHostname,
makeWildcard: makeWildcard,
DomainsError: DomainsError
};
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
@@ -69,7 +75,7 @@ DomainsError.STILL_BUSY = 'Still busy';
DomainsError.IN_USE = 'In Use';
DomainsError.INTERNAL_ERROR = 'Internal error';
DomainsError.ACCESS_DENIED = 'Access denied';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, manual or caas';
DomainsError.INVALID_PROVIDER = 'provider must be route53, gcdns, digitalocean, gandi, cloudflare, namecom, noop, wildcard, manual or caas';
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
@@ -86,12 +92,13 @@ function api(provider) {
case 'namecom': return require('./dns/namecom.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
case 'wildcard': return require('./dns/wildcard.js');
default: return null;
}
}
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert(config && typeof config === 'object'); // the dns config to test with
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, callback) {
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
@@ -99,17 +106,92 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert.strictEqual(typeof callback, 'function');
var backend = api(provider);
if (!backend) return callback(new DomainsError(DomainsError.INVALID_PROVIDER));
if (!backend) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid provider'));
api(provider).verifyDnsConfig(config, domain, zoneName, ip, callback);
api(provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Incorrect configuration. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Configuration error: ' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
callback(null, result);
});
}
function fqdn(location, domainObject) {
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domainObject.domain;
}
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name (and not dns name)
function validateHostname(location, domainObject) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
const hostname = fqdn(location, domainObject);
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
if (hostname === config.adminFqdn()) return new DomainsError(DomainsError.BAD_FIELD, location + ' is reserved');
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
if (!tld.isValid(tmp)) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname is not a valid domain name');
if (hostname.length > 253) return new DomainsError(DomainsError.BAD_FIELD, 'Hostname length exceeds 253 characters');
if (location) {
// label validation
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new DomainsError(DomainsError.BAD_FIELD, 'Invalid subdomain length');
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot');
if (/^[-.]/.test(location)) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot');
}
if (domainObject.config.hyphenatedSubdomains) {
if (location.indexOf('.') !== -1) return new DomainsError(DomainsError.BAD_FIELD, 'Subdomain cannot contain a dot');
}
return null;
}
function validateTlsConfig(tlsConfig, dnsProvider) {
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof dnsProvider, 'string');
switch (tlsConfig.provider) {
case 'letsencrypt-prod':
case 'letsencrypt-staging':
case 'fallback':
case 'caas':
break;
default:
return new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging');
}
if (tlsConfig.wildcard) {
if (!tlsConfig.provider.startsWith('letsencrypt')) return new DomainsError(DomainsError.BAD_FIELD, 'wildcard can only be set with letsencrypt');
if (dnsProvider === 'manual' || dnsProvider === 'noop' || dnsProvider === 'wildcard') return new DomainsError(DomainsError.BAD_FIELD, 'wildcard cert requires a programmable DNS backend');
}
return null;
}
function add(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -129,20 +211,16 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
}
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record: ' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
if (error) return callback(error);
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
@@ -158,6 +236,10 @@ function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig,
});
}
function isLocked(domain) {
return domain === config.adminDomain() && config.isAdminDomainLocked();
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -167,6 +249,8 @@ function get(domain, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.locked = isLocked(domain);
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
@@ -188,15 +272,17 @@ function getAll(callback) {
domaindb.getAll(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.forEach(function (r) { r.locked = isLocked(r.domain); });
return callback(null, result);
});
}
function update(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
function update(domain, zoneName, provider, dnsConfig, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -216,20 +302,16 @@ function update(domain, zoneName, provider, config, fallbackCertificate, tlsConf
if (error) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
}
error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (dnsConfig.hyphenatedSubdomains && !config.allowHyphenatedSubdomains()) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Not allowed in this edition'));
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Error adding A record:' + error.message));
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.INVALID_PROVIDER) return callback(new DomainsError(DomainsError.BAD_FIELD, error.message));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, function (error, result) {
if (error) return callback(error);
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
@@ -252,6 +334,8 @@ function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new DomainsError(DomainsError.IN_USE));
domaindb.del(domain, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error && error.reason === DatabaseError.IN_USE) return callback(new DomainsError(DomainsError.IN_USE));
@@ -261,7 +345,8 @@ function del(domain, callback) {
});
}
function getName(domain, subdomain) {
// returns the 'name' that needs to be inserted into zone
function getName(domain, subdomain, type) {
// support special caas domains
if (domain.provider === 'caas') return subdomain;
@@ -269,7 +354,13 @@ function getName(domain, subdomain) {
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
return subdomain === '' ? part : subdomain + '.' + part;
if (subdomain === '') {
return part;
} else if (type === 'TXT') {
return `${subdomain}.${part}`;
} else {
return subdomain + (domain.config.hyphenatedSubdomains ? '-' : '.') + part;
}
}
function getDnsRecords(subdomain, domain, type, callback) {
@@ -281,7 +372,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -289,6 +380,7 @@ function getDnsRecords(subdomain, domain, type, callback) {
});
}
// note: for TXT records the values must be quoted
function upsertDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -301,7 +393,7 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
if (error) return callback(error);
callback(null);
@@ -321,7 +413,7 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
callback(null);
@@ -329,18 +421,20 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
});
}
// only wait for A record
function waitForDnsRecord(fqdn, domain, value, options, callback) {
assert.strictEqual(typeof fqdn, 'string');
function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
const hostname = fqdn(subdomain, domainObject);
api(domainObject.provider).waitForDns(hostname, domainObject.zoneName, type, value, options, callback);
});
}
@@ -360,7 +454,7 @@ function setAdmin(domain, callback) {
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
callback();
@@ -369,19 +463,27 @@ function setAdmin(domain, callback) {
});
}
function fqdn(location, domain, provider) {
return location + (location ? (provider !== 'caas' ? '.' : '-') : '') + domain;
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate');
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
if (result.fallbackCertificate) delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
return result;
}
// removes all fields that are not accessible by a normal user
function removeRestrictedFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'locked');
// always ensure config object
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
return result;
}
}
function makeWildcard(hostname) {
assert.strictEqual(typeof hostname, 'string');
let parts = hostname.split('.');
parts[0] = '*';
return parts.join('.');
}
+30 -38
View File
@@ -10,7 +10,6 @@ var assert = require('assert'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
eventlog = require('./eventlog.js'),
@@ -29,37 +28,32 @@ var NOOP = function () {};
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
function getAppByRequest(req, callback) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof callback, 'function');
// Will attach req.app if successful
function authenticateApp(req, res, next) {
var sourceIp = req.connection.ldap.id.split(':')[0];
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
if (sourceIp.split('.').length !== 4) return next(new ldap.InsufficientAccessRightsError('Missing source identifier'));
apps.getByIpAddress(sourceIp, function (error, app) {
if (error) return callback(new ldap.OperationsError(error.message));
if (error) return next(new ldap.OperationsError(error.message));
if (!app) return next(new ldap.OperationsError('Could not detect app source'));
if (!app) return callback(new ldap.OperationsError('Could not detect app source'));
req.app = app;
callback(null, app);
next();
});
}
function getUsersWithAccessToApp(req, callback) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof req.app, 'object');
assert.strictEqual(typeof callback, 'function');
getAppByRequest(req, function (error, app) {
if (error) return callback(error);
users.list(function (error, result) {
if (error) return callback(new ldap.OperationsError(error.toString()));
users.list(function (error, result) {
async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) {
if (error) return callback(new ldap.OperationsError(error.toString()));
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, allowedUsers) {
if (error) return callback(new ldap.OperationsError(error.toString()));
callback(null, allowedUsers);
});
callback(null, allowedUsers);
});
});
}
@@ -140,7 +134,7 @@ function userSearch(req, res, next) {
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
if (entry.admin || req.app.ownerId === entry.id) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
var nameParts = displayName.split(' ');
@@ -160,7 +154,7 @@ function userSearch(req, res, next) {
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
isadmin: entry.admin ? 1 : 0,
isadmin: (entry.admin || req.app.ownerId === entry.id) ? 1 : 0,
memberof: groups
}
};
@@ -200,7 +194,7 @@ function groupSearch(req, res, next) {
groups.forEach(function (group) {
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
var members = group.admin ? result.filter(function (entry) { return entry.admin || req.app.ownerId === entry.id; }) : result;
var obj = {
dn: dn.toString(),
@@ -249,7 +243,7 @@ function groupAdminsCompare(req, res, next) {
// we only support memberuid here, if we add new group attributes later add them here
if (req.attribute === 'memberuid') {
var found = result.find(function (u) { return u.id === req.value; });
if (found && found.admin) return res.end(true);
if (found && (found.admin || req.app.ownerId == found.id)) return res.end(true);
}
res.end(false);
@@ -414,6 +408,7 @@ function mailingListSearch(req, res, next) {
});
}
// Will attach req.user if successful
function authenticateUser(req, res, next) {
debug('user bind: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
@@ -445,21 +440,18 @@ function authenticateUser(req, res, next) {
}
function authorizeUserForApp(req, res, next) {
assert(req.user);
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.app, 'object');
getAppByRequest(req, function (error, app) {
if (error) return next(error);
apps.hasAccessTo(req.app, req.user, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
apps.hasAccessTo(app, req.user, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id, app: req.app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
res.end();
});
res.end();
});
}
@@ -526,9 +518,9 @@ function start(callback) {
gServer = ldap.createServer({ log: logger });
gServer.search('ou=users,dc=cloudron', userSearch);
gServer.search('ou=groups,dc=cloudron', groupSearch);
gServer.bind('ou=users,dc=cloudron', authenticateUser, authorizeUserForApp);
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
gServer.search('ou=groups,dc=cloudron', authenticateApp, groupSearch);
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
@@ -539,8 +531,8 @@ function start(callback) {
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
gServer.compare('cn=users,ou=groups,dc=cloudron', groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', groupAdminsCompare);
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
// this is the bind for addons (after bind, they might search and authenticate)
gServer.bind('ou=addons,dc=cloudron', function(req, res /*, next */) {
+37 -6
View File
@@ -278,6 +278,15 @@ function checkMx(domain, callback) {
});
}
function txtToDict(txt) {
var dict = {};
txt.split(';').forEach(function(v) {
var p = v.trim().split('=');
dict[p[0]]=p[1];
});
return dict;
}
function checkDmarc(domain, callback) {
var dmarc = {
domain: '_dmarc.' + domain,
@@ -293,7 +302,9 @@ function checkDmarc(domain, callback) {
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
dmarc.status = (dmarc.value === dmarc.expected);
// allow extra fields in dmarc like rua
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
}
callback(null, dmarc);
@@ -586,6 +597,20 @@ function restartMail(callback) {
});
}
function restartMailIfActivated(callback) {
assert.strictEqual(typeof callback, 'function');
users.count(function (error, count) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
if (!count) {
debug('restartMailIfActivated: skipping restart of mail container since Cloudron is not activated yet');
return callback(); // not provisioned yet, do not restart container after dns setup
}
restartMail(callback);
});
}
function getDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -614,7 +639,7 @@ function txtRecordsWithSpf(domain, callback) {
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
if (error) return callback(error);
if (error) return new MailError(MailError.EXTERNAL_ERROR, error.message);
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
@@ -730,10 +755,14 @@ function setDnsRecords(domain, callback) {
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, record.domain, 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);
if (error) {
debug(`addDnsRecords: failed to update: ${error}`);
return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
}
callback(error);
debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
callback(null);
});
});
});
@@ -750,7 +779,7 @@ function addDomain(domain, callback) {
async.series([
setDnsRecords.bind(null, domain), // do this first to ensure DKIM keys
restartMail
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
callback();
@@ -761,6 +790,8 @@ function removeDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === config.adminDomain()) return callback(new MailError(MailError.IN_USE));
maildb.del(domain, function (error) {
if (error && error.reason === DatabaseError.IN_USE) return callback(new MailError(MailError.IN_USE));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, error.message));
-17
View File
@@ -4,15 +4,6 @@ Dear <%= cloudronName %> Admin,
A new user with email <%= user.email %> was added to <%= cloudronName %>.
<% if (inviteLink) { %>
As requested, this user has not been sent an invitation email.
To set a password and perform any configuration on behalf of the user, please use this link:
<%= inviteLink %>
<% } %>
Powered by https://cloudron.io
<% } else { %>
@@ -27,14 +18,6 @@ Powered by https://cloudron.io
A new user with email <%= user.email %> was added to <%= cloudronName %>.
</p>
<% if (inviteLink) { %>
<p>
As requested, this user has not been sent an invitation email.<br/>
<br/>
<a href="<%= inviteLink %>">Set a password and perform any configuration on behalf of the user</a>
</p>
<% } %>
<br/>
<br/>
+2 -4
View File
@@ -226,11 +226,10 @@ function sendInvite(user, invitor) {
});
}
function userAdded(user, inviteSent) {
function userAdded(user) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof inviteSent, 'boolean');
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
debug('Sending mail for userAdded');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
@@ -239,7 +238,6 @@ function userAdded(user, inviteSent) {
var templateData = {
user: user,
inviteLink: inviteSent ? null : `${config.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
+1
View File
@@ -30,6 +30,7 @@ function resolve(hostname, rrtype, options, callback) {
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
// for TXT records, result is 2d array of strings
callback(error, result);
});
}
+113 -92
View File
@@ -22,10 +22,10 @@ exports = module.exports = {
removeAppConfigs: removeAppConfigs,
// exported for testing
_getApi: getApi
_getCertApi: getCertApi
};
var acme = require('./cert/acme.js'),
var acme2 = require('./cert/acme2.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
@@ -77,22 +77,22 @@ ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
function getCertApi(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.get(app.domain, function (error, domain) {
domains.get(domain, function (error, result) {
if (error) return callback(error);
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
if (result.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
var api = result.tlsConfig.provider === 'caas' ? caas : acme2;
var options = { };
if (domain.tlsConfig.provider === 'caas') {
options.prod = true;
} else { // acme
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
if (result.tlsConfig.provider !== 'caas') { // matches 'le-prod' or 'letsencrypt-prod'
options.prod = result.tlsConfig.provider.match(/.*-prod/) !== null;
options.performHttpAuthorization = result.provider.match(/noop|manual|wildcard/) !== null;
options.wildcard = !!result.tlsConfig.wildcard;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
@@ -127,14 +127,6 @@ function validateCertificate(domain, cert, key) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
function matchesDomain(candidate) {
if (typeof candidate !== 'string') return false;
if (candidate === domain) return true;
if (candidate.indexOf('*') === 0 && candidate.slice(2) === domain.slice(domain.indexOf('.') + 1)) return true;
return false;
}
// check for empty cert and key strings
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
@@ -203,83 +195,98 @@ function getFallbackCertificate(domain, callback) {
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath, type: 'provisioned' });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath });
callback(null, { certFilePath, keyFilePath, type: 'fallback' });
}
function getCertificateByHostname(hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
let certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.cert`);
let keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
if (hostname !== domain && domainObject.tlsConfig.wildcard) { // bare domain is not part of wildcard SAN
let certName = domains.makeWildcard(hostname).replace('*.', '_.');
certFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${certName}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
} else {
certFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${hostname}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
}
callback(null);
});
}
function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.key`);
getCertificateByHostname(app.fqdn, app.domain, function (error, result) {
if (error || result) return callback(error, result);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
return getFallbackCertificate(app.domain, callback);
return getFallbackCertificate(app.domain, callback);
});
}
function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
function ensureCertificate(vhost, domain, auditSource, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const vhost = app.fqdn;
getCertificateByHostname(vhost, domain, function (error, result) {
if (result) {
debug(`ensureCertificate: ${vhost}. certificate already exists at ${result.keyFilePath}`);
var certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.user.key`);
if (result.certFilePath.endsWith('.user.cert')) return callback(null, result); // user certs cannot be renewed
if (!isExpiringSync(result.certFilePath, 24 * 30)) return callback(null, result);
debug(`ensureCertificate: ${vhost} cert require renewal`);
} else {
debug(`ensureCertificate: ${vhost} cert does not exist`);
}
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. user certificate already exists at %s', vhost, keyFilePath);
return callback(null, { certFilePath, keyFilePath, reason: 'user' });
}
getCertApi(domain, function (error, api, apiOptions) {
if (error) return callback(error);
certFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${vhost}.key`);
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', vhost, keyFilePath);
api.getCertificate(vhost, domain, apiOptions, function (error, certFilePath, keyFilePath) {
var errorMessage = error ? error.message : '';
if (!isExpiringSync(certFilePath, 24 * 30)) return callback(null, { certFilePath, keyFilePath, reason: 'existing-le' });
debug('ensureCertificate: %s cert require renewal', vhost);
} else {
debug('ensureCertificate: %s cert does not exist', vhost);
}
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
mailer.certificateRenewalError(vhost, errorMessage);
}
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(domain, callback);
api.getCertificate(vhost, apiOptions, function (error, certFilePath, keyFilePath) {
var errorMessage = error ? error.message : '';
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
mailer.certificateRenewalError(vhost, errorMessage);
}
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.domain, callback);
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
callback(null, { certFilePath, keyFilePath, type: 'new-le' });
});
});
});
}
function configureAdminInternal(bundle, configFileName, vhost, callback) {
function writeAdminConfig(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -308,15 +315,14 @@ function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
ensureCertificate(config.adminFqdn(), config.adminDomain(), auditSource, function (error, bundle) {
if (error) return callback(error);
configureAdminInternal(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
writeAdminConfig(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
});
}
function configureAppInternal(app, bundle, callback) {
function writeAppConfig(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -349,7 +355,7 @@ function configureAppInternal(app, bundle, callback) {
reload(callback);
}
function configureAppRedirect(app, fqdn, bundle, callback) {
function writeAppRedirectConfig(app, fqdn, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof bundle, 'object');
@@ -385,20 +391,17 @@ function configureApp(app, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
ensureCertificate({ fqdn: app.fqdn, domain: app.domain }, auditSource, function (error, bundle) {
ensureCertificate(app.fqdn, app.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAppInternal(app, bundle, function (error) {
writeAppConfig(app, bundle, function (error) {
if (error) return callback(error);
// now setup alternateDomain redirects if any
async.eachSeries(app.alternateDomains, function (domain, callback) {
var fqdn = (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain;
ensureCertificate({ fqdn: fqdn, domain: domain.domain }, auditSource, function (error, bundle) {
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAppRedirect(app, fqdn, bundle, callback);
writeAppRedirectConfig(app, alternateDomain.fqdn, bundle, callback);
});
}, callback);
});
@@ -426,24 +429,42 @@ function renewAll(auditSource, callback) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
var appDomains = [];
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
// add webadmin domain
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: constants.NGINX_ADMIN_CONFIG_FILE_NAME });
// add app main
allApps.forEach(function (app) {
appDomains.push({ domain: app.domain, fqdn: app.fqdn, type: 'main', app: app, nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf') });
app.alternateDomains.forEach(function (alternateDomain) {
let nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, `${app.id}-redirect-${alternateDomain.fqdn}.conf`);
appDomains.push({ domain: alternateDomain.domain, fqdn: alternateDomain.fqdn, type: 'alternate', app: app, nginxConfigFilename: nginxConfigFilename });
});
});
async.eachSeries(appDomains, function (appDomain, iteratorCallback) {
ensureCertificate(appDomain.fqdn, appDomain.domain, auditSource, function (error, bundle) {
if (error) return iteratorCallback(error); // this can happen if cloudron is not setup yet
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
// reconfigure for the case where we got a renewed cert after fallback
var configureFunc = app.fqdn === config.adminFqdn() ?
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureAppInternal.bind(null, app, bundle);
// hack to check if the app's cert changed or not
let currentNginxConfig = safe.fs.readFileSync(appDomain.nginxConfigFilename, 'utf8') || '';
if (currentNginxConfig.includes(bundle.certFilePath)) return iteratorCallback();
// reconfigure since the cert changed
var configureFunc;
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return callback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
if (ignoredError) debug('renewAll: error reconfiguring app', ignoredError);
platform.handleCertChanged(app.fqdn);
platform.handleCertChanged(appDomain.fqdn);
iteratorCallback(); // move to next app
iteratorCallback(); // move to next domain
});
});
});
@@ -470,7 +491,7 @@ function configureDefaultServer(callback) {
safe.child_process.execSync(certCommand);
}
configureAdminInternal({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
writeAdminConfig({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
+1 -27
View File
@@ -5,13 +5,10 @@ exports = module.exports = {
uninitialize: uninitialize,
scope: scope,
websocketAuth: websocketAuth,
verifyAppOwnership: verifyAppOwnership
websocketAuth: websocketAuth
};
var accesscontrol = require('../accesscontrol.js'),
apps = require('../apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
@@ -21,7 +18,6 @@ var accesscontrol = require('../accesscontrol.js'),
HttpError = require('connect-lastmile').HttpError,
LocalStrategy = require('passport-local').Strategy,
passport = require('passport'),
settings = require('../settings.js'),
users = require('../users.js'),
UsersError = users.UsersError;
@@ -142,25 +138,3 @@ function websocketAuth(requiredScopes, req, res, next) {
next();
});
}
function verifyAppOwnership(req, res, next) {
if (req.user.admin) return next();
const appCreate = !('id' in req.params);
settings.getSpacesConfig(function (error, spaces) {
if (error) return next(new HttpError(500, error));
if (!spaces.enabled) return next();
if (appCreate) return next(); // ok to install app
apps.get(req.params.id, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
if (app.ownerId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
next();
});
});
}
+32 -5
View File
@@ -1,6 +1,8 @@
'use strict';
exports = module.exports = {
verifyOwnership: verifyOwnership,
getApp: getApp,
getApps: getApps,
getAppIcon: getAppIcon,
@@ -30,6 +32,7 @@ exports = module.exports = {
var apps = require('../apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:routes/apps'),
fs = require('fs'),
HttpError = require('connect-lastmile').HttpError,
@@ -44,6 +47,25 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function verifyOwnership(req, res, next) {
if (req.user.admin) return next();
if (!config.isSpacesEnabled()) return next();
const appCreate = !('id' in req.params);
if (appCreate) return next(); // ok to install app
apps.get(req.params.id, function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error) return next(new HttpError(500, error));
if (app.ownerId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
next();
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
@@ -117,9 +139,14 @@ function installApp(req, res, next) {
if (data.robotsTxt && typeof data.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt must be a string'));
if ('alternateDomains' in data) {
if (!Array.isArray(data.alternateDomains)) return next(new HttpError(400, 'alternateDomains must be an array'));
if (data.alternateDomains.some(function (d) { return (typeof d.domain !== 'string' || typeof d.subdomain !== 'string'); })) return next(new HttpError(400, 'alternateDomains array must contain objects with domain and subdomain strings'));
}
debug('Installing app :%j', data);
apps.install(data, auditSource(req), function (error, app) {
apps.install(data, req.user, auditSource(req), function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
@@ -127,7 +154,7 @@ function installApp(req, res, next) {
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, error.message));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, app));
@@ -169,7 +196,7 @@ function configureApp(req, res, next) {
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data, auditSource(req), function (error) {
apps.configure(req.params.id, data, req.user, auditSource(req), function (error) {
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -219,7 +246,7 @@ function cloneApp(req, res, next) {
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
if (('portBindings' in data) && typeof data.portBindings !== 'object') return next(new HttpError(400, 'portBindings must be an object'));
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
apps.clone(req.params.id, data, req.user, auditSource(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -243,7 +270,7 @@ function backupApp(req, res, next) {
apps.backup(req.params.id, function (error) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(503, error));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { }));
+1 -1
View File
@@ -24,7 +24,7 @@ function get(req, res, next) {
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { backups: result }));
+2 -1
View File
@@ -80,8 +80,9 @@ function addToken(req, res, next) {
var data = req.body;
var expiresAt = data.expiresAt ? parseInt(data.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future'));
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, function (error, result) {
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, { name: req.body.name || '' }, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { token: result }));
+2 -1
View File
@@ -24,7 +24,8 @@ function login(req, res, next) {
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
clients.issueDeveloperToken(user, ip, function (error, result) {
const auditSource = { authType: 'cli', ip: ip };
clients.issueDeveloperToken(user, auditSource, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
+31 -7
View File
@@ -5,7 +5,9 @@ exports = module.exports = {
get: get,
getAll: getAll,
update: update,
del: del
del: del,
verifyDomainLock: verifyDomainLock
};
var assert = require('assert'),
@@ -14,18 +16,33 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function verifyDomainLock(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
if (domains.isLocked(req.params.domain)) return next(new HttpError(423, 'This domain is locked'));
next();
}
function add(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be a string'));
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('hyphenatedSubdomains' in req.body.config && typeof req.body.config.hyphenatedSubdomains !== 'boolean') return next(new HttpError(400, 'hyphenatedSubdomains must be a boolean'));
if ('wildcard' in req.body.config && typeof req.body.config.wildcard !== 'boolean') return next(new HttpError(400, 'wildcard must be a boolean'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.cert || typeof req.body.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.key || typeof req.body.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
if ('tlsConfig' in req.body) {
if (!req.body.tlsConfig || typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string') return next(new HttpError(400, 'tlsConfig.provider must be a string'));
}
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
@@ -66,13 +83,20 @@ function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider must be an object'));
if (typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('hyphenatedSubdomains' in req.body.config && typeof req.body.config.hyphenatedSubdomains !== 'boolean') return next(new HttpError(400, 'hyphenatedSubdomains must be a boolean'));
if ('wildcard' in req.body.config && typeof req.body.config.wildcard !== 'boolean') return next(new HttpError(400, 'wildcard must be a boolean'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
if ('tlsConfig' in req.body) {
if (!req.body.tlsConfig || typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string') return next(new HttpError(400, 'tlsConfig.provider must be a string'));
}
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
+1
View File
@@ -88,6 +88,7 @@ function setDnsRecords(req, res, next) {
mail.setDnsRecords(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201));
+12 -5
View File
@@ -91,7 +91,7 @@ function initialize() {
authcodedb.del(code, function (error) {
if(error) return callback(error);
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(error);
debug('exchange: new access token for client %s user %s token %s', client.id, authCode.userId, result.accessToken.slice(0, 6)); // partial token for security
@@ -104,7 +104,7 @@ function initialize() {
// implicit token grant that skips issuing auth codes. this is used by our webadmin
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(error);
debug('grant token: new access token for client %s user %s token %s', client.id, user.id, result.accessToken.slice(0, 6)); // partial token for security
@@ -362,10 +362,13 @@ function accountSetup(req, res, next) {
// setPassword clears the resetToken
users.setPassword(userObject.id, req.body.password, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
res.redirect(config.adminOrigin());
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
});
@@ -409,7 +412,11 @@ function passwordReset(req, res, next) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
res.redirect(config.adminOrigin());
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${config.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
}
+5 -27
View File
@@ -23,10 +23,7 @@ exports = module.exports = {
setAppstoreConfig: setAppstoreConfig,
getPlatformConfig: getPlatformConfig,
setPlatformConfig: setPlatformConfig,
setSpacesConfig: setSpacesConfig,
getSpacesConfig: getSpacesConfig
setPlatformConfig: setPlatformConfig
};
var assert = require('assert'),
@@ -158,6 +155,7 @@ function setBackupConfig(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
if ('key' in req.body && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if ('syncConcurrency' in req.body) {
if (typeof req.body.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
@@ -171,7 +169,7 @@ function setBackupConfig(req, res, next) {
settings.setBackupConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
@@ -199,27 +197,7 @@ function setPlatformConfig(req, res, next) {
settings.setPlatformConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function getSpacesConfig(req, res, next) {
settings.getSpacesConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, config));
});
}
function setSpacesConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
settings.setSpacesConfig(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
@@ -247,7 +225,7 @@ function setAppstoreConfig(req, res, next) {
settings.setAppstoreConfig(options, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(406, error.message));
if (error && error.reason === SettingsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
settings.getAppstoreConfig(function (error, result) {
+3 -3
View File
@@ -54,7 +54,7 @@ function setupTokenAuth(req, res, next) {
caas.verifySetupToken(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(401, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
@@ -120,7 +120,7 @@ function activate(req, res, next) {
caas.setupDone(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(401, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
@@ -147,7 +147,7 @@ function restore(req, res, next) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
+12 -11
View File
@@ -216,7 +216,7 @@ function startBox(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, accesscontrol.SCOPE_ANY, callback);
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, accesscontrol.SCOPE_APPS_READ, '', callback);
},
function (callback) {
@@ -359,7 +359,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('my is reserved');
expect(res.body.message).to.contain('my is reserved');
done();
});
});
@@ -370,7 +370,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
expect(res.body.message).to.contain(constants.API_LOCATION + ' is reserved');
done();
});
});
@@ -381,7 +381,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
expect(res.body.message).to.contain('portBindings must be an object');
done();
});
});
@@ -392,7 +392,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
expect(res.body.message).to.contain('accessRestriction is required');
done();
});
});
@@ -403,7 +403,7 @@ describe('App API', function () {
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '', domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
expect(res.body.message).to.contain('accessRestriction is required');
done();
});
});
@@ -598,11 +598,11 @@ describe('App API', function () {
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expiresAt).toString()).to.not.be('Invalid Date');
expect(result.body.token).to.be.a('string');
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
// overwrite non dev token
token = result.body.token;
token = result.body.accessToken;
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
@@ -630,6 +630,7 @@ describe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false });
var apiHockServer;
var validCert1, validKey1;
@@ -699,7 +700,7 @@ describe('App installation', function () {
});
});
it('installation - image created', function (done) {
xit('installation - image created', function (done) {
expect(imageCreated).to.be.ok();
done();
});
@@ -915,7 +916,7 @@ describe('App installation', function () {
if (!err || err.code !== 'ECONNREFUSED') return setTimeout(waitForAppToDie, 500);
// wait for app status to be updated
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID).query({ access_token: token_1 }).end(function (error, result) {
superagent.get(SERVER_URL + '/api/v1/apps/' + APP_ID).query({ access_token: token }).end(function (error, result) {
if (error || result.statusCode !== 200 || result.body.runState !== 'stopped') return setTimeout(waitForAppToDie, 500);
done();
});
+1 -1
View File
@@ -122,7 +122,7 @@ describe('Caas', function () {
.query({ setupToken: 'somesetuptoken' })
.send({ username: 'someuser', password: 'strong#A3asdf', email: 'admin@foo.bar' })
.end(function (error, result) {
expect(result.statusCode).to.equal(503);
expect(result.statusCode).to.equal(424);
expect(scope.isDone()).to.be.ok();
done();
});
+1 -1
View File
@@ -167,7 +167,7 @@ describe('Cloudron', function () {
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', callback);
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', '', callback);
});
}
], done);
+2 -2
View File
@@ -63,7 +63,7 @@ function setup(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, callback);
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
}
], done);
@@ -141,7 +141,7 @@ describe('Eventlog API', function () {
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(1);
expect(result.body.eventlogs.length).to.equal(2);
done();
});
+1 -1
View File
@@ -70,7 +70,7 @@ function setup(done) {
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, callback);
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
});
}
], done);
+61 -4
View File
@@ -18,6 +18,15 @@ var async = require('async'),
var SERVER_URL = 'http://localhost:' + config.get('port');
const ADMIN_DOMAIN = {
domain: 'admin.com',
zoneName: 'admin.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
const DOMAIN_0 = {
domain: 'example-mail-test.com',
zoneName: 'example-mail-test.com',
@@ -33,13 +42,21 @@ var userId = '';
function setup(done) {
config._reset();
config.setFqdn(DOMAIN_0.domain);
config.setAdminFqdn('my.example-mail-test.com');
async.series([
server.start.bind(null),
database._clear.bind(null),
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function dnsSetup(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, adminFqdn: 'my.' + ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
callback();
});
},
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
@@ -57,6 +74,17 @@ function setup(done) {
});
},
function createDomain(callback) {
superagent.post(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.send(DOMAIN_0)
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
callback();
});
},
function getUserId(callback) {
userdb.getByUsername(USERNAME, function (error, result) {
expect(error).to.not.be.ok();
@@ -178,7 +206,17 @@ describe('Mail API', function () {
});
});
it('can delete domain', function (done) {
it('cannot delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('can delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.send({ password: PASSWORD })
.query({ access_token: token })
@@ -408,6 +446,25 @@ describe('Mail API', function () {
});
});
it('succeeds with modified DMARC1 values', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; rua=mailto:rua@example.com; pct=100']];
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/status')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.status).to.eql(true);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC1; p=reject; rua=mailto:rua@example.com; pct=100');
done();
});
});
it('succeeds with all correct records', function (done) {
clearDnsAnswerQueue();
+3 -1
View File
@@ -1481,9 +1481,11 @@ describe('Password', function () {
it('succeeds', function (done) {
var scope = nock(config.adminOrigin())
.filteringPath(function (path) {
path = path.replace(/accessToken=[^&]*/, 'accessToken=token');
path = path.replace(/expiresAt=[^&]*/, 'expiresAt=1234');
return path;
})
.get('/').reply(200, {});
.get('/?accessToken=token&expiresAt=1234').reply(200, {});
superagent.post(SERVER_URL + '/api/v1/session/password/reset')
.send({ password: '12345678', email: USER_0.email, resetToken: USER_0.resetToken })
+1 -1
View File
@@ -115,7 +115,7 @@ describe('Profile API', function () {
var token = tokendb.generateToken();
var expires = Date.now() - 2000; // 1 sec
tokendb.add(token, user_0.id, null, expires, 'profile', function (error) {
tokendb.add(token, user_0.id, null, expires, 'profile', 'tokenname', function (error) {
expect(error).to.not.be.ok();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
+2 -2
View File
@@ -320,7 +320,7 @@ describe('Settings API', function () {
.send({ userId: 'nebulon', token: 'sometoken' })
.end(function (err, res) {
expect(scope.isDone()).to.be.ok();
expect(res.statusCode).to.equal(406);
expect(res.statusCode).to.equal(424);
expect(res.body.message).to.equal('invalid appstore token');
done();
@@ -350,7 +350,7 @@ describe('Settings API', function () {
.send({ userId: 'nebulon', token: 'sometoken' })
.end(function (err, res) {
expect(scope.isDone()).to.be.ok();
expect(res.statusCode).to.equal(406);
expect(res.statusCode).to.equal(424);
expect(res.body.message).to.equal('wrong user');
done();
+87 -46
View File
@@ -8,7 +8,6 @@
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
constants = require('../../constants.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
tokendb = require('../../tokendb.js'),
@@ -17,7 +16,8 @@ var accesscontrol = require('../../accesscontrol.js'),
mail = require('../../mail.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
server = require('../../server.js');
server = require('../../server.js'),
users = require('../../users.js');
const SERVER_URL = 'http://localhost:' + config.get('port');
@@ -83,7 +83,7 @@ describe('Users API', function () {
this.timeout(5000);
var user_0, user_1, user_2, user_4;
var token = null;
var token = null, userToken = null;
var token_1 = tokendb.generateToken();
before(setup);
@@ -176,7 +176,7 @@ describe('Users API', function () {
var token = tokendb.generateToken();
var expires = Date.now() + 2000; // 1 sec
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_PROFILE, function (error) {
tokendb.add(token, user_0.id, null, expires, accesscontrol.SCOPE_PROFILE, 'tokenname', function (error) {
expect(error).to.not.be.ok();
setTimeout(function () {
@@ -266,11 +266,9 @@ describe('Users API', function () {
});
it('cannot create user without email', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, invite: true })
.send({ username: USERNAME_1 })
.end(function (error, result) {
expect(error).to.be.ok();
expect(result.statusCode).to.equal(400);
@@ -279,46 +277,63 @@ describe('Users API', function () {
});
it('create second user succeeds', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_1, email: EMAIL_1, invite: true })
.send({ username: USERNAME_1, email: EMAIL_1 })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
user_1 = result.body;
checkMails(2, function () {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, accesscontrol.SCOPE_PROFILE, done);
});
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, user_1.id, 'test-client-id', Date.now() + 10000, accesscontrol.SCOPE_PROFILE, 'fromtest', done);
});
});
it('reinvite unknown user fails', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/invite')
superagent.post(SERVER_URL + '/api/v1/users/' + USERNAME_1+USERNAME_1 + '/create_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be.an(Error);
expect(res.statusCode).to.equal(404);
checkMails(0, done);
done();
});
});
it('reinvite second user succeeds', function (done) {
mailer._clearMailQueue();
it('send invite without creating invite fails succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be.an(Error);
expect(res.statusCode).to.equal(409);
done();
});
});
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/invite')
it('create invite second user succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/create_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.not.be.ok();
expect(res.statusCode).to.equal(200);
expect(res.body.resetToken).to.be.ok();
done();
});
});
it('can send invite', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users/' + user_1.id + '/send_invite')
.query({ access_token: token })
.send({})
.end(function (err, res) {
expect(err).to.be(null);
expect(res.statusCode).to.equal(200);
checkMails(1, done);
});
});
@@ -384,12 +399,12 @@ describe('Users API', function () {
});
});
it('create user missing username fails', function (done) {
it('create user missing username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ email: EMAIL_2 })
.send({ email: `unnamed${EMAIL_2}` })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
expect(result.statusCode).to.equal(201);
done();
});
});
@@ -404,16 +419,6 @@ describe('Users API', function () {
});
});
it('create user missing invite fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('create user reserved name fails', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
@@ -436,10 +441,9 @@ describe('Users API', function () {
it('create second and third user', function (done) {
mailer._clearMailQueue();
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_2, email: EMAIL_2, invite: false })
.send({ username: USERNAME_2, email: EMAIL_2 })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
@@ -447,12 +451,12 @@ describe('Users API', function () {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_3, email: EMAIL_3, invite: true })
.send({ username: USERNAME_3, email: EMAIL_3 })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
// one mail for first user creation, two mails for second user creation (see 'invite' flag)
checkMails(3, done);
// two mails for user creation
checkMails(2, done);
});
});
});
@@ -496,13 +500,13 @@ describe('Users API', function () {
expect(error).to.be(null);
expect(res.statusCode).to.equal(200);
expect(res.body.users).to.be.an('array');
expect(res.body.users.length).to.equal(4);
expect(res.body.users.length).to.equal(5);
res.body.users.forEach(function (user) {
expect(user).to.be.an('object');
expect(user.id).to.be.ok();
expect(user.username).to.be.ok();
expect(user.email).to.be.ok();
if (!user.email.startsWith('unnamed')) expect(user.username).to.be.ok();
expect(user.password).to.not.be.ok();
expect(user.salt).to.not.be.ok();
expect(user.groupIds).to.not.be.ok();
@@ -676,7 +680,7 @@ describe('Users API', function () {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_4, email: EMAIL_4, invite: false, password: 'tooweak' })
.send({ username: USERNAME_4, email: EMAIL_4, password: 'tooweak' })
.end(function (error, result) {
expect(error).to.be.ok();
expect(result.statusCode).to.equal(400);
@@ -687,23 +691,23 @@ describe('Users API', function () {
it('can create user with a password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: USERNAME_4, email: EMAIL_4, invite: false, password: 'Secret1#' })
.send({ username: USERNAME_4, email: EMAIL_4, password: 'Secret1#' })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.equal(201);
user_4 = result.body;
token = tokendb.generateToken();
userToken = tokendb.generateToken();
var expires = Date.now() + 2000; // 1 sec
tokendb.add(token, user_4.id, null, expires, accesscontrol.SCOPE_PROFILE, done);
tokendb.add(userToken, user_4.id, null, expires, accesscontrol.SCOPE_PROFILE, '', done);
});
});
it('can get profile of user with pre-set password', function (done) {
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.query({ access_token: userToken })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
@@ -712,5 +716,42 @@ describe('Users API', function () {
done();
});
});
// Change password
it('change password fails due to missing token', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
.send({ password: 'youdontsay' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('change password fails due to small password', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
.query({ access_token: token })
.send({ password: 'small' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('change password succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/users/' + user_0.id + '/password')
.query({ access_token: token })
.send({ password: 'bigenough' })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
done();
});
});
it('did change the user password', function (done) {
users.verify(user_0.id, 'bigenough', function (error) {
expect(error).to.be(null);
done();
});
});
});
+44 -8
View File
@@ -6,13 +6,17 @@ exports = module.exports = {
list: list,
create: create,
remove: remove,
changePassword: changePassword,
verifyPassword: verifyPassword,
createInvite: createInvite,
sendInvite: sendInvite,
setGroups: setGroups,
transferOwnership: transferOwnership
transferOwnership: transferOwnership,
verifyOperator: verifyOperator
};
var assert = require('assert'),
config = require('../config.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
users = require('../users.js'),
@@ -23,22 +27,27 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function verifyOperator(req, res, next) {
if (config.allowOperatorActions()) return next();
next(new HttpError(401, 'Not allowed in this edition'));
}
function create(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if (typeof req.body.invite !== 'boolean') return next(new HttpError(400, 'invite must be boolean'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if ('admin' in req.body && typeof req.body.admin !== 'boolean') return next(new HttpError(400, 'admin flag must be a boolean'));
var password = req.body.password || null;
var email = req.body.email;
var sendInvite = req.body.invite;
var username = 'username' in req.body ? req.body.username : null;
var displayName = req.body.displayName || '';
users.create(username, password, email, displayName, { invitor: req.user, sendInvite: sendInvite }, auditSource(req), function (error, user) {
users.create(username, password, email, displayName, { invitor: req.user, admin: req.body.admin }, auditSource(req), function (error, user) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error) return next(new HttpError(500, error));
@@ -137,14 +146,26 @@ function verifyPassword(req, res, next) {
});
}
function sendInvite(req, res, next) {
function createInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.sendInvite(req.params.userId, { invitor: req.user }, function (error, result) {
users.createInvite(req.params.userId, function (error, resetToken) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { resetToken: result }));
next(new HttpSuccess(200, { resetToken: resetToken }));
});
}
function sendInvite(req, res, next) {
assert.strictEqual(typeof req.params.userId, 'string');
users.sendInvite(req.params.userId, { invitor: req.user }, function (error) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(409, 'Call createInvite API first'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { }));
});
}
@@ -174,4 +195,19 @@ function transferOwnership(req, res, next) {
next(new HttpSuccess(200, {}));
});
}
}
function changePassword(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.userId, 'string');
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
users.setPassword(req.params.userId, req.body.password, function (error) {
if (error && error.reason === UsersError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === UsersError.NOT_FOUND) return next(new HttpError(404, 'User not found'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+22 -19
View File
@@ -94,7 +94,7 @@ function initializeExpressSync() {
var usersReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_READ);
var usersManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_USERS_MANAGE);
var appsReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_READ);
var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.accesscontrol.verifyAppOwnership ];
var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.apps.verifyOwnership ];
var settingsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_SETTINGS);
var mailScope = routes.accesscontrol.scope(accesscontrol.SCOPE_MAIL);
var clientsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_CLIENTS);
@@ -102,6 +102,9 @@ function initializeExpressSync() {
var domainsManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE);
var appstoreScope = routes.accesscontrol.scope(accesscontrol.SCOPE_APPSTORE);
const verifyOperator = routes.users.verifyOperator;
const verifyDomainLock = routes.domains.verifyDomainLock;
// csrf protection
var csrf = routes.oauth2.csrf();
@@ -126,10 +129,10 @@ function initializeExpressSync() {
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs);
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, verifyOperator, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, verifyOperator, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, verifyOperator, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, verifyOperator, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
// config route (for dashboard)
@@ -149,8 +152,10 @@ function initializeExpressSync() {
router.get ('/api/v1/users/:userId', usersManageScope, routes.users.get); // this is manage scope because it returns non-restricted fields
router.del ('/api/v1/users/:userId', usersManageScope, routes.users.verifyPassword, routes.users.remove);
router.post('/api/v1/users/:userId', usersManageScope, routes.users.update);
router.post('/api/v1/users/:userId/password', usersManageScope, routes.users.changePassword);
router.put ('/api/v1/users/:userId/groups', usersManageScope, routes.users.setGroups);
router.post('/api/v1/users/:userId/invite', usersManageScope, routes.users.sendInvite);
router.post('/api/v1/users/:userId/send_invite', usersManageScope, routes.users.sendInvite);
router.post('/api/v1/users/:userId/create_invite', usersManageScope, routes.users.createInvite);
router.post('/api/v1/users/:userId/transfer', usersManageScope, routes.users.transferOwnership);
// Group management
@@ -207,7 +212,7 @@ function initializeExpressSync() {
router.get ('/api/v1/apps/:id/logs', appsManageScope, routes.apps.getLogs);
router.get ('/api/v1/apps/:id/exec', appsManageScope, routes.apps.exec);
// websocket cannot do bearer authentication
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, [ accesscontrol.SCOPE_APPS_MANAGE ]), routes.accesscontrol.verifyAppOwnership, routes.apps.execWebSocket);
router.get ('/api/v1/apps/:id/execws', routes.accesscontrol.websocketAuth.bind(null, [ accesscontrol.SCOPE_APPS_MANAGE ]), routes.apps.verifyOwnership, routes.apps.execWebSocket);
router.post('/api/v1/apps/:id/clone', appsManageScope, routes.apps.cloneApp);
router.get ('/api/v1/apps/:id/download', appsManageScope, routes.apps.downloadFile);
router.post('/api/v1/apps/:id/upload', appsManageScope, multipart, routes.apps.uploadFile);
@@ -222,17 +227,15 @@ function initializeExpressSync() {
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, routes.settings.setBackupConfig);
router.get ('/api/v1/settings/platform_config', settingsScope, routes.settings.getPlatformConfig);
router.post('/api/v1/settings/platform_config', settingsScope, routes.settings.setPlatformConfig);
router.get ('/api/v1/settings/spaces_config', settingsScope, routes.settings.getSpacesConfig);
router.post('/api/v1/settings/spaces_config', settingsScope, routes.settings.setSpacesConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, verifyOperator, routes.settings.getBackupConfig);
router.post('/api/v1/settings/backup_config', settingsScope, verifyOperator, routes.settings.setBackupConfig);
router.get ('/api/v1/settings/platform_config', settingsScope, verifyOperator, routes.settings.getPlatformConfig);
router.post('/api/v1/settings/platform_config', settingsScope, verifyOperator, routes.settings.setPlatformConfig);
router.get ('/api/v1/settings/time_zone', settingsScope, routes.settings.getTimeZone);
router.post('/api/v1/settings/time_zone', settingsScope, routes.settings.setTimeZone);
router.get ('/api/v1/settings/appstore_config', appstoreScope, routes.settings.getAppstoreConfig);
router.post('/api/v1/settings/appstore_config', appstoreScope, routes.settings.setAppstoreConfig);
router.get ('/api/v1/settings/appstore_config', appstoreScope, verifyOperator, routes.settings.getAppstoreConfig);
router.post('/api/v1/settings/appstore_config', appstoreScope, verifyOperator, routes.settings.setAppstoreConfig);
// email routes
router.get ('/api/v1/mail/:domain', mailScope, routes.mail.getDomain);
@@ -261,7 +264,7 @@ function initializeExpressSync() {
router.del ('/api/v1/mail/:domain/lists/:name', mailScope, routes.mail.removeList);
// feedback
router.post('/api/v1/feedback', cloudronScope, routes.cloudron.feedback);
router.post('/api/v1/feedback', cloudronScope, verifyOperator, routes.cloudron.feedback);
// backup routes
router.get ('/api/v1/backups', settingsScope, routes.backups.get);
@@ -270,9 +273,9 @@ function initializeExpressSync() {
// domain routes
router.post('/api/v1/domains', domainsManageScope, routes.domains.add);
router.get ('/api/v1/domains', domainsReadScope, routes.domains.getAll);
router.get ('/api/v1/domains/:domain', domainsManageScope, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, routes.domains.update);
router.del ('/api/v1/domains/:domain', domainsManageScope, routes.users.verifyPassword, routes.domains.del);
router.get ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.get); // this is manage scope because it returns non-restricted fields
router.put ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.domains.update);
router.del ('/api/v1/domains/:domain', domainsManageScope, verifyDomainLock, routes.users.verifyPassword, routes.domains.del);
// caas routes
router.get('/api/v1/caas/config', cloudronScope, routes.caas.getConfig);
+3 -33
View File
@@ -38,9 +38,6 @@ exports = module.exports = {
getPlatformConfig: getPlatformConfig,
setPlatformConfig: setPlatformConfig,
getSpacesConfig: getSpacesConfig,
setSpacesConfig: setSpacesConfig,
getAll: getAll,
// booleans. if you add an entry here, be sure to fix getAll
@@ -53,7 +50,6 @@ exports = module.exports = {
APPSTORE_CONFIG_KEY: 'appstore_config',
CAAS_CONFIG_KEY: 'caas_config',
PLATFORM_CONFIG_KEY: 'platform_config',
SPACES_CONFIG_KEY: 'spaces_config',
// strings
APP_AUTOUPDATE_PATTERN_KEY: 'app_autoupdate_pattern',
@@ -90,14 +86,14 @@ var gDefaults = (function () {
provider: 'filesystem',
key: '',
backupFolder: '/var/backups',
retentionSecs: 172800
retentionSecs: 2 * 24 * 60 * 60, // 2 days
intervalSecs: 24 * 60 * 60 // ~1 day
};
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
result[exports.APPSTORE_CONFIG_KEY] = {};
result[exports.CAAS_CONFIG_KEY] = {};
result[exports.EMAIL_DIGEST] = true;
result[exports.PLATFORM_CONFIG_KEY] = {};
result[exports.SPACES_CONFIG_KEY] = { enabled: false };
return result;
})();
@@ -357,32 +353,6 @@ function setEmailDigest(enabled, callback) {
});
}
function getSpacesConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settingsdb.get(exports.SPACES_CONFIG_KEY, function (error, value) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.SPACES_CONFIG_KEY]);
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
callback(null, JSON.parse(value));
});
}
function setSpacesConfig(value, callback) {
assert.strictEqual(typeof value, 'object');
assert.strictEqual(typeof callback, 'function');
if ('enabled' in value && typeof value.enabled !== 'boolean') return callback(new SettingsError(SettingsError.BAD_FIELD, 'enabled must be a boolean'));
settingsdb.set(exports.SPACES_CONFIG_KEY, JSON.stringify(value), function (error) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
exports.events.emit(exports.SPACES_CONFIG_KEY, value);
callback(null);
});
}
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -505,7 +475,7 @@ function getAll(callback) {
result[exports.DYNAMIC_DNS_KEY] = !!result[exports.DYNAMIC_DNS_KEY];
// convert JSON objects
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY, exports.SPACES_CONFIG_KEY ].forEach(function (key) {
[exports.BACKUP_CONFIG_KEY, exports.UPDATE_CONFIG_KEY, exports.APPSTORE_CONFIG_KEY, exports.PLATFORM_CONFIG_KEY ].forEach(function (key) {
result[key] = typeof result[key] === 'object' ? result[key] : safe.JSON.parse(result[key]);
});
+6 -4
View File
@@ -144,7 +144,7 @@ function configureWebadmin(callback) {
debug('addWebadminDnsRecord: updated records with error:', error);
if (error) return configureReverseProxy(error);
domains.waitForDnsRecord(config.adminFqdn(), config.adminDomain(), ip, { interval: 30000, times: 50000 }, function (error) {
domains.waitForDnsRecord(config.adminLocation(), config.adminDomain(), 'A', ip, { interval: 30000, times: 50000 }, function (error) {
if (error) return configureReverseProxy(error);
gWebadminStatus.dns = true;
@@ -247,7 +247,7 @@ function activate(username, password, email, displayName, ip, auditSource, callb
if (error && error.reason === UsersError.BAD_FIELD) return callback(new SetupError(SetupError.BAD_FIELD, error.message));
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, function (error, result) {
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(new SetupError(SetupError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
@@ -293,8 +293,9 @@ function restore(backupConfig, backupId, version, callback) {
async.series([
backups.restore.bind(null, backupConfig, backupId),
autoprovision,
// currently, our suggested restore flow is after a dnsSetup. This re-creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore. Once we have a 100% IP based restore, we can skip this
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
// Once we have a 100% IP based restore, we can skip this
mail.setDnsRecords.bind(null, config.adminDomain()),
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
@@ -322,6 +323,7 @@ function getStatus(callback) {
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: count !== 0,
edition: config.edition(),
webadminStatus: gWebadminStatus // only valid when !activated
});
});
+4
View File
@@ -48,6 +48,10 @@ function S3_NOT_FOUND(error) {
return error.code === 'NoSuchKey' || error.code === 'NotFound' || error.code === 'ENOENT';
}
function S3_ACCESS_DENIED(error) {
return error.code === 'AccessDenied' || error.statusCode === 403;
}
function getCaasConfig(apiConfig, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof callback, 'function');
+12 -50
View File
@@ -162,9 +162,9 @@ describe('Apps', function () {
groupdb.add.bind(null, GROUP_0.id, GROUP_0.name),
groupdb.add.bind(null, GROUP_1.id, GROUP_1.name),
groups.addMember.bind(null, GROUP_0.id, USER_1.id),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.ownerId, APP_1.portBindings, APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.ownerId, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.domain, APP_1.ownerId, apps._translatePortBindings(APP_1.portBindings, APP_1.manifest), APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.domain, APP_2.ownerId, apps._translatePortBindings(APP_2.portBindings, APP_2.manifest), APP_2),
settingsdb.set.bind(null, settings.BACKUP_CONFIG_KEY, JSON.stringify({ provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' }))
], done);
});
@@ -176,66 +176,28 @@ describe('Apps', function () {
], done);
});
describe('validateHostname', function () {
it('does not allow admin subdomain', function () {
expect(apps._validateHostname('my', DOMAIN_0.domain, 'my.' + DOMAIN_0.domain)).to.be.an(Error);
});
it('cannot have >63 length subdomains', function () {
var s = Array(64).fill('s').join('');
expect(apps._validateHostname(s, 'example.com', s + '.example.com')).to.be.an(Error);
expect(apps._validateHostname(`dev.${s}`, 'example.com', `dev.${s}.example.com`)).to.be.an(Error);
});
it('allows only alphanumerics and hypen', function () {
expect(apps._validateHostname('#2r', 'example.com', '#2r.example.com')).to.be.an(Error);
expect(apps._validateHostname('a%b', 'example.com', 'a%b.example.com')).to.be.an(Error);
expect(apps._validateHostname('ab_', 'example.com', 'ab_.example.com')).to.be.an(Error);
expect(apps._validateHostname('ab.', 'example.com', 'ab.example.com')).to.be.an(Error);
expect(apps._validateHostname('ab..c', 'example.com', 'ab..c.example.com')).to.be.an(Error);
expect(apps._validateHostname('.ab', 'example.com', '.ab.example.com')).to.be.an(Error);
expect(apps._validateHostname('-ab', 'example.com', '-ab.example.com')).to.be.an(Error);
expect(apps._validateHostname('ab-', 'example.com', 'ab-.example.com')).to.be.an(Error);
});
it('total length cannot exceed 255', function () {
var s = '';
for (var i = 0; i < (255 - 'example.com'.length); i++) s += 's';
expect(apps._validateHostname(s, 'example.com', s + '.example.com')).to.be.an(Error);
});
it('allow valid domains', function () {
expect(apps._validateHostname('a', 'example.com', 'a.example.com')).to.be(null);
expect(apps._validateHostname('a0-x', 'example.com', 'a0-x.example.com')).to.be(null);
expect(apps._validateHostname('a0.x', 'example.com', 'a0-x.example.com')).to.be(null);
expect(apps._validateHostname('a0.x.y', 'example.com', 'a0.x.y.example.com')).to.be(null);
expect(apps._validateHostname('01', 'example.com', '01.example.com')).to.be(null);
});
});
describe('validatePortBindings', function () {
it('does not allow invalid host port', function () {
expect(apps._validatePortBindings({ port: -1 }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 0 }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 'text' }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 65536 }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 470 }, { port: 5000 })).to.be.an(Error);
expect(apps._validatePortBindings({ port: -1 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 0 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 'text' }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 65536 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 470 }, { tcpPorts: { port: 5000 } })).to.be.an(Error);
});
it('does not allow ports not as part of manifest', function () {
expect(apps._validatePortBindings({ port: 1567 }, { })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 1567 }, { port3: null })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { } })).to.be.an(Error);
expect(apps._validatePortBindings({ port: 1567 }, { tcpPorts: { port3: null } })).to.be.an(Error);
});
it('allows valid bindings', function () {
expect(apps._validatePortBindings({ port: 1024 }, { port: 5000 })).to.be(null);
expect(apps._validatePortBindings({ port: 1024 }, { tcpPorts: { port: 5000 } })).to.be(null);
expect(apps._validatePortBindings({
port1: 4033,
port2: 3242,
port3: 1234
}, { port1: null, port2: null, port3: null })).to.be(null);
}, { tcpPorts: { port1: null, port2: null, port3: null } })).to.be(null);
});
});
-5
View File
@@ -129,14 +129,9 @@ describe('Appstore', function () {
.post(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/apps/${APP_ID}?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(201, {});
var scope2 = nock('http://localhost:6060')
.get(`/api/v1/users/${APPSTORE_USER_ID}/cloudrons/${CLOUDRON_ID}/subscription?accessToken=${APPSTORE_TOKEN}`, function () { return true; })
.reply(200, { subscription: { id: 'basic' }});
appstore.purchase(APP_ID, APPSTORE_APP_ID, function (error) {
expect(error).to.not.be.ok();
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
done();
});
+12 -9
View File
@@ -221,7 +221,7 @@ describe('database', function () {
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: null,
portBindings: { port: 5678 },
portBindings: { port: { hostPort: 5678, type: 'tcp' } },
health: null,
accessRestriction: null,
lastBackupId: null,
@@ -553,6 +553,7 @@ describe('database', function () {
describe('token', function () {
var TOKEN_0 = {
name: 'token0',
accessToken: tokendb.generateToken(),
identifier: '0',
clientId: 'clientid-0',
@@ -560,6 +561,7 @@ describe('database', function () {
scope: 'clients'
};
var TOKEN_1 = {
name: 'token1',
accessToken: tokendb.generateToken(),
identifier: '1',
clientId: 'clientid-1',
@@ -567,6 +569,7 @@ describe('database', function () {
scope: 'settings'
};
var TOKEN_2 = {
name: 'token2',
accessToken: tokendb.generateToken(),
identifier: '2',
clientId: 'clientid-2',
@@ -582,14 +585,14 @@ describe('database', function () {
});
it('add succeeds', function (done) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) {
expect(error).to.be(null);
done();
});
});
it('add of same token fails', function (done) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) {
expect(error).to.be.a(DatabaseError);
expect(error.reason).to.be(DatabaseError.ALREADY_EXISTS);
done();
@@ -642,7 +645,7 @@ describe('database', function () {
});
it('delByIdentifier succeeds', function (done) {
tokendb.add(TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, function (error) {
tokendb.add(TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, '', function (error) {
expect(error).to.be(null);
tokendb.delByIdentifier(TOKEN_1.identifier, function (error) {
@@ -661,7 +664,7 @@ describe('database', function () {
});
it('getByIdentifierAndClientId succeeds', function (done) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) {
expect(error).to.be(null);
tokendb.getByIdentifierAndClientId(TOKEN_0.identifier, TOKEN_0.clientId, function (error, result) {
@@ -675,7 +678,7 @@ describe('database', function () {
});
it('delExpired succeeds', function (done) {
tokendb.add(TOKEN_2.accessToken, TOKEN_2.identifier, TOKEN_2.clientId, TOKEN_2.expires, TOKEN_2.scope, function (error) {
tokendb.add(TOKEN_2.accessToken, TOKEN_2.identifier, TOKEN_2.clientId, TOKEN_2.expires, TOKEN_2.scope, TOKEN_2.name, function (error) {
expect(error).to.be(null);
tokendb.delExpired(function (error, result) {
@@ -706,7 +709,7 @@ describe('database', function () {
});
it('delByClientId succeeds', function (done) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, function (error) {
tokendb.add(TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name, function (error) {
expect(error).to.be(null);
tokendb.delByClientId(TOKEN_0.clientId, function (error) {
@@ -735,7 +738,7 @@ describe('database', function () {
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: null,
portBindings: { port: 5678 },
portBindings: { port: { hostPort: 5678, type: 'tcp' } },
health: null,
accessRestriction: null,
restoreConfig: null,
@@ -821,7 +824,7 @@ describe('database', function () {
appdb.getPortBindings(APP_0.id, function (error, bindings) {
expect(error).to.be(null);
expect(bindings).to.be.an(Object);
expect(bindings).to.be.eql({ port: '5678' });
expect(bindings).to.be.eql({ port: { hostPort: '5678', type: 'tcp' } });
done();
});
});
+1
View File
@@ -506,6 +506,7 @@ describe('dns provider', function () {
before(function (done) {
DOMAIN_0.provider = 'namecom';
DOMAIN_0.config = {
username: 'fake',
token: TOKEN
};
+106
View File
@@ -0,0 +1,106 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var dockerProxy = require('../dockerproxy.js'),
config = require('../config.js'),
exec = require('child_process').exec,
expect = require('expect.js');
const DOCKER = `docker -H tcp://localhost:${config.get('dockerProxyPort')} `;
describe('Dockerproxy', function () {
var containerId;
// create a container to test against
before(function (done) {
dockerProxy.start(function (error) {
expect(error).to.not.be.ok();
exec(`${DOCKER} run -d ubuntu "bin/bash" "-c" "while true; do echo 'perpetual walrus'; sleep 1; done"`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
containerId = stdout.slice(0, -1); // removes the trailing \n
done();
});
});
});
after(function (done) {
exec(`${DOCKER} rm -f ${containerId}`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
dockerProxy.stop(done);
});
});
// uncomment this to run the proxy for manual testing
// this.timeout(1000000);
// it('wait', function (done) {} );
it('can get info', function (done) {
exec(DOCKER + ' info', function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stdout).to.contain('Containers:');
// expect(stderr).to.be.empty(); // on some machines, i get 'No swap limit support'
done();
});
});
it('can create container', function (done) {
var cmd = DOCKER + ` run ubuntu "/bin/bash" "-c" "echo 'hello'"`;
exec(cmd, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stdout).to.contain('hello');
expect(stderr).to.be.empty();
done();
});
});
it('proxy overwrites the container network option', function (done) {
var cmd = `${DOCKER} run --network ifnotrewritethiswouldfail ubuntu "/bin/bash" "-c" "echo 'hello'"`;
exec(cmd, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stdout).to.contain('hello');
expect(stderr).to.be.empty();
done();
});
});
it('cannot see logs through docker logs, since syslog is configured', function (done) {
exec(`${DOCKER} logs ${containerId}`, function (error, stdout, stderr) {
expect(error.message).to.contain('configured logging driver does not support reading');
expect(stderr).to.contain('configured logging driver does not support reading');
expect(stdout).to.be.empty();
done();
});
});
it('can use PUT to upload archive into a container', function (done) {
exec(`${DOCKER} cp -a ${__dirname}/proxytestarchive.tar ${containerId}:/tmp/`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
expect(stdout).to.be.empty();
done();
});
});
it('can exec into a container', function (done) {
exec(`${DOCKER} exec ${containerId} ls`, function (error, stdout, stderr) {
expect(error).to.be(null);
expect(stderr).to.be.empty();
expect(stdout).to.equal('bin\nboot\ndev\netc\nhome\nlib\nlib64\nmedia\nmnt\nopt\nproc\nroot\nrun\nsbin\nsrv\nsys\ntmp\nusr\nvar\n');
done();
});
});
});
+88
View File
@@ -0,0 +1,88 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
config = require('../config.js'),
database = require('../database.js'),
domains = require('../domains.js'),
expect = require('expect.js'),
_ = require('underscore');
describe('Domains', function () {
before(function (done) {
config._reset();
async.series([
database.initialize,
database._clear
], done);
});
after(function (done) {
async.series([
database._clear,
database.uninitialize
], done);
});
const domain = {
domain: 'example.com',
zoneName: 'example.com',
config: {}
};
describe('validateHostname', function () {
it('does not allow admin subdomain', function () {
config.setFqdn('example.com');
config.setAdminFqdn('my.example.com');
expect(domains.validateHostname('my', domain)).to.be.an(Error);
});
it('cannot have >63 length subdomains', function () {
var s = Array(64).fill('s').join('');
expect(domains.validateHostname(s, domain)).to.be.an(Error);
domain.zoneName = `dev.${s}.example.com`;
expect(domains.validateHostname(`dev.${s}`, domain)).to.be.an(Error);
});
it('allows only alphanumerics and hypen', function () {
expect(domains.validateHostname('#2r', domain)).to.be.an(Error);
expect(domains.validateHostname('a%b', domain)).to.be.an(Error);
expect(domains.validateHostname('ab_', domain)).to.be.an(Error);
expect(domains.validateHostname('ab.', domain)).to.be.an(Error);
expect(domains.validateHostname('ab..c', domain)).to.be.an(Error);
expect(domains.validateHostname('.ab', domain)).to.be.an(Error);
expect(domains.validateHostname('-ab', domain)).to.be.an(Error);
expect(domains.validateHostname('ab-', domain)).to.be.an(Error);
});
it('total length cannot exceed 255', function () {
var s = '';
for (var i = 0; i < (255 - 'example.com'.length); i++) s += 's';
expect(domains.validateHostname(s, domain)).to.be.an(Error);
});
it('allow valid domains', function () {
expect(domains.validateHostname('a', domain)).to.be(null);
expect(domains.validateHostname('a0-x', domain)).to.be(null);
expect(domains.validateHostname('a0.x', domain)).to.be(null);
expect(domains.validateHostname('a0.x.y', domain)).to.be(null);
expect(domains.validateHostname('01', domain)).to.be(null);
});
it('hyphenatedSubdomains', function () {
let domainCopy = _.extend({}, domain);
domainCopy.config.hyphenatedSubdomains = true;
expect(domains.validateHostname('a', domain)).to.be(null);
expect(domains.validateHostname('a0-x', domain)).to.be(null);
expect(domains.validateHostname('a0.x', domain)).to.be.an(Error);
});
});
});
+5 -3
View File
@@ -33,7 +33,8 @@ describe('janitor', function () {
identifier: '0',
clientId: 'clientid-0',
expires: Date.now() + 60 * 60 * 1000,
scope: 'settings'
scope: 'settings',
name: 'clientid0'
};
var TOKEN_1 = {
accessToken: tokendb.generateToken(),
@@ -41,6 +42,7 @@ describe('janitor', function () {
clientId: 'clientid-1',
expires: Date.now() - 1000,
scope: 'apps',
name: 'clientid1'
};
before(function (done) {
@@ -49,8 +51,8 @@ describe('janitor', function () {
database._clear,
authcodedb.add.bind(null, AUTHCODE_0.authCode, AUTHCODE_0.clientId, AUTHCODE_0.userId, AUTHCODE_0.expiresAt),
authcodedb.add.bind(null, AUTHCODE_1.authCode, AUTHCODE_1.clientId, AUTHCODE_1.userId, AUTHCODE_1.expiresAt),
tokendb.add.bind(null, TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope),
tokendb.add.bind(null, TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope)
tokendb.add.bind(null, TOKEN_0.accessToken, TOKEN_0.identifier, TOKEN_0.clientId, TOKEN_0.expires, TOKEN_0.scope, TOKEN_0.name),
tokendb.add.bind(null, TOKEN_1.accessToken, TOKEN_1.identifier, TOKEN_1.clientId, TOKEN_1.expires, TOKEN_1.scope, TOKEN_1.name)
], done);
});
+77 -1
View File
@@ -7,6 +7,7 @@
'use strict';
var appdb = require('../appdb.js'),
apps = require('../apps.js'),
assert = require('assert'),
async = require('async'),
database = require('../database.js'),
@@ -105,7 +106,7 @@ function setup(done) {
USER_0.id = APP_0.ownerId = result.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, callback);
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, callback);
});
},
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
@@ -539,6 +540,42 @@ describe('Ldap', function () {
});
});
});
it ('lists the owner as admin', function (done) {
// make a normal user the owner
appdb.update(APP_0.id, { ownerId: USER_1.id, accessRestriction: { users: [], groups: [ GROUP_ID ] } }, function (error) {
expect(error).to.be(null);
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: 'objectcategory=person'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
entries.sort(function (a, b) { return a.username > b.username; });
expect(entries[0].username).to.equal(USER_0.username.toLowerCase());
expect(entries[1].username).to.equal(USER_1.username.toLowerCase());
expect(entries[1].isadmin).to.equal('1');
client.unbind();
appdb.update(APP_0.id, { ownerId: USER_0.id, accessRestriction: null }, done);
});
});
});
});
});
describe('search groups', function () {
@@ -705,6 +742,45 @@ describe('Ldap', function () {
});
});
});
it ('shows owner as admin', function (done) {
appdb.update(APP_0.id, { ownerId: USER_1.id, accessRestriction: { users: [], groups: [ GROUP_ID ] } }, function (error) {
expect(error).to.be(null);
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectclass=group)(cn=*)'
};
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid[0]).to.equal(USER_0.id);
expect(entries[0].memberuid[1]).to.equal(USER_1.id);
expect(entries[1].cn).to.equal('admins');
expect(entries[1].memberuid.length).to.equal(2);
expect(entries[1].memberuid[0]).to.equal(USER_0.id);
expect(entries[1].memberuid[1]).to.equal(USER_1.id);
client.unbind();
appdb.update(APP_0.id, { ownerId: USER_0.id, accessRestriction: null }, done);
});
});
});
});
});
function ldapSearch(dn, filter, callback) {
Binary file not shown.
+6 -15
View File
@@ -128,19 +128,10 @@ describe('Certificates', function () {
after(cleanup);
it('returns prod caas for prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
done();
});
});
it('returns prod caas for dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('caas');
expect(options.prod).to.be(true);
expect(options).to.eql({ email: 'support@cloudron.io' });
done();
});
});
@@ -159,7 +150,7 @@ describe('Certificates', function () {
after(cleanup);
it('returns prod acme in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -168,7 +159,7 @@ describe('Certificates', function () {
});
it('returns prod acme in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(true);
@@ -190,7 +181,7 @@ describe('Certificates', function () {
after(cleanup);
it('returns staging acme in prod cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
@@ -199,7 +190,7 @@ describe('Certificates', function () {
});
it('returns staging acme in dev cloudron', function (done) {
reverseProxy._getApi({ domain: DOMAIN_0.domain }, function (error, api, options) {
reverseProxy._getCertApi(DOMAIN_0.domain, function (error, api, options) {
expect(error).to.be(null);
expect(api._name).to.be('acme');
expect(options.prod).to.be(false);
-15
View File
@@ -120,21 +120,6 @@ describe('Settings', function () {
});
});
it('can set spaces config', function (done) {
settings.setSpacesConfig({ enabled: true }, function (error) {
expect(error).to.be(null);
done();
});
});
it('can get backup config', function (done) {
settings.getSpacesConfig(function (error, spacesConfig) {
expect(error).to.be(null);
expect(spacesConfig.enabled).to.be(true);
done();
});
});
it('can enable mail digest', function (done) {
settings.setEmailDigest(true, function (error) {
expect(error).to.be(null);
+4 -3
View File
@@ -6,6 +6,7 @@
'use strict';
var appdb = require('../appdb.js'),
apps = require('../apps.js'),
async = require('async'),
config = require('../config.js'),
constants = require('../constants.js'),
@@ -300,7 +301,7 @@ describe('updatechecker - app - manual (email)', function () {
if (error) return next(error);
APP_0.ownerId = userObject.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
});
},
settings.setAppAutoupdatePattern.bind(null, constants.AUTOUPDATE_PATTERN_NEVER),
@@ -423,7 +424,7 @@ describe('updatechecker - app - automatic (no email)', function () {
if (error) return next(error);
APP_0.ownerId = userObject.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
});
},
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
@@ -496,7 +497,7 @@ describe('updatechecker - app - automatic free (email)', function () {
if (error) return next(error);
APP_0.ownerId = userObject.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, APP_0.portBindings, APP_0, next);
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.domain, APP_0.ownerId, apps._translatePortBindings(APP_0.portBindings, APP_0.manifest), APP_0, next);
});
},
settings.setAppAutoupdatePattern.bind(null, '00 00 1,3,5,23 * * *'),
+21 -7
View File
@@ -176,7 +176,7 @@ describe('User', function () {
});
});
it('succeeds and attempts to send invite', function (done) {
it('succeeds', function (done) {
users.createOwner(USERNAME, PASSWORD, EMAIL, DISPLAY_NAME, AUDIT_SOURCE, function (error, result) {
expect(error).not.to.be.ok();
expect(result).to.be.ok();
@@ -184,8 +184,7 @@ describe('User', function () {
expect(result.email).to.equal(EMAIL.toLowerCase());
expect(result.fallbackEmail).to.equal(EMAIL.toLowerCase());
// first user is owner, do not send mail to admins
checkMails(0, done);
done();
});
});
@@ -222,7 +221,7 @@ describe('User', function () {
expect(result.fallbackEmail).to.equal(EMAIL_1.toLowerCase());
// first user is owner, do not send mail to admins
checkMails(2, { sentTo: EMAIL_1.toLowerCase() }, function (error) {
checkMails(1, { sentTo: EMAIL_1.toLowerCase() }, function (error) {
expect(error).not.to.be.ok();
maildb.update(DOMAIN_0.domain, { enabled: false }, done);
@@ -830,7 +829,7 @@ describe('User', function () {
});
});
describe('send invite', function () {
describe('invite', function () {
before(createOwner);
after(cleanupUsers);
@@ -843,9 +842,24 @@ describe('User', function () {
});
});
it('succeeds', function (done) {
it('fails as expected', function (done) {
users.sendInvite(userObject.id, { }, function (error) {
expect(error).to.eql(null);
expect(error).to.be.ok(); // have to create resetToken first
done();
});
});
it('can create token', function (done) {
users.createInvite(userObject.id, function (error, resetToken) {
expect(error).to.be(null);
expect(resetToken).to.be.ok();
done();
});
});
it('send invite', function (done) {
users.sendInvite(userObject.id, { }, function (error) {
expect(error).to.be(null);
checkMails(1, done);
});
});
+5 -4
View File
@@ -22,7 +22,7 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror'),
hat = require('./hat.js');
var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires' ].join(',');
var TOKENS_FIELDS = [ 'accessToken', 'identifier', 'clientId', 'scope', 'expires', 'name' ].join(',');
function generateToken() {
return hat(8 * 32); // TODO: make this stronger
@@ -40,16 +40,17 @@ function get(accessToken, callback) {
});
}
function add(accessToken, identifier, clientId, expires, scope, callback) {
function add(accessToken, identifier, clientId, expires, scope, name, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof identifier, 'string');
assert(typeof clientId === 'string' || clientId === null);
assert.strictEqual(typeof expires, 'number');
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO tokens (accessToken, identifier, clientId, expires, scope) VALUES (?, ?, ?, ?, ?)',
[ accessToken, identifier, clientId, expires, scope ], function (error, result) {
database.query('INSERT INTO tokens (accessToken, identifier, clientId, expires, scope, name) VALUES (?, ?, ?, ?, ?, ?)',
[ accessToken, identifier, clientId, expires, scope, name ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+1
View File
@@ -185,6 +185,7 @@ function doUpdate(boxUpdateInfo, callback) {
adminFqdn: config.adminFqdn(),
adminLocation: config.adminLocation(),
isDemo: config.isDemo(),
edition: config.edition(),
appstore: {
apiServerOrigin: config.apiServerOrigin()
+2 -1
View File
@@ -162,12 +162,13 @@ function del(userId, callback) {
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE userId = ?', args: [ userId ] });
queries.push({ query: 'DELETE FROM tokens WHERE identifier = ?', args: [ userId ] });
queries.push({ query: 'DELETE FROM users WHERE id = ?', args: [ userId ] });
database.transaction(queries, function (error, result) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (result[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(error);
});
+26 -12
View File
@@ -21,6 +21,7 @@ exports = module.exports = {
update: updateUser,
createOwner: createOwner,
getOwner: getOwner,
createInvite: createInvite,
sendInvite: sendInvite,
setMembership: setMembership,
setTwoFactorAuthenticationSecret: setTwoFactorAuthenticationSecret,
@@ -150,9 +151,9 @@ function create(username, password, email, displayName, options, auditSource, ca
assert(options && typeof options === 'object');
assert.strictEqual(typeof auditSource, 'object');
var invitor = options.invitor || null,
sendInvite = !!options.sendInvite,
owner = !!options.owner;
const isOwner = !!options.owner;
const isAdmin = !!options.admin;
const invitor = options.invitor || null;
var error;
@@ -192,9 +193,9 @@ function create(username, password, email, displayName, options, auditSource, ca
salt: salt.toString('hex'),
createdAt: now,
modifiedAt: now,
resetToken: hat(256),
resetToken: '',
displayName: displayName,
admin: owner
admin: isOwner || isAdmin
};
userdb.add(user.id, user, function (error) {
@@ -203,10 +204,9 @@ function create(username, password, email, displayName, options, auditSource, ca
callback(null, user);
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user) });
eventlog.add(eventlog.ACTION_USER_ADD, auditSource, { userId: user.id, email: user.email, user: removePrivateFields(user), invitor: invitor });
if (!owner) mailer.userAdded(user, sendInvite);
if (sendInvite) mailer.sendInvite(user, invitor);
if (!isOwner) mailer.userAdded(user);
});
});
});
@@ -523,9 +523,8 @@ function getOwner(callback) {
});
}
function sendInvite(userId, options, callback) {
function createInvite(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
userdb.get(userId, function (error, userObject) {
@@ -538,13 +537,28 @@ function sendInvite(userId, options, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
mailer.sendInvite(userObject, options.invitor || null);
callback(null, userObject.resetToken);
});
});
}
function sendInvite(userId, options, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
userdb.get(userId, function (error, userObject) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new UsersError(UsersError.NOT_FOUND));
if (error) return callback(new UsersError(UsersError.INTERNAL_ERROR, error));
if (!userObject.resetToken) return callback(new UsersError(UsersError.BAD_FIELD, 'Must generate resetToken to send inivitation'));
mailer.sendInvite(userObject, options.invitor || null);
callback(null);
});
}
function setTwoFactorAuthenticationSecret(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');