Compare commits

..

1433 Commits

Author SHA1 Message Date
Girish Ramakrishnan de3845226e 3.2.3 changes 2018-10-22 21:57:36 -07:00
Girish Ramakrishnan d959e9a75b Move apphealthmonitor into a cron job
This makes sure that it only runs post activation

See also a9c1af50f7

(cherry picked from commit a49969f2be)
2018-10-22 20:10:14 -07:00
Girish Ramakrishnan e67fc64e65 Update mail container 2018-10-03 09:45:27 -07:00
Girish Ramakrishnan 0189539b27 Add 3.2.1 changes (updated dashboard) 2018-09-30 15:03:31 -07:00
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
Girish Ramakrishnan 8671c4c015 3.0.1 changes 2018-08-06 19:45:46 -07:00
Girish Ramakrishnan 2612cc18fa Expire existing webadmin token so that the UI gets a new token
This is because we added a new appstore scope
2018-08-06 19:40:46 -07:00
Girish Ramakrishnan 36a21acae8 skip verification for prereleases because we remove it from release.json 2018-08-06 16:23:23 -07:00
Girish Ramakrishnan 1ed4710c68 Better error message 2018-08-05 23:38:49 -07:00
Girish Ramakrishnan 75b6688734 Clear timeout when setting backup config 2018-08-05 22:29:27 -07:00
Girish Ramakrishnan f7a7e4e95a Fix text in account setup screen 2018-08-05 21:58:07 -07:00
Girish Ramakrishnan a8ba0b91f7 Allow subdomain in the location field
This allows one to easily add "dev.staging@domain.com" etc without having to create
yet another domain. This plays well with the concept that we have a
mail domain for every domain. So we get mails from @domain.com working for
these subdomain installations.
2018-08-04 09:37:18 -07:00
Girish Ramakrishnan 95540e8cbc Do ownership check in exec route 2018-08-03 22:19:42 -07:00
Girish Ramakrishnan 9ebd22d6f7 add api note 2018-08-03 22:19:36 -07:00
Girish Ramakrishnan 1cf5807fb9 Send spaces config in config route
This is here because normal users cannot access settings route
2018-08-03 18:26:16 -07:00
Girish Ramakrishnan cc7824681b Return ownerId in apps response 2018-08-03 18:15:22 -07:00
Girish Ramakrishnan a0a523ae71 spaces: verify app ownership in app management routes 2018-08-03 17:35:58 -07:00
Girish Ramakrishnan fba70d888b Remove tokenScopes
decided that the UI won't use this any more and will just the admin flag
2018-08-03 10:07:23 -07:00
Girish Ramakrishnan ce9fc7b3f7 Better error message 2018-08-03 09:46:22 -07:00
Girish Ramakrishnan 3d4df8e26c Add test for spaces config 2018-08-03 09:46:16 -07:00
Girish Ramakrishnan d3f9647cd5 Add settings for Spaces 2018-08-03 09:40:57 -07:00
Girish Ramakrishnan 2a49569805 Send admin flag in the profile 2018-08-03 09:34:25 -07:00
Girish Ramakrishnan 47c8700d42 make scopesForUser async 2018-08-03 09:34:19 -07:00
Girish Ramakrishnan d302dbc634 Fix tests 2018-08-02 14:59:50 -07:00
Girish Ramakrishnan eab3cd6b2b ownerId for domains is not implemented yet 2018-08-01 18:53:49 -07:00
Girish Ramakrishnan 92151b1e42 Add note on npm rebuild 2018-08-01 18:37:40 -07:00
Girish Ramakrishnan 621d00a5c6 GPG verify releases
Part of #189
2018-08-01 17:16:42 -07:00
Girish Ramakrishnan 5bd7cd6749 Check if syslog binary exists first
The command fails in a subshell and strange things happen

==> installer: update cloudron-syslog
/tmp/box-src-hotfix/scripts/installer.sh: line 99: /usr/local/cloudron-syslog/bin/cloudron-syslog: No such file or directory
/tmp/box-src-hotfix/scripts/installer.sh: line 99: : command not found
2018-08-01 12:18:56 -07:00
Girish Ramakrishnan 5fb525f011 Commit binary public key
GPG's --verify option can only use binary format
2018-07-31 20:25:46 -07:00
Girish Ramakrishnan d8257c4745 refactor updater logic into updater.js 2018-07-31 20:20:10 -07:00
Girish Ramakrishnan ef5dc7311f Do not encrypt empty path 2018-07-31 19:41:03 -07:00
Girish Ramakrishnan 498642b670 Fix debugs 2018-07-31 19:32:56 -07:00
Girish Ramakrishnan daa8514654 Pass the message and not error object 2018-07-31 10:46:35 -07:00
Girish Ramakrishnan 608de479fb Add releases public key
Part of #189
2018-07-31 10:34:01 -07:00
Johannes Zellner 51f7a47ea6 Fix db migration script 2018-07-30 22:58:09 +02:00
Girish Ramakrishnan 480aed9f33 Update mail addon 2018-07-30 12:37:37 -07:00
Girish Ramakrishnan 74ae0a1787 More changes 2018-07-30 10:46:14 -07:00
Girish Ramakrishnan ed8351b0dc Add re-installation check
Fixes #536
2018-07-30 10:10:30 -07:00
Girish Ramakrishnan a1070b7da3 storage: fix file not found message 2018-07-30 07:47:52 -07:00
Girish Ramakrishnan 3067d87ca9 handle decryption errors 2018-07-29 21:01:20 -07:00
Girish Ramakrishnan 56ca6f449f Set any restore errors in status object
Fixes #505
2018-07-29 20:31:58 -07:00
Girish Ramakrishnan 99ad3e499d s3: do not rely on entry.s3 2018-07-29 09:00:57 -07:00
Girish Ramakrishnan 7182ad4205 s3: Remove usage of entries.s3 2018-07-29 07:51:12 -07:00
Girish Ramakrishnan 0b10e2b332 s3: add listDir
Fixes #440
2018-07-28 09:05:44 -07:00
Girish Ramakrishnan f546d53ca2 gcs: add listDir
part of #440
2018-07-28 03:51:32 -07:00
Girish Ramakrishnan 2bcc0eef96 fs: downloadDir is unused 2018-07-27 16:56:30 -07:00
Girish Ramakrishnan a5daad2e1a rsync: encrypted download (fs backend)
Part of #440
2018-07-27 16:06:13 -07:00
Girish Ramakrishnan b3c8767d79 rsync: encrypted upload (fs backend)
Part of #440
2018-07-27 15:34:48 -07:00
Girish Ramakrishnan f97f528f05 backup: Implement downloadDir using listDir 2018-07-27 15:24:35 -07:00
Girish Ramakrishnan ba8a549235 fs: open the source stream using download() 2018-07-27 15:23:48 -07:00
Girish Ramakrishnan 737541f707 storage: standardize the listDir API 2018-07-27 15:23:26 -07:00
Girish Ramakrishnan 94cb222869 filesystem: Use listDir
We can now refactor all the iterating/fs write logic into backups.js
(required for restoring encrypted backups)
2018-07-27 14:09:52 -07:00
Girish Ramakrishnan df98847535 Remove backupDone from storage interface
This is only used by caas
2018-07-27 12:08:19 -07:00
Girish Ramakrishnan 3d22458f9b Add the 3.0 UI changes 2018-07-27 00:08:08 -07:00
Girish Ramakrishnan d76381fa26 More 3.0 changes 2018-07-27 00:04:31 -07:00
Girish Ramakrishnan 606cd4da36 Allow - in mailbox names (for no-reply) 2018-07-26 23:48:21 -07:00
Girish Ramakrishnan 554006683e Only unset of admin flag is disallowed 2018-07-26 23:43:44 -07:00
Girish Ramakrishnan 0966edd8fe Fix missing require 2018-07-26 22:36:20 -07:00
Girish Ramakrishnan 78a2176d1d Make admin simply a boolean instead of group
This simplifies a lot of logic. Keeping an admin group has no benefit
2018-07-26 22:29:57 -07:00
Girish Ramakrishnan 39848a25a8 Refactor a bit 2018-07-26 15:35:41 -07:00
Girish Ramakrishnan ea946396e7 Use users.isAdmin in all places 2018-07-26 13:23:06 -07:00
Girish Ramakrishnan b4d5def56d Revert role support 2018-07-26 13:23:06 -07:00
Johannes Zellner 477abf53f3 Add initial 3.0.0 changes 2018-07-26 21:36:43 +02:00
Girish Ramakrishnan 0cb03e3789 Add REST route for platform config
Fixes #566
2018-07-25 14:09:15 -07:00
Girish Ramakrishnan f4d7d4e7f2 Set the domain field 2018-07-25 11:41:42 -07:00
Girish Ramakrishnan c09ae963e9 merge set/addDnsRecords
This also makes the update mail domain route redundant
2018-07-25 10:51:21 -07:00
Girish Ramakrishnan fa30312cea lint 2018-07-25 10:34:57 -07:00
Girish Ramakrishnan c063267c72 Split setting of dns records from mail config route
This allows us to enable email server but not setup the dns records
yet. Useful for importing existing mailboxes from another server
without adjusting the DNS.
2018-07-25 10:29:28 -07:00
Girish Ramakrishnan 589602cdb0 Add note 2018-07-25 08:29:39 -07:00
Girish Ramakrishnan 6be062f8fd Return empty object for angular 2018-07-24 22:19:34 -07:00
Girish Ramakrishnan 837ec4eb12 Send subdomain field in mail status 2018-07-24 14:25:21 -07:00
Girish Ramakrishnan 4a4166764a relay: Add a connect timeout 2018-07-23 17:05:15 -07:00
Girish Ramakrishnan 7654f36e23 Do temporary redirect instead
Permanent redirect can be cached forever in the browser which
means that changing redirect settings appear to not work.
2018-07-15 17:43:36 -07:00
Girish Ramakrishnan 6810c61e58 Add audit event for ownership transfer 2018-07-05 13:51:22 -07:00
Johannes Zellner 75f9b19db2 Ensure we uri encode the email query arguments for invite, reset and setup links 2018-07-04 11:09:29 +02:00
Johannes Zellner 17410c9432 Remove notes about updates only on paid plan for the digest 2018-07-04 10:59:17 +02:00
Johannes Zellner 8a1de81284 Cleanup sysadmin backup trigger test to only test for backup start event 2018-06-30 14:33:51 +02:00
Johannes Zellner 7b540a1d2d Fixup database tests 2018-06-30 14:12:35 +02:00
Johannes Zellner 8e8488a8e6 Lets be explicit about the error 2018-06-30 14:02:40 +02:00
Johannes Zellner b1b843fdd8 Fix dns tests 2018-06-30 13:59:23 +02:00
Johannes Zellner c13c4d0b28 Ensure we purge all nginx configs of an app 2018-06-29 23:14:06 +02:00
Johannes Zellner 2371c8053f We have added a query for removing subdomains in the transaction 2018-06-29 23:14:06 +02:00
Johannes Zellner 7dc2f3cb5b Also wait for alternateDomains to be in sync 2018-06-29 23:14:06 +02:00
Johannes Zellner 163563f400 Remove now unused dnsRecordId 2018-06-29 23:14:06 +02:00
Johannes Zellner 868ed977b3 Cleanup alternateDomain dns records and nginx config 2018-06-29 23:14:06 +02:00
Johannes Zellner 262fe18fb2 Set dns record id for alternateDomains 2018-06-29 23:14:06 +02:00
Johannes Zellner 1eba79660e Move dnsRecordId to subdomains table 2018-06-29 23:14:06 +02:00
Johannes Zellner 3088ac098f Put redirect label into alternateDomain nginx configs 2018-06-29 23:14:06 +02:00
Johannes Zellner 45a41ea161 Fix typo 2018-06-29 23:14:06 +02:00
Johannes Zellner 6c17709d2a Fix fqdn building for alternateDomains 2018-06-29 23:14:06 +02:00
Johannes Zellner 2a52543087 The property is called subdomain not location 2018-06-29 23:14:06 +02:00
Johannes Zellner f4f6f4e7e0 Use business logic for getting all apps in taskmanager 2018-06-29 23:14:06 +02:00
Johannes Zellner f53c526677 Remove superflous . for user cert key file location 2018-06-29 23:14:06 +02:00
Johannes Zellner 1aa58a3905 ignore alternateDomains field for appdb updates 2018-06-29 23:14:06 +02:00
Johannes Zellner 2d58a6bdff Fix typo 2018-06-29 23:14:06 +02:00
Johannes Zellner 40c22a1ad7 send alternateDomains over the wire 2018-06-29 23:14:06 +02:00
Johannes Zellner 3d0da34960 Create nginx configs for app redirects 2018-06-29 23:14:06 +02:00
Johannes Zellner a6e53e3617 Ensure certificates for alternateDomains 2018-06-29 23:14:06 +02:00
Johannes Zellner 8efab41d37 Amend the alternateDomains property to app objects 2018-06-29 23:14:06 +02:00
Johannes Zellner 9af456cc7d Fix appdb.add as location and domain columns are gone 2018-06-29 23:14:06 +02:00
Johannes Zellner 9ba78b5b87 Setup alternate domains in apptask configure 2018-06-29 23:14:06 +02:00
Johannes Zellner b1b848de21 Support configuring alternateDomains for apps 2018-06-29 23:14:06 +02:00
Johannes Zellner 5497a7d4d8 Do not handle location and domain field for apps table updates 2018-06-29 23:14:06 +02:00
Johannes Zellner 18887b27e6 Fix migration script typo 2018-06-29 23:14:06 +02:00
Johannes Zellner fb42b54210 Remove apps.location and apps.domain
This is now managed in the subdomains table
2018-06-29 23:14:06 +02:00
Johannes Zellner 4d2ba2adaa Use subdomains table in appdb 2018-06-29 23:14:06 +02:00
Johannes Zellner c97e8d6bd4 add subdomains table with migration scripts 2018-06-29 23:14:06 +02:00
Girish Ramakrishnan b15029de11 Send tokenScopes as an array 2018-06-29 09:53:12 -07:00
Girish Ramakrishnan 9aa74c99fc Make setup route return tokenScopes
This is used by the dashboard logic during initial activation
2018-06-29 08:27:34 -07:00
Girish Ramakrishnan 35c9e99102 Move the update info into separate route (since it is sensitive) 2018-06-28 17:50:27 -07:00
Girish Ramakrishnan cab9bc3a61 Unify the config routes into /api/v1/config
No more separate config routes for different types of user
2018-06-28 17:40:57 -07:00
Girish Ramakrishnan 712c920b86 Move caas config to separate route 2018-06-28 17:28:46 -07:00
Girish Ramakrishnan 9978dff627 Add API to set and transfer ownership 2018-06-28 16:48:04 -07:00
Girish Ramakrishnan ff5bd42bef remove mostly dead comment 2018-06-28 14:19:56 -07:00
Girish Ramakrishnan dfa318e898 Add ownerId for apps
This tracks who installed the app.
2018-06-28 14:13:12 -07:00
Girish Ramakrishnan 38977858aa When issuing token intersect with the existing user roles
Also:
* Move token validation to accesscontrol.js
* Use clients.addTokenByUserId everywhere
2018-06-28 00:07:43 -07:00
Girish Ramakrishnan 6510240c0a Fix accesscontrol.intersectScopes 2018-06-27 18:08:38 -07:00
Girish Ramakrishnan d66dc11f01 Make canonicalScopeString return sorted array 2018-06-27 14:07:25 -07:00
Girish Ramakrishnan ce4424d115 debug out the scope 2018-06-27 13:54:10 -07:00
Girish Ramakrishnan a958c01974 digest: drop the Cloudron prefix in the subject 2018-06-27 11:00:41 -07:00
Girish Ramakrishnan 877f181f8d Put ts in app listing as well 2018-06-26 19:58:21 -07:00
Girish Ramakrishnan 02c0137dc1 Add a timestamp column to apps table
this field can be used in UI to see if more detailed app information
has to be fetched (since it calls the list apps REST API periodically)
2018-06-26 18:16:39 -07:00
Girish Ramakrishnan d0b34cc43e Fix schema 2018-06-26 18:05:34 -07:00
Girish Ramakrishnan 93a2cab355 Ignore internal mailbox conflict errors 2018-06-26 17:44:14 -07:00
Girish Ramakrishnan 6907475f7a Add app management scope
This splits the apps API into those who have just 'read' access
and those who have 'manage' access.
2018-06-26 08:56:30 -07:00
Girish Ramakrishnan 9bf93b026b rename to removeInternalFields 2018-06-25 16:40:16 -07:00
Girish Ramakrishnan f932f8b3d3 Add user management scope
This splits the user and groups API into those who have just 'read' access
and those who have 'manage' access.
2018-06-25 16:10:00 -07:00
Girish Ramakrishnan 7ab5d5e50d Add domain management scope
This splits the domains API into those who have just 'read' access
(i.e without configuration details) and those who have 'manage' access.
2018-06-25 15:12:22 -07:00
Johannes Zellner 5028230354 Update cloudron-syslog
Fixes #564
2018-06-25 19:35:13 +02:00
Girish Ramakrishnan 80e9214f5b Reduce password length for sendmail/recvmail
Fixes #565
2018-06-22 16:37:34 -07:00
Girish Ramakrishnan 5ca64dd642 Normalize host path to prevent access to arbitrary paths 2018-06-22 08:29:08 -07:00
Girish Ramakrishnan 24d9d3063b Allow specifying bindMounts array to localstorage addon 2018-06-21 22:04:06 -07:00
Girish Ramakrishnan 74b1df17c0 Fix groups test and route to make name optional 2018-06-20 09:31:50 -07:00
Girish Ramakrishnan 7880a2f9c3 API returns 403 for incorrect password 2018-06-20 09:27:24 -07:00
Girish Ramakrishnan 8a84872704 Wrong password logs out the user 2018-06-18 18:52:35 -07:00
Girish Ramakrishnan 5d13cc363f Allow group name to be changed 2018-06-18 18:30:40 -07:00
Girish Ramakrishnan 987a42b448 Fix syntax 2018-06-18 17:58:35 -07:00
Girish Ramakrishnan 3601e4f8a6 if -> while 2018-06-18 17:39:15 -07:00
Girish Ramakrishnan 60ed290179 validate role names against existing roles 2018-06-18 17:32:07 -07:00
Girish Ramakrishnan ff73bc121f Make tokenScope plural 2018-06-18 15:10:02 -07:00
Girish Ramakrishnan 6cd0601629 Map group roles to scopes 2018-06-18 14:52:39 -07:00
Girish Ramakrishnan b5c8e7a52a Rename to getGroups 2018-06-18 14:10:29 -07:00
Girish Ramakrishnan 7f3114e67d Rename to get/setMembership (to indicate IDs and not group objects 2018-06-18 13:57:17 -07:00
Girish Ramakrishnan 1dbcf2a46a Rename to groups.update 2018-06-18 13:41:27 -07:00
Girish Ramakrishnan 898cbd01b3 tokens table always has canonical scope 2018-06-17 23:11:36 -07:00
Girish Ramakrishnan b6b7d08af3 Rename to accesscontrol.canonicalScopeString 2018-06-17 22:43:42 -07:00
Girish Ramakrishnan 6a2dacb08a Make intersectScopes take an array 2018-06-17 22:39:33 -07:00
Girish Ramakrishnan 1015b0ad9c validateScope -> validateScopeString 2018-06-17 22:29:17 -07:00
Girish Ramakrishnan 106e17f7ff caas: verify dns config
this is so that it fails if someone gives invalid token via API call
2018-06-17 22:22:05 -07:00
Girish Ramakrishnan 6ca28d9a58 validate dns config parameters 2018-06-17 22:21:51 -07:00
Girish Ramakrishnan ad6bc191f9 Make hasScopes take an array 2018-06-17 21:06:17 -07:00
Girish Ramakrishnan 682f7a710c Add an appstore scope for subscription settings 2018-06-17 18:09:13 -07:00
Girish Ramakrishnan f24a099e79 Remove user.admin property
The UI will now base itself entirely off the scopes of the token
2018-06-17 16:49:56 -07:00
Girish Ramakrishnan 156ffb40c9 Remove scope from users.get 2018-06-17 16:07:20 -07:00
Girish Ramakrishnan db8b6838bb Move skip password verification logic to accesscontrol.js 2018-06-17 15:20:27 -07:00
Girish Ramakrishnan c3631350cf We can skip user.scope here since we will intersect at access time anyway 2018-06-17 15:11:10 -07:00
Girish Ramakrishnan 669a1498aa Do not dump token in logs 2018-06-17 15:01:42 -07:00
Girish Ramakrishnan 12e55d1fab Use _.flatten instead 2018-06-16 13:03:52 -07:00
Girish Ramakrishnan ca9cd2cf0f Add groups.getRoles 2018-06-16 00:29:56 -07:00
Girish Ramakrishnan e8d9597345 Fix various error codes
401 - bad password/wrong password
403 - authenticated but not authorized
409 - conflict
2018-06-15 23:15:30 -07:00
Girish Ramakrishnan 24b0a96f07 Move passport logic to routes 2018-06-15 17:32:40 -07:00
Girish Ramakrishnan 858ffcec72 Add note 2018-06-15 17:04:18 -07:00
Girish Ramakrishnan 05a8911cca condense the comments 2018-06-15 16:56:35 -07:00
Girish Ramakrishnan 89b41b11a4 Remove bogus accesscontrol logic 2018-06-15 16:26:14 -07:00
Girish Ramakrishnan 491d1c1273 getByResetToken already has the user object 2018-06-15 16:22:28 -07:00
Girish Ramakrishnan 0a0884bf93 lint 2018-06-15 16:10:06 -07:00
Girish Ramakrishnan a1ac7f2ef9 Remove support for authenticating non-oauth2 clients via BasicStrategy
This is not used anywhere
2018-06-15 15:38:58 -07:00
Girish Ramakrishnan 6aef9213aa Add notes on the various strategies 2018-06-15 15:38:53 -07:00
Girish Ramakrishnan 2e92172794 Do not dump the entire token 2018-06-15 14:56:52 -07:00
Girish Ramakrishnan c210359046 Create the backup logs dir 2018-06-15 14:56:32 -07:00
Girish Ramakrishnan 042ea081a0 Typo 2018-06-15 14:35:52 -07:00
Girish Ramakrishnan 1c32224a8a Move backup logs to make the REST API work 2018-06-15 09:47:34 -07:00
Johannes Zellner b3fa5afe3a First show apptask logs, then app logs 2018-06-15 14:58:07 +02:00
Johannes Zellner 843fec9dcb Fix wrong hasSubscription logic for digest email 2018-06-15 13:44:33 +02:00
Johannes Zellner 35d9cc3c02 Update cloudron-syslog 2018-06-15 10:20:03 +02:00
Girish Ramakrishnan 02d5d2f808 Add API to add and update the group roles 2018-06-15 00:28:27 -07:00
Girish Ramakrishnan a77d45f5de Add rolesJson to groups table
This will contain the roles ('role definition') of a group of
users. We will internally map these to our API scopes.
2018-06-14 22:54:52 -07:00
Girish Ramakrishnan 5e09f3dcb2 Group names can contain - and end with .app
now that we have decoupled it from mailbox names, this restriction
can be removed
2018-06-14 22:22:09 -07:00
Girish Ramakrishnan eb566d28e7 Remove groupdb._addDefaultGroups
Putting this in db code causes issues when we merge in the
accesscontrol/roles variables (groupdb needs to source those
variables)
2018-06-14 21:56:34 -07:00
Girish Ramakrishnan 8795da5d20 Allow subscopes
We can now have scopes as apps:read, apps:write etc
2018-06-14 20:56:04 -07:00
Girish Ramakrishnan a9ec46c97e Add test for accesscontrol.validateScope 2018-06-14 20:51:22 -07:00
Girish Ramakrishnan dc86b0f319 validateRequestedScopes -> hasScopes 2018-06-14 20:31:48 -07:00
Girish Ramakrishnan f7089c52ff normalizeScope -> intersectScope 2018-06-14 20:23:56 -07:00
Girish Ramakrishnan 62793ca7b3 Add accesscontrol.canonicalScope tests 2018-06-14 20:17:59 -07:00
Girish Ramakrishnan 92e6909567 Move feedback route to cloudronScope 2018-06-14 20:03:51 -07:00
Girish Ramakrishnan 55e5c319fe Fix failing log test 2018-06-14 13:43:44 -07:00
Johannes Zellner 1f8451fedb Do not print tail file headers in the logs 2018-06-14 12:53:49 +02:00
Johannes Zellner cdc78936b5 Ignore faulty empty log lines 2018-06-14 12:53:19 +02:00
Girish Ramakrishnan eaf0b4e56e mail api: handle not found errors 2018-06-13 07:51:22 -07:00
Girish Ramakrishnan 7339c37b98 Fix invite link in userAdded notification 2018-06-12 20:08:59 -07:00
Girish Ramakrishnan 3176938ea0 Add 2.4.1 changes 2018-06-12 19:25:24 -07:00
Girish Ramakrishnan c3c77c5a97 Fix wording 2018-06-12 18:18:41 -07:00
Girish Ramakrishnan 32e6b9024c Add email query param to reset code path
This reduces any attack surface
2018-06-12 17:56:41 -07:00
Girish Ramakrishnan 5a6ea33694 Display apptask logs 2018-06-12 14:55:58 -07:00
Girish Ramakrishnan 60bff95d9f Add journalctl fallback for box logs 2018-06-12 14:39:33 -07:00
Girish Ramakrishnan 0cc2838b8b lint 2018-06-12 13:49:52 -07:00
Girish Ramakrishnan 0fc4f4bbff Explicitly pass port and logdir 2018-06-11 22:53:12 -07:00
Girish Ramakrishnan 0b82146b3e Install cloudron-syslog service file from box repo 2018-06-11 22:42:49 -07:00
Girish Ramakrishnan 4369b3046e Make options non-optional 2018-06-11 15:14:59 -07:00
Girish Ramakrishnan ac75b60f47 Fix password validation text in setup and reset forms 2018-06-11 14:01:40 -07:00
Girish Ramakrishnan d752ef5fad Move password generation logic to model code 2018-06-11 13:06:08 -07:00
Girish Ramakrishnan c099d5d3fa Make password strength be 8 chars
Fixes #434
2018-06-11 12:55:24 -07:00
Girish Ramakrishnan 6534297a5d Remove hat module
It's not been updated for 6 years!
2018-06-11 12:38:29 -07:00
Girish Ramakrishnan 2aa6350c94 Change the function names
We had these because user and mailbox code used to be mixed
2018-06-11 11:43:41 -07:00
Girish Ramakrishnan 8b4a399b8f More changes 2018-06-11 11:43:41 -07:00
Johannes Zellner 177243b7f2 Support new platform/addon log style 2018-06-11 20:09:45 +02:00
Girish Ramakrishnan c2ca827458 Add 2.4.0 changes 2018-06-11 10:13:48 -07:00
Girish Ramakrishnan 90d7dc893c Fix test 2018-06-11 10:01:54 -07:00
Girish Ramakrishnan eeaaa95ca3 Put email on free plan 2018-06-09 18:26:00 -07:00
Girish Ramakrishnan 04be582573 make the mailbox name follow the apps new location, if the user did not set it explicitly 2018-06-09 11:05:54 -07:00
Girish Ramakrishnan 0953787559 Fix docker exec terminal resize issue
Fixes #549
2018-06-08 11:44:24 -07:00
Girish Ramakrishnan 3bd8a58ea5 Update docker to 18.03.1 2018-06-08 09:13:46 -07:00
Girish Ramakrishnan 275181824f Not sure why package lock keeps changing 2018-06-07 18:45:30 -07:00
Girish Ramakrishnan f814ffb14f Update node version 2018-06-07 18:43:52 -07:00
Girish Ramakrishnan 95ae948fce more package lock changes 2018-06-07 18:42:47 -07:00
Girish Ramakrishnan 9debf1f6c6 Update some packages 2018-06-07 18:36:36 -07:00
Girish Ramakrishnan 0e583b5afe Update node to 8.11.2 2018-06-07 17:06:47 -07:00
Girish Ramakrishnan fa47031a63 cloudron-activate: Set externalDisk to true 2018-06-07 11:38:07 -07:00
Girish Ramakrishnan 7fd1bb8597 backup: Add externalDisk option to fs backend
This merely confirms from the user understands that backups have to
stored on an external disk.
2018-06-07 11:14:13 -07:00
Johannes Zellner 8c5b550caa Explicitly use cloudron-syslog 1.0.0 instead of moving master 2018-06-07 16:38:44 +02:00
Johannes Zellner 3d57c32853 Explicitly send empty object for successful post to avoid angular warnings 2018-06-07 16:10:47 +02:00
Johannes Zellner 898d928dd6 logrotate files under platformdata must be owned by root 2018-06-06 18:45:54 +02:00
Johannes Zellner c578a048dd Ensure app logrotate file is owned by root 2018-06-06 18:39:52 +02:00
Johannes Zellner 2a475c1199 Add logrotate for app and addon logs 2018-06-06 17:36:48 +02:00
Johannes Zellner 57e195883c Use plain syslog tags to be compatible with the format 2018-06-06 14:09:50 +02:00
Johannes Zellner f2178d9b81 Setup addons to log to cloudron-syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner df1ac43f40 Use subshells correctly 2018-06-06 14:09:50 +02:00
Johannes Zellner 39059c627b Add --unsafe-perm for cloudron-syslog installation 2018-06-06 14:09:50 +02:00
Johannes Zellner d942c77ceb Bump infra version to reconfigure the container 2018-06-06 14:09:50 +02:00
Johannes Zellner c39240c518 Install cloudron-syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner fd0e2782d8 Deliver the correct utc timestamp instead of the ISO string 2018-06-06 14:09:50 +02:00
Johannes Zellner 36aaa0406e Fix comment about firewall rule setup 2018-06-06 14:09:50 +02:00
Johannes Zellner 17ecb366af Bring back json log format for now 2018-06-06 14:09:50 +02:00
Johannes Zellner 1a83281e16 use port 2514 for syslog 2018-06-06 14:09:50 +02:00
Johannes Zellner ec41e0eef5 Use tail instead of journalctl to deliver logs 2018-06-06 14:09:50 +02:00
Johannes Zellner d4097ed4e0 Move logs into platformdata/logs 2018-06-06 14:09:50 +02:00
Johannes Zellner 8fa99fae1a Put all apptask logs of an app in the same log file 2018-06-06 14:09:50 +02:00
Johannes Zellner e9400e5dce support test usecase to not put logs in /var/log 2018-06-06 14:09:50 +02:00
Johannes Zellner 372a17dc37 Cleanup logs on app uninstall 2018-06-06 14:09:50 +02:00
Johannes Zellner 5ca60b2d3c Since we use log files now, lets keep the apptask timestamp for debug() 2018-06-06 14:09:50 +02:00
Johannes Zellner 1dc649b7a2 Put apptask logs alongside the app logs 2018-06-06 14:09:50 +02:00
Johannes Zellner 74437db740 Use syslog logging backend for apps and addons 2018-06-06 14:09:50 +02:00
Girish Ramakrishnan 70128458b2 Fix crash when renewAll is called when cloudron is not setup yet 2018-06-05 21:27:32 -07:00
Girish Ramakrishnan 900225957e typo: code should return SetupError 2018-06-05 21:19:47 -07:00
Girish Ramakrishnan fd8f5e3c71 Return error for trailing dot instead 2018-06-05 21:09:07 -07:00
Girish Ramakrishnan 7382ea2b04 Handle my subdomain already existing 2018-06-05 20:53:28 -07:00
Girish Ramakrishnan 09163b8a2b strip any trailing dot in the domain and zoneName 2018-06-05 20:33:14 -07:00
Girish Ramakrishnan 953398c427 lint 2018-06-05 20:02:47 -07:00
Girish Ramakrishnan 9f7406c235 cloudron-activate: Add option to setup backup dir 2018-06-05 19:40:46 -07:00
Girish Ramakrishnan 2e427aa60e Add 2.3.2 changes 2018-06-05 09:51:56 -07:00
Girish Ramakrishnan ab80cc9ea1 Add username to the TOTP secret name
This works around issue in FreeOTP app which crashed when
the same name is used.

https://github.com/freeotp/freeotp-ios/issues/69
https://github.com/freeotp/freeotp-android/issues/69
2018-06-04 16:08:03 -07:00
Girish Ramakrishnan 321f11c644 mysql: _ prefix is hardcoded in mysql addon already
Fixes #560
2018-06-04 12:31:40 -07:00
Girish Ramakrishnan 47f85434db cloudron-activate: always login since activate return token and not accessToken 2018-06-01 00:12:19 -07:00
Girish Ramakrishnan 7717c7b1cd Add cloudron-activate script to automate activation from VM image 2018-05-31 23:46:44 -07:00
Johannes Zellner 7618aa786c Handle AppstoreError properly when no appstore account was set 2018-05-30 20:33:58 +02:00
Girish Ramakrishnan f752cb368c Remove spamcannibal
Fixes #559
2018-05-30 11:07:17 -07:00
Girish Ramakrishnan ca500e2165 mailer: do not send notifications to fallback email 2018-05-30 09:26:59 -07:00
Johannes Zellner 371f81b980 Add test for mail enabling without a subscription 2018-05-30 00:02:18 +02:00
Johannes Zellner c68cca9a54 Fixup mail test, which requires a subscription 2018-05-29 23:59:53 +02:00
Johannes Zellner 9194be06c3 Fix app purchase test 2018-05-29 23:24:08 +02:00
Johannes Zellner 9eb58cdfe5 Check for plan when enabling email 2018-05-29 13:31:43 +02:00
Johannes Zellner 99be89012d No need to check for active subscription state, as the appstore already does this 2018-05-29 13:31:43 +02:00
Johannes Zellner 541fabcb2e Add convenience function to determine if subscription is 'free' or not 2018-05-29 13:31:43 +02:00
Johannes Zellner 915e04eb08 We do not have an 'undecided' plan state anymore 2018-05-29 13:31:43 +02:00
Girish Ramakrishnan 48896d4e50 more changes 2018-05-28 10:06:46 -07:00
Johannes Zellner 29682c0944 Only allow max of 2 apps on the free plan 2018-05-26 18:53:20 +02:00
Girish Ramakrishnan 346b1cb91c more changes 2018-05-26 08:11:19 -07:00
Girish Ramakrishnan e552821c01 Add 2.3.1 changes 2018-05-25 11:44:04 -07:00
Girish Ramakrishnan bac3ba101e Add mailboxName to app configure route
Fixes #558
2018-05-24 16:26:34 -07:00
Girish Ramakrishnan 87c46fe3ea apps: return mailbox name as part of app
part of cloudron/box#558
2018-05-24 15:50:46 -07:00
Girish Ramakrishnan f9763b1ad3 namecom: MX record not set properly 2018-05-24 09:41:52 -07:00
Girish Ramakrishnan f1e6116b83 Fix copyright years 2018-05-23 20:02:33 -07:00
Girish Ramakrishnan 273948c3c7 Fix tests 2018-05-22 13:22:48 -07:00
Girish Ramakrishnan 9c073e7bee Preserve addons credentials when restoring 2018-05-22 13:07:58 -07:00
Girish Ramakrishnan 8b3edf6efc Bump mail container for managesieve fix 2018-05-18 18:26:19 -07:00
Girish Ramakrishnan 07e649a2d3 Add more changes 2018-05-17 20:17:24 -07:00
Girish Ramakrishnan 8c63b6716d Trigger a re-configure 2018-05-17 20:16:51 -07:00
Girish Ramakrishnan 6fd314fe82 Do not change password on app update
Fixes #554
2018-05-17 19:48:57 -07:00
Girish Ramakrishnan 0c7eaf09a9 bump container versions 2018-05-17 10:00:00 -07:00
Girish Ramakrishnan d0988e2d61 Generate password for mongodb on platform side
Part of #554
2018-05-17 10:00:00 -07:00
Girish Ramakrishnan 4bedbd7167 Generate password for postgresql on platform side
Part of #554
2018-05-17 10:00:00 -07:00
Girish Ramakrishnan 7ca7901a73 Generate password for mysql on platform side
Part of #554
2018-05-17 09:59:57 -07:00
Girish Ramakrishnan d28dfdbd03 Add 2.3.0 changes 2018-05-17 09:24:47 -07:00
Girish Ramakrishnan c85ca3c6e2 account setup simply redirects to main page now 2018-05-17 09:17:08 -07:00
Girish Ramakrishnan da934d26af call callback 2018-05-17 09:16:32 -07:00
Girish Ramakrishnan f7cc49c5f4 move platform config to db
this way it can be tied up to some REST API later

part of #555
2018-05-16 17:34:56 -07:00
Girish Ramakrishnan 27e263e7fb lint 2018-05-16 14:08:54 -07:00
Girish Ramakrishnan 052050f48b Add a way to persist addon memory configuration
Fixes #555
2018-05-16 14:00:55 -07:00
Girish Ramakrishnan 81e29c7c2b Make the INFRA_VERSION_FILE more readable 2018-05-16 09:54:42 -07:00
Girish Ramakrishnan c3fbead658 Allow zoneName to be changed in domain update route 2018-05-15 15:39:30 -07:00
Girish Ramakrishnan 36f5b6d678 manual dns: handle ENOTFOUND
Fixes #548
2018-05-15 15:39:18 -07:00
Girish Ramakrishnan a45b1449de Allow ghost users to skip 2fa 2018-05-14 15:07:01 -07:00
Girish Ramakrishnan a1020ec6b8 remove /user from profile route 2018-05-13 21:53:06 -07:00
Johannes Zellner d384284ec8 Add name.com DNS provider in the CHANGES file 2018-05-11 10:03:58 +02:00
Girish Ramakrishnan bd29447a7f gcdns: Fix typo 2018-05-10 10:05:42 -07:00
Johannes Zellner aa5952fe0b Wait longer for dns in apptask
name.com often takes longer to sync all nameservers, which means we
timeout too early for them
2018-05-10 15:37:47 +02:00
Johannes Zellner 39dc5da05a We have to return a value on dns record upserting 2018-05-09 18:58:09 +02:00
Johannes Zellner d0e07d995a Add name.com dns tests 2018-05-09 18:13:21 +02:00
Johannes Zellner 94408c1c3d Add name.com DNS provider 2018-05-09 18:13:14 +02:00
Girish Ramakrishnan 66f032a7ee route53: use credentials instead of dnsConfig 2018-05-07 23:41:03 -07:00
Girish Ramakrishnan 4356df3676 bump timeout 2018-05-07 16:28:11 -07:00
Girish Ramakrishnan 1e730d2fc0 route53: more test fixing 2018-05-07 16:20:03 -07:00
Girish Ramakrishnan e8875ccd2e godaddy: add tests 2018-05-07 16:09:00 -07:00
Girish Ramakrishnan 2b3656404b route53: fix tests 2018-05-07 15:53:08 -07:00
Girish Ramakrishnan 60b5e6f711 gandi: add tests 2018-05-07 15:51:51 -07:00
Girish Ramakrishnan b9166b382d route53: set listHostedZonesByName for new/updated domains 2018-05-07 13:42:10 -07:00
Girish Ramakrishnan d0c427b0df Add more 2.2 changes 2018-05-07 11:46:27 -07:00
Girish Ramakrishnan da5d0c61b4 godaddy: workaround issue where there is no del record API 2018-05-07 11:41:37 -07:00
Girish Ramakrishnan 1f75c2cc48 route53: add backward compat for pre-2.2 IAM perms
backward compat for 2.2, where we only required access to "listHostedZones"
2018-05-07 11:24:34 -07:00
Girish Ramakrishnan d0197aab15 Revert "No need to iterate over the hosted zones anymore"
This reverts commit e4a70b95f5.

We will add backward compat route for pre-2.2 cloudrons
2018-05-07 11:23:28 -07:00
Johannes Zellner e4a70b95f5 No need to iterate over the hosted zones anymore 2018-05-07 16:35:32 +02:00
Johannes Zellner f4d3d79922 Query only requested Route53 zone
Fixes #550
2018-05-07 16:30:42 +02:00
Girish Ramakrishnan e3827ee25f Add more 2.2 changes 2018-05-06 23:52:02 -07:00
Girish Ramakrishnan 9981ff2495 Add GoDaddy Domain API 2018-05-06 23:07:52 -07:00
Girish Ramakrishnan 722b14b13d Add note on MX records 2018-05-06 22:14:39 -07:00
Girish Ramakrishnan eb2fb6491c gandi: more fixes 2018-05-06 21:16:47 -07:00
Girish Ramakrishnan a53afbce91 Add Gandi LiveDNS backend 2018-05-06 19:48:51 -07:00
Girish Ramakrishnan 31af6c64d0 Expire existing webadmin token so that the UI gets a new token 2018-05-06 13:08:22 -07:00
Girish Ramakrishnan e8efc5a1b2 Fix test 2018-05-06 12:58:39 -07:00
Girish Ramakrishnan 0c07c6e4d0 Allow "-" in usernames
now that username and mailboxes are totally separate, we can allow '-'.
'+' is still reserved because LDAP it.

Fixes #509
2018-05-05 09:56:21 -07:00
Girish Ramakrishnan da5fd71aaa Bump mail container for CRAM-MD5 login fix 2018-05-04 21:57:26 -07:00
Girish Ramakrishnan d57d590363 2.2.0 changes 2018-05-04 10:30:24 -07:00
Johannes Zellner d6e49415d4 Only list user mailboxes in ldap search 2018-05-04 17:02:04 +02:00
Johannes Zellner cb73eb61d4 Allow binds against mailboxes 2018-05-04 17:02:04 +02:00
Johannes Zellner 4ce3a262a3 Allow search for mailboxes over ldap for a specific domain 2018-05-04 17:02:04 +02:00
Girish Ramakrishnan d18d1a977a Add duplicate profile route for compat with old apps using oauth addon 2018-05-03 09:29:46 -07:00
Girish Ramakrishnan 616e38189c Revert "x"
This reverts commit 182ea3dac3.
2018-05-03 09:29:23 -07:00
Johannes Zellner 726cafcee4 Rollback appdb record on clone and install if appstore purchase fails 2018-05-03 13:20:34 +02:00
Girish Ramakrishnan e5c43e9acd Remove debug 2018-05-02 12:41:22 -07:00
Girish Ramakrishnan f09e8664d1 Return canonical scope in REST responses
The '*' scope is purely an implementation detail. It cannot
be requested as such.
2018-05-02 12:36:41 -07:00
Girish Ramakrishnan 182ea3dac3 x 2018-05-01 15:17:48 -07:00
Girish Ramakrishnan 97acd40829 remove obsolete roleSdk from the database
the file is renamed because cloudron.io was patched by mistake and we want to run this
migration there
2018-05-01 14:53:46 -07:00
Girish Ramakrishnan f1abb2149d gravatar url is already generated client side 2018-05-01 14:30:48 -07:00
Girish Ramakrishnan 8c4015851a merge auth.js into accesscontrol.js 2018-05-01 14:03:10 -07:00
Girish Ramakrishnan a545bdd574 merge developer.js into clients.js 2018-05-01 14:02:59 -07:00
Girish Ramakrishnan d1135accbd lint 2018-05-01 13:58:13 -07:00
Girish Ramakrishnan d5b594fade return the scope as part of the user profile
send canonical scope in the profile response
2018-05-01 13:25:47 -07:00
Girish Ramakrishnan c5ffb65563 Fix usage of normalizeScope 2018-05-01 13:21:53 -07:00
Girish Ramakrishnan f76a5a7ba7 Move the clients API out of oauth prefix 2018-05-01 11:30:51 -07:00
Girish Ramakrishnan 17bcd95961 typo: return the scope as the scope 2018-05-01 10:59:46 -07:00
Girish Ramakrishnan 23bc0e8db7 Remove SDK Role
Just compare with the token's clientId instead
2018-04-30 23:03:30 -07:00
Girish Ramakrishnan 240ee5f563 Ensure we hand out max user.scope
The token.scope was valid at token creation time. The user's scope
could since have changed (maybe we got kicked out of a group).
2018-04-30 22:51:57 -07:00
Girish Ramakrishnan 200f43a58e lint 2018-04-30 22:41:23 -07:00
Girish Ramakrishnan 61d803f528 Use SCOPE_ANY everywhere 2018-04-30 21:44:24 -07:00
Girish Ramakrishnan e7c8791356 lint 2018-04-30 21:41:09 -07:00
Girish Ramakrishnan bc4f9cf596 Remove redundant requireAdmin
We already hand out scopes based on the user's access control
2018-04-30 21:38:48 -07:00
Girish Ramakrishnan 9789966017 Set the scope for a token basedon what the user has access to 2018-04-30 21:21:18 -07:00
Girish Ramakrishnan 1432d90f37 lint 2018-04-30 21:13:23 -07:00
Girish Ramakrishnan 68317a89cb remove the analytics hooks 2018-04-30 15:33:25 -07:00
Girish Ramakrishnan c84f984205 No need to create a token on password reset 2018-04-30 15:15:05 -07:00
Johannes Zellner 6e19153350 Remove now unused result argument 2018-04-30 20:37:12 +02:00
Johannes Zellner 4dc778f7c2 Change reset password button text 2018-04-30 20:17:56 +02:00
Johannes Zellner c5c3748aa9 Do not auto login on password reset 2018-04-30 19:55:59 +02:00
Girish Ramakrishnan f809e359c9 refactor the global variables as functions 2018-04-29 20:17:45 -07:00
Girish Ramakrishnan 91e846d976 Add SCOPE_DOMAINS 2018-04-29 18:11:33 -07:00
Girish Ramakrishnan b5f8ca6c16 Fix nasssty typo 2018-04-29 17:50:12 -07:00
Girish Ramakrishnan 922ab3bde1 lint 2018-04-29 17:50:12 -07:00
Girish Ramakrishnan 3b7bcc1f61 refactor scopes into accesscontrol.js
this will be our authorization layer for oauth and non-oauth tokens.
2018-04-29 17:50:07 -07:00
Girish Ramakrishnan 6e3b060615 Use SCOPE_PROFILE constant 2018-04-29 17:12:30 -07:00
Girish Ramakrishnan cc113d0bb5 Add SCOPE_CLIENTS for oauth clients API 2018-04-29 17:03:10 -07:00
Girish Ramakrishnan 3e22d513eb Add SCOPE_MAIL for mail APIs 2018-04-29 17:01:12 -07:00
Girish Ramakrishnan 9cf51ef680 Update cid-webadmin to have all the scopes possible 2018-04-29 16:57:49 -07:00
Girish Ramakrishnan 1c55a3e310 typo 2018-04-29 11:29:21 -07:00
Girish Ramakrishnan d8acf92929 UserError -> UsersError 2018-04-29 11:22:15 -07:00
Girish Ramakrishnan 7bb8d059b5 GroupError -> GroupsError 2018-04-29 11:21:01 -07:00
Girish Ramakrishnan 863afc68cb DomainError -> DomainsError 2018-04-29 11:20:12 -07:00
Girish Ramakrishnan 4fd58fb46b Rename user.js to users.js 2018-04-29 11:19:04 -07:00
Girish Ramakrishnan b1b664ceca Move removeInternalAppFields to model code 2018-04-29 10:47:34 -07:00
Girish Ramakrishnan 1a27009fb5 Make expiresAt a body parameter 2018-04-28 22:02:07 -07:00
Girish Ramakrishnan 6c8c206e89 Move input validation logic to routes 2018-04-28 21:58:56 -07:00
Girish Ramakrishnan 82207c3ccd Keep naming consistent with delToken 2018-04-28 21:55:57 -07:00
Girish Ramakrishnan 6768994bbe Capitalize the 2fa text 2018-04-28 10:20:33 -07:00
Girish Ramakrishnan b72efb1018 Remove private fields when listing domains
Currently, domains list route does not return the fallback cert.
make it future proof, just in case.
2018-04-27 11:41:30 -07:00
Johannes Zellner 7a8c525beb Add 2fa Oauth route tests 2018-04-27 14:08:22 +02:00
Johannes Zellner 9372d8797a Add 2fa tests for developer login api 2018-04-27 12:29:11 +02:00
Johannes Zellner faeb89b258 Add 2fa token login to oauth login form 2018-04-27 11:46:41 +02:00
Johannes Zellner 50d7ade0d9 Remove unused property on set 2fa secret 2018-04-27 08:20:12 +02:00
Johannes Zellner 497c76a905 Add 2fa token check to developer login 2018-04-27 08:18:13 +02:00
Johannes Zellner bbc434dc21 Attach the whole user object also for basic authed routes 2018-04-27 08:18:13 +02:00
Girish Ramakrishnan a7bb5d6b5c add route to query apps specific to user
it's not nice to overload a route to mean different things depending
on who queries it.
2018-04-26 20:07:12 -07:00
Girish Ramakrishnan e0da6679e9 Move user routes to /api/v1/user 2018-04-26 19:57:44 -07:00
Girish Ramakrishnan 561d2d9f8b admin column is no more in users table 2018-04-26 19:55:26 -07:00
Girish Ramakrishnan 7549b3e837 enhance user creation API to take a password 2018-04-26 14:13:40 -07:00
Girish Ramakrishnan 7756c07bc6 Add better text for the secret 2018-04-26 09:39:35 -07:00
Johannes Zellner 0d58a6bf33 Send 2fa auth status with profile info 2018-04-26 16:29:40 +02:00
Johannes Zellner fbba636fb3 Handle more 2fa route errors 2018-04-26 16:14:37 +02:00
Johannes Zellner 9cd6333cf7 2fa routest work with the req.user object 2018-04-26 15:12:14 +02:00
Johannes Zellner eb02c182e5 Avoid linter issue 2018-04-26 14:45:12 +02:00
Johannes Zellner 6574b22cf6 Add 2fa routest and business logic 2018-04-26 08:46:35 +02:00
Girish Ramakrishnan d1ed2aa2ce remove untested route set_admin 2018-04-25 21:43:21 -07:00
Girish Ramakrishnan c2a762cb29 Do not reserve mailbox names
Now that user management is split from mailboxes, we don't need to
reserve mailbox names anymore.
2018-04-25 21:14:33 -07:00
Girish Ramakrishnan 34d40edef4 Fix issue where docker needs more recent packages
The following packages have unmet dependencies:
 docker-ce : Depends: libseccomp2 (>= 2.3.0) but 2.2.3-3ubuntu3 is to be installed
E: Unable to correct problems, you have held broken packages.
2018-04-25 13:44:53 -07:00
Johannes Zellner 5ceb14cbca Add required 2fa node modules qrcode and speakeasy 2018-04-25 17:03:16 +02:00
Johannes Zellner 38668937ad Fixup database tests 2018-04-25 17:03:16 +02:00
Johannes Zellner 0167f83d4a Handle 2fa fields in userdb code 2018-04-25 17:03:16 +02:00
Johannes Zellner 9e66adb6d0 Add 2fa db record fields to users table 2018-04-25 17:03:16 +02:00
Girish Ramakrishnan 0a537029bc add mailbox domain to the constraint 2018-04-23 10:14:16 -07:00
Girish Ramakrishnan c0716e86a7 Remove the "or Email"
The email here can be confused with the Cloudron email which it is not.
The preferred approach is to login via username anyway.
2018-04-22 18:29:10 -07:00
Girish Ramakrishnan 50185adcf4 Add 2.1.1 changes 2018-04-18 12:49:11 -07:00
Johannes Zellner 0c728c6af5 Fix mail rest api tests 2018-04-13 12:54:40 +02:00
Johannes Zellner 34d3d79b12 Improve error message when alias name is already taken 2018-04-13 12:37:27 +02:00
Johannes Zellner ff856a5978 Rename 'address' catchall property to 'addresses' to better indiciate this being an array 2018-04-13 12:15:15 +02:00
Johannes Zellner c4dad2f55f Fix address property error response in catchall 2018-04-13 12:15:15 +02:00
Girish Ramakrishnan 734286ba2e Add support for installing private docker images 2018-04-12 11:43:57 -07:00
Girish Ramakrishnan 0f7f8af4b2 Use docker 18.03.0-ce
17.12.0-ce has strange issues like https://github.com/moby/moby/issues/34097
2018-04-11 18:25:19 -07:00
Johannes Zellner 60381d938e Fix search and replace mistake 2018-04-11 15:29:37 +02:00
Johannes Zellner ddaa52163b Update ssl ciphers according to mozillas recommendation 2018-04-11 15:15:29 +02:00
Johannes Zellner 799c1ba05d Improve on the csp header restriction 2018-04-11 13:00:08 +02:00
Johannes Zellner 838838b90d nginx would drop other headers if add_header is defined in the location section 2018-04-11 12:29:57 +02:00
Girish Ramakrishnan 4554d9f2f8 Add more changes 2018-04-10 15:13:04 -07:00
Johannes Zellner 573d0e993e Add CSP header for dashboard 2018-04-10 17:59:06 +02:00
Johannes Zellner 97313fe1c8 Remove other unused assets from the release tarball 2018-04-10 14:08:13 +02:00
Johannes Zellner 944f743438 Use the node modules defined in the dashboard repo 2018-04-10 13:51:01 +02:00
Johannes Zellner 96a5b0e6ba Remove dashboard related node modules 2018-04-10 13:12:42 +02:00
Girish Ramakrishnan 95f7e50065 bump mail container 2018-04-10 00:00:27 -07:00
Girish Ramakrishnan d6a8837716 mail: verify with the owner id 2018-04-09 13:17:07 -07:00
Johannes Zellner cc759e3550 set the mailbox record type for apps 2018-04-09 15:39:36 +02:00
Girish Ramakrishnan bf0dd935e5 mail: add type field 2018-04-07 21:29:44 -07:00
Girish Ramakrishnan 1d761deec0 Fix test 2018-04-07 18:39:17 -07:00
Girish Ramakrishnan b6335a327c Rename TYPE_* to OWNER_TYPE_* 2018-04-07 18:33:30 -07:00
Johannes Zellner 55d53ef311 Do not succeed if mailbox name is already taken 2018-04-06 16:55:01 +02:00
Johannes Zellner 878940edae Fix sql syntax 2018-04-06 15:54:55 +02:00
Johannes Zellner 15648a3ab2 fix typo name -> username 2018-04-06 14:53:20 +02:00
Johannes Zellner 2fae98dd5b pass the dashboard version as a revision to the gulp file 2018-04-06 07:47:42 +02:00
Girish Ramakrishnan 9beeb33090 mail: validate list and mailbox names 2018-04-05 17:49:16 -07:00
Girish Ramakrishnan 605dc00422 mail: add members field for lists
we have to track the members of a list in the mail app separately
from groups. this is required because users can now have multiple
mailboxes. and because of that we cannot do a 1-1 mapping of group
members to mailboxes anymore. the ui is changed to select mailboxes
when creating a list.
2018-04-05 16:07:38 -07:00
Girish Ramakrishnan 2c8fa01d6d mail: split the functions to add list and mailbox 2018-04-05 15:01:28 -07:00
Girish Ramakrishnan 467bfa2859 remove mailboxdb from groups code 2018-04-04 20:08:52 -07:00
Girish Ramakrishnan affb420181 cloudron-setup: highlight reboot in red 2018-04-04 09:55:22 -07:00
Girish Ramakrishnan e7b26e5655 Add note on accepting self-signed cert 2018-04-04 09:54:14 -07:00
Girish Ramakrishnan 5af657ee22 rename mail crud functions 2018-04-03 15:06:14 -07:00
Girish Ramakrishnan 7fac92c519 validate user id when adding mailbox 2018-04-03 14:27:09 -07:00
Girish Ramakrishnan f8a731f63a Add routes to change the mailbox and list owner 2018-04-03 14:12:43 -07:00
Girish Ramakrishnan a1f4a4d614 mail: make mailbox API based on mailbox name
this decouples mail API from users
2018-04-03 13:59:03 -07:00
Girish Ramakrishnan 696e864459 mail: make list API based on list name
this decouples mail API from groups
2018-04-03 12:06:22 -07:00
Girish Ramakrishnan 678ea50f87 validateAlias -> validateName 2018-04-03 09:47:15 -07:00
Girish Ramakrishnan 69d3b3cac8 2.0.2 -> 2.1.0 2018-04-02 13:37:06 -07:00
Girish Ramakrishnan 76915b99a8 Fix linter 2018-04-02 09:46:30 -07:00
Girish Ramakrishnan 255a5a12a5 Decouple mailbox deletion from user delete 2018-04-02 09:45:46 -07:00
Johannes Zellner 602291895c Mention which alias is reserved 2018-04-02 14:59:10 +02:00
Johannes Zellner 045ea4681a Do not return an error on mailinglist listing if none exists
We usually return the empty array, to avoid the need for specific error
handling
2018-04-01 21:51:56 +02:00
Johannes Zellner e364661813 Send correct status code if mail alias already exists 2018-04-01 19:29:47 +02:00
Johannes Zellner df9a191434 Add rest api to list all aliases for a given domain 2018-04-01 18:23:54 +02:00
Johannes Zellner b4aac42032 Add more changes for 2.0.2 2018-04-01 15:15:52 +02:00
Johannes Zellner 2a8be279e7 The package lock now uses sha512 for checksum 2018-04-01 13:15:05 +02:00
Johannes Zellner 4af69fb8c8 Do not show a warning like log, but just dump the tag and detail 2018-03-29 17:36:00 +02:00
Girish Ramakrishnan cbc98a48ef Slight wording change 2018-03-28 10:17:17 -07:00
Girish Ramakrishnan 874541b988 Add issue templates 2018-03-28 10:14:35 -07:00
Girish Ramakrishnan 0aa1b758ec Update docker to 17.12.0-ce 2018-03-26 16:34:33 -07:00
Girish Ramakrishnan 2e0c632942 Do not crash if mail alias does not validate 2018-03-25 21:08:15 -07:00
Girish Ramakrishnan 82a593e82a Forward stats calls to mail container 2018-03-23 10:52:07 -07:00
Girish Ramakrishnan e33ebe7304 Revert "mysql: increase max_allowed_packet"
This reverts commit 9123ea7016.

Not needed. This was a db corruption issue
2018-03-22 21:49:08 -07:00
Girish Ramakrishnan d81930be72 add note on conn limit 2018-03-22 21:07:06 -07:00
Girish Ramakrishnan aac914182f remove options from database.initialize 2018-03-22 20:34:49 -07:00
Girish Ramakrishnan 26d4a11c44 cleanup eventlog more aggressively
Those login entries are really adding up on old cloudrons
2018-03-22 20:31:32 -07:00
Girish Ramakrishnan f498443cae remove unused exports 2018-03-22 20:29:26 -07:00
Girish Ramakrishnan d84d761bad Remove unused export 2018-03-22 19:40:38 -07:00
Girish Ramakrishnan 07601d1292 Fix schema 2018-03-22 18:41:10 -07:00
Girish Ramakrishnan 6cbe964301 Add note 2018-03-22 17:13:32 -07:00
Girish Ramakrishnan 84dcdbba33 Re-assign 2020 to mail server 2018-03-21 23:15:30 -07:00
Girish Ramakrishnan 9123ea7016 mysql: increase max_allowed_packet
some cloudrons are reporting some errors after 2.0. maybe all those
additional joins/fields we put in is causing this
2018-03-21 17:52:22 -07:00
Girish Ramakrishnan 2a18070016 do-spaces: Force retry of 4xx error codes when copying 2018-03-21 15:41:21 -07:00
Girish Ramakrishnan e0ece06b26 s3: improved copy logging 2018-03-21 14:22:41 -07:00
Girish Ramakrishnan 83d2eb31dd clarify debug 2018-03-21 11:39:16 -07:00
Girish Ramakrishnan c6b8ad88dd 2.0.2 changes 2018-03-20 20:04:35 -07:00
Girish Ramakrishnan 6adf88a6e5 Make uploads work with very slow upload speeds
chunk uploads get a timeout of 2mins (derived from http.timeout).
On servers like kimsufi, uploads takes forever (100 MB/sec limit).
Currently, our upload code does not dynamically adapt itself to
changing the concurrency when network is slow.
2018-03-20 19:37:45 -07:00
Girish Ramakrishnan 7699f6721d Add hack to figure out the position in the queue
this helps us track the progress a bit in the logs
2018-03-20 19:37:35 -07:00
Girish Ramakrishnan ce33681c37 Dump etag info 2018-03-20 18:19:14 -07:00
Girish Ramakrishnan 565eed015f Add better backup logs 2018-03-20 16:41:45 -07:00
Girish Ramakrishnan dd296544be Remove extra prefix 2018-03-15 14:30:10 -07:00
Girish Ramakrishnan a07c4423c4 Rename webadmin to dashboard
The box nginx config has to be re-generated but this is always
done at box restart time
2018-03-15 14:14:23 -07:00
Girish Ramakrishnan 65f07cb7c0 Add more changes 2018-03-14 09:15:58 -07:00
Girish Ramakrishnan 8d1a6cb06b Add more changes 2018-03-14 09:14:45 -07:00
Girish Ramakrishnan 873ea0fecd Restart mail server after DKIM keys are generated
Haraka won't do change notification on those
2018-03-13 09:53:41 -07:00
Girish Ramakrishnan ace1f36f9c 2.0.1 changes 2018-03-13 00:36:58 -07:00
Girish Ramakrishnan 4cc9818139 remove error prone short-circuit update
when we do pre-releases, there really is no way for us to update
all the cloudrons. this worked when everything was managed cloudron.
2018-03-13 00:36:03 -07:00
Girish Ramakrishnan 390639bac0 Bump mail container
This fixes delivery of incoming mail from an outbound only domain
2018-03-13 00:20:48 -07:00
Girish Ramakrishnan 830c685ead recreate mail configs when mail domain is added 2018-03-12 21:14:45 -07:00
Girish Ramakrishnan 65b174f950 Domain removal can fail because of mailbox as well 2018-03-12 09:54:16 -07:00
Girish Ramakrishnan 331ed4e6b9 Pass on any appstore purchase error 2018-03-11 12:43:24 -07:00
Girish Ramakrishnan afef548097 cloudron-setup: make sure --help runs as non-root 2018-03-09 10:37:18 -08:00
Johannes Zellner 60e924d5b8 We do require a domain entry in the mails table always 2018-03-09 14:26:33 +01:00
Johannes Zellner c0ea91a688 We have to parse the JSON data from the raw db results 2018-03-09 10:27:13 +01:00
Girish Ramakrishnan ecf1f9255d relay: cloudron-smtp can always be set 2018-03-08 23:23:02 -08:00
Girish Ramakrishnan 1125643a80 Add Haraka 2.8.18 to changelog 2018-03-08 20:12:34 -08:00
Girish Ramakrishnan 61243f6a09 Wait for DNS records call to finish 2018-03-08 20:08:01 -08:00
Girish Ramakrishnan 2e156aa34a simplify the configureWebadmin logic 2018-03-08 18:26:44 -08:00
Girish Ramakrishnan 440629530f remove redundant check for "test" mode 2018-03-08 18:15:09 -08:00
Girish Ramakrishnan 3922824dc6 no reason to keep retrying 2018-03-08 18:00:16 -08:00
Girish Ramakrishnan 6bc5add023 Add a way to re-sync mail DNS records
Also, make restore resync the admin domain record which gets messed
up by the dns setup
2018-03-08 17:59:53 -08:00
Girish Ramakrishnan f284245e16 dkim keys are needed for the test 2018-03-08 16:10:33 -08:00
Girish Ramakrishnan ac62ee5a16 better debug 2018-03-08 15:29:18 -08:00
Girish Ramakrishnan 66f251be06 dnsSetup must fail if domain already exists 2018-03-08 15:10:38 -08:00
Girish Ramakrishnan ab932c4f5c Do not regenerate domain key if domain already exists 2018-03-08 15:01:08 -08:00
Girish Ramakrishnan 074c6fdba3 More renaming to forum 2018-03-08 10:50:18 -08:00
Girish Ramakrishnan b36f4becbc remove bad changelog 2018-03-08 10:22:46 -08:00
Girish Ramakrishnan ac69b96f92 clear timeout when getting mail status 2018-03-08 09:32:06 -08:00
Girish Ramakrishnan 6da7a7d2f4 clear the request timeout when adding/updating domain
DO API takes very long to respond at times :/ Currently, there is
no easy way to reset the timeout middleware timeout. We should add
this feature upstream (https://github.com/expressjs/timeout/issues/26)
2018-03-08 09:27:56 -08:00
Girish Ramakrishnan 22c54ced05 email: add MAIL_DOMAINS 2018-03-07 20:39:58 -08:00
Girish Ramakrishnan c7b1d49de6 cloudron-setup: add some color 2018-03-07 15:51:00 -08:00
Girish Ramakrishnan b7bf5b180c Display the format as well 2018-03-07 13:59:17 -08:00
Girish Ramakrishnan 12aba46893 use apps.getAll since app.fqdn is otherwise undefined 2018-03-07 13:39:40 -08:00
Johannes Zellner 9d4eee0dfe reword error message, all apps using a domain have to be deleted 2018-03-07 10:45:03 +01:00
Girish Ramakrishnan d69c8f49e5 Migrate daily update pattern 2018-03-06 21:29:08 -08:00
Girish Ramakrishnan dd5f41aee8 Fix failing test 2018-03-06 01:35:38 -08:00
Girish Ramakrishnan 0b20b265de Do not crash if domain is not found 2018-03-06 01:30:40 -08:00
Girish Ramakrishnan ac94d0b5c7 Make apps also auto-update like before by default 2018-03-05 21:33:18 -08:00
Girish Ramakrishnan c5a70d10d7 Add robotsTxt to appdb.add 2018-03-05 16:14:23 -08:00
Girish Ramakrishnan b83eb993d8 Add sso and robotsTxt to app backup config.json 2018-03-05 15:03:03 -08:00
Girish Ramakrishnan 6cadaca307 clone: copy enableBackup and robotsTxt 2018-03-05 14:56:24 -08:00
Girish Ramakrishnan 36b91ae7db Add PSBL 2018-03-05 14:26:53 -08:00
Johannes Zellner 3115432309 Fix missed eventlog.getAllPaged() usage 2018-03-05 17:53:18 +01:00
Johannes Zellner 8340f77e20 Fixup the database tests 2018-03-05 17:17:01 +01:00
Johannes Zellner 75932e2805 Collect app information for feedback email 2018-03-05 17:03:54 +01:00
Johannes Zellner ff6d468604 Support multiple actions for eventlog api 2018-03-05 11:46:06 +01:00
Girish Ramakrishnan 161b2ac6f5 clone: Fix crash where port conflict is not handled 2018-03-02 19:37:15 -08:00
Johannes Zellner 9775ab5e8e make correct use of eventlog for developer/cli login 2018-03-02 19:26:55 +01:00
Johannes Zellner 726202b040 Amend app object where applicable to login event 2018-03-02 19:21:11 +01:00
Johannes Zellner 39d6ec96b7 amend full user object to login action 2018-03-02 19:21:11 +01:00
Johannes Zellner 87fedb71b7 Use shared function to remove private user fields for api 2018-03-02 11:24:27 +01:00
Johannes Zellner 8424e687cb Amend full user object to user action eventlog entries 2018-03-02 11:24:06 +01:00
Johannes Zellner c0d030c978 Amend full user object to user eventlog action entries 2018-03-02 11:02:32 +01:00
Johannes Zellner 53470e286f Use app model code to get all amended properties 2018-03-02 10:58:05 +01:00
Girish Ramakrishnan e22c17eabe Fix issue where new package versions are getting skipped 2018-03-01 11:39:10 -08:00
Girish Ramakrishnan 5ac1fccb98 mail: Fix crashes when user has no username yet 2018-02-28 13:31:28 -08:00
Girish Ramakrishnan 0cc58fafd6 Do not crash if user does not have username 2018-02-28 13:18:41 -08:00
Girish Ramakrishnan 98e19e6df5 fix upload errors causing double callback 2018-02-27 19:16:03 -08:00
Girish Ramakrishnan 441e514119 scheduler: give scheduler tasks twice the memory by default 2018-02-27 15:03:09 -08:00
Girish Ramakrishnan ff4b09a342 Use the container StartedAt instead of lastDate
CronJob.lastDate keeps resetting on every tick. Also, it doesn't
work across box code restarts.
2018-02-27 14:26:40 -08:00
Girish Ramakrishnan f8c8133148 scheduler: better debugs 2018-02-27 13:54:38 -08:00
Girish Ramakrishnan 938a41e12c scheduler: give cron jobs a grace period of 30 mins to complete 2018-02-27 13:28:42 -08:00
Girish Ramakrishnan 5d231f4fef scheduler: do no start all cronjobs at once 2018-02-27 12:44:11 -08:00
Girish Ramakrishnan a4e6181edf Fix tests 2018-02-27 11:59:15 -08:00
Girish Ramakrishnan 6685118b03 Use safe.JSON.parse instead
safe.require() caches the credentials which is annoying
2018-02-27 11:24:08 -08:00
Girish Ramakrishnan 4c9919a98b Drop the "your" 2018-02-27 09:22:43 -08:00
Girish Ramakrishnan 470c9971f8 mail exchange does not have trailing dot 2018-02-23 17:26:28 -08:00
Girish Ramakrishnan b6fb49956f s3: better debug output when copying 2018-02-22 12:41:18 -08:00
Girish Ramakrishnan 0bba985ff1 storage: Add implementation note 2018-02-22 12:30:55 -08:00
Girish Ramakrishnan 3c8c15db01 s3: use a constant backoff since it takes forever to fail otherwise 2018-02-22 12:30:44 -08:00
Girish Ramakrishnan c8a6294772 lint 2018-02-22 12:24:16 -08:00
Girish Ramakrishnan cea83889ec s3: Fix issue where it takes forever to timeout if the backend is down 2018-02-22 12:19:23 -08:00
Girish Ramakrishnan 2ecb66afd7 s3: cleanup code 2018-02-22 12:16:01 -08:00
Girish Ramakrishnan f5d426fd69 debug out the progress message 2018-02-22 11:11:36 -08:00
Girish Ramakrishnan e6c07fc148 merge the done callback into the main code 2018-02-22 11:06:28 -08:00
Girish Ramakrishnan 1f30a4f3ea Make s3.deleteObjects return error 2018-02-22 11:05:29 -08:00
Girish Ramakrishnan 0bfdaeb2fb rename to chunkSize 2018-02-22 11:01:04 -08:00
Girish Ramakrishnan e022dbf8a6 Revert "merge the done callback into the main code"
This reverts commit c39bec8cc1.

This was committed with extra stuff by mistake
2018-02-22 10:58:56 -08:00
Girish Ramakrishnan 0e7e672dd2 Update node modules 2018-02-22 10:52:42 -08:00
Girish Ramakrishnan 6075a7a890 typo 2018-02-22 10:34:48 -08:00
Girish Ramakrishnan 28b864c346 sos: Copy in 96M chunks as recommended by exoscale 2018-02-22 10:31:56 -08:00
Girish Ramakrishnan e9437131ff mail: set domain_selector to be mail_from 2018-02-21 20:46:32 -08:00
Girish Ramakrishnan c39bec8cc1 merge the done callback into the main code 2018-02-21 20:17:58 -08:00
Girish Ramakrishnan 727a25f491 DO Spaces: multipart copy now works 2018-02-20 14:48:03 -08:00
Johannes Zellner 26bacfcbd6 Allow partial match of eventlog actions 2018-02-20 11:20:17 -08:00
Johannes Zellner a777e7aeb3 add full app object to app related eventlog actions 2018-02-20 10:34:09 -08:00
Johannes Zellner 676625a3f6 Add more appstore tests 2018-02-18 22:43:11 -08:00
Johannes Zellner f41603ea94 Add appstore.sendAliveStatus() tests 2018-02-18 21:42:37 -08:00
Johannes Zellner 18ae958e87 Send all domains with provider with the alive post 2018-02-18 21:36:21 -08:00
Johannes Zellner d68d4295de Remove unused require 2018-02-18 20:16:17 -08:00
Girish Ramakrishnan 0244529b45 Add more changelog 2018-02-18 02:45:46 -08:00
Girish Ramakrishnan 1d044a7392 Bump mail container for multi-domain support 2018-02-18 00:54:41 -08:00
Girish Ramakrishnan 06eab93f0e restart mail container when mail.ini changes 2018-02-18 00:54:11 -08:00
Girish Ramakrishnan 84b7672509 caas can be a provider 2018-02-17 10:28:03 -08:00
Girish Ramakrishnan c9cd4ed363 Fix changelog version 2018-02-16 16:43:04 -08:00
Girish Ramakrishnan 05c98ccadb Enable auto-updates for major versions
Cloudron is always rolling releases and we never break compat
2018-02-16 16:01:10 -08:00
Johannes Zellner cb62cdcfa1 Report dependency error for clone if backup or domain was not found 2018-02-16 10:45:06 -08:00
Girish Ramakrishnan c0fddf5d8a Version 1.11.0 changes 2018-02-11 01:22:26 -08:00
Girish Ramakrishnan bcf3e71979 Add API to remove mailboxes by domain 2018-02-11 01:18:29 -08:00
Girish Ramakrishnan baf5cae58a Fix tests 2018-02-11 00:04:41 -08:00
Girish Ramakrishnan 5c1f9d5686 typo 2018-02-11 00:04:28 -08:00
Girish Ramakrishnan 4d89340c7d Handle FK error when deleting mail domain 2018-02-10 22:49:35 -08:00
Girish Ramakrishnan 0b6846787e The mailboxes domain column must reference the mail domain column 2018-02-10 21:31:50 -08:00
Girish Ramakrishnan 79976cd29d add an extra newline in config 2018-02-10 21:29:00 -08:00
Girish Ramakrishnan 574cf1057e mail: ensure mail is disabled when deleting mail domain 2018-02-10 10:38:45 -08:00
Johannes Zellner 1b3450e3a2 update the altDomain migration to also generate certs with SAN 2018-02-10 15:55:22 +01:00
Girish Ramakrishnan bec032702d Remove SAN check
-checkhost already checks the SAN. It is implementation dependent
as to whether the CN is checked for.
2018-02-09 14:20:03 -08:00
Girish Ramakrishnan fc79047bbf Generate fallback cert to contain naked domain in SAN 2018-02-09 13:44:29 -08:00
Girish Ramakrishnan 5263ea860d Add cert tests 2018-02-09 11:19:47 -08:00
Johannes Zellner 5140dee81d Generate a fallback cert for domains added during altDomain migration 2018-02-09 13:08:45 +01:00
Johannes Zellner 24d3195660 Add dns setup and activation route tests 2018-02-09 12:43:20 +01:00
Johannes Zellner 721a4c4349 Validate the adminFqdn in dns setup route 2018-02-09 12:43:03 +01:00
Girish Ramakrishnan 83ff295f6d debug: authenticateMailbox 2018-02-08 18:49:27 -08:00
Girish Ramakrishnan 6decc790d6 Follow CNAME records
DNS records can now be a A record or a CNAME record. All we care
about is them resolving to the public IP of the server somehow.

The main reason for this change is that altDomain is migrated into
domains table and the DNS propagation checks have to work after that.
(previously, the 'altDomain' was a signal for a CNAME check which now
cannot be done post-migration).

In the future, we can make this more sophisticated to instead maybe
do a well-known URI query. That way it will work even if there is
some proxy like Cloudflare in the middle.

Fixes #503
2018-02-08 15:43:31 -08:00
Girish Ramakrishnan 459cf8d0cd Add note on unbound at 127.0.0.1 2018-02-08 14:43:49 -08:00
Girish Ramakrishnan 58386b0c54 remove resolveNs 2018-02-08 14:39:35 -08:00
Girish Ramakrishnan 101c1bda25 translate cancelled errors to timeout errors 2018-02-08 14:27:02 -08:00
Girish Ramakrishnan d31c948d3e Remove type argument from waitForDns
The function is going to be changed to handle only A/CNAME records
2018-02-08 14:24:11 -08:00
Girish Ramakrishnan 0927c8161c Add note on return value of dns.resolve 2018-02-08 14:10:53 -08:00
Girish Ramakrishnan 4d92aea2f3 Fix usage of callback 2018-02-08 14:10:32 -08:00
Girish Ramakrishnan 0ca2451eaa fix tests 2018-02-08 12:09:06 -08:00
Girish Ramakrishnan 3b987f1970 DNS -> Dns 2018-02-08 12:05:29 -08:00
Girish Ramakrishnan a7b0ba2178 PTR must be resolved by the domain and not IP 2018-02-08 11:56:25 -08:00
Girish Ramakrishnan 744e6b8af0 replace the verizon smtp 2018-02-08 11:48:55 -08:00
Girish Ramakrishnan 8254e795be add missing export 2018-02-08 11:42:45 -08:00
Girish Ramakrishnan 26c95a25b6 Use the native dns resolver
it now supports cancel()

also, fixes #514
2018-02-08 11:37:58 -08:00
Girish Ramakrishnan 209f37312b createReleaseTarball must use the local branch for master branch 2018-02-08 08:58:20 -08:00
Johannes Zellner 5bd218b3b6 Fix intrinsicFqdn removal breakage 2018-02-08 15:23:38 +01:00
Johannes Zellner d57b772ada We can use js multiline strings 2018-02-08 15:19:00 +01:00
Johannes Zellner b6384d5025 Remove intrinsicFqdn 2018-02-08 15:07:49 +01:00
Johannes Zellner fa65576688 Remove unused require 2018-02-08 15:04:13 +01:00
Johannes Zellner 3572b4eb91 Do not crash if certs cannot be found. Error object does not exist 2018-02-08 10:27:30 +01:00
Johannes Zellner e710a210fd Fixup the unit tests 2018-02-08 09:00:31 +01:00
Johannes Zellner 265db7d0f7 Fix typo in appdb 2018-02-08 09:00:31 +01:00
Johannes Zellner b1939e73f4 Remove all occurances of altDomain in the code
Tests are pending
2018-02-08 09:00:31 +01:00
Johannes Zellner 28f5f62414 Add altDomain migration script 2018-02-08 09:00:31 +01:00
Girish Ramakrishnan ff577a8ed5 stop and disable postfix for good measure 2018-02-07 09:08:04 -08:00
Johannes Zellner 63d06d7024 Use fresh settings key for app autoupdate pattern 2018-02-07 16:51:53 +01:00
Johannes Zellner 4d4b77d6fb Add 1.10.2 changes 2018-02-07 16:27:54 +01:00
Johannes Zellner 3b4ff18881 Keep the invite email for users, which have not yet setup a username 2018-02-07 16:27:49 +01:00
Girish Ramakrishnan d65cb93158 Remove obsolete action 2018-02-06 23:14:02 -08:00
Girish Ramakrishnan e00f98884c setup SPF record of non-primary domain correctly 2018-02-06 23:11:47 -08:00
Girish Ramakrishnan 21016cc2e0 createReleaseTarball: Make sure we pick the current branch on webadmin 2018-02-06 16:20:29 -08:00
Girish Ramakrishnan d12803bb9d Add 1.10.1 changes 2018-02-06 16:11:06 -08:00
Girish Ramakrishnan 039a31318a Generate per-domain enable_outbound relay settings 2018-02-06 14:43:14 -08:00
Johannes Zellner 3eb11ee20a Fixup updatechecker tests 2018-02-06 19:25:03 +01:00
Johannes Zellner 11d740682e Split box and app autoupdate pattern settings 2018-02-06 19:25:03 +01:00
Johannes Zellner 09b33e7ef9 Disable autoupdates by default 2018-02-06 19:25:03 +01:00
Johannes Zellner 19fafca9df Drop users email unique constraint for the migration timeframe 2018-02-06 12:14:11 +01:00
Girish Ramakrishnan da29c69be4 generate per-domain mail configuration 2018-02-05 15:13:35 -08:00
Johannes Zellner c4531e32d5 Fix all app route tests 2018-02-05 22:17:16 +01:00
Johannes Zellner 8f74cacfd0 Remove unused require 2018-02-05 20:45:53 +01:00
Girish Ramakrishnan 9ba830ab21 "installing" is easier to understand 2018-02-05 11:13:51 -08:00
Girish Ramakrishnan ad152bacdd Do not allow dns setup and restore to run in parallel
In the e2e, we did not check the webadminStatus after a dnsSetup
and immediately rushed into restore. This ended up mangling the
cert/key files of the admin domain.
2018-02-05 09:35:16 -08:00
Johannes Zellner 89673fa7f0 Make more of the app route tests work 2018-02-05 17:28:30 +01:00
Johannes Zellner c8613e646b Show more descriptive error message if minBoxVersion blocks update 2018-02-05 15:20:42 +01:00
Girish Ramakrishnan faef3114f5 Add more 1.10 changes 2018-02-04 09:33:44 -08:00
Girish Ramakrishnan 087f14643a catch_all_address in settings table is JSON 2018-02-04 01:27:40 -08:00
Girish Ramakrishnan 77fe595970 lint 2018-02-04 00:15:41 -08:00
Girish Ramakrishnan 14529d313a sos: CopySource requires fancier encoding rules 2018-02-04 00:12:28 -08:00
Girish Ramakrishnan 72f56ff91c rename the gcdns key file 2018-02-03 22:18:04 -08:00
Girish Ramakrishnan 2a7eabfa68 fix test mail route 2018-02-03 18:34:11 -08:00
Girish Ramakrishnan d18fe0a40c app: Check altDomain when saving certs 2018-02-03 01:03:23 -08:00
Girish Ramakrishnan 8f5105388f Fix crash when cert renewal fails 2018-02-02 21:21:51 -08:00
Girish Ramakrishnan 4c0da7a8c9 use fallback cert of altDomain 2018-02-02 20:29:04 -08:00
Girish Ramakrishnan f607010396 sos: remove exoscale specific hacks which are obsolete now 2018-02-02 20:13:51 -08:00
Girish Ramakrishnan 909db5b80e put the ui version in the string as well 2018-02-02 19:57:55 -08:00
Girish Ramakrishnan 7563dd4ac8 remove ununsed require 2018-02-02 18:49:55 -08:00
Girish Ramakrishnan de1af3ac72 typo 2018-02-02 16:01:20 -08:00
Girish Ramakrishnan 2b9e90397d Do not delete the old conf dir (migration might use it) 2018-02-02 15:44:49 -08:00
Girish Ramakrishnan 8e258f11ec caas: import appstore and caas configs for existing caas 2018-02-02 13:29:57 -08:00
Johannes Zellner 2e818fd689 Make first half of the app route tests pass 2018-02-02 14:06:01 +01:00
Girish Ramakrishnan f85b7a4336 How many commits to fix a typo? 2018-02-01 22:24:41 -08:00
Girish Ramakrishnan f4a021b751 Fix path to nginx cert 2018-02-01 21:58:42 -08:00
Johannes Zellner 272b0489ff Fixup the app route test startup sequence 2018-02-01 18:04:43 +01:00
Johannes Zellner 1b25a0d7b7 Fix copy'n'paste error 2018-02-01 17:28:04 +01:00
Johannes Zellner d8b62f95be Fix domains tests 2018-02-01 11:19:42 +01:00
Johannes Zellner b337300a7b Do not use removed setTlsConfig in apptask tests 2018-02-01 11:16:55 +01:00
Girish Ramakrishnan 2083b035e8 create images in sfo2 now 2018-01-31 22:36:00 -08:00
Girish Ramakrishnan 2873793e7b typo 2018-01-31 22:31:19 -08:00
Girish Ramakrishnan 17128f0b56 Start the platform code only after 3 secs 2018-01-31 22:16:06 -08:00
Girish Ramakrishnan 1f5ecd5ff8 Set default for CAAS_CONFIG_KEY 2018-01-31 21:48:33 -08:00
Girish Ramakrishnan 52e23c1299 createReleaseTarball: warn about uncommitted changes in webadmin 2018-01-31 20:48:12 -08:00
Girish Ramakrishnan 298a2d2f0f cloudron-setup: remove unused source-url 2018-01-31 20:14:31 -08:00
Girish Ramakrishnan 38b6e49d44 cloudron-setup: remove unused dns-provider 2018-01-31 20:11:13 -08:00
Girish Ramakrishnan d915ea348f cloudron-setup: tls-provider is obsolete 2018-01-31 20:10:47 -08:00
Girish Ramakrishnan 8014cc8ae1 le -> letsencrypt 2018-01-31 18:53:29 -08:00
Girish Ramakrishnan 7dc7c56e97 Fix tests 2018-01-31 18:23:49 -08:00
Girish Ramakrishnan a5af87e47a caas: never return key 2018-01-31 18:22:33 -08:00
Girish Ramakrishnan ab7448926f Fix use of fallback certs
We used to always use nginx cert dir. When custom fallback certs
were set, we used to copy it in boxdata cert dir and then nginx cert dir.

The issue is then that we have to copy all certs to nginx cert dir on
cloudron restore.

To fix this, we simply give priority to nginx cert dir and not copy
around certs anymore. caas cert will reside in nginx cert dir and
not get backed up, as expected.
2018-01-31 18:20:29 -08:00
Johannes Zellner a727fc5efa Use tlsConfig from domain, not settings in reverseproxy 2018-01-31 18:37:08 +01:00
Johannes Zellner 0b31568c14 Remove tlsConfig from the settings code 2018-01-31 18:27:31 +01:00
Johannes Zellner 9b21167a8d Use tlsConfig from the domain, not from settings 2018-01-31 18:27:18 +01:00
Johannes Zellner 3c198550be Do not send tlsConfig with alive status 2018-01-31 18:27:02 +01:00
Johannes Zellner 31be178210 Add tlsConfig provider validation 2018-01-31 18:20:11 +01:00
Johannes Zellner d1ef35ae1d Fixup all the tests to use domain tlsConfig 2018-01-31 18:09:38 +01:00
Johannes Zellner 1ec294a04b Add tlsConfig to domains and setup rest apis 2018-01-31 17:42:26 +01:00
Johannes Zellner 75775fa192 Add tlsConfig in domain model code 2018-01-31 16:57:59 +01:00
Johannes Zellner 5db1716664 Add tlsConfig handling in domaindb.js 2018-01-31 16:56:22 +01:00
Johannes Zellner 2db35e42de Add migration script for tlsConfig in domains 2018-01-31 16:53:00 +01:00
Johannes Zellner 5521e17313 Remove progress.json generation in start.sh 2018-01-31 11:56:51 +01:00
Johannes Zellner 35d2755cfb Remove splashpage logic 2018-01-31 09:24:43 +01:00
Johannes Zellner 8ee1c87c45 Do not setup the splashscreen anymore 2018-01-31 09:11:04 +01:00
Johannes Zellner e1533ccd54 Prefix all installer log output for better tracking 2018-01-31 09:10:51 +01:00
Girish Ramakrishnan 7907dd5c4f migrate any existing host.cert/key to domain based host.cert/key
we don't remove the old one's yet since existing nginx config
might still be referencing them (not sure)
2018-01-30 22:09:55 -08:00
Girish Ramakrishnan be66d1ff4d arg_fqdn is no more 2018-01-30 21:27:27 -08:00
Girish Ramakrishnan 686a01b3e6 autoprovision: Save tlsKey/tlsCert with domain prefix 2018-01-30 20:41:52 -08:00
Girish Ramakrishnan 3299efc113 typos 2018-01-30 20:38:58 -08:00
Girish Ramakrishnan 7a15777ca5 1.9.4 changes 2018-01-30 19:54:04 -08:00
Girish Ramakrishnan a553a5de79 Copy over the correct license file 2018-01-30 19:52:03 -08:00
Girish Ramakrishnan 21f11c4136 cloudron-setup: remove many obsolete args 2018-01-30 19:51:58 -08:00
Girish Ramakrishnan cd31ed23bc rewrite renewAll to use existing functions 2018-01-30 16:34:26 -08:00
Girish Ramakrishnan 639a0eb43b Move the BOX_ENV check for more test coverage 2018-01-30 16:14:05 -08:00
Girish Ramakrishnan 86cf8bf9e7 Add auditSource to ensureCertificate 2018-01-30 15:16:34 -08:00
Girish Ramakrishnan 8e500e0243 caas: make the cert provider use domain fallback certs 2018-01-30 14:18:34 -08:00
Girish Ramakrishnan 781cc3b67a Merge ensureCertificate and configuring nginx 2018-01-30 13:54:13 -08:00
Girish Ramakrishnan f379724128 merge certificates.js and nginx.js to reverseproxy.js
when certs change, we have to call into nginx anyway. since they
go hand in hand, just merge those files. modern reverse proxies
do this job integrated already.
2018-01-30 12:26:09 -08:00
Girish Ramakrishnan 8e63d63509 Move configureDefaultServer to nginx.js 2018-01-30 12:01:53 -08:00
Girish Ramakrishnan c84f84b9fe host.cert and host.key are not used anymore 2018-01-30 11:58:26 -08:00
Girish Ramakrishnan fd913de913 Fallback certs are in host.cert/host.key 2018-01-30 11:43:51 -08:00
Girish Ramakrishnan 3336614702 Make getFallbackCertificate return path like getCertificate 2018-01-30 11:30:35 -08:00
Girish Ramakrishnan f2372c2c75 Fallback certs are named host.cert and host.key 2018-01-30 11:08:58 -08:00
Girish Ramakrishnan 4a4f1b883a Rename getAdminCerticate to getCertificate 2018-01-30 11:04:13 -08:00
Johannes Zellner 79f2709f3a Ensure certificates and nginx configs on startup 2018-01-30 16:55:13 +01:00
Johannes Zellner 8dea0f71f3 Fix typo with intrinsicFqdn 2018-01-30 16:46:51 +01:00
Johannes Zellner 28cffbb168 fix config-test, fqdn is gone 2018-01-30 14:09:59 +01:00
Johannes Zellner a662362df7 Add mailinglist rest api tests 2018-01-30 12:23:08 +01:00
Johannes Zellner f54197afe4 return 204 on mailinglist removal api 2018-01-30 12:22:55 +01:00
Johannes Zellner 2745511e67 Fix typo when using HttpSuccess instead of HttpError 2018-01-30 12:14:08 +01:00
Girish Ramakrishnan 2c60c4eb82 move all setup logic to setup.js 2018-01-29 15:51:36 -08:00
Girish Ramakrishnan c57c372adf remove config.fqdn() completely 2018-01-29 15:11:43 -08:00
Girish Ramakrishnan 1791617f33 typo 2018-01-29 15:11:43 -08:00
Girish Ramakrishnan e844e1400e zoneName is gone from config 2018-01-29 15:11:43 -08:00
Girish Ramakrishnan 1f2cfc45b5 adminFqdn is only valid if we have a admin domain set 2018-01-29 15:11:43 -08:00
Johannes Zellner 3c3d44e7f8 Add email alias route tests 2018-01-29 19:39:07 +01:00
Johannes Zellner 6470803604 Do not check if email is enabled when an app tries to do sendmail auth through ldap 2018-01-29 19:29:04 +01:00
Johannes Zellner edb02c859b Add mailbox rest api tests 2018-01-29 18:53:51 +01:00
Girish Ramakrishnan 351b5fcd70 Use config.adminDomain in tests 2018-01-29 09:28:26 -08:00
Girish Ramakrishnan 4c78a2933f config.setZoneName is gone 2018-01-29 09:06:01 -08:00
Johannes Zellner 9041da62e7 fix email route tests 2018-01-29 17:45:10 +01:00
Johannes Zellner 1e9b37053d Add email domain rest api CRUD testing 2018-01-29 17:18:01 +01:00
Johannes Zellner 6115b1cecf return 409 when an email domain conflict is found 2018-01-29 17:17:47 +01:00
Johannes Zellner ec7b550ca6 Fixup the ldap tests 2018-01-29 13:35:22 +01:00
Johannes Zellner 66ece2243b Actually return an error if we try to update a non existing email domain 2018-01-29 13:28:11 +01:00
Johannes Zellner 77961e51ec mail.get() returns a MailError 2018-01-29 13:14:08 +01:00
Johannes Zellner c95de547eb Fix config tests to match the removal of zoneName and repurpose of fqdn 2018-01-29 10:11:09 +01:00
Girish Ramakrishnan b2363271aa add config.adminDomain 2018-01-28 14:26:41 -08:00
Johannes Zellner 3b2f286ac5 certificates init/uninitialize are gone 2018-01-28 20:59:20 +01:00
Girish Ramakrishnan 9ff1b19c3f Remove unused config.zoneName 2018-01-27 09:26:17 -08:00
Girish Ramakrishnan 64f90abac7 Remove onDomainConfigured 2018-01-26 22:56:15 -08:00
Girish Ramakrishnan 7ce79505ee remove overcomplicated certificate events 2018-01-26 22:47:05 -08:00
Girish Ramakrishnan b4f945f977 do not regenerate fallback certificate 2018-01-26 22:47:05 -08:00
Girish Ramakrishnan c2d348fe72 remove unused /api/v1/settings/admin_certificate 2018-01-26 20:49:48 -08:00
Girish Ramakrishnan 44324f4501 make certificates.validateCertificate return CertificatesError 2018-01-26 20:39:58 -08:00
Girish Ramakrishnan 6789e9cfe7 change order of validateCertificate args 2018-01-26 20:36:31 -08:00
Girish Ramakrishnan 4d72dfd3da remove ensureFallbackCertificate 2018-01-26 20:31:48 -08:00
Girish Ramakrishnan 66d90c36fc generate fallback cert for domains if not provided 2018-01-26 20:30:37 -08:00
Girish Ramakrishnan d0bf315859 certificates: cert/key cannot be null 2018-01-26 20:06:17 -08:00
Girish Ramakrishnan f49ff2985c certificates: setFallbackCertificate does not validate anymore 2018-01-26 20:03:24 -08:00
Girish Ramakrishnan ca839ea5cb remove dead comment 2018-01-26 19:55:01 -08:00
Girish Ramakrishnan ce2d39d54c cloudron-setup: add hetzner in the help text 2018-01-26 18:53:50 -08:00
Johannes Zellner 6ef57d3f23 Fix various route handling tests 2018-01-26 21:31:22 +01:00
Girish Ramakrishnan de8f7415c3 use config.adminFqdn instead of config.fqdn 2018-01-26 12:12:22 -08:00
Johannes Zellner 7441e11c2d Further test fixes to oauth, ldap and mail tests 2018-01-26 21:10:53 +01:00
Johannes Zellner c3211c7603 Add domain and enable mail on it for many tests 2018-01-26 20:36:27 +01:00
Johannes Zellner f028b4a232 Remove mailing list logic from groups tests 2018-01-26 20:07:23 +01:00
Johannes Zellner 004211a683 Remove mailbox and alias related tests in the user logic testing 2018-01-26 20:05:09 +01:00
Johannes Zellner 7d65f341db Ensure user tests have the domain setup correctly and remove implicit mailbox check 2018-01-26 18:47:51 +01:00
Johannes Zellner 7e378b426e Fixup the database tests 2018-01-26 18:32:13 +01:00
Johannes Zellner 3135c227d7 Do not add default domain records when we clear the db for testing 2018-01-26 18:32:01 +01:00
Johannes Zellner 3948cfc33b Parse sql error message to deliver correct conflict errors 2018-01-26 17:56:07 +01:00
Johannes Zellner ccdf926976 move mailbox cleanup code from userdb to user businesslogic 2018-01-26 12:26:03 +01:00
Johannes Zellner 40f73f6c4b Cleanup the mailboxes for groups on deletion 2018-01-26 11:40:28 +01:00
Johannes Zellner 50e0856803 Do not automatically create mailboxes for groups 2018-01-26 11:36:26 +01:00
Johannes Zellner d95a670dd7 Add mailing lists routes and logic 2018-01-26 11:25:45 +01:00
Girish Ramakrishnan 16b1b27bfb readDkimPublicKeySync has moved 2018-01-25 15:38:29 -08:00
Girish Ramakrishnan 15fbfd3042 create maildb entry in database._clear 2018-01-25 15:20:53 -08:00
Girish Ramakrishnan aabd1e7df6 Fix crash when adding a domain 2018-01-25 14:51:07 -08:00
Girish Ramakrishnan 9059a30b89 refactor dns logic in cloudron.js 2018-01-25 14:46:48 -08:00
Girish Ramakrishnan 669b94b0d1 test: remove used of config.fqdn 2018-01-25 14:11:52 -08:00
Girish Ramakrishnan 6cb9779537 mail: Add DNS records when mail domain is created 2018-01-25 14:11:46 -08:00
Girish Ramakrishnan caf8da331c Do not export configureWebadmin 2018-01-25 13:40:06 -08:00
Girish Ramakrishnan 67eb7a290f Fix mail domain route 2018-01-25 13:40:02 -08:00
Girish Ramakrishnan af6d8f41ee create dkim key when mail domain is added 2018-01-25 10:38:59 -08:00
Girish Ramakrishnan 7c361a87b0 validate appstore update responses 2018-01-25 09:39:34 -08:00
Johannes Zellner aab175ea05 Remove alias handling from user logic to mail logic 2018-01-25 18:03:26 +01:00
Johannes Zellner 647582a246 Add alias route handler and logic to mail.js 2018-01-25 18:03:02 +01:00
Johannes Zellner 7bba63d911 rename mailbox routes to match overall scheme of plurals 2018-01-25 12:27:28 +01:00
Girish Ramakrishnan b71c0bde55 add routes to create and delete mail domain 2018-01-25 12:09:30 +01:00
Girish Ramakrishnan ef3ab44199 update cloudron-manifestformat (for prerelease in semver) 2018-01-24 20:09:14 -08:00
Girish Ramakrishnan ed3f128bcd Send relay, catchall and domain info from mail table 2018-01-24 15:38:45 -08:00
Girish Ramakrishnan 2f5ab98284 remove config.zoneName() use 2018-01-24 15:01:54 -08:00
Girish Ramakrishnan ee66893875 more config.fqdn() removal in tests 2018-01-24 14:58:37 -08:00
Girish Ramakrishnan 45456f2cf7 Remove unused maildb.addDefaultDomain 2018-01-24 14:42:45 -08:00
Girish Ramakrishnan df3c127584 fqdn -> domain 2018-01-24 14:28:35 -08:00
Girish Ramakrishnan 9d409a67fd Default to domain because tld.getDomain returns null for some tld's 2018-01-24 14:17:26 -08:00
Girish Ramakrishnan 2e05483d54 refactor: addDnsRecords now takes a domain 2018-01-24 14:08:25 -08:00
Girish Ramakrishnan 4e267c7cd1 mail: remove config.fqdn use 2018-01-24 12:28:47 -08:00
Girish Ramakrishnan efc6a5acd0 Return NOT_FOUND if app is missing in appstore 2018-01-24 10:56:26 -08:00
Johannes Zellner 962ebc835d Do not error if mailbox state is already correct 2018-01-24 15:44:05 +01:00
Johannes Zellner c7282e861c Add missing MailError type 2018-01-24 15:38:19 +01:00
Johannes Zellner 358048e02b Add domain specific mailbox routes and logic 2018-01-24 13:11:35 +01:00
Johannes Zellner 666f42f4ef Do not automatically create mailboxes for users 2018-01-24 12:56:12 +01:00
Johannes Zellner aca07765c9 remove unused require 2018-01-24 12:55:44 +01:00
Johannes Zellner a3caad46a2 Add unique constraint to our schema description file 2018-01-24 10:51:46 +01:00
Girish Ramakrishnan 5e688944e8 dns: compute subdomain correctly when zone name and domain differs 2018-01-23 20:25:45 -08:00
Girish Ramakrishnan ed75364e2b Default to tld.getDomain if zone name is not provided 2018-01-23 18:54:05 -08:00
Girish Ramakrishnan d33e35fda2 Move send_test_mail under mail domain api 2018-01-23 16:10:23 -08:00
Girish Ramakrishnan ccaf687e91 remove config.fqdn from settings-test 2018-01-23 15:47:41 -08:00
Girish Ramakrishnan ab447120dc cloudflare: del returns 200 now 2018-01-23 14:52:41 -08:00
Girish Ramakrishnan 9e0e99cb0c Use new mail.ini parameters 2018-01-23 14:25:15 -08:00
Girish Ramakrishnan 992a32a8d9 add hetzner as supported provider 2018-01-23 11:37:15 -08:00
Johannes Zellner 4a7b26f940 mail_relay from settings table is also JSON 2018-01-23 16:29:15 +01:00
Johannes Zellner 147c728743 Rename maildb table to mail 2018-01-23 15:45:30 +01:00
Johannes Zellner ec910e8ca1 Fix hotfix to be able to deal with different webadmin and box code revisions 2018-01-23 12:15:38 +01:00
Johannes Zellner 681813eddd Ensure the mail status check api has consitent toplevel properties 2018-01-23 12:01:00 +01:00
Girish Ramakrishnan e6f4a9e4a8 Use domain in mail test 2018-01-22 14:49:30 -08:00
Johannes Zellner 27bd0be1fc Remove config.fqdn() from ldap tests 2018-01-22 20:35:21 +01:00
Johannes Zellner f152dbefad Also check if the domain has mail enabled for ldap sendmail auth 2018-01-22 20:35:08 +01:00
Johannes Zellner 687ba0e248 Verify mailbox against username instead of email 2018-01-22 20:06:18 +01:00
Girish Ramakrishnan 61b5d3e60d createReleaseTarball: Use HEAD instead of --revision 2018-01-22 11:02:21 -08:00
Girish Ramakrishnan b69d6c42e1 Get webadmin from a level below 2018-01-22 11:02:21 -08:00
Girish Ramakrishnan 924e35294f move ui to separate repo
This separates the platform code from the ui
2018-01-22 11:01:42 -08:00
Johannes Zellner a6f79854db Further ldap test fixes 2018-01-22 17:53:17 +01:00
Johannes Zellner 591f01bb45 Do not automatically generate the user's email for password verification 2018-01-22 16:12:13 +01:00
Johannes Zellner 8bcd807010 Use main email address for avatar 2018-01-22 16:09:25 +01:00
Johannes Zellner 14dcd71429 Add ui components for fallbackEmail property 2018-01-22 16:06:35 +01:00
Johannes Zellner 9f29438b34 Allow changing fallbackEmail via the profile api 2018-01-22 15:55:55 +01:00
Johannes Zellner cf94f26d62 Fixup the test using fallbackEmail 2018-01-22 11:52:58 +01:00
Girish Ramakrishnan 6fdb093595 add and remove maildb entries when domain is created and removed 2018-01-21 20:33:21 -08:00
Girish Ramakrishnan 93d5ce63ae fix indent 2018-01-21 15:34:35 -08:00
Johannes Zellner 32152a8b88 Add missing migration file 2018-01-21 14:50:55 +01:00
Johannes Zellner 48d557b242 Replace alternateEmail with fallbackEmail 2018-01-21 14:50:24 +01:00
Johannes Zellner 1e8aa209b1 Add fallbackEmail to user data model 2018-01-21 14:25:39 +01:00
Girish Ramakrishnan 00c1c42b58 Fix mail route tests 2018-01-21 00:59:20 -08:00
Girish Ramakrishnan f4e1b8874c add maildb entry when domain entry is created 2018-01-21 00:27:28 -08:00
Girish Ramakrishnan f5b685465f make ldap test use maildb 2018-01-21 00:27:13 -08:00
Girish Ramakrishnan f49a36f667 Use mail.get() 2018-01-21 00:17:25 -08:00
Girish Ramakrishnan 70fecb8a75 Add mail test 2018-01-21 00:06:08 -08:00
Girish Ramakrishnan 04868f0983 maildb tests 2018-01-20 23:52:16 -08:00
Girish Ramakrishnan 16ac205c7f make mail routes domain based 2018-01-20 23:47:12 -08:00
Girish Ramakrishnan 3ed794e486 Add a single getter for all mail settings 2018-01-20 23:47:12 -08:00
Girish Ramakrishnan f93963540e Add maildb
also, migrate values from settings table to maildb
2018-01-20 23:47:08 -08:00
Girish Ramakrishnan 777269810f Move mail container creation to mail.js
At this point, mail.js is like an app of it's own
2018-01-20 20:38:35 -08:00
Girish Ramakrishnan a7de17a160 Move mail related settings to new mail route
there is quite a bit of circular dep between settings, platform and
mail code. this will be removed in future commits.
2018-01-20 20:02:01 -08:00
Girish Ramakrishnan e724913b6c Move email_status into mail route 2018-01-20 18:37:15 -08:00
Girish Ramakrishnan b68db9bf05 email -> mail 2018-01-20 18:22:43 -08:00
Girish Ramakrishnan 8da04f6f51 make domaindb.update take object 2018-01-20 10:24:11 -08:00
Girish Ramakrishnan b64c41758e domaindb.upsert is not used 2018-01-20 10:18:06 -08:00
Girish Ramakrishnan 0eaea12818 make domaindb.add take an object 2018-01-20 10:17:01 -08:00
Girish Ramakrishnan b098db16cf domaindb: add DOMAINS_FIELDS 2018-01-20 10:05:31 -08:00
Girish Ramakrishnan dc952f1dd8 remove config.fqdn() from caas-test 2018-01-20 09:50:30 -08:00
Girish Ramakrishnan ee733d54ea Add mail_domains and mail_default_domain to mail.ini 2018-01-19 23:17:00 -08:00
Girish Ramakrishnan 0e4a0658b2 Remove postman location (unused by dovecot) 2018-01-19 22:10:10 -08:00
Girish Ramakrishnan 20166cd41c make catch_all fully qualified 2018-01-19 20:02:56 -08:00
Girish Ramakrishnan 98d493b2d0 ldap: make mailbox search return fully qualified names 2018-01-19 12:14:43 -08:00
Girish Ramakrishnan af25485fa0 ldap: Make alias return fully qualified alias 2018-01-19 12:11:33 -08:00
Girish Ramakrishnan 2015e7bce9 ldap: make mailing list search return fully qualified members 2018-01-19 12:11:26 -08:00
Johannes Zellner 2370b12795 Use the db also for crashnotifier to obtain a valid server domain for sending the mail 2018-01-19 19:26:45 +01:00
Johannes Zellner 18a781b956 Collect common mail sending configs in one function 2018-01-19 18:58:34 +01:00
Johannes Zellner 77206a9d3c Remove fqdn usage from all email templates 2018-01-19 18:56:52 +01:00
Girish Ramakrishnan 73800ac6a7 Linter fixes 2018-01-19 09:55:27 -08:00
Girish Ramakrishnan 27dfd1d6c1 Set the dkim selector when generating keys
The box code owns the dkim dir and selector can change per domain
2018-01-18 21:49:45 -08:00
Girish Ramakrishnan 0833f8830c retire script does not require fqdn 2018-01-18 19:44:43 -08:00
Girish Ramakrishnan 28a240a701 send adminFqdn instead of domain in alive route 2018-01-18 19:44:34 -08:00
Girish Ramakrishnan 5be827cd4e remove use of config.fqdn() in ldap test 2018-01-18 19:35:05 -08:00
Girish Ramakrishnan 0f47dcfae6 ldap: mailbox routes now require the cn to be fully qualified 2018-01-18 19:33:38 -08:00
Girish Ramakrishnan 614f13ffd0 move caas tests to separate file 2018-01-18 14:22:07 -08:00
Girish Ramakrishnan a850c0813b caas: use caas_config for token verification 2018-01-18 13:51:02 -08:00
Girish Ramakrishnan 4b642a407f remove caas dep from tests 2018-01-18 13:50:57 -08:00
Girish Ramakrishnan 6a87558b52 set default provider to generic 2018-01-18 13:41:40 -08:00
Girish Ramakrishnan 606efb8038 remove caas related logic from oauth test 2018-01-18 13:31:08 -08:00
Girish Ramakrishnan cfa523f3c1 Remove config.fqdn() fallback in apptask 2018-01-18 12:06:20 -08:00
Girish Ramakrishnan 0e7ebc9637 Move SMTPS port to 2465
This curious change is because gogs and gitea have no way to configure
the SMTPS port and rely on port number ending with 465 to determine
SMTPS (!)
2018-01-18 10:38:39 -08:00
Johannes Zellner 94f7b90705 Add 1.9.3 changes 2018-01-18 15:45:17 +01:00
Johannes Zellner 876b4feb49 The DNS provider property moved to the root dns config object 2018-01-18 12:15:44 +01:00
Girish Ramakrishnan 27252fb2cc 1.10.0 changes 2018-01-17 21:10:11 -08:00
Girish Ramakrishnan 6513aebba6 MAIL_IMAP_USERNAME and MAIL_SMTP_USERNAME should be fully qualified
Re-configure the whole infra to propagate the changes
2018-01-17 21:07:53 -08:00
Girish Ramakrishnan e9be2a7fb7 Remove is_custom_domain use (use adminFqdn instead) 2018-01-17 21:07:13 -08:00
Girish Ramakrishnan 28dac3fdb3 Fix indentation 2018-01-17 21:05:43 -08:00
Girish Ramakrishnan f4c3ae639e Use app.domain instead of config.fqdn when setting up mail addon 2018-01-17 21:05:22 -08:00
Girish Ramakrishnan 8c8e387012 bump addons 2018-01-17 20:25:57 -08:00
Girish Ramakrishnan 9e8be3fa50 do basic backupId validation 2018-01-17 16:22:50 -08:00
Girish Ramakrishnan 21058f8b61 exchangeBoxTokenWithUserToken is obsolete 2018-01-17 15:59:33 -08:00
Girish Ramakrishnan cef0cd4b25 config.token() and arg_token are not used anymore 2018-01-17 15:39:32 -08:00
Girish Ramakrishnan 9cd690e8b4 Do not cache box and user config
this is not needed anymore since webadmin does not keep polling this
2018-01-17 15:38:24 -08:00
Girish Ramakrishnan 116befd111 fix test 2018-01-17 15:13:53 -08:00
Girish Ramakrishnan fe0ff45c37 caas: autoprovision and use caas and appstore configs 2018-01-17 14:11:19 -08:00
Girish Ramakrishnan 337f919451 dnsConfig is unused 2018-01-17 12:25:33 -08:00
Girish Ramakrishnan 03d2e74e1d use constants 2018-01-17 12:25:14 -08:00
Girish Ramakrishnan 2c42653c24 1.9.2 changes 2018-01-17 10:53:00 -08:00
Johannes Zellner b1109ba6ea Only require the GCS key when this backup provider is selected 2018-01-17 19:38:05 +01:00
Johannes Zellner 7700d236a5 Create new changes for 1.9.1 2018-01-16 22:02:09 +01:00
Johannes Zellner b10abb1944 Attempt to bring existing logs or terminal window to the front 2018-01-16 19:09:33 +01:00
Johannes Zellner dd6eeac000 Do not source the xterm assets in the main app 2018-01-16 16:49:34 +01:00
Johannes Zellner 7b8bb5dac4 Add js-terminal task to the default task chain for gulp 2018-01-16 16:49:17 +01:00
Johannes Zellner bf444a722d Remove debug entry from main menu 2018-01-16 16:32:56 +01:00
Johannes Zellner a954a23add Add terminal action to apps grid 2018-01-16 16:30:15 +01:00
Johannes Zellner 98aa785ad0 Add showTerminal button to logs view 2018-01-16 16:24:32 +01:00
Johannes Zellner ee485d8b2a Add separate terminal window 2018-01-16 16:24:18 +01:00
Girish Ramakrishnan 081b596ebf add note that the migration is br0ken 2018-01-15 20:18:32 -08:00
Girish Ramakrishnan 56f4cbe44a Fix double callback invocation in cleanupBackup 2018-01-15 20:08:55 -08:00
Girish Ramakrishnan ab5b754c22 Add adminFqdn to update params 2018-01-15 13:47:26 -08:00
Johannes Zellner f030aa95ba Open logs viewer in new window 2018-01-15 16:29:17 +01:00
Johannes Zellner bad947e2ac Add separate logs viewer 2018-01-15 16:29:03 +01:00
Johannes Zellner 02b43382c8 Remove logs view from the main angular app 2018-01-15 16:28:19 +01:00
Johannes Zellner 4ed35c25a5 Align text size in account and settings view 2018-01-15 14:52:25 +01:00
Johannes Zellner 0d4f963756 Ensure we use the same collation on all tables
This is required since some older cloudrons have a mixed bag of
collations and thus charsets and we add foreign keys across those, which
require the same collation
2018-01-15 11:01:35 +01:00
Girish Ramakrishnan 1139c077b0 Fix usage of domain.provider 2018-01-12 09:25:33 -08:00
Girish Ramakrishnan 84afdb2e3a remove broken links 2018-01-11 15:14:14 -08:00
Girish Ramakrishnan 115f9b408f cloudron-setup: Add adminFqdn 2018-01-11 15:11:49 -08:00
Girish Ramakrishnan d6ce51dabd Various minor UI fixes 2018-01-11 13:56:51 -08:00
Girish Ramakrishnan 54bc4b32c8 provider cannot be empty string 2018-01-11 11:05:37 -08:00
Girish Ramakrishnan 6537cf700f Fix crash in apps.clone 2018-01-11 10:59:30 -08:00
Johannes Zellner c5e0b45b22 setupdns view needs to query other domains during migration
The risk for cross site scripting during Cloudron setup is very small
2018-01-11 16:52:39 +01:00
Johannes Zellner cbfd7cf1a6 Make the new domain available to setupdns view on migration 2018-01-11 16:48:28 +01:00
Johannes Zellner e96199927d Do not show migrate button for already active domain 2018-01-11 14:57:29 +01:00
Johannes Zellner a67d690291 Ensure we can migrate caas Cloudrons back to caas domain 2018-01-11 14:33:09 +01:00
Girish Ramakrishnan 30ddda723d typo 2018-01-11 01:00:15 -08:00
Girish Ramakrishnan d9bf2f1724 If oldConfig.domain is missing use config.fqdn()
do this in configure() because the code asserts on domain being
a string
2018-01-11 00:35:27 -08:00
Girish Ramakrishnan 915cfbe7dd Remove use of isCustomDomain
Use adminFqdn instead as needed
2018-01-11 00:35:12 -08:00
Girish Ramakrishnan aeb883623b handle location being empty 2018-01-11 00:31:51 -08:00
Girish Ramakrishnan 2d163c1e76 caas: do not special case handling of naked domains
on the caas side, adding naked domain is removed as well
2018-01-11 00:13:16 -08:00
Girish Ramakrishnan 74e79c00fc Fix cert api selection 2018-01-11 00:05:35 -08:00
Girish Ramakrishnan f87f92708b Fix waitForDNSRecord with external domain 2018-01-10 23:50:59 -08:00
Girish Ramakrishnan b2ff16eb1e Do not save intrinsicFqdn in db 2018-01-10 23:44:46 -08:00
Girish Ramakrishnan 0c9f557d21 let unregisterSubdomain succeed if domain was removed 2018-01-10 23:17:07 -08:00
Girish Ramakrishnan f7dd8c0a23 Do not rewrap domain errors 2018-01-10 23:16:46 -08:00
Girish Ramakrishnan 3067e0940d compare with adminFqdn instead of adminLocation 2018-01-10 18:14:29 -08:00
Girish Ramakrishnan 969fba83ea Do not use config.fqdn to determine application name 2018-01-10 15:44:12 -08:00
Girish Ramakrishnan 70a15d01c9 Remove use of isCustomDomain in tokens.html 2018-01-10 14:13:43 -08:00
Girish Ramakrishnan efc0a3b68d Remove usage of config.appFqdn() 2018-01-10 13:58:05 -08:00
Girish Ramakrishnan c108cd2d5f Set max email recepient limit (in outgoing emails) to 500 2018-01-10 11:42:41 -08:00
Girish Ramakrishnan e67f023a56 Remove unused variable 2018-01-09 21:09:10 -08:00
Girish Ramakrishnan 208e4267df Fix validateHostname to handle empty location 2018-01-09 18:17:09 -08:00
Girish Ramakrishnan 92b6464cd7 caas: fix migration of zoneName into domains table 2018-01-09 17:50:04 -08:00
Girish Ramakrishnan ab66c8cb81 debug the zoneName in waitForDns 2018-01-09 16:09:47 -08:00
Girish Ramakrishnan 2ac12de204 Add domains.provider 2018-01-09 15:32:49 -08:00
Girish Ramakrishnan 373c003223 provider is always set in domain config 2018-01-09 14:25:58 -08:00
Girish Ramakrishnan f236bd3316 remove isCustomDomain usage from the UI
This also adds domains.provider that we should add to the db itself
at some point
2018-01-09 11:00:32 -08:00
Girish Ramakrishnan 9d386bd071 Fix indent 2018-01-09 10:15:35 -08:00
Girish Ramakrishnan 665aa2ad3d Open the logs in new tab 2018-01-08 17:41:31 -08:00
Girish Ramakrishnan e8ca423ac4 make setupdns try to get status forever
During domain migration, the box code restarts. the getStatus() will
fail temporarily. In the future, we can make this code forward to
error.html after a few retries.
2018-01-08 17:34:28 -08:00
Girish Ramakrishnan a53214cb29 Call jquery event handlers in $scope.$apply
This causes all sorts of strange race conditions when $location.path()
is changed by the hide and hidden events handlers
2018-01-08 17:30:50 -08:00
Girish Ramakrishnan af4296e40c Fix periodic fetching of apps/config/profile
All these are already fetched the first time in main.js

* Fetch apps periodically only in the apps view. This is mostly for the
  installationState. We can optimize this a bit more later depending on
  if any app is in non-running state.

* profile hardly changes, no need to fetch this over and over

* config hardly changes, but is fetched primarily for the update flag
2018-01-08 16:17:02 -08:00
Girish Ramakrishnan 50d396725e Update app state immediately on dialog close 2018-01-08 14:53:28 -08:00
Girish Ramakrishnan e0c894d333 remove redundant call to fetch user info
main.js already does this
2018-01-08 14:21:25 -08:00
Girish Ramakrishnan 044c25311f add note on why we only use href and not the hostname 2018-01-08 14:21:16 -08:00
Girish Ramakrishnan d56575facf Add autofocus to error and info dialogs 2018-01-08 14:21:04 -08:00
Girish Ramakrishnan 05775a843d Keep apps sorted 2018-01-08 14:16:07 -08:00
Johannes Zellner 5261831ca2 We have a new year 2018-01-08 12:12:55 +01:00
Johannes Zellner b0c967ba57 Remove logging parts from terminal/debug view 2018-01-08 11:51:10 +01:00
Johannes Zellner 2902c6ca7a Add logs button in app grid 2018-01-08 11:09:46 +01:00
Johannes Zellner 0c5aea2fb2 Add separate logs view with deep-linking support 2018-01-08 11:05:14 +01:00
Girish Ramakrishnan de2999cb56 tarjs is not used anymore 2018-01-07 18:34:36 -08:00
Girish Ramakrishnan 28c1a70ae1 Fix display of app install dialog when no version is provided 2018-01-05 11:58:53 -08:00
Girish Ramakrishnan ff4d3de1b1 redirect to setupdns instead since it will redirect to new domain automatically 2018-01-02 17:34:53 -08:00
Girish Ramakrishnan ac4f12447b add set admin button 2018-01-02 16:25:30 -08:00
Girish Ramakrishnan 325814e7ca Display the add domain button for caas 2018-01-02 14:52:27 -08:00
Girish Ramakrishnan 00728dc833 caas: move getBoxAndUserDetails 2018-01-02 13:05:30 -08:00
Girish Ramakrishnan c95684af1e Move caas heartbeat code to caas.js 2018-01-02 12:47:33 -08:00
Girish Ramakrishnan 0a80bff055 Fix indent 2018-01-01 20:09:04 -08:00
Girish Ramakrishnan 9e7b10860d webterminal: Sort entries in dropdown based on location 2017-12-27 07:23:16 -08:00
Girish Ramakrishnan 41eab11641 Add 1.8.5 changes 2017-12-26 07:06:50 -08:00
Girish Ramakrishnan b7abf404f3 Display external error if appstore download fails 2017-12-26 07:06:08 -08:00
Girish Ramakrishnan dc644570f7 Use updateConfig addons instead of manifest addons to setup
Even though app.manifest variable is updated by updatedApp, the
setupAddons is called with the _old_ value because it is a bind()
2017-12-21 01:07:40 -08:00
Girish Ramakrishnan c4cb6b5819 lint 2017-12-21 01:04:38 -08:00
Girish Ramakrishnan b7e9f0ed12 Add debug for oauth addon setup 2017-12-21 00:50:53 -08:00
Girish Ramakrishnan 46df1d694a gcs: display prefix in restore UI 2017-12-20 09:32:29 -08:00
Girish Ramakrishnan 3efe8e3393 gcs: add to restore UI 2017-12-20 02:06:55 -08:00
Girish Ramakrishnan e4b12f0c4e gcs: make testfile deletion work
It seems there is a race where the delete gets triggered even before the
file upload is complete. as a result, the delete succeeds but the file
is left on gcs.
2017-12-16 21:36:33 +05:30
Girish Ramakrishnan 61b56d4679 gcs: keyFilename is not used
also, some linter fixes
2017-12-16 21:36:29 +05:30
Girish Ramakrishnan 051ac21fed gcs: make prefix visible 2017-12-15 21:47:54 +05:30
Girish Ramakrishnan 892bd86810 gcs: gcsKey.content is not loaded properly 2017-12-15 21:21:28 +05:30
Girish Ramakrishnan 5c4ae6066d gcs: lint 2017-12-15 17:34:09 +05:30
Girish Ramakrishnan a35e048665 gcs: oldFilePath is not defined 2017-12-15 17:31:05 +05:30
Girish Ramakrishnan 48f6c39ae5 gcs: Make requires alphabetical 2017-12-15 17:28:45 +05:30
Girish Ramakrishnan be03bd2c5b shrinkwrap is gone 2017-12-15 17:09:46 +05:30
Girish Ramakrishnan f108376b25 8.9.3 is required 2017-12-15 17:05:48 +05:30
Girish Ramakrishnan 70e23ed394 Add package-lock.json
https://github.com/npm/npm/pull/16441 has a TLDR
2017-12-15 17:03:37 +05:30
Girish Ramakrishnan 5fbfb7365f Waiting -> Pending 2017-12-15 16:58:38 +05:30
Girish Ramakrishnan 678865fa2a Fix npm warnings
npm WARN The package gulp-sass is included as both a dev and production dependency.
npm WARN The package hock is included as both a dev and production dependency.
npm WARN The package request is included as both a dev and production dependency.
2017-12-15 16:56:54 +05:30
Girish Ramakrishnan 943dc14bf0 Update shrinkwrap for latest node 2017-12-15 16:53:31 +05:30
Girish Ramakrishnan c3919592ff 1.9.0 changes 2017-12-15 16:49:14 +05:30
Girish Ramakrishnan 442eb8a518 Update node to 8.9.3 LTS 2017-12-15 16:47:11 +05:30
Girish Ramakrishnan 192e4f0a75 gulp-ejs added an options param 2017-12-12 14:07:20 +05:30
Johannes Zellner 921550e3ed Make mocha call process.exit for each run to avoid lingering server instances 2017-12-10 17:49:23 +01:00
Girish Ramakrishnan 7d0cf1a754 caas: migrate -> change_plan 2017-12-09 09:00:10 +05:30
Girish Ramakrishnan 6dec02e1bd caas: refactor migrate and upgrade route 2017-12-09 08:43:59 +05:30
Girish Ramakrishnan 14fc066af7 gcs: Add missing label and keep the listing sorted 2017-12-09 05:40:31 +05:30
Girish Ramakrishnan 8fbad34716 Update shrinkwrap 2017-12-08 06:28:13 +05:30
Girish Ramakrishnan 75a344a316 Merge branch 'feature/gcs' into 'master'
Adding Google Cloud Storage support for Backups

See merge request cloudron/box!18
2017-12-08 00:51:54 +00:00
Girish Ramakrishnan 3b8d500636 1.8.4 changes 2017-12-07 20:20:41 +05:30
Girish Ramakrishnan a83bce021b Bump mail container for internal email relay fix 2017-12-07 20:20:09 +05:30
Girish Ramakrishnan 725cf297ab Developer scope is obsolete 2017-12-07 04:33:49 +05:30
Aleksandr Bogdanov 5a2de0bcbb Merge remote-tracking branch 'origin/master' into feature/gcs
# Conflicts:
#	webadmin/src/views/certs.js
#	webadmin/src/views/settings.js
2017-12-06 22:47:26 +01:00
Girish Ramakrishnan cb814a50d7 Fix waitForDNSRecord for subdomain installations 2017-12-06 12:31:25 +05:30
Girish Ramakrishnan 5d34559f0a Fix hostname validation 2017-12-06 07:13:46 +05:30
Girish Ramakrishnan 91ede59241 cloudron-setup: move backupConfig default as migration script
if in autoprovision, then the backupConfig ends up being overwritten
after a restore.
2017-12-05 18:21:25 +05:30
Girish Ramakrishnan 778342906e cloudron-setup: remove dnsConfig
this is not really used since dns setup is the first step now
2017-12-05 18:09:25 +05:30
Girish Ramakrishnan c42f3341ca cloudron-setup: Add back restore-url and key for pre-1.9 2017-12-05 16:01:16 +05:30
Girish Ramakrishnan a838b4c521 cloudron-setup: keep pre-1.9 compat for configs 2017-12-05 15:56:47 +05:30
Girish Ramakrishnan 44d4934546 cloudron-setup: create autoprovision.json 2017-12-05 14:55:06 +05:30
Girish Ramakrishnan 49db0d3641 cloudron-setup: remove boxVersionsUrl 2017-12-05 14:53:26 +05:30
Girish Ramakrishnan 2bebed2c19 Add fqdn to caas domain config 2017-12-05 07:16:00 +05:30
Girish Ramakrishnan 2cf2dddcee Fix display of DNS records when not using cloudron-smtp
Fixes #492
2017-12-04 21:24:17 +05:30
Girish Ramakrishnan 306e11ae88 Remove unused requires 2017-12-04 17:10:06 +05:30
Girish Ramakrishnan 568397ec19 caas: send ids in backupDone instead of filenames 2017-11-29 12:39:10 -08:00
Girish Ramakrishnan 459314df17 lock for platform start, so that apps are not installed in between 2017-11-28 23:18:43 -08:00
Girish Ramakrishnan 693bc094cc caas: make fqdn part of dns and s3 credentials 2017-11-28 22:44:40 -08:00
Girish Ramakrishnan 9cdd2df696 set restoring to false 2017-11-28 15:01:59 -08:00
Girish Ramakrishnan e9b308bb95 Re-purpose the zoneName as the caas domain 2017-11-28 15:00:38 -08:00
Girish Ramakrishnan 432a369bff Add token to dnsConfig 2017-11-28 15:00:38 -08:00
Girish Ramakrishnan 76312495fd Add debug 2017-11-28 15:00:33 -08:00
Girish Ramakrishnan 126d8b9bec stringify and a typo 2017-11-28 02:30:35 -08:00
Girish Ramakrishnan d001647704 Change path of autoprovision.conf since /root is not readable 2017-11-28 01:23:10 -08:00
Girish Ramakrishnan 8701b36123 make dnsSetup return any provisioning error 2017-11-28 01:20:18 -08:00
Girish Ramakrishnan c56a24d4fb Autoprovision from autoprovision.json
This is done so that CaaS restore code path can provision correctly
2017-11-27 22:41:32 -08:00
Girish Ramakrishnan e6eb54d572 More test fixing 2017-11-27 18:19:20 -08:00
Girish Ramakrishnan 68c26c1d12 Fix route/ tests 2017-11-27 16:01:52 -08:00
Girish Ramakrishnan 437312811d wrap seconds 2017-11-27 15:41:37 -08:00
Girish Ramakrishnan 68d4e70823 Add config._reset to tests 2017-11-27 15:27:54 -08:00
Girish Ramakrishnan 74f3a4dd6f remove redundant after() 2017-11-27 14:10:27 -08:00
Girish Ramakrishnan 3a74babcf4 Fix error message 2017-11-27 13:59:56 -08:00
Girish Ramakrishnan ab2f2c9aab Remove setTimeout from cron.js
this causes scripts to not end since the timeout is not killed
2017-11-27 13:43:25 -08:00
Girish Ramakrishnan 8b11692e37 cron: ensure all jobs are cleaned up 2017-11-27 12:44:04 -08:00
Girish Ramakrishnan abe04d7d10 ldap: call client.unbind 2017-11-27 12:14:31 -08:00
Girish Ramakrishnan efe75f0c4e make tests finish
database.uninitialize must be called to drop the connection
2017-11-27 11:57:09 -08:00
Girish Ramakrishnan b6c20877ea lint 2017-11-27 10:43:12 -08:00
Girish Ramakrishnan 172d5bbdff Remove obsolete setting (now migrated into domains table) 2017-11-24 22:45:32 -08:00
Girish Ramakrishnan 6ed7a91cf9 rename migration timestamps so they appear in correct order
The following migrations are already released in 1.8.3:

20171116203507-apps-rename-newConfigJson-to-updateConfigJson.js
20171116224051-apps-rename-lastBackupId-to-restoreConfigJson.js
2017-11-24 22:39:29 -08:00
Johannes Zellner 61a7f1a126 mailer.start() is gone remove from test 2017-11-25 00:47:00 +01:00
Johannes Zellner ba49c1e30c Remove accidentally commited debug lines 2017-11-25 00:39:44 +01:00
Girish Ramakrishnan ca5b69a07d Fix db export/import 2017-11-24 15:31:06 -08:00
Girish Ramakrishnan 998f736e6f Add database.exportToFile 2017-11-24 15:29:56 -08:00
Girish Ramakrishnan 969f8ad11f Add 1.9.0 changes 2017-11-24 14:58:43 -08:00
Johannes Zellner 34ec09588a no need for a special test setup handling in the migration script 2017-11-24 23:48:59 +01:00
Johannes Zellner 4091315589 Make migration down fail if the table cannot be dropped 2017-11-24 23:48:39 +01:00
Johannes Zellner 91fb45584f Add some changes 2017-11-24 23:01:34 +01:00
Girish Ramakrishnan 180a455299 remove mailer.start and stop 2017-11-24 13:58:40 -08:00
Girish Ramakrishnan a77bf54df7 cron.initialize is required in domain setup for heartbeats 2017-11-24 13:56:34 -08:00
Girish Ramakrishnan 74abce99ac Fix some typos in restore api 2017-11-23 16:37:40 -08:00
Johannes Zellner b2d27ee26a add 1.8.3 changes 2017-11-24 01:31:15 +01:00
Johannes Zellner 1466104681 Remove obsolete developer mode 2017-11-24 01:31:15 +01:00
Girish Ramakrishnan 4acd0bcdac Remove --restore-url and --restore-key 2017-11-23 13:33:41 -08:00
Girish Ramakrishnan f9f2bd5c28 Fix crash 2017-11-23 13:17:07 -08:00
Girish Ramakrishnan a752b7139f Add a break 2017-11-23 12:54:25 -08:00
Girish Ramakrishnan 2becf674ee fix wording 2017-11-23 12:42:46 -08:00
Girish Ramakrishnan ef2c44ee2f Instead of exact match, only require major+minor to match 2017-11-23 12:36:43 -08:00
Girish Ramakrishnan a5e5324f97 Add note about restore in setupdns page 2017-11-23 12:19:06 -08:00
Girish Ramakrishnan 479261bcec add restore UI
Add a link from setup page to restore

Part of #439
2017-11-22 23:08:59 -08:00
Girish Ramakrishnan ac94a0b7f2 Add route to restore box from backup
Part of #439
2017-11-22 23:08:59 -08:00
Girish Ramakrishnan 0f191324fa Add backups.restore to import from box backup
Part of #439
2017-11-22 23:08:59 -08:00
Girish Ramakrishnan b507ccaa33 Add database.importFileFile
Part of #439
2017-11-22 23:08:59 -08:00
Girish Ramakrishnan 9f6bc0b779 Start platform only on activated 2017-11-22 23:08:28 -08:00
Girish Ramakrishnan 7306f1ddea chown the toplevel mail directory
this helps the restore box logic extract without sudo
2017-11-22 23:08:01 -08:00
Girish Ramakrishnan dc1d10837b split read/parse of fsmetadata 2017-11-22 23:07:52 -08:00
Girish Ramakrishnan f58d6c04cc Add DO spaces ams3 2017-11-22 21:03:38 -08:00
Girish Ramakrishnan f9dda85a38 Fix error code handling 2017-11-22 21:03:34 -08:00
Johannes Zellner 8773c0f6e1 Remove unused requires 2017-11-23 02:36:33 +01:00
Girish Ramakrishnan 72a96c0d6a lint 2017-11-22 12:19:05 -08:00
Girish Ramakrishnan 136ee363a8 Make backups.download take backupConfig 2017-11-22 10:38:04 -08:00
Girish Ramakrishnan 9c5965311f Handle billing required error in clone 2017-11-22 09:05:06 -08:00
Girish Ramakrishnan 78bd819a36 fix indent 2017-11-21 19:18:03 -08:00
Girish Ramakrishnan 48df8b713d add note on enableBackup 2017-11-21 18:09:44 -08:00
Girish Ramakrishnan 0e15fabf88 Do not put app in errored state if backup fails
this will end up sending an email but will put the app itself back
in installed state

Fixes #468
2017-11-21 15:59:25 -08:00
Johannes Zellner ed83a11248 Hide domain actions 2017-11-21 23:05:07 +01:00
Johannes Zellner 8d69e5f3b9 More test cleanups to support domains api 2017-11-21 02:46:20 +01:00
Girish Ramakrishnan 5dab697fd6 Display backup progress in caas 2017-11-20 14:37:36 -08:00
Johannes Zellner a94d5d1b3e Add domains REST api tests 2017-11-20 22:53:14 +01:00
Johannes Zellner 9c0af8b13e Fixing sysadmin, settings and clients tests 2017-11-20 20:01:50 +01:00
Johannes Zellner a08ff89b78 Fix ldap and dns tests 2017-11-20 20:01:50 +01:00
Johannes Zellner 2e06724927 Add mailboxes unique name/domain constraint 2017-11-20 20:01:50 +01:00
Johannes Zellner f7c7a36fc1 We reuse appFqdn() here for the webadmin 2017-11-20 20:01:50 +01:00
Johannes Zellner 748d1b8471 webadmin: Hide domain actions for caas managed domains 2017-11-20 20:01:50 +01:00
Johannes Zellner 032200b20f cloudron addDnsRecords needs to provide subdomain and domain separately 2017-11-20 20:01:50 +01:00
Johannes Zellner 4cbb751d82 Fix usage of appFqdn in caas dns backend 2017-11-20 20:01:50 +01:00
Johannes Zellner 27e4f0cb82 make *DNSRecords apis take the explicit domain 2017-11-20 20:01:50 +01:00
Johannes Zellner 321bfc6130 Create initial domain record in start.sh if fqdn was provided 2017-11-20 20:01:50 +01:00
Johannes Zellner 635426c37e Drop mailboxes primary key in favor of name+domain constraint 2017-11-20 20:01:50 +01:00
Johannes Zellner 33e7c8e904 Create the admin group only on owner creation
For new cloudrons this will first remove the previously added group and
mailbox entry from the db migration scripts and readds it once we have a
domain on owner creation
2017-11-20 20:01:50 +01:00
Johannes Zellner 616b4b86d8 Reword the dns setup page to indicate more domains can be added later 2017-11-20 20:01:50 +01:00
Johannes Zellner e3e6fd2bc9 For new cloudrons, the migration scripts cannot rely on an existing
domain
2017-11-20 20:01:50 +01:00
Johannes Zellner 07626dacb5 Ensure certificates needs to be multidomain aware 2017-11-20 20:01:50 +01:00
Johannes Zellner bf711c6ebb allow to set domain specific fallback certs 2017-11-20 20:01:50 +01:00
Johannes Zellner a4a3e19a92 Remove configJson field for domain records in postprocess 2017-11-20 20:01:50 +01:00
Johannes Zellner 16db4ac901 Make app configure domain aware 2017-11-20 20:01:50 +01:00
Johannes Zellner 78d6b6d632 Put app configure dialog helper in the correct scope 2017-11-20 20:01:50 +01:00
Johannes Zellner 009b8abf1b dns api now takes full fqdn instead of subdomain 2017-11-20 20:01:50 +01:00
Johannes Zellner 4edd874695 webadmin: add client.getDomain() 2017-11-20 20:01:50 +01:00
Johannes Zellner dda403caa9 Only show domains in apps view if there are more than one 2017-11-20 20:01:50 +01:00
Johannes Zellner de44796b6f Handle errors if domain is still used on deletion attempt 2017-11-20 20:01:50 +01:00
Johannes Zellner 53e3626e51 More test fixes for config database and settings 2017-11-20 20:01:50 +01:00
Johannes Zellner 9aa4fdc829 Fixup the dns provider tests 2017-11-20 20:01:50 +01:00
Johannes Zellner 1ccc3b84b8 Fixup various tests for multidomain 2017-11-20 20:01:50 +01:00
Johannes Zellner d4b6768464 Fixup validateHostname and related tests 2017-11-20 20:01:50 +01:00
Johannes Zellner 6e07a4ec08 Fixup database tests 2017-11-20 20:01:50 +01:00
Johannes Zellner 1cee0f3831 Fix unregisterSubdomain to work during migration from single to multidomain
oldConfig.domain was previously not there and thus might be missing
still
2017-11-20 20:01:50 +01:00
Johannes Zellner a52747cde0 Avoid logging 'undefined' on success, this looks like a bug otherwise 2017-11-20 20:01:50 +01:00
Johannes Zellner 14d575f514 Make mailboxdb aware of domain field 2017-11-20 20:01:50 +01:00
Johannes Zellner e43e904622 Refactor all app.location usages to config.appFqdn(app) 2017-11-20 20:01:50 +01:00
Johannes Zellner 1dfa689d1c Make apptask subdomain cleanup multidomain aware 2017-11-20 19:59:40 +01:00
Johannes Zellner 293e401852 Store domain alongside with location in apps oldConfig 2017-11-20 19:59:40 +01:00
Johannes Zellner c565d0789e webadmin: Show domain where the app is installed 2017-11-20 19:59:40 +01:00
Johannes Zellner 59ae1ac012 Move fallback certificate api to domains 2017-11-20 19:59:40 +01:00
Johannes Zellner 4cf2978088 Remove dns related settings api
This is replaced with the multi domain aware api
2017-11-20 19:59:40 +01:00
Johannes Zellner 707d34cb89 Make app install dialog multi domain aware 2017-11-20 19:59:40 +01:00
Johannes Zellner 20a37030b6 The domain update route returns 204 not 202 2017-11-20 19:59:40 +01:00
Johannes Zellner e1be8b669f Do not rely on admin subdomain for dns backend config validation 2017-11-20 19:59:40 +01:00
Johannes Zellner c723b289dc Only validate the nameservers for manual dns backend 2017-11-20 19:59:40 +01:00
Johannes Zellner 7c51c380ae webadmin: Refactor the domains view 2017-11-20 19:59:40 +01:00
Johannes Zellner d75959772c Add more error handling for domains update route 2017-11-20 19:59:40 +01:00
Johannes Zellner 37e23c9465 Add zoneName support to domains rest API 2017-11-20 19:59:40 +01:00
Johannes Zellner 21c8f63dc1 weadmin: Add domain REST api wrapper 2017-11-20 19:59:40 +01:00
Johannes Zellner ca3b6e542a Require password for domain deletion route 2017-11-20 19:59:40 +01:00
Johannes Zellner 3e4466a41e Fix appdb.add sql query 2017-11-20 19:59:40 +01:00
Johannes Zellner c1b5f56ac6 Send domain with app install request 2017-11-20 19:59:40 +01:00
Johannes Zellner 28c3ef772e Ensure we pass full fqdn to domains api from apptask 2017-11-20 19:59:40 +01:00
Johannes Zellner f1b23005c9 Fix appFqdn() usage to match new api 2017-11-20 19:59:40 +01:00
Johannes Zellner 143ba831f4 Make appFqdn() multidomain aware 2017-11-20 19:59:40 +01:00
Johannes Zellner 5ca31f2484 Send domain as part of the apps routes 2017-11-20 19:59:40 +01:00
Johannes Zellner 5c272fe5d9 Make appdb domain aware 2017-11-20 19:59:40 +01:00
Johannes Zellner 155877534f Fixup apps.validateHostname() 2017-11-20 19:59:26 +01:00
Johannes Zellner a2a1d842fa Add db migration scripts
This adds domains table and adjusts the apps and mailboxes table accordingly

Also ensure we explicitly set the table collation, this is required
for the foreign key from apps table (utf8) and the newly created
domains table, which by default now would be utf8mb4

Put db table constraint for mailboxes.domain

Update the schema file
2017-11-20 19:59:26 +01:00
Johannes Zellner 260ac0afb7 Remove subdomains.js in favor of multidomain capable domains.js 2017-11-20 19:59:26 +01:00
Johannes Zellner fb9372d93e Remove unused dns config change event 2017-11-20 19:59:26 +01:00
Johannes Zellner eb65f9e758 Remove default settings key for DNS_CONFIG 2017-11-20 19:59:26 +01:00
Johannes Zellner 3265d7151c Migrate dns test to domains.js 2017-11-20 19:59:26 +01:00
Johannes Zellner 597af2e034 Do not send obsolete settings.dnsConfig with alive status 2017-11-20 19:59:26 +01:00
Johannes Zellner 0b8f0bf731 Remove subdomains usage in cloudron.js 2017-11-20 19:59:26 +01:00
Johannes Zellner a7e10cead0 Use domains api in platform 2017-11-20 19:59:26 +01:00
Johannes Zellner 0e74a6df35 Deprecate dns settings api and add dns data migration 2017-11-20 19:59:26 +01:00
Johannes Zellner 3fbaa385c4 Add DNS record specific functions to domains.js 2017-11-20 19:59:26 +01:00
Johannes Zellner 29637bb4f4 Add basic domain setting validation 2017-11-20 19:59:26 +01:00
Johannes Zellner 9dba816711 Add domain routes 2017-11-20 19:59:26 +01:00
Johannes Zellner 9155f49d4c Add domaindb logic 2017-11-20 19:59:26 +01:00
Johannes Zellner 0e62780f55 Add domains table 2017-11-20 19:59:26 +01:00
Girish Ramakrishnan 998bc36673 remove manifest arg to backupApp 2017-11-19 17:58:04 -08:00
Girish Ramakrishnan c2dbc40473 Move version to the top 2017-11-19 16:36:00 -08:00
Girish Ramakrishnan cd5a14ce47 Use date object instead of string 2017-11-19 16:11:51 -08:00
Girish Ramakrishnan 917122c812 display last updated in app info 2017-11-19 13:20:20 -08:00
Girish Ramakrishnan 21b8b8deba Fix many links in the readme 2017-11-19 12:16:04 -08:00
Girish Ramakrishnan 44c2aedb57 1.8.2 changes 2017-11-18 02:19:17 -08:00
Girish Ramakrishnan 7e6a83df84 Fix migration callback 2017-11-18 02:11:00 -08:00
Girish Ramakrishnan ec4910a45e Fix restore 2017-11-17 22:35:56 -08:00
Girish Ramakrishnan 6558c78094 change the json blobs to text 2017-11-17 15:52:40 -08:00
Girish Ramakrishnan 5df92d1903 remove dead code 2017-11-17 15:18:06 -08:00
Girish Ramakrishnan 05affa7d26 remove dead code 2017-11-17 15:17:50 -08:00
Girish Ramakrishnan 46c6c5a5a8 remove double .js 2017-11-17 14:50:53 -08:00
Girish Ramakrishnan 75da751c72 1.8.1 changes 2017-11-17 14:50:53 -08:00
Johannes Zellner b84f60671e Also fix the restoreConfigJson migration down script 2017-11-17 23:45:22 +01:00
Johannes Zellner 8dcb06cb02 Fix db migration down step for newConfigJson change 2017-11-17 23:41:22 +01:00
Girish Ramakrishnan 83bf739081 Update the license 2017-11-17 10:46:12 -08:00
Girish Ramakrishnan 48a52fae2e LE agreement URL has changed 2017-11-17 10:35:58 -08:00
Girish Ramakrishnan 0ddbda6068 Fix crash 2017-11-16 15:11:12 -08:00
Girish Ramakrishnan 360fa058ea store format information for restoring
fixes #483
2017-11-16 15:01:27 -08:00
Johannes Zellner 489d2022e6 Do not underline errored links 2017-11-16 23:18:50 +01:00
Girish Ramakrishnan f762d0c0a1 newConfig -> updateConfig 2017-11-16 12:36:07 -08:00
Girish Ramakrishnan 98cad0678d Handle json parse errors with new body-parser module 2017-11-16 11:47:17 -08:00
Girish Ramakrishnan 92acb2954f Rename restoreConfig to manifest in backup table
Only the manifest needs to be preserved in the backup table
2017-11-16 11:25:40 -08:00
Girish Ramakrishnan 00a6e4c982 Show doc url in info dialog
Fixes #486
2017-11-16 10:05:49 -08:00
Girish Ramakrishnan bf9eb4bd87 Switch the default to logs to show some useful information 2017-11-16 10:05:49 -08:00
Girish Ramakrishnan 2f4940acbd update modules 2017-11-16 09:34:00 -08:00
Girish Ramakrishnan 9f7ca552a6 handle various appstore errors 2017-11-16 00:23:34 -08:00
Girish Ramakrishnan 4272d5be8a Send feedback via API
Fixes #484
2017-11-15 23:31:13 -08:00
Girish Ramakrishnan 1babfb6e87 Allow admins to access all apps
Fixes #420
2017-11-15 19:24:11 -08:00
Girish Ramakrishnan 5663cf45f8 remove redundant reset 2017-11-15 19:08:38 -08:00
Girish Ramakrishnan d8cb2d1d25 test: reset is already part of setup 2017-11-15 18:56:27 -08:00
Girish Ramakrishnan 174a60bb07 fix linter warnings 2017-11-15 18:56:27 -08:00
Girish Ramakrishnan 3d7094bf28 Handle error in uploadFile 2017-11-15 18:45:23 -08:00
Girish Ramakrishnan 4d6616930a Fix failing test 2017-11-15 18:41:37 -08:00
Girish Ramakrishnan 24875ba292 Handle all errors and set focus correctly
Fixes #485
2017-11-14 18:26:42 -08:00
Johannes Zellner c58b2677b6 Fixup config tests and do not allow saving random values to the config file
Those will eventually be overwritten by start.sh anyways, we cannot rely
on those
2017-11-15 02:41:40 +01:00
Johannes Zellner 25146e1134 Allow tests to work without a cloudron.conf on disk 2017-11-15 02:40:50 +01:00
Johannes Zellner c0c35964fe Fix backups tests 2017-11-15 02:29:58 +01:00
Johannes Zellner 0bf9ab0a2b No need to put static database config in cloudron.conf 2017-11-15 02:29:36 +01:00
Johannes Zellner 6d86f4cbda Ensure we only save relevant config values 2017-11-15 02:29:07 +01:00
Girish Ramakrishnan d2741bbeb9 Allow mailTo to be configurable
Part of #485
2017-11-14 16:24:34 -08:00
Girish Ramakrishnan 690d02a353 Always show the DNS records in the UI 2017-11-14 15:13:56 -08:00
Johannes Zellner c629db9597 Remove preinstall app bundle support 2017-11-14 23:09:17 +01:00
Aleksandr Bogdanov 994f771d4d Merge remote-tracking branch 'origin/master' into feature/gcs 2017-11-14 20:16:12 +01:00
Girish Ramakrishnan 67fcf85abb Allow restore if already restoring 2017-11-13 18:43:36 -08:00
Girish Ramakrishnan 527eace8f8 Fix j2xml usage 2017-11-13 11:10:42 -08:00
Girish Ramakrishnan e65230b833 update many dev modules 2017-11-13 10:57:36 -08:00
Girish Ramakrishnan 3e8334040b Update many node modules
also, use rimraf instead of del
2017-11-13 10:57:32 -08:00
Girish Ramakrishnan 2bcd3a8e4d Add a hack to stretch the multi-select box a bit 2017-11-12 02:50:28 -08:00
Girish Ramakrishnan e75b85fc3a Bump postgresql container to workaround shm issues
reconfiguring the postgresql configuring seems to fix some shm
issues on docker upgrade
2017-11-11 20:52:34 -08:00
Girish Ramakrishnan c4362d3339 Fix failing ldap test 2017-11-11 17:33:27 -08:00
Girish Ramakrishnan 85e492a632 Fix detection of container id from IP
https://docs.docker.com/engine/api/v1.32/#tag/Network

"Note that it uses a different, smaller representation of a network
than inspecting a single network. For example, the list of containers
attached to the network is not propagated in API versions 1.28 and up."

Verified using:

curl --unix-socket /var/run/docker.sock http::/networks/cloudron
2017-11-11 16:55:43 -08:00
Girish Ramakrishnan b8d4b67043 update aws-sdk and dockerode 2017-11-11 16:38:40 -08:00
Girish Ramakrishnan ffacd31259 bump the node version 2017-11-11 16:25:42 -08:00
Johannes Zellner 19f6da88da Do not disable access control elements if no group was created
There are still users to be selected
2017-11-12 00:09:05 +01:00
Girish Ramakrishnan c0faae4e27 Add more changes for 1.8.0 2017-11-11 11:14:42 -08:00
Girish Ramakrishnan a19c566eea Always show info box that displays app version
Fixes #478
2017-11-11 11:09:59 -08:00
Girish Ramakrishnan 3ec806452c Update node to 6.11.5 2017-11-10 19:25:08 -08:00
Girish Ramakrishnan 0c73cd5219 Update docker to 17.09 2017-11-10 18:49:28 -08:00
Girish Ramakrishnan 9b6bf719ff 1.7.8 changes 2017-11-09 09:40:26 -08:00
Girish Ramakrishnan 25431d3cc4 Fix the spacing 2017-11-09 09:29:42 -08:00
Girish Ramakrishnan e0805df3b1 Only show backup warning if using default location 2017-11-09 09:09:39 -08:00
Girish Ramakrishnan 8392fec570 Remove the bold 2017-11-08 20:57:40 -08:00
Girish Ramakrishnan 1c173ca83f Add UI to select users for access restriction 2017-11-08 20:54:38 -08:00
Girish Ramakrishnan 05a67db761 backup must be stored in ext4
Other file systems like FAT/CIFS can error with cryptic error messages
when saving filenames with special characters such as ':'
2017-11-08 12:26:25 -08:00
Girish Ramakrishnan bb24d5cf9e Order eventlog entries by time 2017-11-08 09:14:55 -08:00
Girish Ramakrishnan 8d2fbe931f Bump max limit to two times ram
part of #466
2017-11-07 10:07:05 -08:00
Girish Ramakrishnan 0a8adaac9f filter out empty usernames from groups
Fixes #472
2017-11-06 11:09:40 -08:00
Girish Ramakrishnan fa6d151325 Fix update mail templates 2017-11-02 21:34:03 -07:00
Girish Ramakrishnan a7296a0339 Rename filename to backupId in backup eventlog 2017-11-02 18:17:08 -07:00
Girish Ramakrishnan a6aee53ec2 Filter out failed backups 2017-11-02 18:13:51 -07:00
Girish Ramakrishnan 963ab2e791 More 1.7.7 changes 2017-11-02 16:30:13 -07:00
Girish Ramakrishnan ca724b8b03 Add cert renewal and user add/remove in weekly digest 2017-11-02 16:30:10 -07:00
Girish Ramakrishnan 88a929c85e Instead of appstore account, include owner alternate email 2017-11-02 15:10:05 -07:00
Girish Ramakrishnan 2bc0270880 1.7.7 changes 2017-11-02 12:18:51 -07:00
Girish Ramakrishnan 014b77b7aa Fix LE cert renewal failures
LE contacts the server by hostname and not by IP. This means that
when installing and reconfiguring the app it hits the default_server
route since nginx configs for the app are not generated at.

When doing in the daily cert renew, the nginx configs exist and we
are unable to renew the certs.
2017-11-02 11:43:43 -07:00
Girish Ramakrishnan 06f8aa8f29 Remove dead code
getNonApprovedCode code flow is ununsed (and broken by design on
the appstore side).
2017-11-02 10:36:30 -07:00
Girish Ramakrishnan a8c64bf9f7 Clarify heartbeat code
heartbeats are not sent for self-hosted cloudrons (only managed ones)
2017-11-02 10:26:21 -07:00
Girish Ramakrishnan 41ef16fbec link to memory limit docs 2017-11-01 09:25:05 -07:00
Girish Ramakrishnan 2a848a481b Add newline 2017-11-01 09:25:05 -07:00
Johannes Zellner 3963d76a80 The update dialog does not contain a form anymore
Fixes #467
2017-11-01 11:55:06 +01:00
Girish Ramakrishnan 8ede37a43d Make the dkim selector dynamic
it has to change with the adminLocation so that multiple cloudrons
can send out emails at the same time.
2017-10-31 12:18:40 -07:00
Girish Ramakrishnan 36534f6bb2 Fix indent 2017-10-31 12:12:02 -07:00
Girish Ramakrishnan 7eddcaf708 Allow setting app memory till memory limit
Fixes #466
2017-10-31 12:12:02 -07:00
Aleksandr Bogdanov 2cad93dfd2 Fixing UI to not require credentials be set (GCP use-case has no credentials field) 2017-10-31 12:33:15 +01:00
Aleksandr Bogdanov 9b1f8febf1 Fixing listDir to support batchSize = -1 for non-chunked listings. Also strings extrapolation fix (ES6) 2017-10-31 11:40:00 +01:00
Girish Ramakrishnan d8d2572aa1 Keep restarting mysql until it succeeds
MySQL restarts randomly fail on our CI systems. This is easily
reproducible:

root@smartserver:~# cp /tmp/mysql.cnf . && systemctl restart mysql && echo "Yes"
Yes
root@smartserver:~# cp /tmp/mysql.cnf . && systemctl restart mysql && echo "Yes"
Yes
root@smartserver:~# cp /tmp/mysql.cnf . && systemctl restart mysql && echo "Yes"
Job for mysql.service failed. See "systemctl status mysql.service" and "journalctl -xe" for details.

There also seems some apparmor issue:
[ 7389.111704] audit: type=1400 audit(1509404778.110:829): apparmor="DENIED" operation="open" profile="/usr/sbin/mysqld" name="/sys/devices/system/node/" pid=15618 comm="mysqld" requested_mask="r" denied_mask="r" fsuid=112 ouid=0

The apparmor issue is reported in https://bugs.launchpad.net/ubuntu/+source/mysql-5.7/+bug/1610765,
https://bugs.launchpad.net/ubuntu/+source/mysql-5.7/+bug/1658233 and
https://bugs.launchpad.net/ubuntu/+source/apparmor/+bug/1658239
2017-10-30 16:14:20 -07:00
Girish Ramakrishnan 96a98a74ac Move the mysql block
The e2e is failing sporadically with:

==> Changing ownership
==> Adding automated configs
mysql: [Warning] Using a password on the command line interface can be insecure.
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

Maybe the dhparam creation is doing something causing mysql to not respond.
2017-10-30 08:03:47 -07:00
Girish Ramakrishnan d0a244e392 stash adminLocation also 2017-10-29 19:09:03 -07:00
Girish Ramakrishnan f09c89e33f Remove confusing batchSize logic from listDir
This also fixes a bug in removeDir in DO spaces

thanks to @syn for reporting
2017-10-29 19:04:10 -07:00
Johannes Zellner d53f0679e5 Also stash the zoneName to settings 2017-10-29 22:40:15 +01:00
Girish Ramakrishnan 527093ebcb Stash the fqdn in the db for the next multi-domain release 2017-10-29 12:08:27 -07:00
Girish Ramakrishnan bd5835b866 send adminFqdn as well 2017-10-29 09:36:51 -07:00
Aleksandr Bogdanov 51ca1c7384 Refactoring gcs to match the new storage interface 2017-10-29 11:10:50 +01:00
Girish Ramakrishnan 6dd70c0ef2 acme challenges must be answered by default_server
The challenge must be answered even before app nginx config
is available.
2017-10-28 23:39:03 -07:00
Girish Ramakrishnan acc90e16d7 1.7.6 changes 2017-10-28 21:07:44 -07:00
Girish Ramakrishnan 4b3aca7413 Bump mail container for sogo disconnect fix 2017-10-28 20:58:26 -07:00
Johannes Zellner 8daee764d2 Only require gcdns form input to be valid if that provider is selected 2017-10-28 22:37:56 +02:00
Aleksandr Bogdanov 8d14832c6a Making gcdns credentials field optional in webadmin 2017-10-28 15:14:23 +02:00
Aleksandr Bogdanov 051d04890b Adding Google Cloud Storage support 2017-10-28 15:14:23 +02:00
Girish Ramakrishnan 3dedda32d4 Configure http server to only listen on known vhosts/IP
For the rest it returns 404

Fixes #446
2017-10-27 00:10:50 -07:00
Girish Ramakrishnan d127b25f0f Only set the custom https agent for HTTPS minio
Otherwise, we get a Cannot set property ‘agent’ of undefined error
2017-10-26 18:38:45 -07:00
Johannes Zellner 6a2b0eedb3 Add ldap pagination support 2017-10-27 01:25:07 +02:00
Girish Ramakrishnan 8c81a97a4b Check that the backup location has perms to create a directory
The backup itself runs as root and this works fine. But when rotating
the backup, the copy fails because it is unable to create a directory.
2017-10-26 11:41:34 -07:00
Girish Ramakrishnan d9ab1a78d5 Make the my location customizable
Fixes #22
2017-10-25 23:00:43 -07:00
Girish Ramakrishnan 593df8ed49 Do not use ADMIN_LOCATION in tests 2017-10-25 21:38:11 -07:00
Girish Ramakrishnan b30def3620 move prerelease check to appstore 2017-10-25 21:34:56 -07:00
Johannes Zellner 9c02785d49 Support ldap group compare
Fixes #463
2017-10-24 02:00:00 +02:00
Johannes Zellner f747343159 Cleanup unused port bindings after an update 2017-10-23 22:11:33 +02:00
Johannes Zellner 2971910ccf Do not accept port bindings on update route 2017-10-23 22:06:28 +02:00
Johannes Zellner 56534b9647 Add appdb.delPortBinding() 2017-10-23 22:05:43 +02:00
Johannes Zellner a8d26067ee Allow autoupdates if new ports are added
Those will simply be disabled after update and the user has to
enable them through the app configuration
2017-10-20 22:27:48 +02:00
Johannes Zellner 4212e4bb00 Do not show any port binding update ui 2017-10-20 22:27:48 +02:00
Johannes Zellner 7b27ace7bf Update cloudron-setup help url 2017-10-20 22:13:54 +02:00
Girish Ramakrishnan d8944da68d 1.7.5 changes 2017-10-19 12:19:10 -07:00
Girish Ramakrishnan 433d797cb7 Add SMTPS port for apps that require TLS connections for mail relay 2017-10-19 12:15:28 -07:00
Girish Ramakrishnan 0b1d940128 cloudscale -> cloudscale.ch 2017-10-19 07:28:07 -07:00
Johannes Zellner 6016024026 Move restore functions into appropriate scope object 2017-10-18 00:40:02 +02:00
Johannes Zellner e199293229 Further reduce ui flickering on restore 2017-10-18 00:40:02 +02:00
Girish Ramakrishnan 2ebe92fec3 Do not chown mail directory 2017-10-16 23:18:37 -07:00
Girish Ramakrishnan 628cf1e3de bump mail container for superfluous sa-update supervisor file 2017-10-16 21:16:58 -07:00
Girish Ramakrishnan 9e9aaf68f0 No need to migrate mail data anymore 2017-10-16 21:13:57 -07:00
Girish Ramakrishnan b595ca422c 1.7.4 changes 2017-10-16 15:28:36 -07:00
Girish Ramakrishnan 9273a6c726 Add option to disable hardlinks
We can probably remove this later based on the use
2017-10-16 15:22:40 -07:00
Johannes Zellner 76d00d4e65 Render changelog markdown as html in app update dialog 2017-10-17 00:07:58 +02:00
Johannes Zellner 668c03a11b Give visual feedback in the restore dialog when fetching backups 2017-10-16 22:31:49 +02:00
Girish Ramakrishnan 1e72d2d651 remove debugs (too noisy) 2017-10-16 12:34:09 -07:00
Girish Ramakrishnan 89fc8efc67 Save as empty array if find output is empty 2017-10-16 10:54:48 -07:00
Girish Ramakrishnan 241dbf160e Remove Unused required 2017-10-15 14:07:03 -07:00
Girish Ramakrishnan e46bdc2caa Force the copy just like tar --overwrite 2017-10-13 23:23:36 -07:00
Girish Ramakrishnan e1cb91ca76 bump mail container 2017-10-13 22:36:54 -07:00
Girish Ramakrishnan 709c742c46 Fix tests 2017-10-12 21:14:13 -07:00
Girish Ramakrishnan ecad9c499c Port binding conflict can never happen in update route 2017-10-12 21:04:38 -07:00
Girish Ramakrishnan ed0879ffcd Stop the app only after the backup completed
App backup can take a long time or possibly not work at all. For such
cases, do not stop the app or leave it in some errored state.

newConfigJson is the new config to be updated to. This ensures that
the db has correct app info during the update.
2017-10-12 18:10:41 -07:00
Girish Ramakrishnan 61e2878b08 save/restore exec bit in files
this covers the case where user might stash some executable files
that are used by plugins.
2017-10-12 16:18:11 -07:00
Girish Ramakrishnan d97034bfb2 Follow backup format for box backups as well 2017-10-12 11:02:52 -07:00
Girish Ramakrishnan 21942552d6 Clarify the per-app backup flag 2017-10-12 11:02:52 -07:00
Girish Ramakrishnan dd68c8f91f Various backup fixes 2017-10-12 11:02:48 -07:00
Girish Ramakrishnan 28ce5f41e3 handle errors in log stream 2017-10-11 12:55:56 -07:00
Girish Ramakrishnan 5694e676bd Set default rentention to a week 2017-10-11 12:55:55 -07:00
Girish Ramakrishnan db8c5a116f Typo 2017-10-11 10:30:03 -07:00
Girish Ramakrishnan fa39f0fbf3 Add 1.7.3 changes 2017-10-11 00:50:41 -07:00
Girish Ramakrishnan 1444bb038f only upload needs to be retried
copy/delete are already retried in the sdk code
2017-10-11 00:08:41 -07:00
Girish Ramakrishnan ac9e421ecf improved backup progress and logging 2017-10-10 22:49:38 -07:00
Girish Ramakrishnan b60cbe5a55 move constant 2017-10-10 19:47:21 -07:00
Girish Ramakrishnan 56d794745b Sprinkle retries in syncer logic 2017-10-10 14:25:03 -07:00
Girish Ramakrishnan fd3b73bea2 typo in format name 2017-10-10 13:54:54 -07:00
Girish Ramakrishnan 78807782df Various hacks for exoscale-sos
SOS does not like multipart uploads. They just fail randomly.

As a fix, we try to detect filesystem files and skip multipart uploads
for files < 5GB. For > 5GB, we do multipart upload anyways (mostly fails).

The box backup is switched to flat-file for exoscale for the reason
above.
2017-10-10 11:03:20 -07:00
Girish Ramakrishnan 754b29b263 Start out empty if the previous run errored 2017-10-09 20:12:21 -07:00
Girish Ramakrishnan 9f97f48634 Add note on s3ForcePathStyle 2017-10-09 18:46:14 -07:00
Girish Ramakrishnan 815e5d9d9a graphs: Compute width of system graph from total memory
Fixes #452
2017-10-09 14:58:32 -07:00
Girish Ramakrishnan 91ec2eaaf5 sos: "/" must separate bucket and key name 2017-10-09 11:50:22 -07:00
378 changed files with 30642 additions and 64932 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
# following files are skipped when exporting using git archive
test export-ignore
docs export-ignore
.jshintrc export-ignore
.gitlab export-ignore
.gitattributes export-ignore
.gitignore export-ignore
-1
View File
@@ -1,7 +1,6 @@
node_modules/
coverage/
webadmin/dist/
setup/splash/website/
installer/src/certs/server.key
# vim swap files
+6
View File
@@ -0,0 +1,6 @@
Please do not use this issue tracker for support requests and bug reports.
This issue tracker is used by the Cloudron development team to track actual
bugs in the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
+7
View File
@@ -0,0 +1,7 @@
Please do not use this issue tracker for support requests and feature reports.
This issue tracker is used by the Cloudron development team to track issues in
the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
+1
View File
@@ -2,6 +2,7 @@
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
+360
View File
@@ -1050,3 +1050,363 @@
* Fix crash in carbon which made graphs disappear on some Cloudrons
* Fix issue where OAuth SSO did not work when alternate domain was used
[1.7.3]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
* Fix issue where OAuth SSO did not work when alternate domain was used
[1.7.4]
* Add rsync format for backups. This feature allows incremental backups
* Add Google DNS backend (thanks @syn)
* Add DigitalOcean spaces backup storage backend
* Add Cloudscale and Exoscale as supported VPS providers
* Display backup progress and status in the web interface
* Preliminary IPv6 support
* Add IP RBL status to web interface
* Add auto-update pattern `Every wednesday night`
* Update Haraka to 2.8.15. This fixes the issue where emails were bounced with the message 'Send MAIL FROM first'
* Do not overwrite existing subdomain when app's location is changed
* Add button to send test email
* Fix crash in carbon which made graphs disappear on some Cloudrons
* Fix issue where OAuth SSO did not work when alternate domain was used
* Changelog is now rendered in markdown format
[1.7.5]
* Expose a TLS relay port from mail container for Go applications
[1.7.6]
* Port bindings cannot be configured in update route anymore
* Implement LDAP group compare
* Pre-releases are now offered by appstore and not handled in box code anymore
* LDAP pagination support. This will fix the warnings in NextCloud and Rocket.Chat
* Check if directories can be created in the backup directory
* Do not set the HTTPS agent when using HTTP with minio backup backend
* Fix regression where a new domain config could not be set in the UI
* New mail container release that fixes email sending with SOGo
* Show 404 page for unknown domains
[1.7.7]
* Allow setting app memory till memory limit
* Make the dkim selector dynamic
* Fix issue where app update dialog did not close
* Fix LE cert renewal failures
* Send user and cert info in digest emails
* Send oom, app failures and other important mails to cloudron owner's alt mail
[1.8.0]
* Fix group email bounce when a group has users that have not signed up yet
* Do not restrict app memory limit to 4GB
* Fix display of the latest backup in the weekly digest
* Add UI to select users for access restriction
* Update docker to 17.09
* Update node to 6.11.5
* Display package version of installed apps in the info dialog
[1.8.1]
* Update node modules
* Allow a restore operation if app is already restoring
* Remove pre-install bundle support since it was hardly used
* Make the test email mail address configurable
* Allow admins to access all apps
* Send feedback via appstore API (instead of email)
* Show documentation URL in the app info dialog
* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)
[1.8.2]
* Update node modules
* Allow a restore operation if app is already restoring
* Remove pre-install bundle support since it was hardly used
* Make the test email mail address configurable
* Allow admins to access all apps
* Send feedback via appstore API (instead of email)
* Show documentation URL in the app info dialog
* Update Lets Encrypt agrement URL (https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf)
[1.8.3]
* Ensure domain database record exists
[1.8.4]
* Fix issue where internal email was not delivered when email relay is enabled
* Fix display of DNS records when email relay is enabled
[1.8.5]
* Fix issues where unused addons were not cleaned on an app update causing uninstall to fail
* Change UI text from 'Waiting' to 'Pending'
[1.9.0]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
[1.9.1]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.2]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.3]
* Prepare Cloudron for supporting multiple domains
* Add Cloudron restore UI
* Do not put app in errored state if backup fails
* Display backup progress in CaaS
* Add Google Cloud Storage backend for backups
* Update node to 8.9.3 LTS
* Set max email recepient limit (in outgoing emails) to 500
* Put terminal and app logs viewer to separate window
[1.9.4]
* Fix typo causing LE cert renewals to fail
[1.10.0]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.1]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.10.2]
* Migrate mailboxes to support multiple domains
* Update addon containers to latest versions
* Add DigitalOcean Spaces region Singapore 1 (SGP1)
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.11.0]
* Update Haraka to 2.8.17 to fix various crashes
* Report dependency error for clone if backup or domain was not found
* Enable auto-updates for major versions
[2.0.0]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
[2.0.1]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
* Rework the eventlog view
* App clone now clones the robotsTxt and backup settings
[2.1.0]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
[2.1.1]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
[2.2.0]
* Add 2FA support for the admin dashboard
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
[2.2.1]
* Add 2FA support for the admin dashboard
* Add Gandi & GoDaddy DNS providers
* Fix zone detection logic on Route53 accounts with more than 100 zones
* Warn using when disabling email
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
* Fix issue where mail container incorrectly advertised CRAM-MD5 support
[2.3.0]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
[2.3.1]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
* Allow mailbox name to be set for apps
* Rework the Email server UI
* Add the ability to manually trigger a backup of an application
* Enable/disable mail from validation within UI
* Allow setting app visibility for non-SSO apps
* Add Clone UI
[2.3.2]
* Fix issue where multi-db apps were not provisioned correctly
* Improve setup, restore views to have field labels
[2.4.0]
* Use custom logging backend to have more control over log rotation
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[2.4.1]
* Use custom logging backend to have more control over log rotation
* Mail logs and box logs UI
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[3.0.0]
* Support alternate app domains with redirects
* Allow hyphen in mailbox names
* Fix issue where the UI timesout when relay server is not reachable
* Add support for personal spaces
* Add UI to edit users in the groups dialog
* Add UI to set groups when creating a user
* Open logs and terminal in a new tab instead of a window
* Add button to view backup logs
* Add Mailjet mail relay support
* Encryption support for incremental backups
* Display restore errors in the UI
* Update Haraka to 2.8.19
* GPG verify releases
* Allow subdomains in location field
[3.0.1]
* Support alternate app domains with redirects
* Allow hyphen in mailbox names
* Fix issue where the UI timesout when relay server is not reachable
* Add support for personal spaces
* Add UI to edit users in the groups dialog
* Add UI to set groups when creating a user
* Open logs and terminal in a new tab instead of a window
* Add button to view backup logs
* Add Mailjet mail relay support
* Encryption support for incremental backups
* Display restore errors in the UI
* Update Haraka to 2.8.19
* GPG verify releases
* Allow subdomains in location field
[3.0.2]
* Fix issue where normal users are shown apps they don't have access to
* 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
[3.2.1]
* 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
[3.2.2]
* Update Haraka to 2.8.20
* (mail) Fix issue where LDAP connections where not cleaned up
[3.2.3]
* Fix crash in restore logic caused by app health monitor
+1 -1
View File
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016 Cloudron UG
Copyright (C) 2016,2017,2018 Cloudron UG
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
+9 -23
View File
@@ -9,10 +9,6 @@ a complex task.
We are building the ultimate platform for self-hosting web apps. The Cloudron allows
anyone to effortlessly host web applications on their server on their own terms.
Support us on
[![Flattr Cloudron](https://button.flattr.com/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=cloudron&url=https://cloudron.io&title=Cloudron&tags=opensource&category=software)
or [pay us a coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=8982CKNM46D8U)
## Features
* Single click install for apps. Check out the [App Store](https://cloudron.io/appstore.html).
@@ -33,9 +29,9 @@ or [pay us a coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_
* Trivially migrate to another server keeping your apps and data (for example, switch your
infrastructure provider or move to a bigger server).
* Comprehensive [REST API](https://cloudron.io/references/api.html).
* Comprehensive [REST API](https://cloudron.io/documentation/developer/api/).
* [CLI](https://git.cloudron.io/cloudron/cloudron-cli) to configure apps.
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
* Alerts, audit logs, graphs, dns management ... and much more
@@ -49,35 +45,25 @@ You can install the Cloudron platform on your own server or get a managed server
from cloudron.io. In either case, the Cloudron platform will keep your server and
apps up-to-date and secure.
* [Selfhosting](https://cloudron.io/references/selfhosting.html) - [Pricing](https://cloudron.io/pricing.html)
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.html)
The wiki has instructions on how you can install and update the Cloudron and the
apps from source.
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
## Documentation
* [User manual](https://cloudron.io/references/usermanual.html)
* [Developer docs](https://cloudron.io/documentation.html)
* [Architecture](https://cloudron.io/references/architecture.html)
* [Documentation](https://cloudron.io/documentation/)
## Related repos
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
the containers in the Cloudron.
The [graphite repo](https://git.cloudron.io/cloudron/docker-graphite) contains the graphite code
that collects metrics for graphs.
The addons are located in separate repositories
* [Redis](https://git.cloudron.io/cloudron/redis-addon)
* [Postgresql](https://git.cloudron.io/cloudron/postgresql-addon)
* [MySQL](https://git.cloudron.io/cloudron/mysql-addon)
* [Mongodb](https://git.cloudron.io/cloudron/mongodb-addon)
* [Mail](https://git.cloudron.io/cloudron/mail-addon)
## Community
* [Chat](https://chat.cloudron.io/)
* [Forum](https://forum.cloudron.io/)
* [Support](mailto:support@cloudron.io)
+1 -1
View File
@@ -29,7 +29,7 @@ function create_droplet() {
local ssh_key_id="$1"
local box_name="$2"
local image_region="sfo1"
local image_region="sfo2"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="1gb"
+15 -7
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"
@@ -47,10 +50,10 @@ apt-get -y install \
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
echo "==> Installing node.js"
mkdir -p /usr/local/node-6.11.3
curl -sL https://nodejs.org/dist/v6.11.3/node-v6.11.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.3
ln -sf /usr/local/node-6.11.3/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.3/bin/npm /usr/bin/npm
mkdir -p /usr/local/node-8.9.3
curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
@@ -61,7 +64,7 @@ echo "==> Installing Docker"
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.03.1~ce-0~ubuntu-xenial_amd64.deb -o /tmp/docker.deb
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/docker.deb
rm /tmp/docker.deb
@@ -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
@@ -105,3 +109,7 @@ systemctl disable bind9 || true
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
+8 -3
View File
@@ -9,10 +9,10 @@ require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
let 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 +25,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,7 +35,7 @@ console.log();
async.series([
server.start,
ldap.start,
appHealthMonitor.start,
dockerProxy.start
], function (error) {
if (error) {
console.error('Error starting server', error);
@@ -46,11 +49,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);
});
+8 -1
View File
@@ -2,6 +2,8 @@
'use strict';
var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
function main() {
@@ -10,7 +12,12 @@ function main() {
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
sendFailureLogs(processName, { unit: processName });
// mailer needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
sendFailureLogs(processName, { unit: processName });
});
}
main();
-204
View File
@@ -1,204 +0,0 @@
/* jslint node:true */
'use strict';
var argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
del = require('del'),
ejs = require('gulp-ejs'),
gulp = require('gulp'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify'),
url = require('url');
gulp.task('3rdparty', function () {
gulp.src([
'webadmin/src/3rdparty/**/*.js',
'webadmin/src/3rdparty/**/*.map',
'webadmin/src/3rdparty/**/*.css',
'webadmin/src/3rdparty/**/*.otf',
'webadmin/src/3rdparty/**/*.eot',
'webadmin/src/3rdparty/**/*.svg',
'webadmin/src/3rdparty/**/*.gif',
'webadmin/src/3rdparty/**/*.ttf',
'webadmin/src/3rdparty/**/*.woff',
'webadmin/src/3rdparty/**/*.woff2'
])
.pipe(gulp.dest('webadmin/dist/3rdparty/'))
.pipe(gulp.dest('setup/splash/website/3rdparty'));
gulp.src('node_modules/bootstrap-sass/assets/javascripts/bootstrap.min.js')
.pipe(gulp.dest('webadmin/dist/3rdparty/js'))
.pipe(gulp.dest('setup/splash/website/3rdparty/js'));
});
// --------------
// JavaScript
// --------------
if (argv.help || argv.h) {
console.log('Supported arguments for "gulp develop":');
console.log(' --client-id <clientId>');
console.log(' --client-secret <clientSecret>');
console.log(' --api-origin <cloudron api uri>');
process.exit(1);
}
gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-update'], function () {});
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
clientSecret: argv.clientSecret || 'unused',
apiOrigin: argv.apiOrigin || '',
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
};
console.log();
console.log('Using OAuth credentials:');
console.log(' ClientId: %s', oauth.clientId);
console.log(' ClientSecret: %s', oauth.clientSecret);
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
console.log();
gulp.task('js-index', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src([
'webadmin/src/js/index.js',
'webadmin/src/js/client.js',
'webadmin/src/js/appstore.js',
'webadmin/src/js/main.js',
'webadmin/src/views/*.js'
])
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('index.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setup', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setup.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-setupdns', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('setupdns.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-update', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/update.js'])
.pipe(sourcemaps.init())
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'))
.pipe(gulp.dest('setup/splash/website/js'));
});
// --------------
// HTML
// --------------
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
});
gulp.task('html-update', function () {
return gulp.src(['webadmin/src/update.html']).pipe(gulp.dest('setup/splash/website'));
});
gulp.task('html-views', function () {
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
});
gulp.task('html-templates', function () {
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
});
// --------------
// CSS
// --------------
gulp.task('css', function () {
return gulp.src('webadmin/src/*.scss')
.pipe(sourcemaps.init())
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
.pipe(autoprefixer())
.pipe(cssnano())
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist'))
.pipe(gulp.dest('setup/splash/website'));
});
gulp.task('images', function () {
return gulp.src('webadmin/src/img/**')
.pipe(gulp.dest('webadmin/dist/img'));
});
// --------------
// Utilities
// --------------
gulp.task('watch', ['default'], function () {
gulp.watch(['webadmin/src/*.scss'], ['css']);
gulp.watch(['webadmin/src/img/*'], ['images']);
gulp.watch(['webadmin/src/**/*.html'], ['html']);
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
gulp.watch(['webadmin/src/js/setupdns.js', 'webadmin/src/js/client.js'], ['js-setupdns']);
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
});
gulp.task('clean', function () {
del.sync(['webadmin/dist', 'setup/splash/website']);
});
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
gulp.task('develop', ['watch'], serve({ root: 'webadmin/dist', port: 4000 }));
-32
View File
@@ -1,32 +0,0 @@
#!/usr/bin/env node
'use strict';
var tar = require('tar-fs'),
fs = require('fs'),
path = require('path'),
zlib = require('zlib');
if (process.argv.length < 4) {
console.error('Usage: tarjs <cwd> <dir>');
process.exit(1);
}
var dir = process.argv[3];
var cwd = process.argv[2];
console.error('Packing directory "'+ dir +'" from within "' + cwd + '" and stream to stdout');
process.chdir(cwd);
var stat = fs.statSync(dir);
if (!stat.isDirectory()) throw(dir + ' is not a directory');
var gzipStream = zlib.createGzip({});
tar.pack(path.resolve(dir), {
ignore: function (name) {
if (name === '.') return true;
return false;
}
}).pipe(gzipStream).pipe(process.stdout);
@@ -2,7 +2,7 @@
var async = require('async');
var ADMIN_GROUP_ID = 'admin'; // see groups.js
var ADMIN_GROUP_ID = 'admin'; // see constants.js
exports.up = function(db, callback) {
async.series([
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN newConfigJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN newConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,40 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN manifestJson TEXT'),
db.runSql.bind(db, 'START TRANSACTION;'),
// fill all the backups with restoreConfigs from current apps
function addManifests(callback) {
console.log('Importing manifests');
db.all('SELECT * FROM backups WHERE type="app"', function (error, backups) {
if (error) return callback(error);
async.eachSeries(backups, function (backup, next) {
var m = backup.restoreConfigJson ? JSON.parse(backup.restoreConfigJson) : null;
if (m) m = JSON.stringify(m.manifest);
db.runSql('UPDATE backups SET manifestJson=? WHERE id=?', [ m, backup.id ], next);
}, callback);
});
},
db.runSql.bind(db, 'COMMIT'),
// remove the restoreConfig
db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN restoreConfigJson')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE backups DROP COLUMN manifestJson'),
db.runSql.bind(db, 'ALTER TABLE backups ADD COLUMN restoreConfigJson TEXT'),
], callback);
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE newConfigJson updateConfigJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE updateConfigJson newConfigJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE lastBackupId restoreConfigJson TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE restoreConfigJson lastBackupId TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,31 @@
'use strict';
// WARNING!!
// At this point the default db collation is utf8mb4_unicode_ci however we already have foreign key constraits
// already with tables on utf8_bin charset, so we cannot convert all tables here to utf8mb4 collation without
// a reimport from a sql dump, as foreign keys across different collations are not supported
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE apps CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE authcodes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE backups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE clients CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE eventlog CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groupMembers CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE migrations CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE settings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE tokens CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE users CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin')
], callback);
};
exports.down = function(db, callback) {
// nothing to be done here
callback();
};
@@ -0,0 +1,70 @@
'use strict';
var async = require('async'),
safe = require('safetydance');
exports.up = function(db, callback) {
// first check precondtion of domain entry in settings
db.all('SELECT * FROM settings WHERE name = ?', [ 'domain' ], function (error, result) {
if (error) return callback(error);
var domain = {};
if (result[0]) domain = safe.JSON.parse(result[0].value) || {};
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function addAppsDomainColumn(done) {
db.runSql('ALTER TABLE apps ADD COLUMN domain VARCHAR(128)', [], done);
},
function setAppDomain(done) {
if (!domain.fqdn) return done(); // skip for new cloudrons without a domain
db.runSql('UPDATE apps SET domain = ?', [ domain.fqdn ], done);
},
function addAppsLocationDomainUniqueConstraint(done) {
db.runSql('ALTER TABLE apps ADD UNIQUE location_domain_unique_index (location, domain)', [], done);
},
function removePresetupAdminGroupIfNew(done) {
// do not delete on update, will update the record in setMailboxesDomain()
if (domain.fqdn) return done();
// this will be finally created once we have a domain when we create the owner in user.js
const ADMIN_GROUP_ID = 'admin'; // see constants.js
db.runSql('DELETE FROM groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
if (error) return done(error);
db.runSql('DELETE FROM mailboxes WHERE ownerId = ?', [ ADMIN_GROUP_ID ], done);
});
},
function addMailboxesDomainColumn(done) {
db.runSql('ALTER TABLE mailboxes ADD COLUMN domain VARCHAR(128)', [], done);
},
function setMailboxesDomain(done) {
if (!domain.fqdn) return done(); // skip for new cloudrons without a domain
db.runSql('UPDATE mailboxes SET domain = ?', [ domain.fqdn ], done);
},
function dropAppsLocationUniqueConstraint(done) {
db.runSql('ALTER TABLE apps DROP INDEX location', [], done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function dropMailboxesDomainColumn(done) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN domain', [], done);
},
function dropLocationDomainUniqueConstraint(done) {
db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index', [], done);
},
function dropAppsDomainColumn(done) {
db.runSql('ALTER TABLE apps DROP COLUMN domain', [], done);
},
function addAppsLocationUniqueConstraint(done) {
db.runSql('ALTER TABLE apps ADD UNIQUE location (location)', [], done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
@@ -0,0 +1,61 @@
'use strict';
var async = require('async'),
safe = require('safetydance'),
tld = require('tldjs');
exports.up = function(db, callback) {
var fqdn, zoneName, configJson;
async.series([
function gatherDomain(done) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'domain' ], function (error, result) {
if (error) return done(error);
var domain = {};
if (result[0]) domain = safe.JSON.parse(result[0].value) || {};
fqdn = domain.fqdn || ''; // will be null pre-setup
zoneName = domain.zoneName || tld.getDomain(fqdn) || fqdn;
done();
});
},
function gatherDNSConfig(done) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'dns_config' ], function (error, result) {
if (error) return done(error);
configJson = (result[0] && result[0].value) ? result[0].value : JSON.stringify({ provider: 'manual'});
// caas dns config needs an fqdn
var config = JSON.parse(configJson);
if (config.provider === 'caas') config.fqdn = fqdn;
configJson = JSON.stringify(config);
done();
});
},
db.runSql.bind(db, 'START TRANSACTION;'),
function createDomainsTable(done) {
var cmd = `
CREATE TABLE domains(
domain VARCHAR(128) NOT NULL UNIQUE,
zoneName VARCHAR(128) NOT NULL,
configJson TEXT,
PRIMARY KEY (domain)) CHARACTER SET utf8 COLLATE utf8_bin
`;
db.runSql(cmd, [], done);
},
function addInitialDomain(done) {
if (!fqdn) return done();
db.runSql('INSERT INTO domains (domain, zoneName, configJson) VALUES (?, ?, ?)', [ fqdn, zoneName, configJson ], done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE domains', callback);
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP FOREIGN KEY apps_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP PRIMARY KEY', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD PRIMARY KEY(name)', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD UNIQUE mailboxes_name_domain_unique_index (name, domain)', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP INDEX mailboxes_name_domain_unique_index', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN updateTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE createdAt creationTime TIMESTAMP(2) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE creationTime createdAt TIMESTAMP(2) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
// NOTE: This migration is incorrect because 'caas' domain is not guaranteed to be present in all Caas cloudrons
exports.up = function(db, callback) {
db.all('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
var caasDomains = domains.filter(function (d) { return JSON.parse(d.configJson).provider === 'caas'; });
if (caasDomains.length === 0) return callback();
var caasDomain = caasDomains[0].domain;
db.all('SELECT * FROM settings WHERE name=?', [ 'backup_config' ], function (error, settings) {
if (error) return callback(error);
var setting = settings[0];
var config = JSON.parse(setting.value);
config.fqdn = caasDomain;
db.runSql('UPDATE settings SET value=? WHERE name=?', [ JSON.stringify(config), setting.name ], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,23 @@
'use strict';
exports.up = function(db, callback) {
var backupConfig = {
"provider": "filesystem",
"backupFolder": "/var/backups",
"format": "tgz",
"retentionSecs": 172800
};
db.runSql('INSERT settings (name, value) VALUES(?, ?)', [ 'backup_config', JSON.stringify(backupConfig) ], function (error) {
if (!error || error.code === 'ER_DUP_ENTRY') return callback(); // dup entry is OK for existing cloudrons
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DELETE FROM settings WHERE name=?', ['backup_config'], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,33 @@
'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);
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN provider VARCHAR(16) DEFAULT ""'),
function setProvider(done) {
async.eachSeries(domains, function (domain, iteratorCallback) {
var config = JSON.parse(domain.configJson);
var provider = config.provider;
delete config.provider;
db.runSql('UPDATE domains SET provider = ?, configJson = ? WHERE domain = ?', [ provider, JSON.stringify(config), domain.domain ], iteratorCallback);
}, done);
},
db.runSql.bind(db, 'ALTER TABLE domains MODIFY provider VARCHAR(16) NOT NULL'),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN provider', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,24 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS mail(' +
'domain VARCHAR(128) NOT NULL UNIQUE,' +
'enabled BOOLEAN DEFAULT 0,' +
'mailFromValidation BOOLEAN DEFAULT 1,' +
'catchAllJson TEXT,' +
'relayJson TEXT,' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'PRIMARY KEY(domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE mail', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,34 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback();
db.all('SELECT * FROM settings', function (error, allSettings) {
if (error) return callback(error);
// defaults
var mailFromValidation = true;
var catchAll = [ ];
var relay = { provider: 'cloudron-smtp' };
var mailEnabled = false;
allSettings.forEach(function (setting) {
switch (setting.name) {
case 'mail_from_validation': mailFromValidation = !!setting.value; break;
case 'catch_all_address': catchAll = JSON.parse(setting.value); break;
case 'mail_relay': relay = JSON.parse(setting.value); break;
case 'mail_config': mailEnabled = JSON.parse(setting.value).enabled; break;
}
});
db.runSql('INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)',
[ domains[0].domain, mailEnabled, mailFromValidation, JSON.stringify(catchAll), JSON.stringify(relay) ], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,44 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM users', [ ], function (error, users) {
if (error) return callback(error);
db.all('SELECT * FROM mail WHERE enabled=1', [ ], function (error, mailDomains) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE users DROP INDEX users_email'),
db.runSql.bind(db, 'ALTER TABLE users ADD COLUMN fallbackEmail VARCHAR(512) DEFAULT ""'),
function setDefaults(done) {
async.eachSeries(users, function (user, iteratorCallback) {
var defaultEmail = '';
var fallbackEmail = '';
if (mailDomains.length === 0) {
defaultEmail = user.email;
fallbackEmail = user.email;
} else {
defaultEmail = user.username ? (user.username + '@' + mailDomains[0].domain) : user.email;
fallbackEmail = user.email;
}
db.runSql('UPDATE users SET email = ?, fallbackEmail = ? WHERE id = ?', [ defaultEmail, fallbackEmail, user.id ], iteratorCallback);
}, done);
},
db.runSql.bind(db, 'ALTER TABLE users ADD UNIQUE users_email (email)'),
db.runSql.bind(db, 'COMMIT')
], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN fallbackEmail', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,26 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'tls_config' ], function (error, result) {
if (error) return callback(error);
var tlsConfig = (result[0] && result[0].value) ? JSON.parse(result[0].value) : { provider: 'letsencrypt-prod'};
tlsConfig.provider = tlsConfig.provider.replace(/$le\-/, 'letsencrypt-'); // old cloudrons had le-prod/le-staging
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE domains ADD COLUMN tlsConfigJson TEXT'),
db.runSql.bind(db, 'UPDATE domains SET tlsConfigJson = ?', [ JSON.stringify(tlsConfig) ]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN tlsConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,47 @@
'use strict';
var async = require('async'),
fs = require('fs'),
superagent = require('superagent');
exports.up = function(db, callback) {
if (!fs.existsSync('/home/yellowtent/configs/cloudron.conf')) {
console.log('Unable to locate cloudron.conf');
return callback();
}
var config = JSON.parse(fs.readFileSync('/home/yellowtent/configs/cloudron.conf', 'utf8'));
if (config.provider !== 'caas' || !config.fqdn) {
console.log('Not caas (%s) or no fqdn', config.provider, config.fqdn);
return callback();
}
db.runSql('SELECT COUNT(*) AS total FROM users', function (error, result) {
if (error) return callback(error);
if (result[0].total === 0) {
console.log('This cloudron is not activated. It will automatically get appstore and caas configs from autoprovision logic');
return callback();
}
console.log('Downloading appstore and caas config');
superagent.get(config.apiServerOrigin + `/api/v1/boxes/${config.fqdn}/config`)
.query({ token: config.token })
.timeout(30 * 1000).end(function (error, result) {
if (error) return callback(error);
console.log('Adding %j config', result.body);
async.series([
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'appstore_config', JSON.stringify(result.body.appstoreConfig) ]),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'caas_config', JSON.stringify(result.body.caasConfig) ])
], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,24 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
// migrate the 'daily' update pattern
var appUpdatePattern = results[0].value;
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,121 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, callback) {
if (!app.altDomain) {
console.log('App %s does not use altDomain, skip', app.id);
return callback();
}
const domain = tldjs.getDomain(app.altDomain);
const subdomain = tldjs.getSubdomain(app.altDomain);
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
async.series([
// Add domain if not exists
function (callback) {
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s', domain);
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
// WARNING this will only work on the cloudron itself not during local testing!
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
safe.fs.unlinkSync(configFile);
}
callback();
});
},
// Add domain to mail table if not exists
function (callback) {
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s to mail table', domain);
callback();
});
},
// Remove old mailbox record if any
function (callback) {
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
const args = [ app.id ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Cleaned up mailbox record for app %s', app.id);
callback();
});
},
// Add new mailbox record
function (callback) {
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Added mailbox record for app %s', app.id);
callback();
});
},
// Update app record
function (callback) {
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
const args = [ subdomain, domain, '', app.id ];
db.runSql(query, args, function (error) {
if (error) return error;
console.log('Updated app %s with new domain', app.id);
callback();
});
}
], callback);
}, function (error) {
if (error) return callback(error);
// finally drop the altDomain db field
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
};
@@ -0,0 +1,19 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,51 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
var users = { }, groupMembers = { };
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN membersJson TEXT'),
function getUsers(done) {
db.all('SELECT * from users', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) { users[result.id] = result; });
done();
});
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) {
var userIds = result.userIds ? result.userIds.split(',') : [];
var members = userIds.map(function (id) { return users[id].username; });
groupMembers[result.id] = members;
});
done();
});
},
function removeGroupIdAndSetMembers(done) {
async.eachSeries(Object.keys(groupMembers), function (gid, iteratorDone) {
console.log(`Migrating group id ${gid} to ${JSON.stringify(groupMembers[gid])}`);
db.runSql('UPDATE mailboxes SET membersJson = ?, ownerId = ? WHERE ownerId = ?', [ JSON.stringify(groupMembers[gid]), 'admin', gid ], iteratorDone);
}, done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,34 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN type VARCHAR(16)'),
function addMailboxType(done) {
db.all('SELECT * from mailboxes', [ ], function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (mailbox, iteratorCallback) {
let type = 'mailbox';
if (mailbox.aliasTarget) {
type = 'alias';
} else if (mailbox.membersJson) {
type = 'list';
}
db.runSql('UPDATE mailboxes SET type = ? WHERE name = ? AND domain = ?', [ type, mailbox.name, mailbox.domain ], iteratorCallback);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY type VARCHAR(16) NOT NULL'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "", ADD COLUMN twoFactorAuthenticationEnabled BOOLEAN DEFAULT false', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP twoFactorAuthenticationSecret, DROP twoFactorAuthenticationEnabled', function (error) {
if (error) console.error(error);
callback(error);
});
};
+21
View File
@@ -0,0 +1,21 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE clients SET scope=? WHERE id=? OR id=? OR id=?', ['*', 'cid-webadmin', 'cid-sdk', 'cid-cli'], function (error) {
if (error) console.error(error);
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) { // remove the roleSdk
if (error) console.error(error);
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
if (error) console.error(error);
callback(error);
});
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN ownerId VARCHAR(128)'),
function (next) {
db.all('SELECT id FROM users ORDER BY createdAt LIMIT 1', [ ], function (error, results) {
if (error || results.length === 0) return next(error);
var ownerId = results[0].id;
db.runSql('UPDATE apps SET ownerId=?', [ ownerId ], next);
});
},
db.runSql.bind(db, 'ALTER TABLE apps MODIFY ownerId VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_owner_constraint FOREIGN KEY(ownerId) REFERENCES users(id)'),
db.runSql.bind(db, 'COMMIT'),
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN ownerId', function (error) {
if (error) console.error(error);
callback(error);
});
};
+15
View File
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN ts ', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,25 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS subdomains(' +
'appId VARCHAR(128) NOT NULL,' +
'domain VARCHAR(128) NOT NULL,' +
'subdomain VARCHAR(128) NOT NULL,' +
'type VARCHAR(128) NOT NULL,' +
'dnsRecordId VARCHAR(512),' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'FOREIGN KEY(appId) REFERENCES apps(id),' +
'UNIQUE (subdomain, domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE subdomains', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from apps', [ ], function (error, results) {
if (error) return callback(error);
var queries = [
db.runSql.bind(db, 'START TRANSACTION;')
];
results.forEach(function (app) {
queries.push(db.runSql.bind(db, 'INSERT INTO subdomains (appId, domain, subdomain, type, dnsRecordId) VALUES (?, ?, ?, ?, ?)', [ app.id, app.domain, app.location, 'primary', app.dnsRecordId ]));
});
queries.push(db.runSql.bind(db, 'COMMIT'));
async.series(queries, callback);
});
};
exports.down = function(db, callback) {
db.runSql('DELETE FROM subdomains', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,41 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index, DROP FOREIGN KEY apps_domain_constraint, DROP COLUMN domain, DROP COLUMN location, DROP COLUMN dnsRecordId', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.all('SELECT * from subdomains WHERE type = ?', [ 'primary' ], function (error, results) {
if (error) return callback(error);
var cmd = 'ALTER TABLE apps'
+ ' ADD COLUMN location VARCHAR(128),'
+ ' ADD COLUMN domain VARCHAR(128),'
+ ' ADD COLUMN dnsRecordId VARCHAR(512)';
db.runSql(cmd, function (error) {
if (error) return callback(error);
var queries = [ db.runSql.bind(db, 'START TRANSACTION;') ];
results.forEach(function (d) {
queries.push(db.runSql.bind(db, 'UPDATE apps SET domain = ?, location = ?, dnsRecordId = ? WHERE id = ?', [ d.domain, d.subdomain, d.appId, d.dnsRecordId ]));
});
queries.push(db.runSql.bind(db, 'COMMIT'));
async.series(queries, function (error) {
if (error) return callback(error);
var cmd = 'ALTER TABLE apps'
+ ' ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain),'
+ ' ADD UNIQUE location_domain_unique_index (location, domain)';
db.runSql(cmd, callback);
});
});
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE subdomains DROP COLUMN dnsRecordId', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE subdomains ADD COLUMN dnsRecordId VARCHAR(512)', function (error) {
if (error) return callback(error);
callback();
});
};
@@ -0,0 +1,34 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN admin BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
db.all('SELECT userId FROM groupMembers WHERE groupId=?', [ 'admin' ], function (error, results) {
if (error) return callback(error);
if (results.length === 0) return callback();
async.eachSeries(results, function (result, iteratorDone) {
db.runSql('UPDATE users SET admin=1 WHERE id=?', [ result.userId ], iteratorDone);
}, function (error) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
], callback);
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN admin', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,13 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -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();
};
+70 -13
View File
@@ -9,6 +9,10 @@
#### BLOB - stored offline from table row (use for binary data)
#### https://dev.mysql.com/doc/refman/5.0/en/storage-requirements.html
# The code uses zero dates. Make sure sql_mode does NOT have NO_ZERO_DATE
# http://johnemb.blogspot.com/2014/09/adding-or-removing-individual-sql-modes.html
# SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE',''));
CREATE TABLE IF NOT EXISTS users(
id VARCHAR(128) NOT NULL UNIQUE,
username VARCHAR(254) UNIQUE,
@@ -17,8 +21,12 @@ CREATE TABLE IF NOT EXISTS users(
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
admin BOOLEAN DEFAULT false,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
@@ -33,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),
@@ -42,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,
@@ -59,26 +68,32 @@ CREATE TABLE IF NOT EXISTS apps(
containerId VARCHAR(128),
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
memoryLimit BIGINT DEFAULT 0,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
robotsTxt TEXT,
enableBackup BOOLEAN DEFAULT 1,
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
// the following fields do not belong here, they can be removed when we use a queue for apptask
lastBackupId VARCHAR(128), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
ownerId VARCHAR(128),
FOREIGN KEY(ownerId) REFERENCES users(id),
PRIMARY KEY(id));
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),
@@ -110,7 +125,8 @@ CREATE TABLE IF NOT EXISTS backups(
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
restoreConfigJson TEXT, /* JSON including the manifest of the backed up app */
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
format VARCHAR(16) DEFAULT "tgz",
PRIMARY KEY (id));
@@ -119,19 +135,60 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data TEXT, /* free flowing json based on action */
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
createdAt TIMESTAMP(2) NOT NULL,
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS domains(
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
tlsConfigJson TEXT, /* JSON containing the tls provider config */
PRIMARY KEY (domain))
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE IF NOT EXISTS mail(
domain VARCHAR(128) NOT NULL UNIQUE,
enabled BOOLEAN DEFAULT 0, /* MDA enabled */
mailFromValidation BOOLEAN DEFAULT 1,
catchAllJson TEXT,
relayJson TEXT,
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(domain))
CHARACTER SET utf8 COLLATE utf8_bin;
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP,
domain VARCHAR(128),
PRIMARY KEY (name));
FOREIGN KEY(domain) REFERENCES mail(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS subdomains(
appId VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL,
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, domain))
CHARACTER SET utf8 COLLATE utf8_bin;
-5487
View File
File diff suppressed because it is too large Load Diff
+8976
View File
File diff suppressed because it is too large Load Diff
+53 -63
View File
@@ -14,98 +14,88 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.6.2",
"@google-cloud/dns": "^0.7.2",
"@google-cloud/storage": "^1.7.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.5.0",
"aws-sdk": "^2.97.0",
"body-parser": "^1.17.2",
"cloudron-manifestformat": "^2.9.0",
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.13.1",
"connect": "^3.6.6",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^0.1.0",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.3.5",
"cookie-session": "^1.1.0",
"cron": "^1.0.9",
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.0-beta.20",
"db-migrate": "^0.11.1",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.0.0",
"dockerode": "^2.4.3",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.15.4",
"express-session": "^1.15.5",
"gulp-sass": "^3.0.0",
"hat": "0.0.3",
"hock": "https://registry.npmjs.org/hock/-/hock-1.3.2.tgz",
"debug": "^3.1.0",
"dockerode": "^2.5.5",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.16.3",
"express-session": "^1.15.6",
"json": "^9.0.3",
"ldapjs": "^1.0.0",
"ldapjs": "^1.0.2",
"lodash.chunk": "^4.2.0",
"mime": "^1.3.4",
"moment-timezone": "^0.5.5",
"morgan": "^1.7.0",
"multiparty": "^4.1.2",
"mysql": "^2.7.0",
"nodemailer": "^4.0.1",
"mime": "^2.3.1",
"moment-timezone": "^0.5.17",
"morgan": "^1.9.0",
"multiparty": "^4.1.4",
"mysql": "^2.15.0",
"nodemailer": "^4.6.5",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.0.1",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
"parse-links": "^0.1.0",
"passport": "^0.2.2",
"passport-http": "^0.2.2",
"passport": "^0.4.0",
"passport-http": "^0.3.0",
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.0.2",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.13.0",
"s3-block-read-stream": "^0.2.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.2.0",
"readdirp": "^2.1.0",
"request": "^2.87.0",
"rimraf": "^2.6.2",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^0.7.1",
"semver": "^4.3.6",
"showdown": "^1.6.0",
"semver": "^5.5.0",
"showdown": "^1.8.6",
"speakeasy": "^2.0.0",
"split": "^1.0.0",
"superagent": "^3.5.2",
"supererror": "^0.7.1",
"tar-fs": "^1.15.3",
"tldjs": "^1.6.2",
"underscore": "^1.7.0",
"uuid": "^3.1.0",
"superagent": "^3.8.3",
"supererror": "^0.7.2",
"tar-fs": "^1.16.2",
"tar-stream": "^1.6.1",
"tldjs": "^2.3.1",
"underscore": "^1.9.1",
"uuid": "^3.2.1",
"valid-url": "^1.0.9",
"validator": "^4.9.0",
"ws": "^2.3.1"
"validator": "^10.3.0",
"ws": "^5.2.0"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
"del": "^1.1.1",
"expect.js": "*",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^2.3.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^1.0.0",
"gulp-sass": "^3.0.0",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^1.5.2",
"gulp-uglify": "^1.1.0",
"hock": "~1.2.0",
"hock": "^1.3.2",
"istanbul": "*",
"js2xmlparser": "^1.0.0",
"mocha": "*",
"mock-aws-s3": "^2.4.0",
"js2xmlparser": "^3.0.0",
"mocha": "^5.2.0",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^3.13.1",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"request": "^2.65.0",
"yargs": "^3.15.0"
"node-sass": "^4.6.1",
"recursive-readdir": "^2.2.2"
},
"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 -- -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 -- -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",
"webadmin": "node_modules/.bin/gulp"
"dashboard": "node_modules/.bin/gulp"
}
}
+122
View File
@@ -0,0 +1,122 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
function get_status() {
key="$1"
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
domain=""
domainProvider=""
domainConfigJson="{}"
domainTlsProvider="letsencrypt-prod"
adminUsername="superadmin"
adminPassword="Secret123#"
adminEmail="admin@server.local"
appstoreUserId=""
appstoreToken=""
backupDir="/var/backups"
args=$(getopt -o "" -l "domain:,domain-provider:,domain-tls-provider:,admin-username:,admin-password:,admin-email:,appstore-user:,appstore-token:,backup-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--domain-provider) domainProvider="$2"; shift 2;;
--domain-tls-provider) domainTlsProvider="$2"; shift 2;;
--admin-username) adminUsername="$2"; shift 2;;
--admin-password) adminPassword="$2"; shift 2;;
--admin-email) adminEmail="$2"; shift 2;;
--appstore-user) appstoreUser="$2"; shift 2;;
--appstore-token) appstoreToken="$2"; shift 2;;
--backup-dir) backupDir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
if [[ $(get_status "webadminStatus") != *'"tls": true'* ]]; then
echo "=> Domain setup"
dnsSetupData=$(printf '{ "domain": "%s", "adminFqdn": "%s", "provider": "%s", "config": %s, "tlsConfig": { "provider": "%s" } }' "${domain}" "my.${domain}" "${domainProvider}" "$domainConfigJson" "${domainTlsProvider}")
if ! $curl -X POST -H "Content-Type: application/json" -d "${dnsSetupData}" http://localhost:3000/api/v1/cloudron/dns_setup; then
echo "DNS Setup Failed"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
else
echo "=> Skipping Domain setup"
fi
activationData=$(printf '{"username": "%s", "password":"%s", "email": "%s" }' "${adminUsername}" "${adminPassword}" "${adminEmail}")
if [[ $(get_status "activated") == "false" ]]; then
echo "=> Activating"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/cloudron/activate); then
echo "Failed to activate with ${activationData}: ${activationResult}"
exit 1
fi
wait_for_status "activated" "true"
else
echo "=> Skipping Activation"
fi
echo "=> Getting token"
if ! activationResult=$($curl -X POST -H "Content-Type: application/json" -d "${activationData}" http://localhost:3000/api/v1/developer/login); then
echo "Failed to login with ${activationData}: ${activationResult}"
exit 1
fi
accessToken=$(echo "${activationResult}" | python3 -c 'import sys, json; print(json.load(sys.stdin)[sys.argv[1]])' "accessToken")
echo "=> Setting up App Store account with accessToken ${accessToken}"
appstoreData=$(printf '{"userId":"%s", "token":"%s" }' "${appstoreUser}" "${appstoreToken}")
if ! appstoreResult=$($curl -X POST -H "Content-Type: application/json" -d "${appstoreData}" "http://localhost:3000/api/v1/settings/appstore_config?access_token=${accessToken}"); then
echo "Failed to setup Appstore account with ${appstoreData}: ${appstoreResult}"
exit 1
fi
echo "=> Setting up Backup Directory with accessToken ${accessToken}"
backupData=$(printf '{"provider":"filesystem", "key":"", "backupFolder":"%s", "retentionSecs": 864000, "format": "tgz", "externalDisk": true}' "${backupDir}")
chown -R yellowtent:yellowtent "${backupDir}"
if ! backupResult=$($curl -X POST -H "Content-Type: application/json" -d "${backupData}" "http://localhost:3000/api/v1/settings/backup_config?access_token=${accessToken}"); then
echo "Failed to setup backup configuration with ${backupDir}: ${backupResult}"
exit 1
fi
echo "=> Done!"
+86 -159
View File
@@ -2,16 +2,6 @@
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
@@ -26,6 +16,10 @@ readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
readonly RED='\033[31m'
readonly GREEN='\033[32m'
readonly DONE='\033[m'
# verify the system has minimum requirements met
if [[ "${rootfs_type}" != "ext4" ]]; then
echo "Error: Cloudron requires '/' to be ext4" # see #364
@@ -42,108 +36,97 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
exit 1
fi
if systemctl -q is-active box; then
echo "Error: Cloudron is already installed. To reinstall, start afresh"
exit 1
fi
initBaseImage="true"
# provisioning data
domain=""
zoneName=""
provider=""
encryptionKey=""
restoreUrl=""
dnsProvider="manual"
tlsProvider="le-prod"
edition=""
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
dataJson=""
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
# TODO this is still there for the restore case, see other occasions below
versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
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
--domain) domain="$2"; shift 2;;
--help) echo "See https://cloudron.io/references/selfhosting.html on how to install Cloudron"; exit 0;;
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--encryption-key) encryptionKey="$2"; shift 2;;
--restore-url) restoreUrl="$2"; shift 2;;
--tls-provider) tlsProvider="$2"; shift 2;;
--dns-provider) dnsProvider="$2"; shift 2;;
--edition) edition="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
versionsUrl="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
versionsUrl="https://s3.amazonaws.com/staging-cloudron-releases/versions.json"
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--data) dataJson="$2"; shift 2;;
--prerelease) prerelease="true"; shift;;
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# Only --help works as non-root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
# Only --help works with mismatched ubuntu
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# validate arguments in the absence of data
if [[ -z "${dataJson}" ]]; then
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "cloudscale" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
"${provider}" != "digitalocean" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
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
if [[ "${tlsProvider}" != "fallback" && "${tlsProvider}" != "le-prod" && "${tlsProvider}" != "le-staging" ]]; then
echo "--tls-provider must be one of: le-prod, le-staging, fallback"
exit 1
fi
if [[ -z "${dnsProvider}" ]]; then
echo "--dns-provider is required (noop, manual)"
exit 1
elif [[ "${dnsProvider}" != "noop" && "${dnsProvider}" != "manual" ]]; then
echo "--dns-provider must be one of : manual, noop"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
fi
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
exit 1
fi
echo ""
@@ -154,94 +137,51 @@ echo ""
echo " Follow setup logs in a second terminal with:"
echo " $ tail -f ${LOG_FILE}"
echo ""
echo " Join us at https://chat.cloudron.io for any questions."
echo " Join us at https://forum.cloudron.io for any questions."
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
echo "=> Checking version"
if [[ "${sourceTarballUrl}" == "" ]]; then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if [[ "$requestedVersion" == "" ]]; then
version=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["version"])')
else
version="${requestedVersion}"
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["info"]["sourceTarballUrl"])'); then
echo "No source code for version '${requestedVersion:-latest}'"
exit 1
fi
# Build data
# TODO versionsUrl is still there for the cloudron restore case
if [[ -z "${dataJson}" ]]; then
if [[ -z "${restoreUrl}" ]]; then
data=$(cat <<EOF
{
"boxVersionsUrl": "${versionsUrl}",
"fqdn": "${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"tlsConfig": {
"provider": "${tlsProvider}"
},
"dnsConfig": {
"provider": "${dnsProvider}"
},
"backupConfig" : {
"provider": "filesystem",
"backupFolder": "/var/backups",
"key": "${encryptionKey}",
"format": "tgz",
"retentionSecs": 172800
},
"updateConfig": {
"prerelease": ${prerelease}
},
"version": "${version}"
}
# from 1.9, we use autoprovision.json
data=$(cat <<EOF
{
"provider": "${provider}",
"edition": "${edition}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
else
data=$(cat <<EOF
{
"boxVersionsUrl": "${versionsUrl}",
"fqdn": "${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"restore": {
"url": "${restoreUrl}",
"key": "${encryptionKey}"
},
"version": "${version}"
}
EOF
)
fi
else
data="${dataJson}"
fi
)
echo "=> Downloading version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
@@ -262,17 +202,9 @@ fi
echo "=> Installing version ${version} (this takes some time) ..."
echo "${data}" > "${DATA_FILE}"
# poor mans semver
if [[ ${version} == "0.10"* ]]; then
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
else
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
@@ -280,20 +212,15 @@ echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
[[ -z "$domain" ]] && break # with no domain, we are up and running
[[ "$status" == *"\"tls\": true"* ]] && break # with a domain, wait for the cert
break # we are up and running
fi
sleep 10
done
if [[ -n "${domain}" ]]; then
echo -e "\n\nVisit https://my.${domain} to finish setup once the server has rebooted.\n"
else
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
fi
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
+36 -42
View File
@@ -7,17 +7,15 @@ set -eu
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "revision:,output:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
commitish="HEAD"
bundle_file=""
while true; do
case "$1" in
--revision) commitish="$2"; shift 2;;
--output) bundle_file="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
@@ -27,60 +25,56 @@ done
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
echo "You have local changes, stash or commit them to proceed"
echo "You have local changes in box, stash or commit them to proceed"
exit 1
fi
if [[ "$(node --version)" != "v6.11.3" ]]; then
echo "This script requires node 6.11.3"
if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
echo "You have local changes in dashboard, stash or commit them to proceed"
exit 1
fi
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
if [[ "$(node --version)" != "v8.11.2" ]]; then
echo "This script requires node 8.11.2"
exit 1
fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
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 "${branch}")
fi
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${version}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "Checking out code [${version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
echo "Reusing dev modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
else
echo "Installing modules with dev dependencies"
(cd "${bundle_dir}" && npm install)
echo "==> Installing modules for dashboard asset generation"
(cd "${bundle_dir}/dashboard.build" && npm install --production)
echo "Caching dev dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-all"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-all/"
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all"
fi
echo "==> Building dashboard assets"
(cd "${bundle_dir}/dashboard.build" && ./node_modules/.bin/gulp --revision ${dashboard_version})
echo "Building webadmin assets"
(cd "${bundle_dir}" && ./node_modules/.bin/gulp)
echo "==> Move built dashboard assets into destination"
mkdir -p "${bundle_dir}/dashboard"
mv "${bundle_dir}/dashboard.build/dist" "${bundle_dir}/dashboard/"
echo "Remove intermediate files required at build-time only"
rm -rf "${bundle_dir}/node_modules/"
rm -rf "${bundle_dir}/webadmin/src"
rm -rf "${bundle_dir}/gulpfile.js"
echo "==> Cleanup dashboard build artifacts"
rm -rf "${bundle_dir}/dashboard.build"
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
echo "Reusing prod modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-prod/." "${bundle_dir}/node_modules"
else
echo "Installing modules for production"
(cd "${bundle_dir}" && npm install --production --no-optional)
echo "==> Installing toplevel node modules"
(cd "${bundle_dir}" && npm install --production --no-optional)
echo "Caching prod dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-prod"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-prod/"
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod"
fi
echo "Create final tarball"
echo "==> Create final tarball"
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
echo "Cleaning up ${bundle_dir}"
echo "==> Cleaning up ${bundle_dir}"
rm -rf "${bundle_dir}"
echo "Tarball saved at ${bundle_file}"
echo "==> Tarball saved at ${bundle_file}"
+58 -15
View File
@@ -34,15 +34,50 @@ while true; do
esac
done
echo "==> installer: updating node"
if [[ "$(node --version)" != "v6.11.3" ]]; then
mkdir -p /usr/local/node-6.11.3
$curl -sL https://nodejs.org/dist/v6.11.3/node-v6.11.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-6.11.3
ln -sf /usr/local/node-6.11.3/bin/node /usr/bin/node
ln -sf /usr/local/node-6.11.3/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.2
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
echo "==> installer: docker binary download is corrupt"
exit 5
fi
echo "==> installer: Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
while ! dpkg --force-confold --configure -a; do
echo "==> installer: Failed to fix packages. Retry"
sleep 1
done
# the latest docker might need newer packages
while ! apt update -y; do
echo "==> installer: Failed to update packages. Retry"
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
echo "==> installer: Failed to install docker. Retry"
sleep 1
done
rm /tmp/docker.deb
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v8.11.2" ]]; then
mkdir -p /usr/local/node-8.11.2
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.5
fi
# this is here (and not in updater.js) because rebuild requires the above node
for try in `seq 1 10`; do
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
@@ -50,23 +85,34 @@ for try in `seq 1 10`; do
# however by default npm drops privileges for npm rebuild
# https://docs.npmjs.com/misc/config#unsafe-perm
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
echo "Failed to rebuild, trying again"
echo "==> installer: Failed to rebuild, trying again"
sleep 5
done
if [[ ${try} -eq 10 ]]; then
echo "npm rebuild failed"
echo "==> installer: npm rebuild failed, giving up"
exit 4
fi
echo "==> installer: update cloudron-syslog"
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
CLOUDRON_SYSLOG_VERSION="1.0.3"
while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLOUDRON_SYSLOG_VERSION} ]]; do
rm -rf "${CLOUDRON_SYSLOG_DIR}"
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@${CLOUDRON_SYSLOG_VERSION}; then break; fi
echo "===> installer: Failed to install cloudron-syslog, trying again"
sleep 5
done
if ! id "${USER}" 2>/dev/null; then
useradd "${USER}" -m
fi
if [[ "${is_update}" == "yes" ]]; then
echo "Setting up update splash screen"
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # show splash from new code
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
echo "==> installer: stop cloudron.target service for update"
${BOX_SRC_DIR}/setup/stop.sh
fi
# setup links to data directory
@@ -81,9 +127,6 @@ fi
# ensure we are not inside the source directory, which we will remove now
cd /root
echo "==> installer: updating packages"
# add logic to update apt packages here
echo "==> installer: switching the box code"
rm -rf "${BOX_SRC_DIR}"
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
+16 -52
View File
@@ -3,27 +3,18 @@
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${source_dir}/../node_modules/.bin/json"
# IMPORTANT: Fix cloudron.js:doUpdate if you add/remove any arg. keep these sorted for readability
arg_api_server_origin=""
arg_fqdn=""
arg_zone_name=""
arg_is_custom_domain="false"
arg_restore_key=""
arg_restore_url=""
arg_fqdn="" # remove after 1.10
arg_admin_domain=""
arg_admin_location=""
arg_admin_fqdn=""
arg_retire_reason=""
arg_retire_info=""
arg_tls_config=""
arg_tls_cert=""
arg_tls_key=""
arg_token=""
arg_version=""
arg_web_server_origin=""
arg_backup_config=""
arg_dns_config=""
arg_update_config=""
arg_provider=""
arg_app_bundle=""
arg_is_demo="false"
arg_edition=""
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
eval set -- "${args}"
@@ -41,10 +32,13 @@ while true; do
--data)
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_zone_name=$(echo "$2" | $json zoneName)
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
arg_is_custom_domain=$(echo "$2" | $json isCustomDomain)
[[ "${arg_is_custom_domain}" == "" ]] && arg_is_custom_domain="true"
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
arg_admin_domain=$(echo "$2" | $json adminDomain)
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
@@ -52,42 +46,18 @@ while true; do
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
# TODO check if an where this is used
# TODO check if and where this is used
arg_version=$(echo "$2" | $json version)
# read possibly empty parameters here
arg_app_bundle=$(echo "$2" | $json appBundle)
[[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]"
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_tls_cert=$(echo "$2" | $json tlsCert)
[[ "${arg_tls_cert}" == "null" ]] && arg_tls_cert=""
arg_tls_key=$(echo "$2" | $json tlsKey)
[[ "${arg_tls_key}" == "null" ]] && arg_tls_key=""
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
arg_tls_config=$(echo "$2" | $json tlsConfig)
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
arg_restore_url=$(echo "$2" | $json restore.url)
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
arg_restore_key=$(echo "$2" | $json restore.key)
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
arg_backup_config=$(echo "$2" | $json backupConfig)
[[ "${arg_backup_config}" == "null" ]] && arg_backup_config=""
arg_dns_config=$(echo "$2" | $json dnsConfig)
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
arg_update_config=$(echo "$2" | $json updateConfig)
[[ "${arg_update_config}" == "null" ]] && arg_update_config=""
arg_edition=$(echo "$2" | $json edition)
[[ "${arg_edition}" == "" ]] && arg_edition=""
shift 2
;;
@@ -98,15 +68,9 @@ done
echo "Parsed arguments:"
echo "api server: ${arg_api_server_origin}"
echo "admin fqdn: ${arg_admin_fqdn}"
echo "fqdn: ${arg_fqdn}"
echo "custom domain: ${arg_is_custom_domain}"
echo "restore url: ${arg_restore_url}"
echo "tls cert: ${arg_tls_cert}"
# do not dump these as they might become available via logs API
#echo "restore key: ${arg_restore_key}"
#echo "tls key: ${arg_tls_key}"
#echo "token: ${arg_token}"
echo "tlsConfig: ${arg_tls_config}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
echo "edition: ${arg_edition}"
-50
View File
@@ -1,50 +0,0 @@
#!/bin/bash
set -eu -o pipefail
readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_dir="$(realpath ${script_dir}/..)"
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
echo "Setting up nginx update page"
if [[ ! -f "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf" ]]; then
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
exit
fi
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${ADMIN_LOCATION}.${arg_fqdn}" || echo "${ADMIN_LOCATION}-${arg_fqdn}")
admin_origin="https://${admin_fqdn}"
# copy the website
rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
# create nginx config
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
existing_infra="none"
[[ -f "${PLATFORM_DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${PLATFORM_DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
else
echo "Show progress bar only on admin domain for normal update"
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\", \"robotsTxtQuoted\": null, \"hasIPv6\": false }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
fi
if [[ "${arg_retire_reason}" == "migrate" ]]; then
echo "{ \"migrate\": { \"percent\": \"10\", \"message\": \"Migrating cloudron. This could take up to 15 minutes.\", \"info\": ${arg_retire_info} }, \"backup\": null, \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${SETUP_WEBSITE_DIR}/progress.json"
else
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
fi
nginx -s reload
+36 -87
View File
@@ -11,7 +11,6 @@ readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly SETUP_PROGRESS_JSON="${HOME_DIR}/setup/website/progress.json"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
@@ -19,19 +18,11 @@ readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
set_progress() {
local percent="$1"
local message="$2"
echo "==> ${percent} - ${message}"
(echo "{ \"update\": { \"percent\": \"${percent}\", \"message\": \"${message}\" }, \"backup\": {} }" > "${SETUP_PROGRESS_JSON}") 2> /dev/null || true # as this will fail in non-update mode
}
set_progress "20" "Configuring host"
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_fqdn}"
hostnamectl set-hostname "${arg_admin_fqdn}"
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -85,6 +76,8 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
mkdir -p "${PLATFORM_DATA_DIR}/backup"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
mkdir -p "${BOX_DATA_DIR}/certs"
@@ -95,11 +88,6 @@ mkdir -p "${BOX_DATA_DIR}/mail/dkim"
mkdir -p /var/backups
chmod 777 /var/backups
echo "==> Migrating mail data"
if [[ -d "${PLATFORM_DATA_DIR}/mail" ]]; then
find "${PLATFORM_DATA_DIR}/mail" -mindepth 1 -maxdepth 1 -exec mv --target-directory="${BOX_DATA_DIR}/mail" '{}' +
fi
echo "==> Configuring journald"
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
@@ -119,7 +107,7 @@ systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
rm -rf "${CONFIG_DIR}" && mkdir "${CONFIG_DIR}"
mkdir -p "${CONFIG_DIR}"
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
@@ -134,6 +122,7 @@ echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
systemctl enable cloudron.target
systemctl enable cloudron-firewall
@@ -146,6 +135,9 @@ systemctl enable --now cron
# ensure unbound runs
systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
@@ -160,6 +152,8 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
@@ -177,6 +171,9 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# remove this migration after 1.10
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
systemctl start nginx
# bookkeep the version as part of data
@@ -191,7 +188,11 @@ if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf"
echo "Waiting for mysql jobs..."
sleep 1
done
systemctl restart mysql
while true; do
if systemctl restart mysql; then break; fi
echo "Restarting MySql again after sometime since this fails randomly"
sleep 1
done
else
systemctl start mysql
fi
@@ -200,32 +201,7 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
if [[ -n "${arg_restore_url}" ]]; then
set_progress "30" "Downloading restore data"
decrypt=""
if [[ "${arg_restore_url}" == *.tar.gz.enc || -n "${arg_restore_key}" ]]; then
echo "==> Downloading encrypted backup: ${arg_restore_url} and key: ${arg_restore_key}"
decrypt=(openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}")
else
echo "==> Downloading backup: ${arg_restore_url}"
decrypt=(cat -)
fi
while true; do
if $curl -L "${arg_restore_url}" | "${decrypt[@]}" \
| tar -zxf - --overwrite -C "${BOX_DATA_DIR}"; then break; fi
echo "Failed to download data, trying again"
done
set_progress "35" "Setting up MySQL"
if [[ -f "${BOX_DATA_DIR}/box.mysqldump" ]]; then
echo "==> Importing existing database into MySQL"
mysql -u root -p${mysql_root_password} box < "${BOX_DATA_DIR}/box.mysqldump"
fi
fi
set_progress "40" "Migrating data"
echo "==> Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
@@ -236,32 +212,19 @@ echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"token": "${arg_token}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"fqdn": "${arg_fqdn}",
"zoneName": "${arg_zone_name}",
"isCustomDomain": ${arg_is_custom_domain},
"adminDomain": "${arg_admin_domain}",
"adminFqdn": "${arg_admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo},
"database": {
"hostname": "127.0.0.1",
"username": "root",
"password": "${mysql_root_password}",
"port": 3306,
"name": "box"
},
"appBundle": ${arg_app_bundle}
"edition": "${arg_edition}"
}
CONF_END
# pass these out-of-band because they have new lines which interfere with json
if [[ -n "${arg_tls_cert}" && -n "${arg_tls_key}" ]]; then
echo "${arg_tls_cert}" > "${CONFIG_DIR}/host.cert"
echo "${arg_tls_key}" > "${CONFIG_DIR}/host.key"
fi
echo "==> Creating config.json for webadmin"
cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
echo "==> Creating config.json for dashboard"
cat > "${BOX_SRC_DIR}/dashboard/dist/config.json" <<CONF_END
{
"webServerOrigin": "${arg_web_server_origin}"
}
@@ -277,36 +240,22 @@ fi
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
echo "==> Adding automated configs"
if [[ ! -z "${arg_backup_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
fi
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
if [[ ! -z "${arg_dns_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
fi
# do not chown the boxdata/mail directory; dovecot gets upset
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
if [[ ! -z "${arg_update_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
fi
if [[ ! -z "${arg_tls_config}" ]]; then
mysql -u root -p${mysql_root_password} \
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
set_progress "60" "Starting Cloudron"
echo "==> Starting Cloudron"
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
set_progress "90" "Almost done"
echo "==> Almost done"
+10
View File
@@ -0,0 +1,10 @@
# logrotate config for app logs
/home/yellowtent/platformdata/logs/*/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
+50 -38
View File
@@ -4,6 +4,41 @@ map $http_upgrade $connection_upgrade {
'' close;
}
# http server
server {
listen 80;
<% if (hasIPv6) { -%>
listen [::]:80;
<% } -%>
<% if (vhost) { -%>
server_name <%= vhost %>;
<% } else { -%>
# IP based access from collectd or initial cloudron setup. TODO: match the IPv6 address
server_name "~^\d+\.\d+\.\d+\.\d+$";
# collectd
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
<% } -%>
# acme challenges (for cert renewal where the vhost config exists)
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/platformdata/acme/;
}
location / {
# redirect everything to HTTPS
return 301 https://$host$request_uri;
}
}
# https server
server {
<% if (vhost) { -%>
server_name <%= vhost %>;
@@ -31,8 +66,9 @@ server {
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# ciphers according to https://weakdh.org/sysadmin.html
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -54,6 +90,11 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
<% 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';";
<% } -%>
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
@@ -71,7 +112,7 @@ server {
proxy_set_header Connection $connection_upgrade;
# only serve up the status page if we get proxy gateway errors
root <%= sourceDir %>/webadmin/dist;
root <%= sourceDir %>/dashboard/dist;
error_page 502 503 504 /appstatus.html;
location /appstatus.html {
internal;
@@ -119,51 +160,22 @@ server {
}
# graphite paths (uncomment block below and visit /graphite/index.html)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8000;
# client_max_body_size 1m;
# }
location / {
root <%= sourceDir %>/webadmin/dist;
root <%= sourceDir %>/dashboard/dist;
index index.html index.htm;
}
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'splash' ) { %>
root <%= sourceDir %>;
error_page 503 /update.html;
location /update.html {
add_header Cache-Control no-cache;
}
location /theme.css {
add_header Cache-Control no-cache;
}
location /3rdparty/ {
add_header Cache-Control no-cache;
}
location /js/ {
add_header Cache-Control no-cache;
}
location /progress.json {
add_header Cache-Control no-cache;
}
location /api/v1/cloudron/progress {
add_header Cache-Control no-cache;
default_type application/json;
alias <%= sourceDir %>/progress.json;
}
location / {
return 503;
}
<% } else if ( endpoint === 'redirect' ) { %>
# redirect everything to the app. this is temporary because there is no way
# to clear a permanent redirect on the browser
return 302 https://<%= redirectTo %>$request_uri;
<% } %>
}
}
+7 -14
View File
@@ -36,28 +36,21 @@ http {
# zones for rate limiting
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
# HTTP server
# default http server that returns 404 for any domain we are not listening on
server {
listen 80;
listen [::]:80;
listen 80 default_server;
listen [::]:80 default_server;
server_name does_not_match_anything;
# collectd
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
# acme challenges
# acme challenges (for app installation and re-configure when the vhost config does not exist)
location /.well-known/acme-challenge/ {
default_type text/plain;
alias /home/yellowtent/platformdata/acme/;
}
location / {
# redirect everything to HTTPS
return 301 https://$host$request_uri;
return 404;
}
}
+3
View File
@@ -34,3 +34,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrot
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
@@ -0,0 +1,14 @@
[Unit]
Description=Cloudron Syslog
After=network.target
[Service]
ExecStart=/usr/local/cloudron-syslog/bin/cloudron-syslog --port 2514 --logdir /home/yellowtent/platformdata/logs
WorkingDirectory=/usr/local/cloudron-syslog
Environment="NODE_ENV=production"
Restart=always
User=yellowtent
Group=yellowtent
[Install]
WantedBy=multi-user.target
+143
View File
@@ -0,0 +1,143 @@
'use strict';
exports = module.exports = {
SCOPE_APPS_READ: 'apps:read',
SCOPE_APPS_MANAGE: 'apps:manage',
SCOPE_CLIENTS: 'clients',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_DOMAINS_READ: 'domains:read',
SCOPE_DOMAINS_MANAGE: 'domains:manage',
SCOPE_MAIL: 'mail',
SCOPE_PROFILE: 'profile',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS_READ: 'users:read',
SCOPE_USERS_MANAGE: 'users:manage',
SCOPE_APPSTORE: 'appstore',
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
SCOPE_ANY: '*',
validateScopeString: validateScopeString,
hasScopes: hasScopes,
canonicalScopeString: canonicalScopeString,
intersectScopes: intersectScopes,
validateToken: validateToken,
scopesForUser: scopesForUser
};
var assert = require('assert'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
_ = require('underscore');
// returns scopes that does not have wildcards and is sorted
function canonicalScopeString(scope) {
if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(',');
return scope.split(',').sort().join(',');
}
function intersectScopes(allowedScopes, wantedScopes) {
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
let wantedScopesMap = new Map();
let results = [];
// make a map of scope -> [ subscopes ]
for (let w of wantedScopes) {
let parts = w.split(':');
let subscopes = wantedScopesMap.get(parts[0]) || new Set();
subscopes.add(parts[1] || '*');
wantedScopesMap.set(parts[0], subscopes);
}
for (let a of allowedScopes) {
let parts = a.split(':');
let as = parts[1] || '*';
let subscopes = wantedScopesMap.get(parts[0]);
if (!subscopes) continue;
if (subscopes.has('*') || subscopes.has(as)) {
results.push(a);
} else if (as === '*') {
results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; }));
}
}
return results;
}
function validateScopeString(scope) {
assert.strictEqual(typeof scope, 'string');
if (scope === '') return new Error('Empty scope not allowed');
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
// us not write a migration script every time we add a new scope
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; });
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
return null;
}
// tests if all requiredScopes are attached to the request
function hasScopes(authorizedScopes, requiredScopes) {
assert(Array.isArray(authorizedScopes), 'Expecting array');
assert(Array.isArray(requiredScopes), 'Expecting array');
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
for (var i = 0; i < requiredScopes.length; ++i) {
const scopeParts = requiredScopes[i].split(':');
// this allows apps:write if the token has a higher apps scope
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
debug('scope: missing scope "%s".', requiredScopes[i]);
return new Error('Missing required scope "' + requiredScopes[i] + '"');
}
}
return null;
}
function scopesForUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
if (user.admin) return callback(null, exports.VALID_SCOPES);
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
}
function validateToken(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error); // this triggers 'internal error' in passport
users.get(token.identifier, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error);
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
callback(null, user, info);
});
});
});
}
+278 -136
View File
@@ -15,19 +15,22 @@ exports = module.exports = {
_teardownOauth: teardownOauth
};
var appdb = require('./appdb.js'),
var accesscontrol = require('./accesscontrol.js'),
appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
ClientsError = clients.ClientsError,
crypto = require('crypto'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
dockerConnection = docker.connection,
fs = require('fs'),
generatePassword = require('password-generator'),
hat = require('hat'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
once = require('once'),
path = require('path'),
@@ -106,16 +109,21 @@ var KNOWN_ADDONS = {
teardown: NOOP,
backup: NOOP,
restore: NOOP
},
docker: {
setup: NOOP,
teardown: NOOP,
backup: NOOP,
restore: NOOP
}
};
var RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh');
function debugApp(app, args) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function setupAddons(app, addons, callback) {
@@ -125,7 +133,7 @@ function setupAddons(app, addons, callback) {
if (!addons) return callback(null);
debugApp(app, 'setupAddons: Settings up %j', Object.keys(addons));
debugApp(app, 'setupAddons: Setting up %j', Object.keys(addons));
async.eachSeries(Object.keys(addons), function iterator(addon, iteratorCallback) {
if (!(addon in KNOWN_ADDONS)) return iteratorCallback(new Error('No such addon:' + addon));
@@ -197,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; }));
});
}
@@ -205,13 +215,21 @@ function getBindsSync(app, addons) {
assert.strictEqual(typeof app, 'object');
assert(!addons || typeof addons === 'object');
var binds = [ ];
let binds = [ ];
if (!addons) return binds;
for (var addon in addons) {
for (let addon in addons) {
switch (addon) {
case 'localstorage': binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data') + ':/app/data:rw'); break;
case 'localstorage':
binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data') + ':/app/data:rw');
if (!Array.isArray(addons[addon].bindMounts)) break;
for (let mount of addons[addon].bindMounts) {
let [ host, container ] = mount.split(':');
binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data', path.normalize(host)) + ':' + container);
}
break;
default: break;
}
}
@@ -245,11 +263,13 @@ function setupOauth(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'setupOauth');
if (!app.sso) return callback(null);
var appId = app.id;
var redirectURI = 'https://' + (app.altDomain || config.appFqdn(app.location));
var scope = 'profile';
var redirectURI = 'https://' + app.fqdn;
var scope = accesscontrol.SCOPE_PROFILE;
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
@@ -289,20 +309,27 @@ function setupEmail(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
mail.getDomains(function (error, mailDomains) {
if (error) return callback(error);
debugApp(app, 'Setting up Email');
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
appdb.setAddonConfig(app.id, 'email', env, callback);
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains }
];
debugApp(app, 'Setting up Email');
appdb.setAddonConfig(app.id, 'email', env, callback);
});
}
function teardownEmail(app, options, callback) {
@@ -354,22 +381,28 @@ function setupSendMail(app, options, callback) {
debugApp(app, 'Setting up SendMail');
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'sendmail', 'MAIL_SMTP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '2465' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_SMTP_PASSWORD', value: password },
{ name: 'MAIL_FROM', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
});
});
}
@@ -390,23 +423,28 @@ function setupRecvMail(app, options, callback) {
debugApp(app, 'Setting up recvmail');
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
appdb.getAddonConfigByName(app.id, 'recvmail', 'MAIL_IMAP_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
var password = error ? hat(4 * 48) : existingPassword; // see box#565 for password length
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
];
mailboxdb.getByOwnerId(app.id, function (error, results) {
if (error) return callback(error);
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
appdb.setAddonConfig(app.id, 'recvmail', env, callback);
});
});
}
@@ -420,6 +458,14 @@ function teardownRecvMail(app, options, callback) {
appdb.unsetAddonConfig(app.id, 'recvmail', callback);
}
function mysqlDatabaseName(appId) {
assert.strictEqual(typeof appId, 'string');
var md5sum = crypto.createHash('md5'); // get rid of "-"
md5sum.update(appId);
return md5sum.digest('hex').substring(0, 16); // max length of mysql usernames is 16
}
function setupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
@@ -427,16 +473,36 @@ function setupMySql(app, options, callback) {
debugApp(app, 'Setting up mysql');
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'mysql', 'MYSQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const dbname = mysqlDatabaseName(app.id);
const password = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'add-prefix' : 'add', dbname, password ];
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'MYSQL_USERNAME', value: dbname },
{ name: 'MYSQL_PASSWORD', value: password },
{ name: 'MYSQL_HOST', value: 'mysql' },
{ name: 'MYSQL_PORT', value: '3306' }
];
if (options.multipleDatabases) {
env = env.concat({ name: 'MYSQL_DATABASE_PREFIX', value: `${dbname}_` });
} else {
env = env.concat(
{ name: 'MYSQL_URL', value: `mysql://${dbname}:${password}@mysql/${dbname}` },
{ name: 'MYSQL_DATABASE', value: dbname }
);
}
debugApp(app, 'Setting mysql addon config to %j', env);
appdb.setAddonConfig(app.id, 'mysql', env, callback);
});
});
}
@@ -445,7 +511,8 @@ function teardownMySql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'remove-prefix' : 'remove', dbname ];
debugApp(app, 'Tearing down mysql');
@@ -457,6 +524,10 @@ function teardownMySql(app, options, callback) {
}
function backupMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mysql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -464,12 +535,17 @@ function backupMySql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
output.on('error', callback);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', dbname ];
docker.execContainer('mysql', cmd, { stdout: output }, callback);
}
function restoreMySql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMySql(app, options, function (error) {
@@ -480,7 +556,8 @@ function restoreMySql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
input.on('error', callback);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
const dbname = mysqlDatabaseName(app.id);
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', dbname ];
docker.execContainer('mysql', cmd, { stdin: input }, callback);
});
}
@@ -492,16 +569,29 @@ function setupPostgreSql(app, options, callback) {
debugApp(app, 'Setting up postgresql');
var cmd = [ '/addons/postgresql/service.sh', 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'postgresql', 'POSTGRESQL_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const password = error ? hat(4 * 128) : existingPassword;
const appId = app.id.replace(/-/g, '');
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
var cmd = [ '/addons/postgresql/service.sh', 'add', appId, password ];
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'POSTGRESQL_URL', value: `postgres://user${appId}:${password}@postgresql/db${appId}` },
{ name: 'POSTGRESQL_USERNAME', value: `user${appId}` },
{ name: 'POSTGRESQL_PASSWORD', value: password },
{ name: 'POSTGRESQL_HOST', value: 'postgresql' },
{ name: 'POSTGRESQL_PORT', value: '5432' },
{ name: 'POSTGRESQL_DATABASE', value: `db${appId}` }
];
debugApp(app, 'Setting postgresql addon config to %j', env);
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
});
});
}
@@ -510,7 +600,9 @@ function teardownPostgreSql(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/postgresql/service.sh', 'remove', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'remove', appId ];
debugApp(app, 'Tearing down postgresql');
@@ -522,6 +614,10 @@ function teardownPostgreSql(app, options, callback) {
}
function backupPostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up postgresql');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -529,12 +625,17 @@ function backupPostgreSql(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
output.on('error', callback);
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'backup', appId ];
docker.execContainer('postgresql', cmd, { stdout: output }, callback);
}
function restorePostgreSql(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
setupPostgreSql(app, options, function (error) {
@@ -545,7 +646,8 @@ function restorePostgreSql(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
input.on('error', callback);
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
const appId = app.id.replace(/-/g, '');
var cmd = [ '/addons/postgresql/service.sh', 'restore', appId ];
docker.execContainer('postgresql', cmd, { stdin: input }, callback);
});
@@ -558,16 +660,30 @@ function setupMongoDb(app, options, callback) {
debugApp(app, 'Setting up mongodb');
var cmd = [ '/addons/mongodb/service.sh', 'add', app.id ];
appdb.getAddonConfigByName(app.id, 'mongodb', 'MONGODB_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
if (error) return callback(error);
const password = error ? hat(4 * 128) : existingPassword;
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
const dbname = app.id;
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
var cmd = [ '/addons/mongodb/service.sh', 'add', dbname, password ];
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error) {
if (error) return callback(error);
var env = [
{ name: 'MONGODB_URL', value : `mongodb://${dbname}:${password}@mongodb/${dbname}` },
{ name: 'MONGODB_USERNAME', value : dbname },
{ name: 'MONGODB_PASSWORD', value: password },
{ name: 'MONGODB_HOST', value : 'mongodb' },
{ name: 'MONGODB_PORT', value : '27017' },
{ name: 'MONGODB_DATABASE', value : dbname }
];
debugApp(app, 'Setting mongodb addon config to %j', env);
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
});
});
}
@@ -576,7 +692,8 @@ function teardownMongoDb(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = [ '/addons/mongodb/service.sh', 'remove', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'remove', dbname ];
debugApp(app, 'Tearing down mongodb');
@@ -588,6 +705,10 @@ function teardownMongoDb(app, options, callback) {
}
function backupMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up mongodb');
callback = once(callback); // ChildProcess exit may or may not be called after error
@@ -595,12 +716,17 @@ function backupMongoDb(app, options, callback) {
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
output.on('error', callback);
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'backup', dbname ];
docker.execContainer('mongodb', cmd, { stdout: output }, callback);
}
function restoreMongoDb(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // ChildProcess exit may or may not be called after error
setupMongoDb(app, options, function (error) {
@@ -611,7 +737,9 @@ function restoreMongoDb(app, options, callback) {
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
input.on('error', callback);
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
const dbname = app.id;
var cmd = [ '/addons/mongodb/service.sh', 'restore', dbname ];
docker.execContainer('mongodb', cmd, { stdin: input }, callback);
});
}
@@ -622,57 +750,67 @@ function setupRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var redisPassword = generatePassword(128, false /* memorable */, /[\w\d_]/); // ensure no / in password for being sed friendly (and be uri friendly)
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
appdb.getAddonConfigByName(app.id, 'redis', 'REDIS_PASSWORD', function (error, existingPassword) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(error);
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
return callback(new Error('Error writing redis config'));
}
const redisPassword = error ? hat(4 * 48) : existingPassword; // see box#362 for password length
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
return callback(new Error('Error writing redis config'));
}
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${app.location} \
--net cloudron \
--net-alias ${redisName} \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
var env = [
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: 'REDIS_PASSWORD', value: redisPassword },
{ name: 'REDIS_HOST', value: redisName },
{ name: 'REDIS_PORT', value: '6379' }
];
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
async.series([
// stop so that redis can flush itself with SIGTERM
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.fqdn;
// note that we do not add appId label because this interferes with the stop/start app logic
const cmd = `docker run --restart=always -d --name=${redisName} \
--label=location=${label} \
--net cloudron \
--net-alias ${redisName} \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag="${redisName}" \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
var env = [
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
{ name: 'REDIS_PASSWORD', value: redisPassword },
{ name: 'REDIS_HOST', value: redisName },
{ name: 'REDIS_PORT', value: '6379' }
];
async.series([
// stop so that redis can flush itself with SIGTERM
shell.execSync.bind(null, 'stopRedis', `docker stop --time=10 ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'stopRedis', `docker rm --volumes ${redisName} 2>/dev/null || true`),
shell.execSync.bind(null, 'startRedis', cmd),
appdb.setAddonConfig.bind(null, app.id, 'redis', env)
], function (error) {
if (error) debug('Error setting up redis: ', error);
callback(error);
});
});
}
@@ -681,27 +819,31 @@ function teardownRedis(app, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var container = dockerConnection.getContainer('redis-' + app.id);
var container = dockerConnection.getContainer('redis-' + app.id);
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container
};
var removeOptions = {
force: true, // kill container if it's running
v: true // removes volumes associated with the container
};
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
container.remove(removeOptions, function (error) {
if (error && error.statusCode !== 404) return callback(new Error('Error removing container:' + error));
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
safe.fs.unlinkSync(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error, stdout, stderr) {
shell.sudo('teardownRedis', [ RMAPPDIR_CMD, app.id + '/redis', true /* delete directory */ ], function (error /* ,stdout , stderr*/) {
if (error) return callback(new Error('Error removing redis data:' + error));
appdb.unsetAddonConfig(app.id, 'redis', callback);
});
});
});
}
function backupRedis(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Backing up redis');
var cmd = [ '/addons/redis/service.sh', 'backup' ]; // the redis dir is volume mounted
+186 -51
View File
@@ -10,6 +10,7 @@ exports = module.exports = {
update: update,
getAll: getAll,
getPortBindings: getPortBindings,
delPortBinding: delPortBinding,
setAddonConfig: setAddonConfig,
getAddonConfig: getAddonConfig,
@@ -23,6 +24,9 @@ exports = module.exports = {
setRunCommand: setRunCommand,
getAppStoreIds: getAppStoreIds,
setOwner: setOwner,
transferOwnership: transferOwnership,
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
@@ -46,6 +50,10 @@ exports = module.exports = {
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
_clear: clear
};
@@ -58,11 +66,14 @@ var assert = require('assert'),
util = require('util');
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup' ].join(',');
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'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');
@@ -75,18 +86,28 @@ function postProcess(result) {
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
delete result.oldConfigJson;
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
delete result.updateConfigJson;
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
delete result.restoreConfigJson;
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
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');
@@ -103,6 +124,12 @@ function postProcess(result) {
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
delete result.debugModeJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
delete d.type;
});
}
function get(id, callback) {
@@ -110,14 +137,23 @@ 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'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE apps.id = ? GROUP BY apps.id', [ id ], function (error, result) {
+ '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 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));
postProcess(result[0]);
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));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
})
});
}
@@ -126,14 +162,22 @@ 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'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE httpPort = ? GROUP BY apps.id', [ httpPort ], function (error, result) {
+ '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 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));
postProcess(result[0]);
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));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
@@ -142,14 +186,22 @@ 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'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
+ '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 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));
postProcess(result[0]);
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));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
@@ -157,23 +209,39 @@ 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'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' GROUP BY apps.id ORDER BY apps.id', function (error, results) {
+ '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 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));
results.forEach(postProcess);
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));
callback(null, results);
alternateDomains.forEach(function (d) {
var domain = results.find(function (a) { return d.appId === a.id; });
if (!domain) return;
domain.alternateDomains = domain.alternateDomains || [];
domain.alternateDomains.push(d);
});
results.forEach(postProcess);
callback(null, results);
});
});
}
function add(id, appStoreId, manifest, location, portBindings, data, callback) {
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
@@ -185,36 +253,53 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var lastBackupId = data.lastBackupId || null; // used when cloning
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
});
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
});
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 ]
});
});
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)',
args: [ data.mailboxName, id, mailboxdb.TYPE_APP ]
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
});
}
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
});
});
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
@@ -241,18 +326,32 @@ 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, type, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
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));
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
var queries = [
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
@@ -260,7 +359,7 @@ function del(id, callback) {
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -270,6 +369,7 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
database.query.bind(null, 'DELETE FROM subdomains'),
database.query.bind(null, 'DELETE FROM appPortBindings'),
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
database.query.bind(null, 'DELETE FROM apps')
@@ -290,6 +390,7 @@ function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
var queries = [ ];
@@ -298,26 +399,32 @@ 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 });
});
}
if ('location' in app) {
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('domain' in app) {
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('alternateDomains' in app) {
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
});
}
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest') {
fields.push('manifestJson = ?');
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p === 'oldConfig') {
fields.push('oldConfigJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'accessRestriction') {
fields.push('accessRestrictionJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p === 'debugMode') {
fields.push('debugModeJson = ?');
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings') {
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains') {
fields.push(p + ' = ?');
values.push(app[p]);
}
@@ -368,14 +475,14 @@ function setInstallationCommand(appId, installationState, values, callback) {
// Rules are:
// uninstall is allowed in any state
// force update is allowed in any state including pending_uninstall! (for better or worse)
// restore is allowed from installed or error state
// restore is allowed from installed or error state or currently restoring
// configure is allowed in installed state or currently configuring or in error state
// update and backup are allowed only in installed state
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
updateWithConstraints(appId, values, '', callback);
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
@@ -489,3 +596,31 @@ function getAddonConfigByName(appId, addonId, name, callback) {
callback(null, results[0].value);
});
}
function setOwner(appId, ownerId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
callback(null);
});
}
function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof oldOwnerId, 'string');
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
+52 -65
View File
@@ -12,24 +12,19 @@ var appdb = require('./appdb.js'),
util = require('util');
exports = module.exports = {
start: start,
stop: stop
run: run
};
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
const NOOP_CALLBACK = function (error) { if (error) console.error(error); };
function debugApp(app) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
}
function setHealth(app, health, callback) {
@@ -70,6 +65,9 @@ function setHealth(app, health, callback) {
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
@@ -111,48 +109,23 @@ function checkAppHealth(app, callback) {
});
}
function processApps(callback) {
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, checkAppHealth, function (error) {
if (error) console.error(error);
var alive = result
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
debug('apps alive: [%s]', alive);
callback(null);
});
});
}
function run() {
processApps(function (error) {
if (error) console.error(error);
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents() {
// note that for some reason, the callback is called only on the first event
debug('Listening for docker events');
function processDockerEvents(interval, callback) {
assert.strictEqual(typeof interval, 'number');
assert.strictEqual(typeof callback, 'function');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
let lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
const since = ((new Date().getTime() / 1000) - interval).toFixed(0);
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
gDockerEventStream = stream;
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return callback(error);
stream.setEncoding('utf8');
stream.on('data', function (data) {
@@ -174,34 +147,48 @@ function processDockerEvents() {
});
stream.on('error', function (error) {
console.error('Error reading docker events', error);
gDockerEventStream = null; // TODO: reconnect?
debug('Error reading docker events', error);
callback();
});
stream.on('end', function () {
console.error('Docker event stream ended');
gDockerEventStream = null; // TODO: reconnect?
stream.on('end', callback);
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
});
}
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, checkAppHealth, function (error) {
if (error) console.error(error);
var alive = result
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
debug('apps alive: [%s]', alive);
callback(null);
});
});
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
function run(interval, callback) {
assert.strictEqual(typeof interval, 'number');
debug('Starting apphealthmonitor');
callback = callback || NOOP_CALLBACK;
processDockerEvents();
async.series([
processDockerEvents.bind(null, interval),
processApp
], function (error) {
if (error) debug(error);
run();
callback();
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gRunTimeout);
if (gDockerEventStream) gDockerEventStream.end();
callback();
callback();
});
}
+466 -250
View File
File diff suppressed because it is too large Load Diff
+149 -72
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
unpurchase: unpurchase,
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
@@ -13,14 +14,23 @@ exports = module.exports = {
getAccount: getAccount,
sendFeedback: sendFeedback,
AppstoreError: AppstoreError
};
var assert = require('assert'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
os = require('os'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util');
@@ -54,23 +64,12 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function getAppstoreConfig(callback) {
assert.strictEqual(typeof callback, 'function');
// Caas Cloudrons do not store appstore credentials in their local database
if (config.provider() === 'caas') {
var url = config.apiServerOrigin() + '/api/v1/exchangeBoxTokenWithUserToken';
superagent.post(url).query({ token: config.token() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
callback(null, result.body);
});
} else {
settings.getAppstoreConfig(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
callback(null, result);
});
}
callback(null, result);
});
}
function getSubscription(callback) {
@@ -92,6 +91,11 @@ function getSubscription(callback) {
});
}
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');
@@ -133,7 +137,7 @@ function unpurchase(appId, appstoreId, callback) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 404) return callback(null); // was never purchased
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)));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
@@ -146,64 +150,87 @@ function unpurchase(appId, appstoreId, callback) {
});
}
function sendAliveStatus(data, callback) {
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var allSettings, allDomains, mailDomains, loginEvents;
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
loginEvents = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
dnsConfig: {
provider: result[settings.DNS_CONFIG_KEY].provider,
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
},
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
mailRelay: {
provider: result[settings.MAIL_RELAY_KEY].provider
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailCatchAll: {
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
};
var data = {
domain: config.fqdn(),
version: config.version(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
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));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
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));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
callback(null);
});
});
});
@@ -218,12 +245,18 @@ function getBoxUpdate(callback) {
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/boxupdate';
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
// { version, changelog, upgrade, sourceTarballUrl}
callback(null, result.body);
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
callback(null, updateInfo);
});
});
}
@@ -240,10 +273,21 @@ function getAppUpdate(app, callback) {
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
const updateInfo = result.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
// { id, creationDate, manifest }
callback(null, result.body);
callback(null, updateInfo);
});
});
}
@@ -265,3 +309,36 @@ function getAccount(callback) {
});
});
}
function sendFeedback(info, callback) {
assert.strictEqual(typeof info, 'object');
assert.strictEqual(typeof info.email, 'string');
assert.strictEqual(typeof info.displayName, 'string');
assert.strictEqual(typeof info.type, 'string');
assert.strictEqual(typeof info.subject, 'string');
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
}
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null);
});
});
});
}
+252 -150
View File
@@ -8,50 +8,44 @@ exports = module.exports = {
// exported for testing
_reserveHttpPort: reserveHttpPort,
_configureNginx: configureNginx,
_unconfigureNginx: unconfigureNginx,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation
};
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apptask'),
docker = require('./docker.js'),
domains = require('./domains.js'),
DomainsError = domains.DomainsError,
ejs = require('ejs'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
net = require('net'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
shell = require('./shell.js'),
SubdomainError = require('./subdomains.js').SubdomainError,
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
@@ -71,8 +65,26 @@ function initialize(callback) {
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
var prefix = app ? (app.location || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
// updates the app object and the database
function updateApp(app, values, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
if (error) return callback(error);
for (var value in values) {
app[value] = values[value];
}
return callback(null);
});
}
function reserveHttpPort(app, callback) {
@@ -93,23 +105,19 @@ function reserveHttpPort(app, callback) {
});
}
function configureNginx(app, callback) {
function configureReverseProxy(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
certificates.ensureCertificate(app, function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
nginx.configureApp(app, certFilePath, keyFilePath, callback);
});
reverseProxy.configureApp(app, { userId: null, username: 'apptask' }, callback);
}
function unconfigureNginx(app, callback) {
function unconfigureReverseProxy(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// TODO: maybe revoke the cert
nginx.unconfigureApp(app, callback);
reverseProxy.unconfigureApp(app, callback);
}
function createContainer(app, callback) {
@@ -126,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');
@@ -202,13 +224,10 @@ function removeLogrotateConfig(app, callback) {
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], callback);
}
function verifyManifest(app, callback) {
assert.strictEqual(typeof app, 'object');
function verifyManifest(manifest, callback) {
assert.strictEqual(typeof manifest, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Verifying manifest');
var manifest = app.manifest;
var error = manifestFormat.parse(manifest);
if (error) return callback(new Error(util.format('Manifest error: %s', error.message)));
@@ -254,66 +273,118 @@ function registerSubdomain(app, overwrite, callback) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.location, overwrite);
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
// get the current record before updating it
subdomains.get(app.location, 'A', function (error, values) {
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
// note that the appstore sets up the naked domain for non-custom domains
if (config.isCustomDomain() && values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
subdomains.upsert(app.location, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
// dnsRecordId tracks whether we created this DNS record so that we can unregister later
updateApp(app, { dnsRecordId: result }, callback);
callback(null);
});
});
}
function unregisterSubdomain(app, location, callback) {
function unregisterSubdomain(app, location, domain, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// do not unregister bare domain because we show a error/cloudron info page there
if (!config.isCustomDomain() && location === '') {
debugApp(app, 'Skip unregister of empty subdomain');
return callback(null);
}
if (!app.dnsRecordId) {
debugApp(app, 'Skip unregister of record not created by cloudron');
return callback(null);
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', location);
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
subdomains.remove(location, 'A', [ ip ], function (error) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: null }, callback);
callback(null);
});
});
}
function registerAlternateDomains(app, overwrite, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (domain, callback) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering alternate subdomain [%s] overwrite: %s', (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain, overwrite);
// get the current record before updating it
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
});
}, callback);
});
}
function unregisterAlternateDomains(app, all, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof all, 'boolean');
assert.strictEqual(typeof callback, 'function');
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; }); });
if (obsoleteDomains.length === 0) return callback();
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(obsoleteDomains, function (domain, callback) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
});
}, callback);
});
}
function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -324,6 +395,16 @@ function removeIcon(app, callback) {
});
}
function cleanupLogs(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
callback(null);
});
}
function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -336,43 +417,14 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback);
});
}
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
sysinfo.getPublicIp(function (error, ip) {
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
if (error) return callback(error);
subdomains.waitForDns(app.altDomain, ip, 'A', { interval: 10000, times: 60 }, callback);
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, iteratorCallback);
}, callback);
});
} else {
subdomains.waitForDns(app.altDomain, config.appFqdn(app.location) + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
}
// updates the app object and the database
function updateApp(app, values, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
if (error) return callback(error);
for (var value in values) {
app[value] = values[value];
}
return callback(null);
});
}
@@ -386,24 +438,33 @@ function updateApp(app, values, callback) {
// - setup addons (requires the above volume)
// - setup the container (requires image, volumes, addons)
// - setup collectd (requires container id)
// restore is also handled here since restore is just an install with some oldConfig to clean up
function install(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const backupId = app.lastBackupId, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
const restoreConfig = app.restoreConfig, isRestoring = app.installationState === appdb.ISTATE_PENDING_RESTORE;
async.series([
verifyManifest.bind(null, app),
// this protects against the theoretical possibility of an app being marked for install/restore from
// a previous version of box code
verifyManifest.bind(null, app.manifest),
// teardown for re-installs
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
unconfigureReverseProxy.bind(null, app),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
// oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
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
? app.manifest.addons
: _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
// for restore case
@@ -421,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),
@@ -428,7 +492,7 @@ function install(app, callback) {
createVolume.bind(null, app),
function restoreFromBackup(next) {
if (!backupId) {
if (!restoreConfig) {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
@@ -436,7 +500,7 @@ function install(app, callback) {
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
backups.restoreApp.bind(null, app, app.manifest.addons, backupId),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
], next);
}
},
@@ -455,11 +519,8 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
// done!
function (callback) {
@@ -479,10 +540,9 @@ function backup(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, app.manifest),
backups.backupApp.bind(null, app),
// done!
function (callback) {
@@ -504,28 +564,34 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && app.oldConfig.location !== app.location;
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
unconfigureNginx.bind(null, app),
unconfigureReverseProxy.bind(null, app),
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();
unregisterSubdomain(app, app.oldConfig.location, next);
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
},
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, true /* overwrite */), // figure out when to overwrite
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
@@ -550,11 +616,8 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
configureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
// done!
function (callback) {
@@ -575,51 +638,80 @@ function update(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'Updating to %s', safe.query(app, 'manifest.version'));
debugApp(app, `Updating to ${app.updateConfig.manifest.version}`);
// app does not want these addons anymore
// FIXME: this does not handle option changes (like multipleDatabases)
var unusedAddons = _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
var unusedAddons = _.omit(app.manifest.addons, Object.keys(app.updateConfig.manifest.addons));
async.series([
// this protects against the theoretical possibility of an app being marked for update from
// a previous version of box code
updateApp.bind(null, app, { installationProgress: '0, Verify manifest' }),
verifyManifest.bind(null, app),
// download new image before app is stopped. this is so we can reduce downtime
// and also not remove the 'common' layers when the old image is deleted
updateApp.bind(null, app, { installationProgress: '15, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
updateApp.bind(null, app, { installationProgress: '25, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
function deleteImageIfChanged(done) {
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
docker.deleteImage(app.oldConfig.manifest, done);
},
verifyManifest.bind(null, app.updateConfig.manifest),
function (next) {
if (app.installationState === appdb.ISTATE_PENDING_FORCE_UPDATE) return next(null);
async.series([
updateApp.bind(null, app, { installationProgress: '30, Backing up app' }),
backups.backupApp.bind(null, app, app.oldConfig.manifest)
], next);
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app)
], function (error) {
if (error) error.backupError = true;
next(error);
});
},
// download new image before app is stopped. this is so we can reduce downtime
// and also not remove the 'common' layers when the old image is deleted
updateApp.bind(null, app, { installationProgress: '25, Downloading image' }),
docker.downloadImage.bind(null, app.updateConfig.manifest),
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
updateApp.bind(null, app, { installationProgress: '35, Cleaning up old install' }),
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
docker.deleteImage(app.manifest, done);
},
// only delete unused addons after backup
addons.teardownAddons.bind(null, app, unusedAddons),
// free unused ports
function (next) {
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 (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
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);
// also delete from app object for further processing (the db is updated in the next step)
delete app.portBindings[portName];
callback();
});
}, next);
},
// switch over to the new config. manifest, memoryLimit, portBindings, appstoreId are updated here
updateApp.bind(null, app, app.updateConfig),
updateApp.bind(null, app, { installationProgress: '45, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '70, Updating addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.setupAddons.bind(null, app, app.updateConfig.manifest.addons),
updateApp.bind(null, app, { installationProgress: '80, Creating container' }),
createContainer.bind(null, app),
@@ -635,14 +727,18 @@ function update(app, callback) {
// done!
function (callback) {
debugApp(app, 'updated');
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null }, callback);
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null, updateTime: new Date() }, callback);
}
], function seriesDone(error) {
if (error) {
if (error && error.backupError) {
debugApp(app, 'update aborted because backup failed', error);
updateApp(app, { installationState: appdb.ISTATE_INSTALLED, installationProgress: '', health: null, updateConfig: null }, callback.bind(null, error));
} else if (error) {
debugApp(app, 'Error updating app: %s', error);
return updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message }, callback.bind(null, error));
updateApp(app, { installationState: appdb.ISTATE_ERROR, installationProgress: error.message, updateTime: new Date() }, callback.bind(null, error));
} else {
callback(null);
}
callback(null);
});
}
@@ -674,14 +770,18 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
unregisterSubdomain.bind(null, app, app.location),
updateApp.bind(null, app, { installationProgress: '60, Unregistering domains' }),
unregisterAlternateDomains.bind(null, app, true /* all */),
unregisterSubdomain.bind(null, app, app.location, app.domain),
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '80, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Cleanup logs' }),
cleanupLogs.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
@@ -739,7 +839,7 @@ function startTask(appId, callback) {
assert.strictEqual(typeof callback, 'function');
// determine what to do
appdb.get(appId, function (error, app) {
apps.get(appId, function (error, app) {
if (error) return callback(error);
debugApp(app, 'startTask installationState: %s runState: %s', app.installationState, app.runState);
@@ -771,6 +871,8 @@ function startTask(appId, callback) {
if (require.main === module) {
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
// add a separator for the log file
debug('------------------------------------------------------------');
debug('Apptask for %s', process.argv[2]);
process.on('SIGTERM', function () {
-125
View File
@@ -1,125 +0,0 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
accessTokenAuth: accessTokenAuth
};
var assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clients = require('./clients'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
DatabaseError = require('./databaseerror'),
debug = require('debug')('box:auth'),
LocalStrategy = require('passport-local').Strategy,
crypto = require('crypto'),
passport = require('passport'),
tokendb = require('./tokendb'),
user = require('./user'),
UserError = user.UserError,
_ = require('underscore');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
passport.deserializeUser(function(userId, callback) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.alternateEmail || result.email).digest('hex');
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
callback(null, result);
});
});
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
});
} else {
user.verifyWithEmail(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
// username is actually client id here
// password is client secret
clients.get(username, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret != password) return callback(null, false);
return callback(null, client);
});
} else {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
}
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) { return callback(error); }
if (client.clientSecret != clientSecret) { return callback(null, false); }
return callback(null, client);
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
function accessTokenAuth(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
});
});
}
+7 -7
View File
@@ -6,7 +6,7 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'restoreConfigJson', 'format' ];
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format' ];
exports = module.exports = {
add: add,
@@ -34,8 +34,8 @@ function postProcess(result) {
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
result.restoreConfig = result.restoreConfigJson ? safe.JSON.parse(result.restoreConfigJson) : null;
delete result.restoreConfigJson;
result.manifest = result.manifestJson ? safe.JSON.parse(result.manifestJson) : null;
delete result.manifestJson;
}
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
@@ -109,15 +109,15 @@ function add(backup, callback) {
assert.strictEqual(typeof backup.version, 'string');
assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX);
assert(util.isArray(backup.dependsOn));
assert.strictEqual(typeof backup.restoreConfig, 'object');
assert.strictEqual(typeof backup.manifest, 'object');
assert.strictEqual(typeof backup.format, 'string');
assert.strictEqual(typeof callback, 'function');
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
var restoreConfig = backup.restoreConfig ? JSON.stringify(backup.restoreConfig) : '';
var manifestJson = JSON.stringify(backup.manifest);
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, restoreConfigJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), restoreConfig, backup.format ],
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), manifestJson, backup.format ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+384 -166
View File
@@ -8,18 +8,19 @@ exports = module.exports = {
getByStatePaged: getByStatePaged,
getByAppIdPaged: getByAppIdPaged,
getRestoreConfig: getRestoreConfig,
get: get,
ensureBackup: ensureBackup,
backup: backup,
restore: restore,
backupApp: backupApp,
restoreApp: restoreApp,
backupBoxAndApps: backupBoxAndApps,
upload: upload,
download: download,
cleanup: cleanup,
cleanupCacheFilesSync: cleanupCacheFilesSync,
@@ -28,8 +29,8 @@ exports = module.exports = {
_getBackupFilePath: getBackupFilePath,
_createTarPackStream: createTarPackStream,
_tarExtract: tarExtract,
_createEmptyDirs: createEmptyDirs,
_saveEmptyDirs: saveEmptyDirs
_restoreFsMetadata: restoreFsMetadata,
_saveFsMetadata: saveFsMetadata
};
var addons = require('./addons.js'),
@@ -41,6 +42,7 @@ var addons = require('./addons.js'),
backupdb = require('./backupdb.js'),
config = require('./config.js'),
crypto = require('crypto'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:backups'),
eventlog = require('./eventlog.js'),
@@ -56,6 +58,7 @@ var addons = require('./addons.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
syncer = require('./syncer.js'),
tar = require('tar-fs'),
util = require('util'),
@@ -66,10 +69,9 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var BACKUPTASK_CMD = path.join(__dirname, 'backuptask.js');
function debugApp(app) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? app.location : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function BackupsError(reason, errorOrMessage) {
@@ -102,6 +104,7 @@ function api(provider) {
switch (provider) {
case 'caas': return require('./storage/s3.js');
case 's3': return require('./storage/s3.js');
case 'gcs': return require('./storage/gcs.js');
case 'filesystem': return require('./storage/filesystem.js');
case 'minio': return require('./storage/s3.js');
case 's3-v4-compat': return require('./storage/s3.js');
@@ -121,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);
}
@@ -150,16 +156,15 @@ function getByAppIdPaged(page, perPage, appId, callback) {
});
}
function getRestoreConfig(backupId, callback) {
function get(backupId, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
backupdb.get(backupId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (!result.restoreConfig) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
callback(null, result.restoreConfig);
callback(null, result);
});
}
@@ -176,6 +181,97 @@ function getBackupFilePath(backupConfig, backupId, format) {
}
}
function log(detail) {
safe.fs.appendFileSync(paths.BACKUP_LOG_FILE, detail + '\n', 'utf8');
progress.setDetail(progress.BACKUP, detail);
}
function encryptFilePath(filePath, key) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof key, 'string');
var encryptedParts = filePath.split('/').map(function (part) {
const cipher = crypto.createCipher('aes-256-cbc', key);
let crypt = cipher.update(part);
crypt = Buffer.concat([ crypt, cipher.final() ]);
return crypt.toString('base64') // ensures path is valid
.replace(/\//g, '-') // replace '/' of base64 since it conflicts with path separator
.replace(/=/g,''); // strip trailing = padding. this is only needed if we concat base64 strings, which we don't
});
return encryptedParts.join('/');
}
function decryptFilePath(filePath, key) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof key, 'string');
let decryptedParts = [];
for (let part of filePath.split('/')) {
part = part + Array(part.length % 4).join('='); // add back = padding
part = part.replace(/-/g, '/'); // replace with '/'
try {
let decrypt = crypto.createDecipher('aes-256-cbc', key);
let text = decrypt.update(Buffer.from(part, 'base64'));
text = Buffer.concat([ text, decrypt.final() ]);
decryptedParts.push(text.toString('utf8'));
} catch (error) {
debug(`Error decrypting file ${filePath} part ${part}:`, error);
return null;
}
}
return decryptedParts.join('/');
}
function createReadStream(sourceFile, key) {
assert.strictEqual(typeof sourceFile, 'string');
assert(key === null || typeof key === 'string');
var stream = fs.createReadStream(sourceFile);
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
stream.on('error', function (error) {
debug('createReadStream: tar stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
ps.on('progress', function(progress) {
debug('createReadStream: %s@%s (%s)', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps', sourceFile);
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('createReadStream: encrypt stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
return stream.pipe(encrypt).pipe(ps);
} else {
return stream.pipe(ps);
}
}
function createWriteStream(destFile, key) {
assert.strictEqual(typeof destFile, 'string');
assert(key === null || typeof key === 'string');
var stream = fs.createWriteStream(destFile);
if (key !== null) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('createWriteStream: decrypt stream error.', error);
});
decrypt.pipe(stream);
return decrypt;
} else {
return stream;
}
}
function createTarPackStream(sourceDir, key) {
assert.strictEqual(typeof sourceDir, 'string');
assert(key === null || typeof key === 'string');
@@ -225,38 +321,70 @@ function sync(backupConfig, backupId, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug('%s', message);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, message);
}
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
var backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), task.path);
// the empty task.path is special to signify the directory
const destPath = task.path && backupConfig.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
const backupFilePath = path.join(getBackupFilePath(backupConfig, backupId, backupConfig.format), destPath);
if (task.operation === 'add') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, 'Adding ' + task.path);
var stream = fs.createReadStream(path.join(dataDir, task.path));
stream.on('error', function () { return iteratorCallback(); }); // ignore error if file disappears
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, iteratorCallback);
if (task.operation === 'removedir') {
setBackupProgress(`Removing directory ${backupFilePath}`);
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
.on('progress', setBackupProgress)
.on('done', iteratorCallback);
} else if (task.operation === 'remove') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, 'Removing ' + task.path);
api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
} else if (task.operation === 'removedir') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, 'Removing directory ' + task.path);
api(backupConfig.provider).removeDir(backupConfig, backupFilePath, iteratorCallback);
setBackupProgress(`Removing ${backupFilePath}`);
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
}
}, 10 /* concurrency */, function (error) {
var retryCount = 0;
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
debug(`${task.operation} ${task.path} try ${retryCount}`);
if (task.operation === 'add') {
setBackupProgress(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
stream.on('error', function (error) {
setBackupProgress(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
setBackupProgress(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
retryCallback(error);
});
}
}, iteratorCallback);
}, backupConfig.syncConcurrency || 10 /* concurrency */, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback();
});
}
function saveEmptyDirs(appDataDir, callback) {
function saveFsMetadata(appDataDir, callback) {
assert.strictEqual(typeof appDataDir, 'string');
assert.strictEqual(typeof callback, 'function');
var emptyDirs = safe.child_process.execSync('find . -type d -empty', { cwd: `${appDataDir}` });
var emptyDirs = safe.child_process.execSync('find . -type d -empty', { cwd: `${appDataDir}`, encoding: 'utf8' });
if (emptyDirs === null) return callback(safe.error);
if (!safe.fs.writeFileSync(`${appDataDir}/emptydirs.txt`, emptyDirs)) return callback(safe.error);
var execFiles = safe.child_process.execSync('find . -type f -executable', { cwd: `${appDataDir}`, encoding: 'utf8' });
if (execFiles === null) return callback(safe.error);
var metadata = {
emptyDirs: emptyDirs.length === 0 ? [ ] : emptyDirs.trim().split('\n'),
execFiles: execFiles.length === 0 ? [ ] : execFiles.trim().split('\n')
};
if (!safe.fs.writeFileSync(`${appDataDir}/fsmetadata.json`, JSON.stringify(metadata, null, 4))) return callback(safe.error);
callback();
}
@@ -267,20 +395,23 @@ function upload(backupId, format, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
debug('upload: id %s format %s dataDir %s', backupId, format, dataDir);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (format === 'tgz') {
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('error', callback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, callback);
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
}, callback);
} else {
async.series([
saveEmptyDirs.bind(null, dataDir),
saveFsMetadata.bind(null, dataDir),
sync.bind(null, backupConfig, backupId, dataDir)
], callback);
}
@@ -327,7 +458,7 @@ function tarExtract(inStream, destination, key, callback) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('tarExtract: decrypt stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
});
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
} else {
@@ -335,43 +466,138 @@ function tarExtract(inStream, destination, key, callback) {
}
}
function createEmptyDirs(appDataDir, callback) {
function restoreFsMetadata(appDataDir, callback) {
assert.strictEqual(typeof appDataDir, 'string');
assert.strictEqual(typeof callback, 'function');
debug('createEmptyDirs: recreating empty directories');
log('Recreating empty directories');
var emptyDirs = safe.fs.readFileSync(path.join(appDataDir, 'emptydirs.txt'), 'utf8');
if (emptyDirs === null) return callback(new Error('emptydirs.txt was not found:' + safe.error.message));
var metadataJson = safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8');
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message));
var metadata = safe.JSON.parse(metadataJson);
if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error parsing fsmetadata.txt:' + safe.error.message));
async.eachSeries(emptyDirs.trim().split('\n'), function createPath(emptyDir, iteratorDone) {
async.eachSeries(metadata.emptyDirs, function createPath(emptyDir, iteratorDone) {
mkdirp(path.join(appDataDir, emptyDir), iteratorDone);
}, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to create path: ${error.message}`));
async.eachSeries(metadata.execFiles, function createPath(execFile, iteratorDone) {
fs.chmod(path.join(appDataDir, execFile), parseInt('0755', 8), iteratorDone);
}, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to chmod: ${error.message}`));
callback();
});
});
}
function downloadDir(backupConfig, backupFilePath, destDir, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
assert.strictEqual(typeof destDir, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
function downloadFile(entry, callback) {
let relativePath = path.relative(backupFilePath, entry.fullPath);
if (backupConfig.key) {
relativePath = decryptFilePath(relativePath, backupConfig.key);
if (!relativePath) return callback(new BackupsError(BackupsError.BAD_STATE, 'Unable to decrypt file'));
}
const destFilePath = path.join(destDir, relativePath);
mkdirp(path.dirname(destFilePath), function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
if (error) return callback(error);
sourceStream.on('error', callback);
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
destStream.on('error', callback);
debug(`downloadDir: Copying ${entry.fullPath} to ${destFilePath}`);
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
});
});
}
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) {
async.each(entries, downloadFile, done);
}, callback);
}
function download(backupId, format, dataDir, callback) {
function download(backupConfig, backupId, format, dataDir, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
debug('download: id %s dataDir %s format %s', backupId, dataDir, format);
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
log(`Downloading ${backupId} of format ${format} to ${dataDir}`);
if (format === 'tgz') {
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
if (error) return callback(error);
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
});
} else {
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, function (error) {
if (error) return callback(error);
restoreFsMetadata(dataDir, callback);
});
}
}
function restore(backupConfig, backupId, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
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();
});
});
}
function restoreApp(app, addonsToRestore, restoreConfig, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof restoreConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
var startTime = new Date();
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
if (format === 'tgz') {
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
if (error) return callback(error);
async.series([
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir),
addons.restoreAddons.bind(null, app, addonsToRestore)
], function (error) {
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
});
} else {
async.series([
api(backupConfig.provider).downloadDir.bind(null, backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir),
createEmptyDirs.bind(null, dataDir)
], callback);
}
callback(error);
});
});
}
@@ -383,7 +609,8 @@ function runBackupTask(backupId, format, dataDir, callback) {
var killTimerId = null, progressTimerId = null;
var cp = shell.sudo(`backup-${backupId}`, [ BACKUPTASK_CMD, backupId, format, dataDir ], { env: process.env, logFile: paths.BACKUP_LOG_FILE }, function (error) {
var logStream = fs.createWriteStream(paths.BACKUP_LOG_FILE, { flags: 'a' });
var cp = shell.sudo(`backup-${backupId}`, [ BACKUPTASK_CMD, backupId, format, dataDir ], { env: process.env, logStream: logStream }, function (error) {
clearTimeout(killTimerId);
clearInterval(progressTimerId);
@@ -408,6 +635,11 @@ function runBackupTask(backupId, format, dataDir, callback) {
debug('runBackupTask: backup task taking too long. killing');
cp.kill();
}, 4 * 60 * 60 * 1000); // 4 hours
logStream.on('error', function (error) {
debug('runBackupTask: error in logging stream', error);
cp.kill();
});
}
function getSnapshotInfo(id) {
@@ -435,13 +667,9 @@ function setSnapshotInfo(id, info, callback) {
function snapshotBox(callback) {
assert.strictEqual(typeof callback, 'function');
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var mysqlDumpArgs = [
'-c',
`/usr/bin/mysqldump -u root ${password} --single-transaction --routines \
--triggers ${config.database().name} > "${paths.BOX_DATA_DIR}/box.mysqldump"`
];
shell.exec('backupBox', '/bin/bash', mysqlDumpArgs, { }, function (error) {
log('Snapshotting box');
database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
return callback();
@@ -457,20 +685,44 @@ function uploadBoxSnapshot(backupConfig, callback) {
snapshotBox(function (error) {
if (error) return callback(error);
// for the moment, box backups are always tarball based. this is because it makes it easy to restore
// in the future, if required, we can move out the mailboxes to a separate virtual app backup
const format = 'tgz';
runBackupTask('snapshot/box', format, paths.BOX_DATA_DIR, function (error) {
runBackupTask('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, function (error) {
if (error) return callback(error);
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
setSnapshotInfo('box', { timestamp: new Date().toISOString(), format: format }, callback);
setSnapshotInfo('box', { timestamp: new Date().toISOString(), format: backupConfig.format }, callback);
});
});
}
function backupDone(apiConfig, backupId, appBackupIds, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
if (apiConfig.provider !== 'caas') return callback();
debug('[%s] backupDone: %s apps %j', backupId, backupId, appBackupIds);
var url = config.apiServerOrigin() + '/api/v1/boxes/' + apiConfig.fqdn + '/backupDone';
var data = {
boxVersion: config.version(),
backupId: backupId,
appId: null, // now unused
appVersion: null, // now unused
appBackupIds: appBackupIds
};
superagent.post(url).send(data).query({ token: apiConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
return callback(null);
});
}
function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof timestamp, 'string');
@@ -482,29 +734,25 @@ function rotateBoxBackup(backupConfig, timestamp, appBackupIds, callback) {
var snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,'');
var backupId = util.format('%s/box_%s_v%s', timestamp, snapshotTime, config.version());
const format = 'tgz';
const format = backupConfig.format;
debug('rotateBoxBackup: rotating to id:%s', backupId);
log(`Rotating box backup to id ${backupId}`);
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, restoreConfig: null, format: format }, function (error) {
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, manifest: null, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
progress.setDetail(progress.BACKUP, 'Rotating box snapshot');
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, 'snapshot/box', format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', function (detail) { progress.setDetail(progress.BACKUP, detail); });
copy.on('progress', log);
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
backupdb.update(backupId, { state: state }, function (error) {
if (copyBackupError) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, copyBackupError.message));
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('rotateBoxBackup: successful id:%s', backupId);
log(`Rotated box backup successfully as id ${backupId}`);
// FIXME this is only needed for caas, hopefully we can remove that in the future
api(backupConfig.provider).backupDone(backupConfig, backupId, appBackupIds, function (error) {
backupDone(backupConfig, backupId, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, backupId);
@@ -539,34 +787,19 @@ function canBackupApp(app) {
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
}
function snapshotApp(app, manifest, callback) {
function snapshotApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var restoreConfig = apps.getAppConfig(app);
restoreConfig.manifest = manifest;
log(`Snapshotting app ${app.id}`);
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(restoreConfig))) {
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
}
addons.backupAddons(app, manifest.addons, function (error) {
addons.backupAddons(app, app.manifest.addons, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
return callback(null, restoreConfig);
});
}
function setRestorePoint(appId, lastBackupId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof lastBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
appdb.update(appId, { lastBackupId: lastBackupId }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app'));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
return callback(null);
});
}
@@ -581,50 +814,42 @@ function rotateAppBackup(backupConfig, app, timestamp, callback) {
if (!snapshotInfo) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Snapshot info missing or corrupt'));
var snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,'');
var restoreConfig = snapshotInfo.restoreConfig;
var manifest = restoreConfig.manifest;
var manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
var backupId = util.format('%s/app_%s_%s_v%s', timestamp, app.id, snapshotTime, manifest.version);
const format = backupConfig.format;
debugApp(app, 'rotateAppBackup: rotating to id:%s', backupId);
log(`Rotating app backup of ${app.id} to id ${backupId}`);
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig, format: format }, function (error) {
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], manifest: manifest, format: format }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
progress.setDetail(progress.BACKUP, 'Rotating app snapshot');
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', function (detail) { progress.setDetail(progress.BACKUP, detail); });
copy.on('progress', log);
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
debugApp(app, 'rotateAppBackup: successful id:%s', backupId);
backupdb.update(backupId, { state: state }, function (error) {
if (copyBackupError) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, copyBackupError.message));
if (copyBackupError) return callback(copyBackupError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
setRestorePoint(app.id, backupId, function (error) {
if (error) return callback(error);
log(`Rotated app backup of ${app.id} successfully to id ${backupId}`);
return callback(null, backupId);
});
callback(null, backupId);
});
});
});
}
function uploadAppSnapshot(backupConfig, app, manifest, callback) {
function uploadAppSnapshot(backupConfig, app, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
if (!canBackupApp(app)) return callback(); // nothing to do
var startTime = new Date();
snapshotApp(app, manifest, function (error, restoreConfig) {
snapshotApp(app, function (error) {
if (error) return callback(error);
var backupId = util.format('snapshot/app_%s', app.id);
@@ -634,14 +859,13 @@ function uploadAppSnapshot(backupConfig, app, manifest, callback) {
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), restoreConfig: restoreConfig, format: backupConfig.format }, callback);
setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: app.manifest, format: backupConfig.format }, callback);
});
});
}
function backupAppWithTimestamp(app, manifest, timestamp, callback) {
function backupAppWithTimestamp(app, timestamp, callback) {
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof timestamp, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -650,7 +874,7 @@ function backupAppWithTimestamp(app, manifest, timestamp, callback) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
uploadAppSnapshot(backupConfig, app, manifest, function (error) {
uploadAppSnapshot(backupConfig, app, function (error) {
if (error) return callback(error);
rotateAppBackup(backupConfig, app, timestamp, callback);
@@ -658,16 +882,16 @@ function backupAppWithTimestamp(app, manifest, timestamp, callback) {
});
}
function backupApp(app, manifest, callback) {
function backupApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
const timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
progress.set(progress.BACKUP, 10, 'Backing up ' + (app.altDomain || config.appFqdn(app.location)));
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
backupAppWithTimestamp(app, manifest, timestamp, function (error) {
backupAppWithTimestamp(app, timestamp, function (error) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
callback(error);
@@ -681,6 +905,7 @@ function backupBoxAndApps(auditSource, callback) {
callback = callback || NOOP_CALLBACK;
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
safe.fs.unlinkSync(paths.BACKUP_LOG_FILE); // start fresh log file
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
@@ -691,22 +916,22 @@ function backupBoxAndApps(auditSource, callback) {
var step = 100/(allApps.length+2);
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
progress.set(progress.BACKUP, step * processed, 'Backing up ' + (app.altDomain || config.appFqdn(app.location)));
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
++processed;
if (!app.enableBackup) {
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || config.appFqdn(app.location)));
return iteratorCallback(null, app.lastBackupId); // just use the last backup
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
return iteratorCallback(null, null); // nothing to backup
}
backupAppWithTimestamp(app, app.manifest, timestamp, function (error, backupId) {
backupAppWithTimestamp(app, timestamp, function (error, backupId) {
if (error && error.reason !== BackupsError.BAD_STATE) {
debugApp(app, 'Unable to backup', error);
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || config.appFqdn(app.location)));
progress.set(progress.BACKUP, step * processed, 'Backed up ' + app.fqdn);
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
});
@@ -720,12 +945,12 @@ function backupBoxAndApps(auditSource, callback) {
progress.set(progress.BACKUP, step * processed, 'Backing up system data');
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, filename) {
backupBoxWithAppBackupIds(backupIds, timestamp, function (error, backupId) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, backupId: backupId, timestamp: timestamp });
callback(error, filename);
callback(error, backupId);
});
});
});
@@ -763,40 +988,18 @@ 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);
}
function restoreApp(app, addonsToRestore, backupId, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addonsToRestore, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof callback, 'function');
assert(app.lastBackupId);
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
var startTime = new Date();
backupdb.get(backupId, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, error));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.series([
download.bind(null, backupId, result.format, appDataDir),
addons.restoreAddons.bind(null, app, addonsToRestore)
], function (error) {
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
callback(error);
backup(auditSource, callback);
});
});
}
@@ -806,13 +1009,12 @@ function cleanupBackup(backupConfig, backup, callback) {
assert.strictEqual(typeof backup, 'object');
assert.strictEqual(typeof callback, 'function');
var removeFunc = backup.format ==='tgz' ? api(backupConfig.provider).remove : api(backupConfig.provider).removeDir;
var backupFilePath = getBackupFilePath(backupConfig, backup.id, backup.format);
removeFunc(backupConfig, backupFilePath, function (error) {
function done(error) {
if (error) {
debug('cleanupBackup: error removing backup %j : %s', backup, error.message);
callback();
return callback();
}
// prune empty directory if possible
@@ -826,7 +1028,15 @@ function cleanupBackup(backupConfig, backup, callback) {
callback();
});
});
});
}
if (backup.format ==='tgz') {
api(backupConfig.provider).remove(backupConfig, backupFilePath, done);
} else {
var events = api(backupConfig.provider).removeDir(backupConfig, backupFilePath);
events.on('progress', function (detail) { debug(`cleanupBackup: ${detail}`); });
events.on('done', done);
}
}
function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
@@ -925,8 +1135,7 @@ function cleanupSnapshots(backupConfig, callback) {
apps.get(appId, function (error /*, app */) {
if (!error || error.reason !== AppsError.NOT_FOUND) return iteratorDone();
var removeFunc = info[appId].format ==='tgz' ? api(backupConfig.provider).remove : api(backupConfig.provider).removeDir;
removeFunc(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${appId}`, info[appId].format), function (/* ignoredError */) {
function done(/* ignoredError */) {
safe.fs.unlinkSync(path.join(paths.BACKUP_INFO_DIR, `${appId}.sync.cache`));
safe.fs.unlinkSync(path.join(paths.BACKUP_INFO_DIR, `${appId}.sync.cache.new`));
@@ -935,7 +1144,15 @@ function cleanupSnapshots(backupConfig, callback) {
iteratorDone();
});
});
}
if (info[appId].format ==='tgz') {
api(backupConfig.provider).remove(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${appId}`, info[appId].format), done);
} else {
var events = api(backupConfig.provider).removeDir(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${appId}`, info[appId].format));
events.on('progress', function (detail) { debug(`cleanupSnapshots: ${detail}`); });
events.on('done', done);
}
});
}, function () {
debug('cleanupSnapshots: done');
@@ -969,3 +1186,4 @@ function cleanup(auditSource, callback) {
});
});
}
+2 -2
View File
@@ -44,9 +44,9 @@ initialize(function (error) {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
backups.upload(backupId, format, dataDir, function resultHandler(error) {
if (error) debug('completed with error', error);
if (error) debug('upload completed with error', error);
debug('completed');
debug('upload completed');
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
+266
View File
@@ -0,0 +1,266 @@
'use strict';
exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord,
CaasError: CaasError
};
var assert = require('assert'),
backups = require('./backups.js'),
config = require('./config.js'),
debug = require('debug')('box:caas'),
locker = require('./locker.js'),
path = require('path'),
progress = require('./progress.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
function CaasError(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(CaasError, Error);
CaasError.BAD_FIELD = 'Field error';
CaasError.BAD_STATE = 'Bad state';
CaasError.INVALID_TOKEN = 'Invalid Token';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function retire(reason, info, callback) {
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, result) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function verifySetupToken(setupToken, callback) {
assert.strictEqual(typeof setupToken, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, caasConfig) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/verify').query({ setupToken: setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
callback(null);
});
});
}
function setupDone(setupToken, callback) {
assert.strictEqual(typeof setupToken, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getCaasConfig(function (error, caasConfig) {
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/done').query({ setupToken: setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
if (result.statusCode !== 201) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
callback(null);
});
});
}
function doMigrate(options, caasConfig, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof caasConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));
function unlock(error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
}
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
if (error) return unlock(error);
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
}
function changePlan(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
}
// this function expects a lock
function upgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
.query({ token: result.token })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
});
}
function sendHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
getCaasConfig(function (error, result) {
if (error) return debug('Caas config missing', error);
var url = config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/heartbeat';
superagent.post(url).query({ token: result.token, version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(null, {});
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
.query({ token: caasConfig.token })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, result.body);
});
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCaasConfig(function (error, result) {
if (error) return callback(error);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/ptr')
.query({ token: result.token })
.send({ domain: domain })
.timeout(5 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null);
});
});
}
+65 -63
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'),
@@ -16,7 +16,7 @@ var assert = require('assert'),
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf';
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
exports = module.exports = {
getCertificate: getCertificate,
@@ -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(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');
debug('getCertificate: using fallback certificate', domain);
debug('getCertificate: using fallback certificate', hostname);
return callback(null, 'cert/host.cert', 'cert/host.key');
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(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');
debug('getCertificate: using fallback certificate', domain);
debug('getCertificate: using fallback certificate', hostname);
return callback(null, 'cert/host.cert', 'cert/host.key');
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');
-433
View File
@@ -1,433 +0,0 @@
'use strict';
exports = module.exports = {
CertificatesError: CertificatesError,
ensureFallbackCertificate: ensureFallbackCertificate,
setFallbackCertificate: setFallbackCertificate,
validateCertificate: validateCertificate,
ensureCertificate: ensureCertificate,
setAdminCertificate: setAdminCertificate,
getAdminCertificate: getAdminCertificate,
renewAll: renewAll,
initialize: initialize,
uninitialize: uninitialize,
events: null,
EVENT_CERT_CHANGED: 'cert_changed',
// exported for testing
_getApi: getApi
};
var acme = require('./cert/acme.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
caas = require('./cert/caas.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:certificates'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
user = require('./user.js'),
util = require('util');
function CertificatesError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(CertificatesError, Error);
CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = new (require('events').EventEmitter)();
callback();
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = null;
callback();
}
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
settings.getTlsConfig(function (error, tlsConfig) {
if (error) return callback(error);
if (tlsConfig.provider === 'fallback') return callback(null, fallback, {});
// use acme if we have altDomain or the tlsConfig is not caas
var api = (app.altDomain || tlsConfig.provider) !== 'caas' ? acme : caas;
var options = { };
if (tlsConfig.provider === 'caas') {
options.prod = true; // with altDomain, we will choose acme setting based on this
} else { // acme
options.prod = tlsConfig.provider.match(/.*-prod/) !== null;
}
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
// we cannot use admin@fqdn because the user might not have set it up.
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
user.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.alternateEmail || owner.email); // can error if not activated yet
callback(null, api, options);
});
});
}
function ensureFallbackCertificate(callback) {
// ensure a fallback certificate that much of our code requires
var certFilePath = path.join(paths.APP_CERTS_DIR, 'host.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, 'host.key');
var fallbackCertPath = path.join(paths.NGINX_CERT_DIR, 'host.cert');
var fallbackKeyPath = path.join(paths.NGINX_CERT_DIR, 'host.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) { // existing custom fallback certs (when restarting, restoring, updating)
debug('ensureFallbackCertificate: using fallback certs provided by user');
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
}
if (config.tlsCert() && config.tlsKey()) {
// cert from CaaS or cloudron-setup. these files should _not_ be part of the backup
debug('ensureFallbackCertificate: using CaaS/cloudron-setup fallback certs');
if (!safe.fs.writeFileSync(fallbackCertPath, config.tlsCert())) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(fallbackKeyPath, config.tlsKey())) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
}
// generate a self-signed cert. it's in backup dir so that we don't create a new cert across restarts
// FIXME: this cert does not cover the naked domain. needs SAN
if (config.fqdn()) {
debug('ensureFallbackCertificate: generating self-signed certificate');
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=*.%s -nodes', keyFilePath, certFilePath, config.fqdn());
safe.child_process.execSync(certCommand);
if (!safe.child_process.execSync('cp ' + certFilePath + ' ' + fallbackCertPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.child_process.execSync('cp ' + keyFilePath + ' ' + fallbackKeyPath)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
return callback();
} else {
debug('ensureFallbackCertificate: cannot generate fallback certificate without domain');
return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, 'No domain set'));
}
}
function isExpiringSync(certFilePath, hours) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof hours, 'number');
if (!fs.existsSync(certFilePath)) return 2; // not found
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
return result.status === 1; // 1 - expired 0 - not expired
}
function renewAll(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('renewAll: Checking certificates for renewal');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
allApps.push({ location: constants.ADMIN_LOCATION }); // inject fake webadmin app
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i].location);
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.user.key');
if (safe.fs.existsSync(certFilePath) && safe.fs.existsSync(keyFilePath)) {
debug('renewAll: existing user key file for %s. skipping', appDomain);
continue;
}
// check if we have an auto cert to be renewed
certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
if (!safe.fs.existsSync(keyFilePath)) {
debug('renewAll: no existing key file for %s. skipping', appDomain);
continue;
}
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
expiringApps.push(allApps[i]);
}
}
debug('renewAll: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = app.altDomain || config.appFqdn(app.location);
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('renewAll: renewing cert for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error) {
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
var errorMessage = error ? error.message : '';
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
if (error) {
debug('renewAll: could not renew cert for %s because %s', domain, error);
mailer.certificateRenewalError(domain, errorMessage);
// check if we should fallback if we expire in the coming day
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
debug('renewAll: using fallback certs for %s since it expires soon', domain, error);
certFilePath = 'cert/host.cert';
keyFilePath = 'cert/host.key';
} else {
debug('renewAll: certificate for %s renewed', domain);
}
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
var configureFunc = app.location === constants.ADMIN_LOCATION ?
nginx.configureAdmin.bind(null, certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
exports.events.emit(exports.EVENT_CERT_CHANGED, domain);
iteratorCallback(); // move to next app
});
});
});
});
});
}
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
// servers certificate appears first (and not the intermediate cert)
function validateCertificate(cert, key, fqdn) {
assert(cert === null || typeof cert === 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof fqdn, 'string');
function matchesDomain(domain) {
if (typeof domain !== 'string') return false;
if (domain === fqdn) return true;
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
return false;
}
if (cert === null && key === null) return null;
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + fqdn + '"', { encoding: 'utf8', input: cert });
if (!result) return new Error(util.format('could not get cert subject'));
// if no match, check alt names
if (result.indexOf('does match certificate') === -1) {
// https://github.com/drwetter/testssl.sh/pull/383
var cmd = `openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
grep "DNS:" | \
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"`;
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
debug('validateCertificate: detected altNames as %j', altNames);
// check altNames
if (!altNames.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, altNames));
}
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (certModulus !== keyModulus) return new Error('key does not match the cert');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new Error('cert expired');
return null;
}
function setFallbackCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateCertificate(cert, key, '*.' + config.fqdn());
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
// copy over fallback cert
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + config.fqdn());
nginx.reload(function (error) {
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
return callback(null);
});
}
function getFallbackCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
// any user fallback cert is always copied over to nginx cert dir
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
}
function setAdminCertificate(cert, key, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
var error = validateCertificate(cert, key, vhost);
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
// backup the cert
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
exports.events.emit(exports.EVENT_CERT_CHANGED, vhost);
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
}
function getAdminCertificatePath(callback) {
assert.strictEqual(typeof callback, 'function');
var vhost = config.adminFqdn();
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.user.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
getFallbackCertificatePath(callback);
}
function getAdminCertificate(callback) {
assert.strictEqual(typeof callback, 'function');
getAdminCertificatePath(function (error, certFilePath, keyFilePath) {
if (error) return callback(error);
var cert = safe.fs.readFileSync(certFilePath);
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
var key = safe.fs.readFileSync(keyFilePath);
if (!cert) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error));
return callback(null, cert, key);
});
}
function ensureCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var domain = app.altDomain || config.appFqdn(app.location);
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.cert');
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.user.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. user certificate already exists at %s', domain, keyFilePath);
return callback(null, certFilePath, keyFilePath);
}
certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) {
debug('ensureCertificate: %s. certificate already exists at %s', domain, keyFilePath);
if (!isExpiringSync(certFilePath, 24 * 1)) return callback(null, certFilePath, keyFilePath);
debug('ensureCertificate: %s cert require renewal', domain);
} else {
debug('ensureCertificate: %s cert does not exist', domain);
}
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs
}
callback(null, certFilePath, keyFilePath);
});
});
}
+3 -3
View File
@@ -182,8 +182,8 @@ function clear(callback) {
function addDefaultClients(callback) {
async.series([
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', 'cloudron,profile,users,apps,settings'),
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*,roleSdk'),
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*,roleSdk')
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', '*'),
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*'),
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*')
], callback);
}
+73 -65
View File
@@ -8,26 +8,16 @@ exports = module.exports = {
del: del,
getAll: getAll,
getByAppIdAndType: getByAppIdAndType,
getClientTokensByUserId: getClientTokensByUserId,
delClientTokensByUserId: delClientTokensByUserId,
getTokensByUserId: getTokensByUserId,
delTokensByUserId: delTokensByUserId,
delByAppIdAndType: delByAppIdAndType,
addClientTokenByUserId: addClientTokenByUserId,
addTokenByUserId: addTokenByUserId,
delToken: delToken,
issueDeveloperToken: issueDeveloperToken,
addDefaultClients: addDefaultClients,
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
SCOPE_APPS: 'apps',
SCOPE_DEVELOPER: 'developer',
SCOPE_PROFILE: 'profile',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS: 'users',
// roles are handled just like the above scopes, they are parallel to scopes
// scopes enclose API groups, roles specify the usage role
SCOPE_ROLE_SDK: 'roleSdk',
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
@@ -35,15 +25,19 @@ exports = module.exports = {
TYPE_PROXY: 'addon-proxy'
};
var appdb = require('./appdb.js'),
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
hat = require('hat'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
accesscontrol = require('./accesscontrol.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
util = require('util'),
uuid = require('uuid');
@@ -74,35 +68,21 @@ 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');
if (name.length > 128) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
if (/[^a-zA-Z0-9\-]/.test(name)) return new ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
if (/[^a-zA-Z0-9-]/.test(name)) return new ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
return null;
}
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
function validateTokenName(name) {
assert.strictEqual(typeof name, 'string');
var VALID_SCOPES = [
exports.SCOPE_APPS,
exports.SCOPE_DEVELOPER,
exports.SCOPE_PROFILE,
exports.SCOPE_CLOUDRON,
exports.SCOPE_SETTINGS,
exports.SCOPE_USERS,
'*', // includes all scopes, but not roles
exports.SCOPE_ROLE_SDK
];
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
return null;
}
@@ -114,14 +94,10 @@ function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
// allow whitespace
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
var error = accesscontrol.validateScopeString(scope);
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
var error = validateScope(scope);
if (error) return callback(error);
// appId is also client name
error = validateName(appId);
error = validateClientName(appId);
if (error) return callback(error);
var id = 'cid-' + uuid.v4();
@@ -183,7 +159,7 @@ function getAll(callback) {
return callback(null);
}
appdb.get(record.appId, function (error, result) {
apps.get(record.appId, function (error, result) {
if (error) {
console.error('Failed to get app details for oauth client', record.appId, error);
return callback(null); // ignore error so we continue listing clients
@@ -192,7 +168,7 @@ function getAll(callback) {
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
record.location = result.location;
record.domain = result.fqdn;
tmp.push(record);
@@ -217,7 +193,7 @@ function getByAppIdAndType(appId, type, callback) {
});
}
function getClientTokensByUserId(clientId, userId, callback) {
function getTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -236,7 +212,7 @@ function getClientTokensByUserId(clientId, userId, callback) {
});
}
function delClientTokensByUserId(clientId, userId, callback) {
function delTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -276,31 +252,65 @@ function delByAppIdAndType(appId, type, callback) {
});
}
function addClientTokenByUserId(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);
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
users.get(userId, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such user'));
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
identifier: userId,
clientId: result.id,
scope: result.id,
expires: expiresAt
accesscontrol.scopesForUser(user, function (error, userScopes) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
var scope = accesscontrol.canonicalScopeString(result.scope);
var authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
tokenScopes: authorizedScopes,
identifier: userId,
clientId: result.id,
expires: expiresAt
});
});
});
});
});
}
// this issues a cid-cli token that does not require a password in various routes
function issueDeveloperToken(userObject, auditSource, callback) {
assert.strictEqual(typeof userObject, 'object');
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) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
callback(null, result);
});
}
function delToken(clientId, tokenId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof tokenId, 'string');
@@ -318,19 +328,17 @@ function delToken(clientId, tokenId, callback) {
});
}
function addDefaultClients(callback) {
function addDefaultClients(origin, callback) {
assert.strictEqual(typeof origin, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Adding default clients');
// The domain might have changed, therefor we have to update the record
// !!! This needs to be in sync with the webadmin, specifically login_callback.js
const ADMIN_SCOPES="cloudron,developer,profile,users,apps,settings";
// id, appId, type, clientSecret, redirectURI, scope
async.series([
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', config.adminOrigin(), ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', config.adminOrigin(), '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', config.adminOrigin(), '*, roleSdk')
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, '*'),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
], callback);
}
+84 -777
View File
@@ -5,90 +5,42 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
activate: activate,
getConfig: getConfig,
getStatus: getStatus,
getDisks: getDisks,
dnsSetup: dnsSetup,
getLogs: getLogs,
sendHeartbeat: sendHeartbeat,
updateToLatest: updateToLatest,
reboot: reboot,
retire: retire,
migrate: migrate,
checkDiskSpace: checkDiskSpace,
onActivated: onActivated,
readDkimPublicKeySync: readDkimPublicKeySync,
refreshDNS: refreshDNS,
configureWebadmin: configureWebadmin
checkDiskSpace: checkDiskSpace
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
child_process = require('child_process'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
nginx = require('./nginx.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
tokendb = require('./tokendb.js'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
UserError = user.UserError,
user = require('./user.js'),
util = require('util'),
_ = require('underscore');
users = require('./users.js'),
util = require('util');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'),
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// result to not depend on the appstore
const BOX_AND_USER_TEMPLATE = {
box: {
region: null,
size: null,
plan: 'Custom Plan'
},
user: {
billing: false,
currency: ''
}
};
var gBoxAndUserDetails = null, // cached cloudron details like region,size...
gWebadminStatus = { dns: false, tls: false, configuring: false };
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -111,8 +63,6 @@ util.inherits(CloudronError, Error);
CloudronError.BAD_FIELD = 'Field error';
CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
CloudronError.ALREADY_SETUP = 'Already Setup';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
@@ -121,234 +71,38 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
gWebadminStatus = { dns: false, tls: false, configuring: false };
gBoxAndUserDetails = null;
async.series([
certificates.initialize,
settings.initialize,
installAppBundle,
configureDefaultServer,
onDomainConfigured
], function (error) {
if (error) return callback(error);
configureWebadmin(NOOP_CALLBACK); // for restore() and caas initial setup. do not block
callback();
});
reverseProxy.configureDefaultServer,
cron.startPreActivationJobs,
onActivated
], callback);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
cron.uninitialize,
mailer.stop,
cron.stopJobs,
platform.stop,
certificates.uninitialize,
settings.uninitialize
], callback);
}
function onDomainConfigured(callback) {
function onActivated(callback) {
callback = callback || NOOP_CALLBACK;
if (!config.fqdn()) return callback();
async.series([
clients.addDefaultClients,
certificates.ensureFallbackCertificate,
ensureDkimKey,
platform.start, // requires fallback certs for mail container
mailer.start, // this requires the "mail" container to be running
cron.initialize
], callback);
}
function dnsSetup(dnsConfig, domain, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
if (!zoneName) zoneName = tld.getDomain(domain) || '';
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
settings.setDnsConfig(dnsConfig, domain, zoneName, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
// Starting the platform after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
users.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (!count) return callback(); // not activated
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setZoneName(zoneName);
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
callback();
});
}
function configureDefaultServer(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureDefaultServer: domain %s', config.fqdn());
if (process.env.BOX_ENV === 'test') return callback();
var certFilePath = path.join(paths.NGINX_CERT_DIR, 'default.cert');
var keyFilePath = path.join(paths.NGINX_CERT_DIR, 'default.key');
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
nginx.configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
function configureWebadmin(callback) {
callback = callback || NOOP_CALLBACK;
debug('configureWebadmin: fqdn:%s status:%j', config.fqdn(), gWebadminStatus);
if (process.env.BOX_ENV === 'test' || !config.fqdn() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function done(error) {
gWebadminStatus.configuring = false;
debug('configureWebadmin: done error:%j', error);
callback(error);
}
function configureNginx(error) {
debug('configureNginx: dns update:%j', error);
certificates.ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
if (error) return done(error);
gWebadminStatus.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
});
}
// update the DNS. configure nginx regardless of whether it succeeded so that
// box is accessible even if dns creds are invalid
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureNginx(error);
addDnsRecords(ip, function (error) {
if (error) return configureNginx(error);
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return configureNginx(error);
gWebadminStatus.dns = true;
configureNginx();
});
});
});
}
function setTimeZone(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setTimeZone ip:%s', ip);
superagent.get('https://geolocation.cloudron.io/json').query({ ip: ip }).timeout(10 * 1000).end(function (error, result) {
if ((error && !error.response) || result.statusCode !== 200) {
debug('Failed to get geo location: %s', error.message);
return callback(null);
}
var timezone = safe.query(result.body, 'location.time_zone');
if (!timezone || typeof timezone !== 'string') {
debug('No timezone in geoip response : %j', result.body);
return callback(null);
}
debug('Setting timezone to ', timezone);
settings.setTimeZone(timezone, callback);
});
}
function activate(username, password, email, displayName, ip, auditSource, callback) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
clients.get('cid-webadmin', function (error, result) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// Also generate a token so the admin creation can also act as a login
var token = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
platform.createMailConfig(NOOP_CALLBACK); // bounces can now be sent to the cloudron owner
callback(null, { token: token, expires: expires });
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
activated: count !== 0,
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.fqdn() ? config.adminFqdn() : null,
webadminStatus: gWebadminStatus
});
});
async.series([
platform.start,
cron.startPostActivationJobs
], callback);
});
}
@@ -378,396 +132,34 @@ function getDisks(callback) {
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
// only supported for caas
if (config.provider() !== 'caas') return callback(null, {});
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
.query({ token: config.token() })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
gBoxAndUserDetails = result.body;
return callback(null, gBoxAndUserDetails);
});
}
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
getBoxAndUserDetails(function (error, result) {
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
settings.getAll(function (error, allSettings) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getDeveloperMode(function (error, developerMode) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.getAll(),
isCustomDomain: config.isCustomDomain(),
isDemo: config.isDemo(),
developerMode: developerMode,
region: result.box.region,
size: result.box.size,
billing: !!result.user.billing,
plan: result.box.plan,
currency: result.user.currency,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
});
});
// be picky about what we send out here since this is sent for 'normal' users as well
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
progress: progress.getAll(),
isDemo: config.isDemo(),
edition: config.edition(),
memory: os.totalmem(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
});
});
}
function sendHeartbeat() {
if (config.provider() !== 'caas') return;
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) debug('Network error sending heartbeat.', error);
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
else debug('Heartbeat sent to %s', url);
});
}
function ensureDkimKey(callback) {
assert(config.fqdn(), 'fqdn is not set');
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
if (!fs.existsSync(dkimPrivateKeyFile) || !fs.existsSync(dkimPublicKeyFile)) {
debug('Generating new DKIM keys');
if (!safe.fs.mkdirSync(dkimPath) && safe.error.code !== 'EEXIST') {
debug('Error creating dkim.', safe.error);
return null;
}
child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
} else {
debug('DKIM keys already present');
}
callback();
}
function readDkimPublicKeySync() {
if (!config.fqdn()) {
debug('Cannot read dkim public key without a domain.', safe.error);
return null;
}
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPublicKeyFile = path.join(dkimPath, 'public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) {
debug('Error reading dkim public key.', safe.error);
return null;
}
// remove header, footer and new lines
publicKey = publicKey.split('\n').slice(1, -2).join('');
return publicKey;
}
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
subdomains.get('', 'TXT', function (error, txtRecords) {
if (error) return callback(error);
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, matches, validSpf;
for (i = 0; i < txtRecords.length; i++) {
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
});
}
function addDnsRecords(ip, callback) {
assert.strictEqual(typeof ip, 'string');
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: constants.DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
// for non-custom domains, we show a noapp.html page
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
}
debug('addDnsRecords: %j', records);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.upsert(record.subdomain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
retryCallback(error);
});
});
}, function (error) {
if (error) debug('addDnsRecords: done updating records with error:', error);
else debug('addDnsRecords: done');
callback(error);
});
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
function update(boxUpdateInfo, auditSource, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!boxUpdateInfo) return callback(null);
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
// ensure tools can 'wait' on progress
progress.set(progress.UPDATE, 0, 'Starting');
// initiate the update/upgrade but do not wait for it
if (boxUpdateInfo.upgrade) {
debug('Starting upgrade');
doUpgrade(boxUpdateInfo, function (error) {
if (error) {
debug('Upgrade failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
} else {
debug('Starting update');
doUpdate(boxUpdateInfo, function (error) {
if (error) {
debug('Update failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
}
callback(null);
}
function updateToLatest(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
if (!boxUpdateInfo.sourceTarballUrl) return callback(new CloudronError(CloudronError.BAD_STATE, 'No automatic update available'));
// check if this is just a version number change
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
doShortCircuitUpdate(boxUpdateInfo, function (error) {
if (error) debug('Short-circuit update failed', error);
});
return callback(null);
}
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
update(boxUpdateInfo, auditSource, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
config.setVersion(boxUpdateInfo.version);
progress.clear(progress.UPDATE);
updateChecker.resetUpdateInfo();
callback();
}
function doUpgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
.query({ token: config.token() })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
}
function doUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
function updateError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for update');
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
if (error) return updateError(error);
// NOTE: this data is opaque and will be passed through the installer.sh
var data= {
provider: config.provider(),
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
tlsCert: config.tlsCert(),
tlsKey: config.tlsKey(),
isCustomDomain: config.isCustomDomain(),
isDemo: config.isDemo(),
zoneName: config.zoneName(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
version: boxUpdateInfo.version
};
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
// Do not add any code here. The installer script will stop the box code any instant
});
});
}
function installAppBundle(callback) {
assert.strictEqual(typeof callback, 'function');
if (fs.existsSync(paths.FIRST_RUN_FILE)) return callback();
var bundle = config.get('appBundle');
debug('initialize: installing app bundle on first run: %j', bundle);
if (!bundle || bundle.length === 0) return callback();
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
var data = {
appStoreId: appInfo.appstoreId,
location: appInfo.location,
portBindings: appInfo.portBindings || null,
accessRestriction: appInfo.accessRestriction || null,
};
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
}, function (error) {
if (error) debug('autoInstallApps: ', error);
fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
callback();
});
}
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
@@ -810,151 +202,66 @@ function checkDiskSpace(callback) {
});
}
function retire(reason, info, callback) {
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
apiServerOrigin: config.apiServerOrigin(),
isCustomDomain: config.isCustomDomain(),
fqdn: config.fqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function doMigrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
function unlock(error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
}
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
if (error) return unlock(error);
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
.query({ token: config.token() })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
}
function migrate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.isDemo()) return callback(new CloudronError(CloudronError.BAD_FIELD, 'Not allowed in demo mode'));
if (!options.domain) return doMigrate(options, callback);
var dnsConfig = _.pick(options, 'domain', 'provider', 'accessKeyId', 'secretAccessKey', 'region', 'endpoint', 'token', 'zoneName');
settings.setDnsConfig(dnsConfig, options.domain, options.zoneName || tld.getDomain(options.domain), function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
// TODO: should probably rollback dns config if migrate fails
doMigrate(options, callback);
});
}
// called for dynamic dns setups where we have to update the IP
function refreshDNS(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('refreshDNS: current ip %s', ip);
addDnsRecords(ip, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for system records');
apps.getAll(function (error, result) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== appdb.ISTATE_INSTALLED) return callback();
subdomains.upsert(app.location, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}
function getLogs(options, callback) {
function getLogs(unit, options, callback) {
assert.strictEqual(typeof unit, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
var units = options.units || [],
lines = options.lines || 100,
var lines = options.lines || 100,
format = options.format || 'json',
follow = !!options.follow;
assert(Array.isArray(units));
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
debug('Getting logs for %j', units);
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var args = [ '--no-pager', '--lines=' + lines ];
units.forEach(function (u) {
if (u === 'box') args.push('--unit=box');
else if (u === 'mail') args.push('CONTAINER_NAME=mail');
});
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
debug('Getting logs for %s as %s', unit, format);
var cp = spawn('/bin/journalctl', args);
var cp, transformStream;
if (unit === 'box') {
let args = [ '--no-pager', `--lines=${lines}` ];
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
args.push('--unit=box');
args.push('--unit=cloudron-updater');
cp = spawn('/bin/journalctl', args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
} else { // mail, mongodb, mysql, postgresql, backup
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
cp = spawn('/usr/bin/tail', args);
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
}
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
+110 -83
View File
@@ -16,42 +16,49 @@ exports = module.exports = {
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
fqdn: fqdn,
setFqdn: setFqdn,
token: token,
adminDomain: adminDomain,
setFqdn: setAdminDomain,
setAdminDomain: setAdminDomain,
setAdminFqdn: setAdminFqdn,
setAdminLocation: setAdminLocation,
version: version,
setVersion: setVersion,
isCustomDomain: isCustomDomain,
database: database,
edition: edition,
// these values are derived
adminOrigin: adminOrigin,
internalAdminOrigin: internalAdminOrigin,
sysadminOrigin: sysadminOrigin, // caas routes
adminLocation: adminLocation,
adminFqdn: adminFqdn,
mailLocation: mailLocation,
mailFqdn: mailFqdn,
appFqdn: appFqdn,
zoneName: zoneName,
setZoneName: setZoneName,
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
isDemo: isDemo,
tlsCert: tlsCert,
tlsKey: tlsKey,
// 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
};
var assert = require('assert'),
constants = require('./constants.js'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
tld = require('tldjs'),
_ = require('underscore');
// assert on unknown environment can't proceed
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
var data = { };
@@ -63,8 +70,24 @@ function baseDir() {
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
// only tests can run without a config file on disk, they use the defaults with runtime overrides
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
function saveSync() {
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
adminFqdn: data.adminFqdn,
adminLocation: data.adminLocation,
provider: data.provider,
isDemo: data.isDemo,
edition: data.edition
};
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
}
function _reset(callback) {
@@ -77,45 +100,41 @@ function _reset(callback) {
function initConfig() {
// setup defaults
data.fqdn = 'localhost';
data.zoneName = '';
data.token = null;
data.adminFqdn = '';
data.adminDomain = '';
data.adminLocation = 'my';
data.port = 3000;
data.version = null;
data.isCustomDomain = true;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.smtpPort = 2525; // // this value comes from mail container
data.provider = 'generic';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.provider = 'caas';
data.appBundle = [ ];
data.dockerProxyPort = 3003;
data.edition = '';
if (exports.CLOUDRON) {
data.port = 3000;
data.apiServerOrigin = null;
data.database = null;
} else if (exports.TEST) {
// keep in sync with start.sh
data.database = {
hostname: '127.0.0.1',
username: 'root',
password: 'password',
port: 3306,
name: 'box'
};
// overrides for local testings
if (exports.TEST) {
data.version = '1.1.1-test';
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database = {
hostname: '127.0.0.1',
username: 'root',
password: '',
port: 3306,
name: 'boxtest'
};
data.token = 'APPSTORE_TOKEN';
} else {
assert(false, 'Unknown environment. This should not happen!');
data.database.password = '';
data.database.name = 'boxtest';
}
if (safe.fs.existsSync(cloudronConfigFileName)) {
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
_.extend(data, existingData); // overwrite defaults with saved config
return;
}
saveSync();
// overwrite defaults with saved config
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
_.extend(data, existingData);
}
initConfig();
@@ -131,6 +150,7 @@ function set(key, value) {
} else {
data = safe.set(data, key, value);
}
saveSync();
}
@@ -148,44 +168,41 @@ function webServerOrigin() {
return get('webServerOrigin');
}
function setFqdn(fqdn) {
set('fqdn', fqdn);
function setAdminDomain(domain) {
set('adminDomain', domain);
}
function fqdn() {
return get('fqdn');
function adminDomain() {
return get('adminDomain');
}
function setZoneName(zone) {
set('zoneName', zone);
function mailLocation() {
return get('adminLocation'); // not a typo! should be same as admin location until we figure out certificates
}
function zoneName() {
var zone = get('zoneName');
if (zone) return zone;
// TODO: move this to migration code path instead
return tld.getDomain(fqdn()) || '';
function setAdminLocation(location) {
set('adminLocation', location);
}
// keep this in sync with start.sh admin.conf generation code
function appFqdn(location) {
assert.strictEqual(typeof location, 'string');
if (location === '') return fqdn();
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
function adminLocation() {
return get('adminLocation');
}
function setAdminFqdn(adminFqdn) {
set('adminFqdn', adminFqdn);
}
function adminFqdn() {
return appFqdn(constants.ADMIN_LOCATION);
return get('adminFqdn');
}
function mailFqdn() {
return appFqdn(constants.MAIL_LOCATION);
return adminFqdn();
}
function adminOrigin() {
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
return 'https://' + adminFqdn();
}
function internalAdminOrigin() {
@@ -196,10 +213,6 @@ function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function token() {
return get('token');
}
function version() {
return get('version');
}
@@ -208,10 +221,6 @@ function setVersion(version) {
set('version', version);
}
function isCustomDomain() {
return get('isCustomDomain');
}
function database() {
return get('database');
}
@@ -220,21 +229,39 @@ 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');
}
function tlsCert() {
var certFile = path.join(baseDir(), 'configs/host.cert');
return safe.fs.readFileSync(certFile, 'utf8');
}
function tlsKey() {
var keyFile = path.join(baseDir(), 'configs/host.key');
return safe.fs.readFileSync(keyFile, 'utf8');
}
function hasIPv6() {
// require here to avoid cyclic dependencies, it is cached anyways
return fs.existsSync(require('./paths.js').IPV6_PROC_FILE);
}
const IPV6_PROC_FILE = '/proc/net/if_inet6';
return fs.existsSync(IPV6_PROC_FILE);
}
// it has to change with the adminLocation so that multiple cloudrons
// can send out emails at the same time.
function dkimSelector() {
var loc = adminLocation();
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
}
function edition() {
return get('edition');
}
+3 -12
View File
@@ -1,19 +1,17 @@
'use strict';
// default admin installation location. keep in sync with ADMIN_LOCATION in setup/start.sh and BOX_ADMIN_LOCATION in appstore constants.js
exports = module.exports = {
ADMIN_LOCATION: 'my',
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
MAIL_LOCATION: 'my', // not a typo! should be same as admin location until we figure out certificates
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
// These are combined into one array because users and groups become mailboxes
RESERVED_NAMES: [
// Reserved usernames
// https://github.com/gogits/gogs/blob/52c8f691630548fe091d30bcfe8164545a05d3d5/models/repo.go#L393
'admin', 'no-reply', 'postmaster', 'mailer-daemon', // apps like wordpress, gogs don't like these
// apps like wordpress, gogs don't like these
// postmaster is used in dovecot and haraka
'admin', 'no-reply', 'postmaster', 'mailer-daemon',
// Reserved groups
'admins', 'users' // ldap code uses 'users' pseudo group
@@ -21,11 +19,6 @@ exports = module.exports = {
ADMIN_NAME: 'Settings',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
ADMIN_APPID: 'admin', // admin appid (settingsdb)
ADMIN_GROUP_ID: 'admin',
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
@@ -36,8 +29,6 @@ exports = module.exports = {
DEMO_USERNAME: 'cloudron',
DKIM_SELECTOR: 'cloudron',
AUTOUPDATE_PATTERN_NEVER: 'never'
};
+137 -129
View File
@@ -1,43 +1,52 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize
startPostActivationJobs: startPostActivationJobs,
startPreActivationJobs: startPreActivationJobs,
stopJobs: stopJobs
};
var apps = require('./apps.js'),
var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
backups = require('./backups.js'),
certificates = require('./certificates.js'),
caas = require('./caas.js'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
constants = require('./constants.js'),
CronJob = require('cron').CronJob,
debug = require('debug')('box:cron'),
digest = require('./digest.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
updater = require('./updater.js'),
updateChecker = require('./updatechecker.js');
var gAliveJob = null, // send periodic stats
gAppUpdateCheckerJob = null,
gAutoupdaterJob = null,
gBackupJob = null,
gBoxUpdateCheckerJob = null,
gCertificateRenewJob = null,
gCheckDiskSpaceJob = null,
gCleanupBackupsJob = null,
gCleanupEventlogJob = null,
gCleanupTokensJob = null,
gDockerVolumeCleanerJob = null,
gDynamicDNSJob = null,
gHeartbeatJob = null, // for CaaS health check
gSchedulerSyncJob = null,
gDigestEmailJob = null;
var gJobs = {
alive: null, // send periodic stats
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
backup: null,
boxUpdateChecker: null,
caasHeartbeat: null,
checkDiskSpace: null,
certificateRenew: null,
cleanupBackups: null,
cleanupEventlog: null,
cleanupTokens: null,
digestEmail: null,
dockerVolumeCleaner: null,
dynamicDNS: null,
schedulerSync: null,
appHealthMonitor: null
};
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' };
@@ -50,39 +59,45 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
// Months: 0-11
// Day of Week: 0-6
function initialize(callback) {
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
var seconds = (new Date()).getSeconds() - 1;
if (seconds === -1) seconds = 59;
gJobs.caasHeartbeat = new CronJob({
cronTime: `${seconds} */1 * * * *`, // every minute
onTick: caas.sendHeartbeat,
start: true
});
}
callback();
}
function startPostActivationJobs(callback) {
assert.strictEqual(typeof callback, 'function');
gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
onTick: cloudron.sendHeartbeat,
start: false
});
// 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
setTimeout(function () {
if (!gHeartbeatJob) return; // already uninitalized
gHeartbeatJob.start();
cloudron.sendHeartbeat();
}, 1000 * 60);
var randomHourMinute = Math.floor(60*Math.random());
gAliveJob = new CronJob({
gJobs.alive = new CronJob({
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
onTick: appstore.sendAliveStatus,
start: true
});
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDNSChanged);
settings.events.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
dynamicDNSChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
});
@@ -93,16 +108,16 @@ function recreateJobs(tz) {
debug('Creating jobs with timezone %s', tz);
if (gBackupJob) gBackupJob.stop();
gBackupJob = new CronJob({
cronTime: '00 00 */6 * * *', // every 6 hours. backups.ensureBackup() will only trigger a backup once per day
if (gJobs.backup) gJobs.backup.stop();
gJobs.backup = new CronJob({
cronTime: '00 00 */6 * * *', // check every 6 hours
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gCheckDiskSpaceJob) gCheckDiskSpaceJob.stop();
gCheckDiskSpaceJob = new CronJob({
if (gJobs.checkDiskSpace) gJobs.checkDiskSpace.stop();
gJobs.checkDiskSpace = new CronJob({
cronTime: '00 30 */4 * * *', // every 4 hours
onTick: cloudron.checkDiskSpace,
start: true,
@@ -112,178 +127,171 @@ function recreateJobs(tz) {
// randomized pattern per cloudron every hour
var randomMinute = Math.floor(60*Math.random());
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
gBoxUpdateCheckerJob = new CronJob({
if (gJobs.boxUpdateCheckerJob) gJobs.boxUpdateCheckerJob.stop();
gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: updateChecker.checkBoxUpdates,
start: true,
timeZone: tz
});
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
gAppUpdateCheckerJob = new CronJob({
if (gJobs.appUpdateChecker) gJobs.appUpdateChecker.stop();
gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: updateChecker.checkAppUpdates,
start: true,
timeZone: tz
});
if (gCleanupTokensJob) gCleanupTokensJob.stop();
gCleanupTokensJob = new CronJob({
if (gJobs.cleanupTokens) gJobs.cleanupTokens.stop();
gJobs.cleanupTokens = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: janitor.cleanupTokens,
start: true,
timeZone: tz
});
if (gCleanupBackupsJob) gCleanupBackupsJob.stop();
gCleanupBackupsJob = new CronJob({
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
gJobs.cleanupBackups = new CronJob({
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
onTick: backups.cleanup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gCleanupEventlogJob) gCleanupEventlogJob.stop();
gCleanupEventlogJob = new CronJob({
if (gJobs.cleanupEventlog) gJobs.cleanupEventlog.stop();
gJobs.cleanupEventlog = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: eventlog.cleanup,
start: true,
timeZone: tz
});
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
gDockerVolumeCleanerJob = new CronJob({
if (gJobs.dockerVolumeCleaner) gJobs.dockerVolumeCleaner.stop();
gJobs.dockerVolumeCleaner = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: janitor.cleanupDockerVolumes,
start: true,
timeZone: tz
});
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
gSchedulerSyncJob = new CronJob({
if (gJobs.schedulerSync) gJobs.schedulerSync.stop();
gJobs.schedulerSync = new CronJob({
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
start: true,
timeZone: tz
});
if (gCertificateRenewJob) gCertificateRenewJob.stop();
gCertificateRenewJob = new CronJob({
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gDigestEmailJob) gDigestEmailJob.stop();
gDigestEmailJob = new CronJob({
if (gJobs.digestEmail) gJobs.digestEmail.stop();
gJobs.digestEmail = new CronJob({
cronTime: '00 00 00 * * 3', // every wednesday
onTick: digest.maybeSend,
start: true,
timeZone: tz
});
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10),
start: true,
timeZone: tz
});
}
function autoupdatePatternChanged(pattern) {
function boxAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gBoxUpdateCheckerJob);
assert(gJobs.boxUpdateCheckerJob);
debug('Auto update pattern changed to %s', pattern);
debug('Box auto update pattern changed to %s', pattern);
if (gAutoupdaterJob) gAutoupdaterJob.stop();
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gAutoupdaterJob = new CronJob({
gJobs.boxAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
if (semver.major(updateInfo.box.version) === semver.major(config.version())) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('Block automatic update for major version');
}
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
debug('Starting autoupdate to %j', updateInfo.box);
updater.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No auto updates available');
debug('No box auto updates available');
}
},
start: true,
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
}
function dynamicDNSChanged(enabled) {
function appAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Apps auto update pattern changed to %s', pattern);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.appAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No app auto updates available');
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
}
function dynamicDnsChanged(enabled) {
assert.strictEqual(typeof enabled, 'boolean');
assert(gBoxUpdateCheckerJob);
assert(gJobs.boxUpdateCheckerJob);
debug('Dynamic DNS setting changed to %s', enabled);
if (enabled) {
gDynamicDNSJob = new CronJob({
gJobs.dynamicDNS = new CronJob({
cronTime: '00 */10 * * * *',
onTick: cloudron.refreshDNS,
onTick: dyndns.sync,
start: true,
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
} else {
if (gDynamicDNSJob) gDynamicDNSJob.stop();
gDynamicDNSJob = null;
if (gJobs.dynamicDNS) gJobs.dynamicDNS.stop();
gJobs.dynamicDNS = null;
}
}
function uninitialize(callback) {
function stopJobs(callback) {
assert.strictEqual(typeof callback, 'function');
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.events.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
if (gAutoupdaterJob) gAutoupdaterJob.stop();
gAutoupdaterJob = null;
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
gBoxUpdateCheckerJob = null;
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
gAppUpdateCheckerJob = null;
if (gHeartbeatJob) gHeartbeatJob.stop();
gHeartbeatJob = null;
if (gAliveJob) gAliveJob.stop();
gAliveJob = null;
if (gBackupJob) gBackupJob.stop();
gBackupJob = null;
if (gCleanupTokensJob) gCleanupTokensJob.stop();
gCleanupTokensJob = null;
if (gCleanupBackupsJob) gCleanupBackupsJob.stop();
gCleanupBackupsJob = null;
if (gCleanupEventlogJob) gCleanupEventlogJob.stop();
gCleanupEventlogJob = null;
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
gDockerVolumeCleanerJob = null;
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
gSchedulerSyncJob = null;
if (gCertificateRenewJob) gCertificateRenewJob.stop();
gCertificateRenewJob = null;
if (gDynamicDNSJob) gDynamicDNSJob.stop();
gDynamicDNSJob = null;
if (gDigestEmailJob) gDigestEmailJob.stop();
gDigestEmailJob = null;
for (var job in gJobs) {
if (!gJobs[job]) continue;
gJobs[job].stop();
gJobs[job] = null;
}
callback();
}
+29 -15
View File
@@ -6,9 +6,8 @@ exports = module.exports = {
query: query,
transaction: transaction,
beginTransaction: beginTransaction,
rollback: rollback,
commit: commit,
importFromFile: importFromFile,
exportToFile: exportToFile,
_clear: clear
};
@@ -24,21 +23,13 @@ var assert = require('assert'),
var gConnectionPool = null,
gDefaultConnection = null;
function initialize(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {
connectionLimit: 5
};
}
assert.strictEqual(typeof options.connectionLimit, 'number');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool !== null) return callback(null);
gConnectionPool = mysql.createPool({
connectionLimit: options.connectionLimit,
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
host: config.database().hostname,
user: config.database().username,
password: config.database().password,
@@ -100,8 +91,7 @@ function clear(callback) {
async.series([
child_process.exec.bind(null, cmd),
require('./clientdb.js')._addDefaultClients,
require('./groupdb.js')._addDefaultGroups
require('./clientdb.js')._addDefaultClients
], callback);
}
@@ -183,3 +173,27 @@ function transaction(queries, callback) {
});
}
function importFromFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysql -u ${config.database().username} ${password} ${config.database().name} < ${file}`;
async.series([
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
child_process.exec.bind(null, cmd)
], callback);
}
function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysqldump -u root ${password} --single-transaction --routines \
--triggers ${config.database().name} > "${file}"`;
child_process.exec(cmd, callback);
}
-102
View File
@@ -1,102 +0,0 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
DeveloperError: DeveloperError,
isEnabled: isEnabled,
setEnabled: setEnabled,
issueDeveloperToken: issueDeveloperToken,
getNonApprovedApps: getNonApprovedApps
};
var assert = require('assert'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:developer'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
settings = require('./settings.js'),
superagent = require('superagent'),
util = require('util');
function DeveloperError(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(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function isEnabled(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getDeveloperMode(function (error, enabled) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
callback(null, enabled);
});
}
function setEnabled(enabled, auditSource, callback) {
assert.strictEqual(typeof enabled, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
settings.setDeveloperMode(enabled, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_CLI_MODE, auditSource, { enabled: enabled });
callback(null);
});
}
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var token = tokendb.generateToken();
var expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
}
function getNonApprovedApps(callback) {
assert.strictEqual(typeof callback, 'function');
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
if (result.statusCode === 401 || result.statusCode === 403) {
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
return callback(null, []);
}
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
callback(null, result.body.apps || []);
});
}
-46
View File
@@ -1,46 +0,0 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:dig');
function resolve(domain, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// dig @server cloudron.io TXT +short
var args = [ ];
if (options.server) args.push('@' + options.server);
if (type === 'PTR') {
args.push('-x', domain);
} else {
args.push(domain, type);
}
args.push('+short');
child_process.execFile('/usr/bin/dig', args, { encoding: 'utf8', killSignal: 'SIGKILL', timeout: options.timeout || 0 }, function (error, stdout, stderr) {
if (error && error.killed) error.code = 'ETIMEDOUT';
if (error || stderr) debug('resolve error (%j): %j %s %s', args, error, stdout, stderr);
if (error) return callback(error);
debug('resolve (%j): %s', args, stdout);
if (!stdout) return callback(); // timeout or no result
var lines = stdout.trim().split('\n');
if (type === 'MX') {
lines = lines.map(function (line) {
var parts = line.split(' ');
return { priority: parts[0], exchange: parts[1] };
});
}
return callback(null, lines);
});
}
+23 -24
View File
@@ -28,37 +28,36 @@ function maybeSend(callback) {
var pendingAppUpdates = updateInfo.apps || {};
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
appstore.getSubscription(function (error, result) {
if (error) debug('Error getting subscription:', error);
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
eventlog.getByActionLastWeek(eventlog.ACTION_APP_UPDATE, function (error, appUpdates) {
if (error) return callback(error);
if (error) return callback(error);
eventlog.getByActionLastWeek(eventlog.ACTION_UPDATE, function (error, boxUpdates) {
if (error) return callback(error);
var info = {
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
var info = {
hasSubscription: hasSubscription,
finishedAppUpdates: appUpdates,
finishedBoxUpdates: boxUpdates,
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
certRenewals: certRenewals,
finishedBackups: finishedBackups, // only the successful backups
usersAdded: usersAdded,
usersRemoved: usersRemoved // unused because we don't have username to work with
};
finishedAppUpdates: (appUpdates || []).map(function (e) { return e.data; }),
finishedBoxUpdates: (boxUpdates || []).map(function (e) { return e.data; })
};
// always send digest for backup failure notification
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
if (info.pendingAppUpdates.length || info.pendingBoxUpdate || info.finishedAppUpdates.length || info.finishedBoxUpdates.length) {
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
} else {
debug('maybeSend: nothing happened, NOT sending digest email');
}
callback();
});
});
callback();
});
});
}

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