Compare commits

..

379 Commits

Author SHA1 Message Date
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
282 changed files with 8954 additions and 54466 deletions
-1
View File
@@ -1,7 +1,6 @@
node_modules/
coverage/
webadmin/dist/
setup/splash/website/
installer/src/certs/server.key
# vim swap files
+1
View File
@@ -2,6 +2,7 @@
"node": true,
"browser": true,
"unused": true,
"multistr": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
+39
View File
@@ -1184,3 +1184,42 @@
[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
+1 -1
View File
@@ -59,6 +59,6 @@ the containers in the Cloudron.
## 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"
+4
View File
@@ -105,3 +105,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 -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();
-257
View File
@@ -1,257 +0,0 @@
/* jslint node:true */
'use strict';
var argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
ejs = require('gulp-ejs'),
gulp = require('gulp'),
rimraf = require('rimraf'),
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-logs', 'js-terminal', 'js-setup', 'js-setupdns', 'js-restore', '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-logs', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('logs.js', { newLine: ';' }))
.pipe(uglifyer)
.pipe(sourcemaps.write())
.pipe(gulp.dest('webadmin/dist/js'));
});
gulp.task('js-terminal', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('terminal.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-restore', function () {
// needs special treatment for error handling
var uglifyer = uglify();
uglifyer.on('error', function (error) {
console.error(error);
});
gulp.src(['webadmin/src/js/restore.js', 'webadmin/src/js/client.js'])
.pipe(ejs({ oauth: oauth }, {}, { ext: '.js' }))
.pipe(sourcemaps.init())
.pipe(concat('restore.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/restore.js', 'webadmin/src/js/client.js'], ['js-restore']);
gulp.watch(['webadmin/src/js/logs.js', 'webadmin/src/js/client.js'], ['js-logs']);
gulp.watch(['webadmin/src/js/terminal.js', 'webadmin/src/js/client.js'], ['js-terminal']);
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 () {
rimraf.sync('webadmin/dist');
rimraf.sync('setup/splash/website');
});
gulp.task('default', ['clean', 'html', 'js', '3rdparty', 'images', 'css'], function () {});
gulp.task('develop', ['watch'], serve({ root: 'webadmin/dist', port: 4000 }));
@@ -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);
});
};
+31 -15
View File
@@ -23,6 +23,8 @@ CREATE TABLE IF NOT EXISTS users(
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
fallbackEmail VARCHAR(512) DEFAULT ""
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
@@ -70,7 +72,6 @@ CREATE TABLE IF NOT EXISTS apps(
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
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
@@ -79,8 +80,8 @@ CREATE TABLE IF NOT EXISTS apps(
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config for apptask (update)
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY(id));
@@ -132,6 +133,31 @@ CREATE TABLE IF NOT EXISTS eventlog(
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
@@ -144,16 +170,6 @@ CREATE TABLE IF NOT EXISTS mailboxes(
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES domains(domain),
PRIMARY KEY (name));
FOREIGN KEY(domain) REFERENCES mail(domain),
UNIQUE (name, domain));
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 */
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;
+252 -225
View File
@@ -5,117 +5,76 @@
"requires": true,
"dependencies": {
"@google-cloud/common": {
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.13.6.tgz",
"integrity": "sha1-qdjhN7xCmkSrqWif5qDkMxeE+FM=",
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.16.1.tgz",
"integrity": "sha512-1sufDsSfgJ7fuBLq+ux8t3TlydMlyWl9kPZx2WdLINkGtf5RjvXX6EWYZiCMKe8flJ3oC0l95j5atN2uX5n3rg==",
"requires": {
"array-uniq": "1.0.3",
"arrify": "1.0.1",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.1",
"duplexify": "3.5.3",
"ent": "2.2.0",
"extend": "3.0.1",
"google-auto-auth": "0.7.2",
"google-auto-auth": "0.9.4",
"is": "3.2.1",
"log-driver": "1.2.5",
"methmeth": "1.1.0",
"modelo": "4.2.0",
"modelo": "4.2.3",
"request": "2.83.0",
"retry-request": "3.3.1",
"split-array-stream": "1.0.3",
"stream-events": "1.0.2",
"string-format-obj": "1.1.0",
"string-format-obj": "1.1.1",
"through2": "2.0.3"
}
},
"@google-cloud/dns": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@google-cloud/dns/-/dns-0.7.0.tgz",
"integrity": "sha512-WL9PXmWiXOAKWq06nLzrMvJdumqFGOsB9q1f/DeCK7a7b/iihkyuQGNsSvpy0pf2FPYmDc8qEFtWjRw9xIApuw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@google-cloud/dns/-/dns-0.7.1.tgz",
"integrity": "sha512-rOq14I6vUc/Fd+8/cY4wCd6af8/+eN2wTIUSAQQM3zccn3HxgmG9mW54Wuq2iI/LBUYP5Z/+H4MNj3ZrkRIxIA==",
"requires": {
"@google-cloud/common": "0.13.6",
"@google-cloud/common": "0.16.1",
"arrify": "1.0.1",
"dns-zonefile": "0.2.2",
"extend": "3.0.1",
"is": "3.2.1",
"methmeth": "1.1.0",
"string-format-obj": "1.1.0"
"string-format-obj": "1.1.1"
}
},
"@google-cloud/storage": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-1.5.1.tgz",
"integrity": "sha512-7yaYzFWQYgi+n1dw/i5ysgzHD3ACJndxR8CbFnunQNT3NduolYSTr5WbYwWdhN5/7QiKx9g+obgP4tVz904bXw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-1.6.0.tgz",
"integrity": "sha512-yQ63bJYoiwY220gn/KdTLPoHppAPwFHfG7VFLPwJ+1R5U1eqUN5XV2a7uPj1szGF8/gxlKm2UbE8DgoJJ76DFw==",
"requires": {
"@google-cloud/common": "0.15.1",
"@google-cloud/common": "0.16.1",
"arrify": "1.0.1",
"async": "2.6.0",
"compressible": "2.0.13",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.1",
"duplexify": "3.5.3",
"extend": "3.0.1",
"gcs-resumable-upload": "0.8.2",
"gcs-resumable-upload": "0.9.0",
"hash-stream-validation": "0.2.1",
"is": "3.2.1",
"mime": "2.2.0",
"mime-types": "2.1.17",
"once": "1.4.0",
"pumpify": "1.3.5",
"pumpify": "1.4.0",
"request": "2.83.0",
"safe-buffer": "5.1.1",
"snakeize": "0.1.0",
"stream-events": "1.0.2",
"string-format-obj": "1.1.0",
"string-format-obj": "1.1.1",
"through2": "2.0.3"
},
"dependencies": {
"@google-cloud/common": {
"version": "0.15.1",
"resolved": "https://registry.npmjs.org/@google-cloud/common/-/common-0.15.1.tgz",
"integrity": "sha512-cnVtHLvyiSQvb1RzXWDp7PA1sA8Jmc47+wp/xwHwdGOlQZfKog5iluZ0C/LB8iklFXpcTwlNMorqLuZ/qH0DDA==",
"requires": {
"array-uniq": "1.0.3",
"arrify": "1.0.1",
"concat-stream": "1.6.0",
"create-error-class": "3.0.2",
"duplexify": "3.5.1",
"ent": "2.2.0",
"extend": "3.0.1",
"google-auto-auth": "0.8.1",
"is": "3.2.1",
"log-driver": "1.2.5",
"methmeth": "1.1.0",
"modelo": "4.2.0",
"request": "2.83.0",
"retry-request": "3.3.1",
"split-array-stream": "1.0.3",
"stream-events": "1.0.2",
"string-format-obj": "1.1.0",
"through2": "2.0.3"
}
},
"google-auth-library": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz",
"integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==",
"requires": {
"gtoken": "1.2.3",
"jws": "3.1.4",
"lodash.isstring": "4.0.1",
"lodash.merge": "4.6.0",
"request": "2.83.0"
}
},
"google-auto-auth": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.8.1.tgz",
"integrity": "sha512-v5a4mHIkhvbtKNILxnOYgOw+cin/jfLR0pEj1ids2jn9p0OyxYUXjSJbCEciuAorQao9Y55w0zJIc8yW1rIPaA==",
"requires": {
"async": "2.6.0",
"gcp-metadata": "0.3.1",
"google-auth-library": "0.12.0",
"request": "2.83.0"
}
"mime": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz",
"integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA=="
}
}
},
@@ -159,11 +118,11 @@
}
},
"JSONStream": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-0.10.0.tgz",
"integrity": "sha1-dDSdDYlSK3HzDwoD/5vSDKbxKsA=",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
"integrity": "sha1-wQI3G27Dp887hHygDCC7D85Mbeo=",
"requires": {
"jsonparse": "0.0.5",
"jsonparse": "1.3.1",
"through": "2.3.8"
}
},
@@ -414,12 +373,11 @@
}
},
"aws-sdk": {
"version": "2.170.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.170.0.tgz",
"integrity": "sha1-T5z9JqJo72opadNHXtgTS7OJlSU=",
"version": "2.201.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.201.0.tgz",
"integrity": "sha1-ekUoJi2Ql6vjfr9Oa/nRhR20cAM=",
"requires": {
"buffer": "4.9.1",
"crypto-browserify": "1.0.9",
"events": "1.1.1",
"jmespath": "0.15.0",
"querystring": "0.2.0",
@@ -428,6 +386,13 @@
"uuid": "3.1.0",
"xml2js": "0.4.17",
"xmlbuilder": "4.2.1"
},
"dependencies": {
"uuid": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
}
}
},
"aws-sign2": {
@@ -440,6 +405,15 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
"integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
},
"axios": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz",
"integrity": "sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=",
"requires": {
"follow-redirects": "1.4.1",
"is-buffer": "1.1.6"
}
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
@@ -469,9 +443,9 @@
}
},
"base64-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.1.tgz",
"integrity": "sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw=="
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.3.tgz",
"integrity": "sha512-MsAhsUW1GxCdgYSO6tAfZrNapmUKk7mWx/k5mFY/A1gBtkaCaNapTg+FExCw1r9yeaZhqx/xPg43xgTFH6KL5w=="
},
"base64url": {
"version": "2.0.0",
@@ -625,7 +599,7 @@
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"requires": {
"base64-js": "1.2.1",
"base64-js": "1.2.3",
"ieee754": "1.1.8",
"isarray": "1.0.0"
}
@@ -651,21 +625,10 @@
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "0.8.5",
"dtrace-provider": "0.8.6",
"moment": "2.19.4",
"mv": "2.1.1",
"safe-json-stringify": "1.0.4"
},
"dependencies": {
"dtrace-provider": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.5.tgz",
"integrity": "sha1-mOu6Ihr6xG4cOf02hY2Pk2dSS5I=",
"optional": true,
"requires": {
"nan": "2.8.0"
}
}
"safe-json-stringify": "1.1.0"
}
},
"bytes": {
@@ -862,6 +825,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
"integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
"dev": true,
"requires": {
"string-width": "1.0.2",
"strip-ansi": "3.0.1",
@@ -872,6 +836,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"dev": true,
"requires": {
"code-point-at": "1.1.0",
"is-fullwidth-code-point": "1.0.0",
@@ -1044,6 +1009,21 @@
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
"integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
},
"compressible": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz",
"integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=",
"requires": {
"mime-db": "1.33.0"
},
"dependencies": {
"mime-db": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
}
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1075,7 +1055,7 @@
"requires": {
"dot-prop": "4.2.0",
"graceful-fs": "4.1.11",
"make-dir": "1.1.0",
"make-dir": "1.2.0",
"unique-string": "1.0.0",
"write-file-atomic": "2.3.0",
"xdg-basedir": "3.0.0"
@@ -1308,11 +1288,6 @@
}
}
},
"crypto-browserify": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-1.0.9.tgz",
"integrity": "sha1-zFRJaF37hesRyYKKzHy4erW7/MA="
},
"crypto-random-string": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
@@ -1550,9 +1525,9 @@
"dev": true
},
"db-migrate": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.10.2.tgz",
"integrity": "sha512-uU3AXQ89DPKAePCHHoZSJRnLam6/zgOBhgSXVXB9Z3JeHms5OMTQuCdj1leUwYvI6ygHdQ3Z9CS6S9yOuELdcw==",
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/db-migrate/-/db-migrate-0.10.5.tgz",
"integrity": "sha512-jddRpFIxe+24U9frQBz51wtjFyVu2LnJ4QPcNRiiR4Hvuu8BKwdN2Ez42M6hLGbl7v1rXL2u+bUcNTvPyL/OwQ==",
"requires": {
"balanced-match": "0.4.2",
"bluebird": "3.5.1",
@@ -1562,14 +1537,13 @@
"final-fs": "1.6.1",
"inflection": "1.12.0",
"mkdirp": "0.5.1",
"moment": "2.19.4",
"optimist": "0.6.1",
"parse-database-url": "0.3.0",
"pkginfo": "0.4.1",
"prompt": "1.0.0",
"rc": "1.2.2",
"rc": "1.2.5",
"resolve": "1.5.0",
"semver": "5.4.1",
"semver": "5.5.0",
"tunnel-ssh": "4.1.3"
}
},
@@ -1735,24 +1709,16 @@
"integrity": "sha1-PqIQoYmgD3Zn93Cnl0KNjmYnJn8="
},
"docker-modem": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.4.tgz",
"integrity": "sha512-pkXB9p7KWagegOXm2NsbVDBluQQLCBJzX9uYJzVbL6CHwe4d2sSbcACJ4K8ISX1l1JUUmFSiwNkBKc1uTiU4MA==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-1.0.5.tgz",
"integrity": "sha512-i3J4TYW9iNp+nWzkgGvj9UwSmT6ZUFg2OsjRlUraHCaDCv8z6f0fN3q4ur0Qq27/1GPYXSjShGaE7fDznIJKUg==",
"requires": {
"JSONStream": "0.10.0",
"debug": "2.6.9",
"JSONStream": "1.3.2",
"debug": "3.1.0",
"readable-stream": "1.0.34",
"split-ca": "1.0.1"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
@@ -1777,12 +1743,12 @@
}
},
"dockerode": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.3.tgz",
"integrity": "sha512-LQKXR5jyI+G/+5OhZCi40m0ArY4j46g7Tl71Vtn10Ekt5TiyDzZAoqXOCS6edQpEuGbdFgSDJxleFqLxACpKJg==",
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-2.5.4.tgz",
"integrity": "sha512-esqrDATdckYhkOFn4BSOrqnkj3jgBkHT07uEqTRwK6na4/Rg60vjXWRopv2BbRpvFruMmKvOSNVY4MbmVBUnWw==",
"requires": {
"concat-stream": "1.5.2",
"docker-modem": "1.0.4",
"docker-modem": "1.0.5",
"tar-fs": "1.12.0"
},
"dependencies": {
@@ -1840,9 +1806,9 @@
"integrity": "sha1-vXWcNXqqcDZeAclrewvsCKbg2Uk="
},
"dtrace-provider": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.7.1.tgz",
"integrity": "sha1-wGswjy8Q1dWDiuycVx5dWI3HHQQ=",
"version": "0.8.6",
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.6.tgz",
"integrity": "sha1-QooiOv4DQl0s1tY0f99AxmkDVj0=",
"optional": true,
"requires": {
"nan": "2.8.0"
@@ -1884,9 +1850,9 @@
}
},
"duplexify": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.1.tgz",
"integrity": "sha512-j5goxHTwVED1Fpe5hh3q9R93Kip0Bg2KVAt4f8CEYM3UEwYcPSvWbXaUQOzdX/HtiNomipv+gU7ASQPDbV7pGQ==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz",
"integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==",
"requires": {
"end-of-stream": "1.4.0",
"inherits": "2.0.3",
@@ -2484,6 +2450,14 @@
"integrity": "sha1-2uRqnXj74lKSJYzB54CkHZXAN4I=",
"dev": true
},
"follow-redirects": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz",
"integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==",
"requires": {
"debug": "3.1.0"
}
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@@ -2632,23 +2606,24 @@
}
},
"gcp-metadata": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.3.1.tgz",
"integrity": "sha512-5kJPX/RXuqoLmHiOOgkSDk/LI0QaXpEvZ3pvQP4ifjGGDKZKVSOjL/GcDjXA5kLxppFCOjmmsu0Uoop9d1upaQ==",
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-0.6.1.tgz",
"integrity": "sha512-Ju3brkV7kYOBP5s3Z6HS2xd7gyH9MDfuKeB+y51SsI8GPrD37NOB5Re9fWXQQVAkd74zzVOScnNic1lcRsWD9w==",
"requires": {
"axios": "0.17.1",
"extend": "3.0.1",
"retry-request": "3.3.1"
"retry-axios": "0.3.0"
}
},
"gcs-resumable-upload": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-0.8.2.tgz",
"integrity": "sha512-PBl1OFABYxubxfYPh000I0+JLbQzBRtNqxzgxYboIQk2tdw7BvjJ2dVukk3YH4QM6GiUwqItyNqWBuxjLH8GhA==",
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/gcs-resumable-upload/-/gcs-resumable-upload-0.9.0.tgz",
"integrity": "sha512-+Zrmr0JKO2y/2mg953TW6JLu+NAMHqQsKzqCm7CIT24gMQakolPJCMzDleVpVjXAqB7ZCD276tcUq2ebOfqTug==",
"requires": {
"buffer-equal": "1.0.0",
"configstore": "3.1.1",
"google-auto-auth": "0.7.2",
"pumpify": "1.3.5",
"google-auto-auth": "0.9.4",
"pumpify": "1.4.0",
"request": "2.83.0",
"stream-events": "1.0.2",
"through2": "2.0.3"
@@ -2893,24 +2868,25 @@
}
},
"google-auth-library": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.10.0.tgz",
"integrity": "sha1-bhW6vuhf0d0U2NEoopW2g41SE24=",
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-0.12.0.tgz",
"integrity": "sha512-79qCXtJ1VweBmmLr4yLq9S4clZB2p5Y+iACvuKk9gu4JitEnPc+bQFmYvtCYehVR44MQzD1J8DVmYW2w677IEw==",
"requires": {
"gtoken": "1.2.3",
"jws": "3.1.4",
"lodash.noop": "3.0.1",
"lodash.isstring": "4.0.1",
"lodash.merge": "4.6.1",
"request": "2.83.0"
}
},
"google-auto-auth": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.7.2.tgz",
"integrity": "sha512-ux2n2AE2g3+vcLXwL4dP/M12SFMRX5dzCzBfhAEkTeAB7dpyGdOIEj7nmUx0BHKaCcUQrRWg9kT63X/Mmtk1+A==",
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/google-auto-auth/-/google-auto-auth-0.9.4.tgz",
"integrity": "sha512-a/gSNZ2RCaJxriBO/A010IHmdiQeoZS0EE83G7R/yV/OGXM9zd3otRqlcfRUomBLXf9XgsJ0h6bCp7bo+qaPvw==",
"requires": {
"async": "2.6.0",
"gcp-metadata": "0.3.1",
"google-auth-library": "0.10.0",
"gcp-metadata": "0.6.1",
"google-auth-library": "0.12.0",
"request": "2.83.0"
}
},
@@ -3618,8 +3594,7 @@
"is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"dev": true
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
},
"is-builtin-module": {
"version": "1.0.0",
@@ -3999,9 +3974,9 @@
"dev": true
},
"jsonparse": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-0.0.5.tgz",
"integrity": "sha1-MwVCrT8KZUZlt3jz6y2an6UHrGQ="
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
"integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
},
"jsonpointer": {
"version": "4.0.1",
@@ -4085,16 +4060,16 @@
}
},
"ldapjs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.1.tgz",
"integrity": "sha1-NSuBKudLCo6WVJpLiWBg7uG5pUY=",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-1.0.2.tgz",
"integrity": "sha1-VE/3Ayt7g8aPBwEyjZKXqmlDQPk=",
"requires": {
"asn1": "0.2.3",
"assert-plus": "1.0.0",
"backoff": "2.5.0",
"bunyan": "1.8.12",
"dashdash": "1.14.1",
"dtrace-provider": "0.7.1",
"dtrace-provider": "0.8.6",
"ldap-filter": "0.2.2",
"once": "1.4.0",
"vasync": "1.6.4",
@@ -4291,9 +4266,9 @@
"dev": true
},
"lodash.merge": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.0.tgz",
"integrity": "sha1-aYhLoUSsM/5plzemCG3v+t0PicU="
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
"integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ=="
},
"lodash.mergewith": {
"version": "4.6.0",
@@ -4301,11 +4276,6 @@
"integrity": "sha1-FQzwoWeR9ZA7iJHqsVRgknS96lU=",
"dev": true
},
"lodash.noop": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-3.0.1.tgz",
"integrity": "sha1-OBiPTWUKOkdCWEObluxFsyYXEzw="
},
"lodash.restparam": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz",
@@ -4391,9 +4361,9 @@
"dev": true
},
"make-dir": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz",
"integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.2.0.tgz",
"integrity": "sha512-aNUAa4UMg/UougV25bbrU4ZaaKNjJ/3/xnvg/twpmKROPdKZPZ9wGgI0opdZzO8q/zUFawoUuixuOv33eZ61Iw==",
"requires": {
"pify": "3.0.0"
}
@@ -4470,7 +4440,7 @@
"resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
"integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
"requires": {
"mimic-fn": "1.1.0"
"mimic-fn": "1.2.0"
}
},
"memoizee": {
@@ -4552,9 +4522,9 @@
}
},
"mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-TrpAd/vX3xaLPDgVRm6JkZwLR0KHfukMdU2wTEbqMDdCnY6Yo3mE+mjs9YE6oMNw2QRfXVeBEYpmpO94BIqiug=="
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-2.2.0.tgz",
"integrity": "sha512-0Qz9uF1ATtl8RKJG4VRfOymh7PyEor6NbrI/61lRfuRe4vx9SNATrvAeTj2EWVRKjEQGskrzWkJBBY5NbaVHIA=="
},
"mime-db": {
"version": "1.30.0",
@@ -4570,9 +4540,9 @@
}
},
"mimic-fn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz",
"integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
},
"minimatch": {
"version": "3.0.4",
@@ -4617,9 +4587,9 @@
}
},
"mocha": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-4.0.1.tgz",
"integrity": "sha512-evDmhkoA+cBNiQQQdSKZa2b9+W2mpLoj50367lhy+Klnx9OV8XlCIhigUnn1gaTFLQCa0kdNhEGDr0hCXOQFDw==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.1.tgz",
"integrity": "sha512-SpwyojlnE/WRBNGtvJSNfllfm5PqEDFxcWluSIgLeSBJtXG4DmoX2NNAeEA7rP5kK+79VgtVq8nG6HskaL1ykg==",
"dev": true,
"requires": {
"browser-stdout": "1.3.0",
@@ -4660,9 +4630,9 @@
}
},
"modelo": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.0.tgz",
"integrity": "sha1-O0tCACOmbKfjK9uhbnEJN+FNGws="
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/modelo/-/modelo-4.2.3.tgz",
"integrity": "sha512-9DITV2YEMcw7XojdfvGl3gDD8J9QjZTJ7ZOUuSAkP+F3T6rDbzMJuPktxptsdHYEvZcmXrCD3LMOhdSAEq6zKA=="
},
"moment": {
"version": "2.19.4",
@@ -4851,7 +4821,7 @@
"mkdirp": "0.5.1",
"propagate": "0.4.0",
"qs": "6.5.1",
"semver": "5.4.1"
"semver": "5.5.0"
},
"dependencies": {
"debug": {
@@ -5086,7 +5056,7 @@
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.4.3",
"uuid": "3.1.0"
"uuid": "3.2.1"
}
},
"sntp": {
@@ -5107,9 +5077,9 @@
}
},
"nodemailer": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.4.1.tgz",
"integrity": "sha512-1bnszJJXatcHJhLpxQ1XMkLDjCjPKvGKMtRQ73FOsoNln3UQjddEQmz6fAwM3aj0GtQ3dQX9qtMHPelz63GU7A=="
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.6.0.tgz",
"integrity": "sha512-yYTtNAmukmu0qI/zAYGggOvOdMgwacp4jmvCyXrGao/84Elz+2lDGOhVg6bkvOhkRK/hnun3XoJ+AV7w8x4wbQ=="
},
"nodemailer-fetch": {
"version": "1.6.0",
@@ -5156,7 +5126,7 @@
"requires": {
"hosted-git-info": "2.5.0",
"is-builtin-module": "1.0.0",
"semver": "5.4.1",
"semver": "5.5.0",
"validate-npm-package-license": "3.0.1"
}
},
@@ -5505,18 +5475,26 @@
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
},
"p-limit": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz",
"integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw="
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz",
"integrity": "sha512-Y/OtIaXtUPr4/YpMv1pCL5L5ed0rumAaAeBSj12F+bSlMdys7i8oQF/GUJmfpTS/QoaRrS/k6pma29haJpsMng==",
"requires": {
"p-try": "1.0.0"
}
},
"p-locate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
"integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
"requires": {
"p-limit": "1.1.0"
"p-limit": "1.2.0"
}
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
"integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
},
"parse-database-url": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/parse-database-url/-/parse-database-url-0.3.0.tgz",
@@ -6878,13 +6856,24 @@
}
},
"pumpify": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.3.5.tgz",
"integrity": "sha1-G2ccYZlAq8rqwK0OOjwWS+dgmTs=",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.4.0.tgz",
"integrity": "sha512-2kmNR9ry+Pf45opRVirpNuIFotsxUGLaYqxIwuR77AYrYRMuFCz9eryHBS52L360O+NcR383CL4QYlMKPq4zYA==",
"requires": {
"duplexify": "3.5.1",
"duplexify": "3.5.3",
"inherits": "2.0.3",
"pump": "1.0.3"
"pump": "2.0.1"
},
"dependencies": {
"pump": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz",
"integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==",
"requires": {
"end-of-stream": "1.4.0",
"once": "1.4.0"
}
}
}
},
"punycode": {
@@ -6940,9 +6929,9 @@
}
},
"rc": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.2.tgz",
"integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz",
"integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=",
"requires": {
"deep-extend": "0.4.2",
"ini": "1.3.5",
@@ -7157,7 +7146,7 @@
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.6.0",
"uuid": "3.1.0"
"uuid": "3.2.1"
}
},
"require-directory": {
@@ -7194,6 +7183,11 @@
"integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=",
"dev": true
},
"retry-axios": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/retry-axios/-/retry-axios-0.3.0.tgz",
"integrity": "sha512-6vOCghodB5p5N/ZOqug7A3WsT42TULZ7NErUi4lP3KtwtXgz4hE/43LWHsFuHuBfXRmOm/tjXBWAjnObrcy+yg=="
},
"retry-request": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-3.3.1.tgz",
@@ -7242,9 +7236,9 @@
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"safe-json-stringify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.4.tgz",
"integrity": "sha1-gaCY9Efku8P/MxKiQ1IbwGDvWRE=",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.1.0.tgz",
"integrity": "sha512-EzBtUaFH9bHYPc69wqjp0efJI/DPNHdFbGE3uIMn4sVbO0zx8vZ8cG4WKxQfOpUOKsQyGBiT2mTqnCw+6nLswA==",
"optional": true
},
"safetydance": {
@@ -7355,9 +7349,9 @@
}
},
"semver": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
"integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA=="
},
"send": {
"version": "0.16.1",
@@ -7471,7 +7465,7 @@
"resolved": "https://registry.npmjs.org/showdown/-/showdown-1.8.5.tgz",
"integrity": "sha1-QYOQzFvS0Y3ql5uaGyKhVqep7Nc=",
"requires": {
"yargs": "10.0.3"
"yargs": "10.1.2"
}
},
"sigmund": {
@@ -7732,7 +7726,7 @@
"integrity": "sha1-URGNFUVV31Rp7h9n4M8efoosDjo=",
"requires": {
"asn1": "0.2.3",
"semver": "5.4.1",
"semver": "5.5.0",
"streamsearch": "0.1.2"
}
},
@@ -7839,9 +7833,9 @@
"dev": true
},
"string-format-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.0.tgz",
"integrity": "sha1-djVhCx7zlwE+hHi+mKFw4EmD0Gg="
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string-format-obj/-/string-format-obj-1.1.1.tgz",
"integrity": "sha512-Mm+sROy+pHJmx0P/0Bs1uxIX6UhGJGj6xDGQZ5zh9v/SZRmLGevp+p0VJxV7lirrkAmQ2mvva/gHKpnF/pTb+Q=="
},
"string-width": {
"version": "2.1.1",
@@ -8081,9 +8075,9 @@
}
},
"tldjs": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.2.0.tgz",
"integrity": "sha512-5b5t+HKprfccAFRAsH/fzDR4O+UgO6vStvbaJo10jvMcUavlwxR3Jrn2WmXfjG3k22T7b4pqqfput38nr1RpJQ==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tldjs/-/tldjs-2.3.1.tgz",
"integrity": "sha512-W/YVH/QczLUxVjnQhFC61Iq232NWu3TqDdO0S/MtXVz4xybejBov4ud+CIwN9aYqjOecEqIy0PscGkwpG9ZyTw==",
"requires": {
"punycode": "1.4.1"
}
@@ -8531,9 +8525,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
"integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA=="
},
"v8flags": {
"version": "2.1.1",
@@ -8560,9 +8554,9 @@
}
},
"validator": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.2.0.tgz",
"integrity": "sha512-6Ij4Eo0KM4LkR0d0IegOwluG5453uqT5QyF5SV5Ezvm8/zmkKI/L4eoraafZGlZPC9guLkwKzgypcw8VGWWnGA=="
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz",
"integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA=="
},
"vary": {
"version": "1.1.2",
@@ -8831,9 +8825,9 @@
}
},
"ws": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.2.tgz",
"integrity": "sha512-t+WGpsNxhMR4v6EClXS8r8km5ZljKJzyGhJf7goJz9k5Ye3+b5Bvno5rjqPuIBn5mnn5GBb7o8IrIWHxX1qOLQ==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz",
"integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==",
"requires": {
"async-limiter": "1.0.0",
"safe-buffer": "5.1.1",
@@ -8884,11 +8878,11 @@
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
},
"yargs": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",
"integrity": "sha512-DqBpQ8NAUX4GyPP/ijDGHsJya4tYqLQrjPr95HNsr1YwL3+daCfvBwg7+gIC6IdJhR2kATh3hb61vjzMWEtjdw==",
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-10.1.2.tgz",
"integrity": "sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig==",
"requires": {
"cliui": "3.2.0",
"cliui": "4.0.0",
"decamelize": "1.2.0",
"find-up": "2.1.0",
"get-caller-file": "1.0.2",
@@ -8899,7 +8893,40 @@
"string-width": "2.1.1",
"which-module": "2.0.0",
"y18n": "3.2.1",
"yargs-parser": "8.0.0"
"yargs-parser": "8.1.0"
},
"dependencies": {
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"cliui": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-4.0.0.tgz",
"integrity": "sha512-nY3W5Gu2racvdDk//ELReY+dHjb9PlIcVDFXP72nVIhq2Gy3LuVXYwJoPVudwQnv1shtohpgkdCKT2YaKY0CKw==",
"requires": {
"string-width": "2.1.1",
"strip-ansi": "4.0.0",
"wrap-ansi": "2.1.0"
}
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
"requires": {
"ansi-regex": "3.0.0"
}
},
"yargs-parser": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz",
"integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==",
"requires": {
"camelcase": "4.1.0"
}
}
}
},
"yargs-parser": {
+15 -15
View File
@@ -14,11 +14,11 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.7.0",
"@google-cloud/storage": "^1.2.1",
"@google-cloud/dns": "^0.7.1",
"@google-cloud/storage": "^1.6.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.0",
"aws-sdk": "^2.151.0",
"aws-sdk": "^2.201.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.11.0",
"connect-ensure-login": "^0.1.1",
@@ -28,24 +28,24 @@
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"csurf": "^1.6.6",
"db-migrate": "^0.10.0-beta.24",
"db-migrate": "^0.10.5",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.3",
"dockerode": "^2.5.4",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"hat": "0.0.3",
"json": "^9.0.3",
"ldapjs": "^1.0.0",
"ldapjs": "^1.0.2",
"lodash.chunk": "^4.2.0",
"mime": "^2.0.3",
"mime": "^2.2.0",
"moment-timezone": "^0.5.14",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"mysql": "^2.15.0",
"nodemailer": "^4.4.0",
"nodemailer": "^4.6.0",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -62,19 +62,19 @@
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"safetydance": "^0.7.1",
"semver": "^5.4.1",
"semver": "^5.5.0",
"showdown": "^1.8.2",
"split": "^1.0.0",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"tldjs": "^2.2.0",
"tldjs": "^2.3.1",
"underscore": "^1.7.0",
"uuid": "^3.1.0",
"uuid": "^3.2.1",
"valid-url": "^1.0.9",
"validator": "^9.1.1",
"ws": "^3.3.1"
"validator": "^9.4.1",
"ws": "^3.3.3"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
@@ -91,12 +91,12 @@
"hock": "^1.3.2",
"istanbul": "*",
"js2xmlparser": "^3.0.0",
"mocha": "*",
"mocha": "^5.0.1",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"yargs": "^10.0.3"
"yargs": "^10.1.2"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
+56 -132
View File
@@ -26,6 +26,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
@@ -44,55 +48,37 @@ fi
initBaseImage="true"
# provisioning data
domain=""
adminLocation="my"
zoneName=""
provider=""
tlsProvider="le-prod"
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
dataJson=""
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
# these are here for pre-1.9 compat
encryptionKey=""
restoreUrl=""
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,admin-location:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--domain) domain="$2"; shift 2;;
--admin-location) adminLocation="$2"; shift 2;;
--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;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
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;;
@@ -100,39 +86,34 @@ while true; do
done
# validate arguments in the absence of data
if [[ -z "${dataJson}" ]]; then
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale.ch, digitalocean, ec2, exoscale, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "cloudscale.ch" && \
"${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.ch, digitalocean, ec2, exoscale, gce, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, 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}" != "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, 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 [[ -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 ""
@@ -143,7 +124,7 @@ 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
@@ -160,74 +141,33 @@ if [[ "${initBaseImage}" == "true" ]]; then
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
# tlsConfig, dnsConfig, backupConfig are here for backward compat with < 1.9
# from 1.9, we use autoprovision.json
if [[ -z "${dataJson}" ]]; then
if [[ -z "${restoreUrl}" ]]; then
data=$(cat <<EOF
{
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"adminFqdn": "${adminLocation}.${domain}",
"zoneName": "${zoneName}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}",
"tlsConfig": {
"provider": "${tlsProvider}"
},
"backupConfig" : {
"provider": "filesystem",
"backupFolder": "/var/backups",
"key": "${encryptionKey}",
"format": "tgz",
"retentionSecs": 172800
}
}
data=$(cat <<EOF
{
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
else
data=$(cat <<EOF
{
"fqdn": "${domain}",
"adminLocation": "${adminLocation}",
"adminFqdn": "${adminLocation}.${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)
@@ -258,31 +198,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
autoprovision_data=$(cat <<EOF
{
"tlsConfig": {
"provider": "${tlsProvider}"
}
}
EOF
)
echo "${autoprovision_data}" > /home/yellowtent/configs/autoprovision.json
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> to finish setup once the server has rebooted.${DONE}"
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
echo -e "\nRebooting this server now to let changes take effect.\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
fi
+19 -8
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,7 +25,12 @@ 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 ! $(cd "${SOURCE_DIR}/../webadmin" && git diff --exit-code >/dev/null); then
echo "You have local changes in webadmin, stash or commit them to proceed"
exit 1
fi
@@ -36,13 +39,21 @@ if [[ "$(node --version)" != "v8.9.3" ]]; then
exit 1
fi
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
if [[ "${branch}" == "master" ]]; then
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git rev-parse "${branch}")
else
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git fetch && git rev-parse "origin/${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}-${webadmin_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 webadmin version [${webadmin_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../webadmin" && git archive --format=tar ${webadmin_version} | (cd "${bundle_dir}" && tar xf -))
(cp "${SOURCE_DIR}/../webadmin/LICENSE" "${bundle_dir}")
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.all" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing dev modules from cache"
+8 -9
View File
@@ -40,22 +40,22 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
echo "docker binary download is corrupt"
echo "==> installer: docker binary download is corrupt"
exit 5
fi
echo "Waiting for all dpkg tasks to finish..."
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 "Failed to fix packages. Retry"
echo "==> installer: Failed to fix packages. Retry"
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
echo "Failed to install docker. Retry"
echo "==> installer: Failed to install docker. Retry"
sleep 1
done
@@ -78,12 +78,12 @@ 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
@@ -92,9 +92,8 @@ if ! id "${USER}" 2>/dev/null; then
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
+6 -16
View File
@@ -3,16 +3,13 @@
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_fqdn="" # remove after 1.10
arg_admin_domain=""
arg_admin_location=""
arg_admin_fqdn=""
arg_zone_name=""
arg_is_custom_domain="false" # can be removed after 1.9
arg_retire_reason=""
arg_retire_info=""
arg_token=""
arg_version=""
arg_web_server_origin=""
arg_provider=""
@@ -35,16 +32,13 @@ while true; do
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
arg_zone_name=$(echo "$2" | $json zoneName)
[[ "${arg_zone_name}" == "" ]] && arg_zone_name="${arg_fqdn}"
# can be removed after 1.9
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)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
@@ -58,8 +52,6 @@ while true; do
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
@@ -72,10 +64,8 @@ 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}"
# do not dump these as they might become available via logs API
#echo "token: ${arg_token}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
-48
View File
@@ -1,48 +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"
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_origin="https://${arg_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\": \"${arg_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
+11 -26
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
@@ -114,7 +105,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)
@@ -172,6 +163,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
@@ -199,31 +193,22 @@ 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'
set_progress "40" "Migrating data"
echo "==> Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
if [[ -z "${arg_admin_fqdn:-}" ]]; then
# can be removed after 1.9
admin_fqdn=$([[ "${arg_is_custom_domain}" == "true" ]] && echo "${arg_admin_location}.${arg_fqdn}" || echo "${arg_admin_location}-${arg_fqdn}")
else
admin_fqdn="${arg_admin_fqdn}"
fi
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}",
"adminFqdn": "${admin_fqdn}",
"adminDomain": "${arg_admin_domain}",
"adminFqdn": "${arg_admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"zoneName": "${arg_zone_name}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo}
}
@@ -256,9 +241,9 @@ find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail"
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
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"
-34
View File
@@ -165,40 +165,6 @@ server {
}
<% } 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;
}
<% } %>
}
}
+69 -34
View File
@@ -28,6 +28,7 @@ var appdb = require('./appdb.js'),
generatePassword = require('password-generator'),
hat = require('hat'),
infra = require('./infra_version.js'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
once = require('once'),
path = require('path'),
@@ -112,10 +113,9 @@ var KNOWN_ADDONS = {
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.intrinsicFqdn : '(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) {
@@ -250,7 +250,7 @@ function setupOauth(app, options, callback) {
if (!app.sso) return callback(null);
var appId = app.id;
var redirectURI = 'https://' + (app.altDomain || app.intrinsicFqdn);
var redirectURI = 'https://' + app.fqdn;
var scope = 'profile';
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
@@ -291,20 +291,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.getAll(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) {
@@ -365,11 +372,11 @@ function setupSendMail(app, options, callback) {
var env = [
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
{ name: 'MAIL_SMTP_PORT', value: '2525' },
{ name: 'MAIL_SMTPS_PORT', value: '4650' },
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
{ 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 + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
{ 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);
@@ -402,10 +409,10 @@ function setupRecvMail(app, options, callback) {
var env = [
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
{ name: 'MAIL_IMAP_PORT', value: '9993' },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name },
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_IMAP_PASSWORD', value: password },
{ name: 'MAIL_TO', value: mailbox.name + '@' + config.fqdn() },
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
{ name: 'MAIL_TO', value: mailbox.name + '@' + app.domain },
{ name: 'MAIL_DOMAIN', value: app.domain }
];
debugApp(app, 'Setting sendmail addon config to %j', env);
@@ -460,6 +467,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
@@ -473,6 +484,10 @@ function backupMySql(app, options, 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) {
@@ -525,6 +540,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
@@ -538,6 +557,10 @@ function backupPostgreSql(app, options, 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) {
@@ -591,6 +614,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
@@ -604,6 +631,10 @@ function backupMongoDb(app, options, 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) {
@@ -647,7 +678,7 @@ function setupRedis(app, options, callback) {
}
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const label = app.intrinsicFqdn;
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} \
@@ -685,27 +716,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
+7 -5
View File
@@ -60,8 +60,8 @@ var assert = require('assert'),
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.domain', 'apps.dnsRecordId',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.altDomain', 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
@@ -196,17 +196,18 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
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 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, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt ]
});
Object.keys(portBindings).forEach(function (env) {
@@ -226,6 +227,7 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
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);
+5 -7
View File
@@ -5,7 +5,6 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
DatabaseError = require('./databaseerror.js'),
config = require('./config.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
@@ -24,13 +23,9 @@ var gRunTimeout = null;
var gDockerEventStream = null;
function debugApp(app) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? app.intrinsicFqdn : '(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) {
@@ -71,6 +66,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);
+79 -83
View File
@@ -54,7 +54,6 @@ var addons = require('./addons.js'),
async = require('async'),
backups = require('./backups.js'),
BackupsError = backups.BackupsError,
certificates = require('./certificates.js'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
@@ -71,6 +70,7 @@ var addons = require('./addons.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
spawn = require('child_process').spawn,
@@ -130,8 +130,7 @@ function validateHostname(location, domain, hostname) {
const RESERVED_LOCATIONS = [
constants.API_LOCATION,
constants.SMTP_LOCATION,
constants.IMAP_LOCATION,
constants.POSTMAN_LOCATION
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new AppsError(AppsError.BAD_FIELD, location + ' is reserved');
@@ -307,17 +306,19 @@ function getDuplicateErrorDetails(location, portBindings, error) {
return new AppsError(AppsError.ALREADY_EXISTS);
}
// app configs that is useful for 'archival' into the app backup config.json
function getAppConfig(app) {
return {
manifest: app.manifest,
location: app.location,
domain: app.domain,
intrinsicFqdn: app.intrinsicFqdn,
fqdn: app.fqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN',
altDomain: app.altDomain
robotsTxt: app.robotsTxt,
sso: app.sso
};
}
@@ -363,10 +364,8 @@ function get(appId, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
callback(null, app);
});
@@ -387,10 +386,8 @@ function getByIpAddress(ip, callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
callback(null, app);
});
@@ -408,10 +405,8 @@ function getAll(callback) {
domaindb.get(app.domain, function (error, result) {
if (error) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
app.intrinsicFqdn = domains.fqdn(app.location, app.domain, result.provider);
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || app.intrinsicFqdn;
app.cnameTarget = app.altDomain ? app.intrinsicFqdn : null;
app.fqdn = domains.fqdn(app.location, app.domain, result.provider);
iteratorDone();
});
@@ -450,7 +445,7 @@ function downloadManifest(appStoreId, manifest, callback) {
superagent.get(url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Network error downloading manifest:' + error.message));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('Failed to get app info from store.', result.statusCode, result.text)));
if (result.statusCode !== 200) return callback(new AppsError(AppsError.NOT_FOUND, util.format('Failed to get app info from store.', result.statusCode, result.text)));
callback(null, parts[0], result.body.manifest);
});
@@ -469,7 +464,6 @@ function install(data, auditSource, callback) {
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
altDomain = data.altDomain || null,
xFrameOptions = data.xFrameOptions || 'SAMEORIGIN',
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
@@ -514,8 +508,6 @@ function install(data, auditSource, callback) {
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
var appId = uuid.v4();
if (icon) {
@@ -530,13 +522,15 @@ function install(data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
var fqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
error = certificates.validateCertificate(cert, key, intrinsicFqdn);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (cert && key) {
error = reverseProxy.validateCertificate(fqdn, cert, key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
}
debug('Will install app with id : ' + appId);
@@ -549,32 +543,36 @@ function install(data, auditSource, callback) {
var data = {
accessRestriction: accessRestriction,
memoryLimit: memoryLimit,
altDomain: altDomain,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
robotsTxt: robotsTxt,
intrinsicFqdn: intrinsicFqdn
robotsTxt: robotsTxt
};
appdb.add(appId, appStoreId, manifest, location, domain, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, error.message));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// save cert to boxdata/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, domain: domain, manifest: manifest, backupId: backupId });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(error);
callback(null, { id : appId });
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, app: result });
callback(null, { id : appId });
});
});
});
});
@@ -587,9 +585,8 @@ function configure(appId, data, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
var domain, location, portBindings, values = { };
if ('location' in data) location = values.location = data.location.toLowerCase();
@@ -604,11 +601,6 @@ function configure(appId, data, auditSource, callback) {
if (error) return callback(error);
}
if ('altDomain' in data) {
values.altDomain = data.altDomain;
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
}
if ('portBindings' in data) {
portBindings = values.portBindings = data.portBindings;
error = validatePortBindings(values.portBindings, app.manifest.tcpPorts);
@@ -645,22 +637,22 @@ function configure(appId, data, auditSource, callback) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
var fqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
error = validateHostname(location, domain, fqdn);
if (error) return callback(error);
// save cert to boxdata/certs. TODO: move this to apptask when we have a real task queue
if ('cert' in data && 'key' in data) {
if (data.cert && data.key) {
error = certificates.validateCertificate(data.cert, data.key, intrinsicFqdn);
error = reverseProxy.validateCertificate(fqdn, data.cert, data.key);
if (error) return callback(new AppsError(AppsError.BAD_CERTIFICATE, error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`), data.cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.key`), data.key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
} else { // remove existing cert/key
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.cert'))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, intrinsicFqdn + '.user.key'))) debug('Error removing key: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}.user.cert`))) debug('Error removing cert: ' + safe.error.message);
if (!safe.fs.unlinkSync(path.join(paths.APP_CERTS_DIR, `${fqdn}..user.key`))) debug('Error removing key: ' + safe.error.message);
}
}
@@ -684,9 +676,14 @@ function configure(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
});
});
});
@@ -726,9 +723,8 @@ function update(appId, data, auditSource, callback) {
}
}
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
@@ -753,7 +749,7 @@ function update(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force });
eventlog.add(eventlog.ACTION_APP_UPDATE, auditSource, { appId: appId, toManifest: manifest, fromManifest: app.manifest, force: data.force, app: app });
// clear update indicator, if update fails, it will come back through the update checker
updateChecker.resetAppUpdateInfo(appId);
@@ -777,10 +773,8 @@ function getLogs(appId, options, callback) {
debug('Getting logs for %s', appId);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
var lines = options.lines || 100,
follow = !!options.follow,
@@ -824,9 +818,8 @@ function restore(appId, data, auditSource, callback) {
debug('Will restore app with id:%s', appId);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
// for empty or null backupId, use existing manifest to mimic a reinstall
var func = data.backupId ? backups.get.bind(null, data.backupId) : function (next) { return next(null, { manifest: app.manifest }); };
@@ -855,7 +848,7 @@ function restore(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
callback(null);
});
@@ -881,13 +874,12 @@ function clone(appId, data, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof portBindings, 'object');
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
backups.get(backupId, function (error, backupInfo) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
if (error && error.reason === BackupsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Backup not found'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
@@ -900,12 +892,10 @@ function clone(appId, data, auditSource, callback) {
if (error) return callback(error);
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error && error.reason === DomainError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
var intrinsicFqdn = domains.fqdn(location, domain, domainObject.provider);
error = validateHostname(location, domain, intrinsicFqdn);
error = validateHostname(location, domain, domains.fqdn(location, domain, domainObject.provider));
if (error) return callback(error);
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
@@ -923,7 +913,9 @@ function clone(appId, data, auditSource, callback) {
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt
};
appdb.add(newAppId, app.appStoreId, manifest, location, domain, portBindings, data, function (error) {
@@ -932,9 +924,14 @@ function clone(appId, data, auditSource, callback) {
taskmanager.restartAppTask(newAppId);
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null, { id : newAppId });
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: result });
callback(null, { id : newAppId });
});
});
});
});
@@ -949,10 +946,10 @@ function uninstall(appId, auditSource, callback) {
debug('Will uninstall app with id:%s', appId);
get(appId, function (error, result) {
get(appId, function (error, app) {
if (error) return callback(error);
appstore.unpurchase(appId, result.appStoreId, function (error) {
appstore.unpurchase(appId, app.appStoreId, function (error) {
if (error && error.reason === AppstoreError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return callback(new AppsError(AppsError.BILLING_REQUIRED, error.message));
if (error && error.reason === AppstoreError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message));
@@ -963,7 +960,7 @@ function uninstall(appId, auditSource, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId, app: app });
taskmanager.startAppTask(appId, callback);
});
@@ -1014,7 +1011,7 @@ function checkManifestConstraints(manifest) {
}
if (semver.valid(manifest.minBoxVersion) && semver.gt(manifest.minBoxVersion, config.version())) {
return new AppsError(AppsError.BAD_FIELD, 'minBoxVersion exceeds Box version');
return new AppsError(AppsError.BAD_FIELD, 'App version requires a new platform version');
}
return null;
@@ -1028,9 +1025,8 @@ function exec(appId, options, callback) {
var cmd = options.cmd || [ '/bin/bash' ];
assert(util.isArray(cmd) && cmd.length > 0);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
get(appId, function (error, app) {
if (error) return callback(error);
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
return callback(new AppsError(AppsError.BAD_STATE, 'App not installed or running'));
@@ -1169,17 +1165,17 @@ function listBackups(page, perPage, appId, callback) {
function restoreInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for restore', app.intrinsicFqdn);
backups.getByAppIdPaged(1, 1, app.id, function (error, results) {
var restoreConfig = !error && results.length ? { backupId: results[0].id, backupFormat: results[0].format } : null;
debug(`marking ${app.fqdn} for restore using restore config ${JSON.stringify(restoreConfig)}`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_RESTORE, { restoreConfig: restoreConfig, oldConfig: null }, function (error) {
if (error) debug('did not mark %s for restore', app.intrinsicFqdn, error);
if (error) debug(`Error marking ${app.fqdn} for restore: ${JSON.stringify(error)}`);
iteratorDone(); // always succeed
});
@@ -1191,14 +1187,14 @@ function restoreInstalledApps(callback) {
function configureInstalledApps(callback) {
assert.strictEqual(typeof callback, 'function');
appdb.getAll(function (error, apps) {
getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
async.map(apps, function (app, iteratorDone) {
debug('marking %s for reconfigure', app.intrinsicFqdn);
debug(`marking ${app.fqdn} for reconfigure`);
appdb.setInstallationCommand(app.id, appdb.ISTATE_PENDING_CONFIGURE, { oldConfig: null }, function (error) {
if (error) debug('did not mark %s for reconfigure', app.intrinsicFqdn, error);
if (error) debug(`Error marking ${app.fqdn} for reconfigure: ${JSON.stringify(error)}`);
iteratorDone(); // always succeed
});
+122 -75
View File
@@ -18,11 +18,17 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var assert = require('assert'),
var 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');
@@ -56,23 +62,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) {
@@ -135,7 +130,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));
@@ -148,62 +143,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.getAll(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 = {
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
mailRelay: {
provider: result[settings.MAIL_RELAY_KEY].provider
},
mailCatchAll: {
count: result[settings.CATCH_ALL_ADDRESS_KEY].length
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
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(),
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
}
};
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);
});
});
});
@@ -220,10 +240,16 @@ function getBoxUpdate(callback) {
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 (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)));
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
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);
callback(null, updateInfo);
});
});
}
@@ -240,10 +266,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);
});
});
}
@@ -275,16 +312,26 @@ function sendFeedback(info, callback) {
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);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
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)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
callback(null);
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);
});
});
});
}
+25 -56
View File
@@ -8,15 +8,14 @@ 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 });
@@ -32,7 +31,6 @@ var addons = require('./addons.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'),
@@ -44,10 +42,10 @@ var addons = require('./addons.js'),
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'),
safe = require('safetydance'),
shell = require('./shell.js'),
superagent = require('superagent'),
@@ -72,8 +70,7 @@ function initialize(callback) {
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
var prefix = app ? (app.intrinsicFqdn || '(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
@@ -113,23 +110,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) {
@@ -271,16 +264,16 @@ 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.intrinsicFqdn, overwrite);
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
// get the current record before updating it
domains.getDNSRecords(app.location, app.domain, '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
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error || changeId);
@@ -310,9 +303,9 @@ function unregisterSubdomain(app, location, domain, callback) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', app.intrinsicFqdn);
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
domains.removeDNSRecords(location, domain, 'A', [ ip ], function (error) {
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
@@ -348,27 +341,10 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDNSRecord(app.intrinsicFqdn, app.domain, ip, 'A', { interval: 5000, times: 120 }, callback);
domains.waitForDnsRecord(app.fqdn, app.domain, ip, { 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) {
if (error) return callback(error);
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback);
});
} else {
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), app.intrinsicFqdn + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -393,7 +369,7 @@ function install(app, callback) {
// 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),
@@ -451,11 +427,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 !restoreConfig
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) {
@@ -499,11 +472,11 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && (app.oldConfig.intrinsicFqdn !== app.intrinsicFqdn);
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),
@@ -511,8 +484,7 @@ function configure(app, callback) {
function (next) {
if (!locationChanged) return next();
// the config.fqdn() fallback can be removed after 1.9
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain || config.fqdn(), next);
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
},
reserveHttpPort.bind(null, app),
@@ -547,11 +519,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) {
@@ -710,8 +679,8 @@ function uninstall(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring Nginx' }),
unconfigureNginx.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
+14 -14
View File
@@ -25,22 +25,22 @@ var assert = require('assert'),
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');
var md5 = crypto.createHash('md5').update(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) {
@@ -60,7 +60,7 @@ function initialize(callback) {
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
@@ -82,7 +82,7 @@ function initialize(callback) {
});
}
}));
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);
@@ -91,15 +91,15 @@ function initialize(callback) {
return callback(null, client);
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
@@ -110,15 +110,15 @@ function accessTokenAuth(accessToken, callback) {
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);
});
});
+19 -16
View File
@@ -68,10 +68,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.intrinsicFqdn : '(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) {
@@ -232,6 +231,11 @@ function sync(backupConfig, backupId, dataDir, callback) {
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof callback, 'function');
function setBackupProgress(message) {
debug(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);
@@ -239,24 +243,23 @@ function sync(backupConfig, backupId, dataDir, callback) {
if (task.operation === 'removedir') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing directory ${task.path}`);
return api(backupConfig.provider).removeDir(backupConfig, backupFilePath)
.on('progress', function (detail) {
debug(`sync: ${detail}`);
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, detail);
})
.on('progress', setBackupProgress)
.on('done', iteratorCallback);
} else if (task.operation === 'remove') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Removing ${task.path}`);
setBackupProgress(`Removing ${task.path}`);
return api(backupConfig.provider).remove(backupConfig, backupFilePath, iteratorCallback);
}
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') {
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, `Adding ${task.path}`);
setBackupProgress(`Adding ${task.path}`);
var stream = fs.createReadStream(path.join(dataDir, task.path));
stream.on('error', function () { return retryCallback(); }); // ignore error if file disappears
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, retryCallback);
}
}, iteratorCallback);
@@ -294,8 +297,6 @@ 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) {
@@ -303,6 +304,8 @@ function upload(backupId, format, dataDir, callback) {
if (format === 'tgz') {
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
@@ -720,7 +723,7 @@ function backupApp(app, callback) {
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 || app.intrinsicFqdn));
progress.set(progress.BACKUP, 10, 'Backing up ' + app.fqdn);
backupAppWithTimestamp(app, timestamp, function (error) {
progress.set(progress.BACKUP, 100, error ? error.message : '');
@@ -747,12 +750,12 @@ 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 || app.intrinsicFqdn));
progress.set(progress.BACKUP, step * processed, 'Backing up ' + app.fqdn);
++processed;
if (!app.enableBackup) {
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + (app.altDomain || app.intrinsicFqdn));
progress.set(progress.BACKUP, step * processed, 'Skipped backup ' + app.fqdn);
return iteratorCallback(null, null); // nothing to backup
}
@@ -762,7 +765,7 @@ function backupBoxAndApps(auditSource, callback) {
return iteratorCallback(error);
}
progress.set(progress.BACKUP, step * processed, 'Backed up ' + (app.altDomain || app.intrinsicFqdn));
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
});
+120 -48
View File
@@ -1,11 +1,16 @@
'use strict';
exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord
setPtrRecord: setPtrRecord,
CaasError: CaasError
};
var assert = require('assert'),
@@ -15,13 +20,13 @@ var assert = require('assert'),
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');
var gBoxAndUserDetails = null; // cached cloudron details like region,size...
function CaasError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -43,9 +48,10 @@ function CaasError(reason, 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';
CaasError.BAD_STATE = 'Bad state';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -56,14 +62,64 @@ function retire(reason, info, callback) {
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn(),
fqdn: config.fqdn()
adminFqdn: config.adminFqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function doMigrate(options, 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);
@@ -84,8 +140,8 @@ function doMigrate(options, callback) {
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() })
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -109,7 +165,11 @@ function changePlan(options, callback) {
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
doMigrate(options, callback);
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
}
// this function expects a lock
@@ -126,69 +186,81 @@ function upgrade(boxUpdateInfo, callback) {
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)));
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
progress.set(progress.UPDATE, 10, 'Updating base system');
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)));
// no need to unlock since this is the last thing we ever do on this box
callback();
progress.set(progress.UPDATE, 10, 'Updating base system');
retire('upgrade');
});
// 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');
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);
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 (gBoxAndUserDetails) return callback(null, gBoxAndUserDetails);
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 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)));
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
gBoxAndUserDetails = result.body;
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, gBoxAndUserDetails);
});
return callback(null, result.body);
});
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/ptr')
.query({ token: config.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)));
getCaasConfig(function (error, result) {
if (error) return callback(error);
return callback(null);
});
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);
});
});
}
+4 -4
View File
@@ -10,12 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', domain);
debug('getCertificate: using fallback certificate', vhost);
return callback(null, 'cert/host.cert', 'cert/host.key');
return callback(null, '', '');
}
+3 -3
View File
@@ -10,12 +10,12 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', domain);
debug('getCertificate: using fallback certificate', vhost);
return callback(null, '', '');
}
-447
View File
@@ -1,447 +0,0 @@
'use strict';
exports = module.exports = {
CertificatesError: CertificatesError,
ensureFallbackCertificate: ensureFallbackCertificate,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
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(fallbackCertPath) && fs.existsSync(fallbackKeyPath)) {
debug('ensureFallbackCertificate: pre-existing fallback certs');
return callback();
}
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();
}
// 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({ intrinsicFqdn: config.adminFqdn() }); // inject fake webadmin app
var expiringApps = [ ];
for (var i = 0; i < allApps.length; i++) {
var appDomain = allApps[i].altDomain || allApps[i].intrinsicFqdn;
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 (app) { return app.altDomain || app.intrinsicFqdn; }));
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
var domain = app.altDomain || app.intrinsicFqdn;
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);
// if no cert was returned use fallback, the fallback provider will not provide any for example
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, domain + '.cert');
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, domain + '.key');
certFilePath = fs.existsSync(fallbackCertFilePath) ? fallbackCertFilePath : 'cert/host.cert';
keyFilePath = fs.existsSync(fallbackKeyFilePath) ? fallbackKeyFilePath : '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.intrinsicFqdn === config.adminFqdn() ?
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('Invalid certificate. Unable to get certificate 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('Certificate 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 certificate.');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new Error('Certificate is expired.');
return null;
}
function setFallbackCertificate(cert, key, fqdn, callback) {
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var error = validateCertificate(cert, key, '*.' + 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, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, fqdn + '.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, fqdn + '.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
exports.events.emit(exports.EVENT_CERT_CHANGED, '*.' + fqdn);
nginx.reload(function (error) {
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
return callback(null);
});
}
function getFallbackCertificate(fqdn, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var cert = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.cert'), 'utf-8');
var key = safe.fs.readFileSync(path.join(paths.NGINX_CERT_DIR, fqdn + '.key'), 'utf-8');
if (!cert || !key) return callback(new CertificatesError(CertificatesError.NOT_FOUND));
callback(null, { cert: cert, key: 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);
// 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 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 || app.intrinsicFqdn;
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);
// if no cert was returned use fallback, the fallback provider will not provide any for example
if (!certFilePath || !keyFilePath) {
var fallbackCertFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.cert');
var fallbackKeyFilePath = path.join(paths.NGINX_CERT_DIR, app.domain + '.key');
certFilePath = fs.existsSync(fallbackCertFilePath) ? fallbackCertFilePath : 'cert/host.cert';
keyFilePath = fs.existsSync(fallbackKeyFilePath) ? fallbackKeyFilePath : 'cert/host.key';
}
callback(null, certFilePath, keyFilePath);
});
});
}
+6 -6
View File
@@ -39,7 +39,6 @@ var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clientdb = require('./clientdb.js'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
hat = require('hat'),
@@ -192,7 +191,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.domain = result.altDomain || result.intrinsicFqdn;
record.domain = result.fqdn;
tmp.push(record);
@@ -318,7 +317,8 @@ 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');
@@ -329,8 +329,8 @@ function addDefaultClients(callback) {
// 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, ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*, roleSdk')
], callback);
}
+11 -494
View File
@@ -5,77 +5,50 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
activate: activate,
getConfig: getConfig,
getStatus: getStatus,
getDisks: getDisks,
dnsSetup: dnsSetup,
getLogs: getLogs,
updateToLatest: updateToLatest,
restore: restore,
reboot: reboot,
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'),
BackupsError = require('./backups.js').BackupsError,
caas = require('./caas.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'),
domains = require('./domains.js'),
DomainError = domains.DomainError,
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'),
semver = require('semver'),
settings = require('./settings.js'),
settingsdb = require('./settingsdb.js'),
SettingsError = settings.SettingsError,
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
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,
util = require('util'),
_ = require('underscore');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh'),
RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -98,8 +71,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';
@@ -108,21 +79,12 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
gWebadminStatus = { dns: false, tls: false, configuring: false, restoring: false };
async.series([
certificates.initialize,
settings.initialize,
configureDefaultServer,
onDomainConfigured,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
onActivated
], function (error) {
if (error) return callback(error);
configureWebadmin(NOOP_CALLBACK); // for restore() and caas initial setup. do not block
callback();
});
], callback);
}
function uninitialize(callback) {
@@ -131,24 +93,10 @@ function uninitialize(callback) {
async.series([
cron.uninitialize,
platform.stop,
certificates.uninitialize,
settings.uninitialize
], callback);
}
function onDomainConfigured(callback) {
callback = callback || NOOP_CALLBACK;
if (!config.fqdn()) return callback();
async.series([
clients.addDefaultClients,
certificates.ensureFallbackCertificate,
ensureDkimKey,
cron.initialize // required for caas heartbeat before activation
], callback);
}
function onActivated(callback) {
callback = callback || NOOP_CALLBACK;
@@ -163,238 +111,6 @@ function onActivated(callback) {
});
}
function autoprovision(callback) {
assert.strictEqual(typeof callback, 'function');
const confJson = safe.fs.readFileSync(paths.AUTO_PROVISION_FILE, 'utf8');
if (!confJson) return callback();
const conf = safe.JSON.parse(confJson);
if (!conf) return callback();
async.eachSeries(Object.keys(conf), function (key, iteratorDone) {
var name;
switch (key) {
case 'dnsConfig': name = 'dns_config'; break;
case 'tlsConfig': name = 'tls_config'; break;
case 'backupConfig': name = 'backup_config'; break;
case 'tlsCert':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.cert'), conf[key], iteratorDone);
case 'tlsKey':
debug(`autoprovision: ${key}`);
return fs.writeFile(path.join(paths.NGINX_CERT_DIR, 'host.key'), conf[key], iteratorDone);
default:
debug(`autoprovision: ${key} ignored`);
return iteratorDone();
}
debug(`autoprovision: ${name}`);
settingsdb.set(name, JSON.stringify(conf[key]), iteratorDone);
}, callback);
}
function dnsSetup(adminFqdn, domain, zoneName, provider, dnsConfig, callback) {
assert.strictEqual(typeof adminFqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.fqdn()) return callback(new CloudronError(CloudronError.ALREADY_SETUP));
if (!zoneName) zoneName = tld.getDomain(domain) || domain;
debug('dnsSetup: Setting up Cloudron with domain %s and zone %s', domain, zoneName);
function done(error) {
if (error && error.reason === DomainError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
autoprovision(function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.setFqdn(domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
config.setAdminFqdn(adminFqdn);
config.setAdminLocation('my');
config.setZoneName(zoneName);
callback();
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
});
}
domains.get(domain, function (error, result) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
if (!result) domains.add(domain, zoneName, provider, dnsConfig, null /* cert */, done);
else domains.update(domain, provider, dnsConfig, null /* cert */, done);
});
}
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({ domain: config.fqdn(), location: config.adminLocation(), intrinsicFqdn: config.adminFqdn() }, 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);
domains.waitForDNSRecord(config.adminFqdn(), config.fqdn(), 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, { });
onActivated();
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
});
});
});
}
function getDisks(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -448,7 +164,7 @@ function getConfig(callback) {
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
fqdn: config.fqdn(),
adminDomain: config.adminDomain(),
adminLocation: config.adminLocation(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
@@ -469,167 +185,6 @@ function getConfig(callback) {
});
}
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');
domains.getDNSRecords('', config.fqdn(), '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: config.adminLocation(), domain: config.fqdn(), type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: config.dkimSelector() + '._domainkey', domain: config.fqdn(), type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
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: '', domain: config.fqdn(), type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
retryCallback(error);
});
});
}, function (error) {
if (error) debug('addDnsRecords: done updating records with error:', error);
else debug('addDnsRecords: done');
callback(error);
});
}
function restore(backupConfig, backupId, version, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new CloudronError(CloudronError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new CloudronError(CloudronError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (count) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider}`);
gWebadminStatus.restoring = true;
callback(null); // do no block
async.series([
backups.restore.bind(null, backupConfig, backupId),
autoprovision,
shell.sudo.bind(null, 'restart', [ RESTART_CMD ])
], function (error) {
debug('restore:', error);
gWebadminStatus.restoring = false;
});
});
});
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
}
@@ -719,21 +274,17 @@ function doUpdate(boxUpdateInfo, callback) {
// 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(),
adminDomain: config.adminDomain(),
adminFqdn: config.adminFqdn(),
adminLocation: config.adminLocation(),
isDemo: config.isDemo(),
zoneName: config.zoneName(),
appstore: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin()
},
caas: {
token: config.token(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
@@ -743,7 +294,7 @@ function doUpdate(boxUpdateInfo, callback) {
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
progress.set(progress.UPDATE, 5, 'Downloading and installing new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
@@ -795,40 +346,6 @@ function checkDiskSpace(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();
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}
function getLogs(options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
+13 -34
View File
@@ -16,12 +16,11 @@ exports = module.exports = {
provider: provider,
apiServerOrigin: apiServerOrigin,
webServerOrigin: webServerOrigin,
fqdn: fqdn,
zoneName: zoneName,
setFqdn: setFqdn,
adminDomain: adminDomain,
setFqdn: setAdminDomain,
setAdminDomain: setAdminDomain,
setAdminFqdn: setAdminFqdn,
setAdminLocation: setAdminLocation,
token: token,
version: version,
setVersion: setVersion,
database: database,
@@ -34,7 +33,6 @@ exports = module.exports = {
adminFqdn: adminFqdn,
mailLocation: mailLocation,
mailFqdn: mailFqdn,
setZoneName: setZoneName,
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
@@ -48,7 +46,6 @@ var assert = require('assert'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
tld = require('tldjs'),
_ = require('underscore');
@@ -73,12 +70,10 @@ function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
token: data.token,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
fqdn: data.fqdn,
adminDomain: data.adminDomain,
adminFqdn: data.adminFqdn,
zoneName: data.zoneName,
adminLocation: data.adminLocation,
provider: data.provider,
isDemo: data.isDemo
@@ -97,16 +92,14 @@ function _reset(callback) {
function initConfig() {
// setup defaults
data.fqdn = '';
data.adminFqdn = '';
data.zoneName = '';
data.adminDomain = '';
data.adminLocation = 'my';
data.port = 3000;
data.token = null;
data.version = null;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'caas';
data.provider = 'generic';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
@@ -124,7 +117,6 @@ function initConfig() {
if (exports.TEST) {
data.version = '1.1.1-test';
data.port = 5454;
data.token = 'APPSTORE_TOKEN';
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.password = '';
data.database.name = 'boxtest';
@@ -148,6 +140,7 @@ function set(key, value) {
} else {
data = safe.set(data, key, value);
}
saveSync();
}
@@ -165,24 +158,12 @@ function webServerOrigin() {
return get('webServerOrigin');
}
function setFqdn(fqdn) {
set('fqdn', fqdn);
function setAdminDomain(domain) {
set('adminDomain', domain);
}
function fqdn() {
return get('fqdn');
}
function setZoneName(zone) {
set('zoneName', zone);
}
function zoneName() {
var zone = get('zoneName');
if (zone) return zone;
// TODO: move this to migration code path instead
return tld.getDomain(fqdn()) || '';
function adminDomain() {
return get('adminDomain');
}
function mailLocation() {
@@ -222,10 +203,6 @@ function sysadminOrigin() {
return 'http://127.0.0.1:' + get('sysadminPort');
}
function token() {
return get('token');
}
function version() {
return get('version');
}
@@ -251,6 +228,8 @@ function hasIPv6() {
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, '')}`;
+3 -2
View File
@@ -4,13 +4,14 @@ exports = module.exports = {
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
SMTP_LOCATION: 'smtp',
IMAP_LOCATION: 'imap',
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
+49 -26
View File
@@ -10,23 +10,24 @@ var apps = require('./apps.js'),
assert = require('assert'),
backups = require('./backups.js'),
caas = require('./caas.js'),
certificates = require('./certificates.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'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
autoUpdater: null,
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
backup: null,
boxUpdateChecker: null,
@@ -77,15 +78,17 @@ function initialize(callback) {
});
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();
});
@@ -174,7 +177,7 @@ function recreateJobs(tz) {
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
});
@@ -188,32 +191,25 @@ function recreateJobs(tz) {
});
}
function autoupdatePatternChanged(pattern) {
function boxAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Auto update pattern changed to %s', pattern);
debug('Box auto update pattern changed to %s', pattern);
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.autoUpdater = 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);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No auto updates available');
debug('No box auto updates available');
}
},
start: true,
@@ -221,7 +217,33 @@ function autoupdatePatternChanged(pattern) {
});
}
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(gJobs.boxUpdateCheckerJob);
@@ -230,7 +252,7 @@ function dynamicDNSChanged(enabled) {
if (enabled) {
gJobs.dynamicDNS = new CronJob({
cronTime: '00 */10 * * * *',
onTick: cloudron.refreshDNS,
onTick: dyndns.sync,
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
@@ -244,8 +266,9 @@ function uninitialize(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.DYNAMIC_DNS_KEY, dynamicDNSChanged);
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);
for (var job in gJobs) {
if (!gJobs[job]) continue;
-1
View File
@@ -104,7 +104,6 @@ function clear(callback) {
async.series([
child_process.exec.bind(null, cmd),
require('./clientdb.js')._addDefaultClients,
require('./domaindb.js')._addDefaultDomain,
require('./groupdb.js')._addDefaultGroups
], callback);
}
+6 -5
View File
@@ -13,6 +13,7 @@ var assert = require('assert'),
constants = require('./constants.js'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
user = require('./user.js'),
util = require('util');
function DeveloperError(reason, errorOrMessage) {
@@ -37,19 +38,19 @@ util.inherits(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
function issueDeveloperToken(userObject, ip, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof ip, 'string');
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) {
tokendb.add(token, userObject.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 });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'cli', ip: ip }, { userId: userObject.id, user: user.removePrivateFields(userObject) });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
-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);
});
}
+52 -52
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('dns'),
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError,
superagent = require('superagent'),
util = require('util'),
@@ -46,19 +46,19 @@ function getZoneByName(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
callback(null, result.body.result[0]);
});
callback(null, result.body.result[0]);
});
}
function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof zoneName, 'string');
@@ -69,18 +69,18 @@ function getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.query({ type: type, name: fqdn })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
var tmp = result.body.result;
var tmp = result.body.result;
return callback(null, tmp);
});
return callback(null, tmp);
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
@@ -100,7 +100,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var zoneId = result.id;
getDNSRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
var dnsRecords = result;
@@ -126,31 +126,31 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
if (i >= dnsRecords.length) {
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
callback(null);
});
callback(null);
});
} else {
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
++i;
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
callback(null);
});
callback(null);
});
}
}, function (error) {
if (error) return callback(error);
@@ -171,7 +171,7 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
@@ -193,7 +193,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
getZoneByName(dnsConfig, zoneName, function(error, result){
if (error) return callback(error);
getDNSRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -206,17 +206,17 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
async.eachSeries(tmp, function (record, callback) {
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
debug('del: done');
debug('del: done');
callback(null);
});
callback(null);
});
}, function (error) {
if (error) return callback(error);
@@ -243,7 +243,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, nameservers) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -11,7 +11,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('dns'),
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError,
safe = require('safetydance'),
superagent = require('superagent'),
@@ -210,7 +210,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, nameservers) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -3
View File
@@ -9,9 +9,8 @@ exports = module.exports = {
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('dns'),
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError,
GCDNS = require('@google-cloud/dns'),
util = require('util'),
@@ -173,7 +172,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
var credentials = getDnsCredentials(dnsConfig);
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, resolvedNS) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !resolvedNS) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+2 -2
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('dns'),
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError,
util = require('util');
@@ -56,7 +56,7 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof callback, 'function');
// Very basic check if the nameservers can be fetched
dns.resolveNs(zoneName, function (error, nameservers) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to get nameservers'));
callback(null, { wildcard: !!dnsConfig.wildcard });
+2 -3
View File
@@ -46,11 +46,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, value, type, options, callback) {
function waitForDns(domain, zoneName, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
+3 -4
View File
@@ -13,9 +13,8 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
config = require('../config.js'),
debug = require('debug')('box:dns/route53'),
dns = require('dns'),
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError,
util = require('util'),
_ = require('underscore');
@@ -193,7 +192,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
};
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
route53.changeResourceRecordSets(params, function(error, result) {
route53.changeResourceRecordSets(params, function(error) {
if (error && error.code === 'AccessDenied') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.code === 'InvalidClientTokenId') return callback(new DomainError(DomainError.ACCESS_DENIED, error.message));
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
@@ -234,7 +233,7 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, nameservers) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
+45 -44
View File
@@ -5,53 +5,59 @@ exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/waitfordns'),
dig = require('../dig.js'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
util = require('util');
dns = require('../native-dns.js'),
DomainError = require('../domains.js').DomainError;
function isChangeSynced(domain, value, type, nameserver, callback) {
function resolveIp(hostname, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// try A record at authoritative server
debug(`resolveIp: Checking if ${hostname} has A record at ${options.server}`);
dns.resolve(hostname, 'A', options, function (error, results) {
if (!error && results.length !== 0) return callback(null, results);
// try CNAME record at authoritative server
debug(`resolveIp: Checking if ${hostname} has CNAME record at ${options.server}`);
dns.resolve(hostname, 'CNAME', options, function (error, results) {
if (error || results.length === 0) return callback(error, results);
// recurse lookup the CNAME record
debug(`resolveIp: Resolving ${hostname}'s CNAME record ${results[0]}`);
dns.resolve(results[0], 'A', { server: '127.0.0.1', timeout: options.timeout }, callback);
});
});
}
function isChangeSynced(domain, value, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isRegExp(value));
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
// ns records cannot have cname
dns.resolve4(nameserver, function (error, nsIps) {
dns.resolve(nameserver, 'A', { timeout: 5000 }, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) {
debug('nameserver %s does not resolve. assuming it stays bad.', nameserver); // it's fine if one or more ns are dead
return callback(true);
debug(`isChangeSynced: cannot resolve NS ${nameserver}`); // it's fine if one or more ns are dead
return callback(null, true);
}
async.every(nsIps, function (nsIp, iteratorCallback) {
dig.resolve(domain, type, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'ETIMEDOUT') {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
resolveIp(domain, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain}`);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain}: ${error}`);
return iteratorCallback(null, false);
}
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s)', nameserver, nsIp, domain, type);
return iteratorCallback(null, false);
}
debug(`isChangeSynced: ${domain} was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}`);
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
var match = answer.some(function (a) {
return ((type === 'A' && value.test(a)) ||
(type === 'CNAME' && value.test(a)) ||
(type === 'TXT' && value.test(a)));
});
if (match) return iteratorCallback(null, true); // done!
iteratorCallback(null, false);
iteratorCallback(null, answer.length === 1 && answer[0] === value);
});
}, callback);
@@ -59,30 +65,25 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, value, type, options, callback) {
function waitForDns(domain, zoneName, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
if (typeof value === 'string') {
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
}
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
var attempt = 1;
var attempt = 0;
async.retry(options, function (retryCallback) {
debug('waitForDNS: %s (zone: %s) attempt %s.', domain, zoneName, attempt++);
++attempt;
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
dns.resolveNs(zoneName, function (error, nameservers) {
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainError(DomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (error, synced) {
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, domain, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainError(DomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
@@ -90,7 +91,7 @@ function waitForDns(domain, zoneName, value, type, options, callback) {
}, function retryDone(error) {
if (error) return callback(error);
debug('waitForDNS: %s done.', domain);
debug(`waitForDns: ${domain} has propagated`);
callback(null);
});
+9 -5
View File
@@ -15,6 +15,7 @@ exports = module.exports = {
createSubcontainer: createSubcontainer,
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
execContainer: execContainer
};
@@ -49,10 +50,9 @@ var addons = require('./addons.js'),
_ = require('underscore');
function debugApp(app, args) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? app.intrinsicFqdn : '(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 pullImage(manifest, callback) {
@@ -129,7 +129,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.altDomain || app.intrinsicFqdn;
var domain = app.fqdn;
var stdEnv = [
'CLOUDRON=1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
@@ -163,6 +163,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
memoryLimit = constants.DEFAULT_MEMORY_LIMIT;
}
// give scheduler tasks twice the memory limit since background jobs take more memory
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
@@ -186,7 +190,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
'/run': {}
},
Labels: {
'fqdn': app.intrinsicFqdn,
'fqdn': app.fqdn,
'appId': app.id,
'isSubcontainer': String(!isAppContainer)
},
+34 -40
View File
@@ -7,22 +7,23 @@ exports = module.exports = {
get: get,
getAll: getAll,
update: update,
upsert: upsert,
del: del,
_clear: clear,
_addDefaultDomain: addDefaultDomain
_clear: clear
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
config = require('./config.js'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
delete data.configJson;
delete data.tlsConfigJson;
return data;
}
@@ -31,7 +32,7 @@ function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT * FROM domains WHERE domain=?', [ domain ], function (error, result) {
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -42,7 +43,7 @@ function get(domain, callback) {
}
function getAll(callback) {
database.query('SELECT * FROM domains ORDER BY domain', function (error, results) {
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
@@ -51,14 +52,16 @@ function getAll(callback) {
});
}
function add(domain, zoneName, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
function add(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof domain.zoneName, 'string');
assert.strictEqual(typeof domain.provider, 'string');
assert.strictEqual(typeof domain.config, 'object');
assert.strictEqual(typeof domain.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -66,27 +69,27 @@ function add(domain, zoneName, provider, config, callback) {
});
}
function upsert(domain, zoneName, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
function update(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('REPLACE INTO domains (domain, zoneName, provider, configJson) VALUES (?, ?, ?, ?)', [ domain, zoneName, provider, JSON.stringify(config) ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
var args = [ ], fields = [ ];
for (var k in domain) {
if (k === 'config') {
fields.push('configJson = ?');
args.push(JSON.stringify(domain[k]));
} else if (k === 'tlsConfig') {
fields.push('tlsConfigJson = ?');
args.push(JSON.stringify(domain[k]));
} else {
fields.push(k + ' = ?');
args.push(domain[k]);
}
}
args.push(name);
callback(null);
});
}
function update(domain, provider, config, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ provider, JSON.stringify(config), domain ], function (error) {
database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -101,7 +104,7 @@ function del(domain, callback) {
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -114,12 +117,3 @@ function clear(callback) {
callback(error);
});
}
function addDefaultDomain(callback) {
assert(config.fqdn(), 'no fqdn set in config, cannot continue');
add(config.fqdn(), config.zoneName(), 'manual', { }, function (error) {
if (error && error.reason !== DatabaseError.ALREADY_EXISTS) return callback(error);
callback();
});
}
+64 -40
View File
@@ -10,11 +10,11 @@ module.exports = exports = {
fqdn: fqdn,
setAdmin: setAdmin,
getDNSRecords: getDNSRecords,
upsertDNSRecords: upsertDNSRecords,
removeDNSRecords: removeDNSRecords,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
removeDnsRecords: removeDnsRecords,
waitForDNSRecord: waitForDNSRecord,
waitForDnsRecord: waitForDnsRecord,
DomainError: DomainError
};
@@ -22,12 +22,13 @@ module.exports = exports = {
var assert = require('assert'),
caas = require('./caas.js'),
config = require('./config.js'),
certificates = require('./certificates.js'),
CertificatesError = certificates.CertificatesError,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
@@ -82,7 +83,6 @@ function api(provider) {
}
}
// TODO make it return a DomainError instead of DomainError
function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
assert(config && typeof config === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
@@ -98,22 +98,32 @@ function verifyDnsConfig(config, domain, zoneName, provider, ip, callback) {
}
function add(domain, zoneName, provider, config, fallbackCertificate, callback) {
function add(domain, zoneName, provider, config, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (!tld.isValid(domain)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid domain'));
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new DomainError(DomainError.BAD_FIELD, 'Invalid zoneName'));
} else {
zoneName = tld.getDomain(domain) || domain;
}
if (fallbackCertificate) {
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or le-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
@@ -125,15 +135,13 @@ function add(domain, zoneName, provider, config, fallbackCertificate, callback)
if (error && error.reason === DomainError.INVALID_PROVIDER) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
domaindb.add(domain, zoneName, provider, result, function (error) {
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainError(DomainError.ALREADY_EXISTS));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
// cert validation already happened above no need to check all errors again
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
});
});
@@ -150,10 +158,15 @@ function get(domain, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
certificates.getFallbackCertificate(domain, function (error, fallbackCertificate) {
if (error && error.reason !== CertificatesError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (fallbackCertificate) result.fallbackCertificate = fallbackCertificate;
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
if (!cert || !key) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'unable to read certificates from disk'));
result.fallbackCertificate = { cert: cert, key: key };
return callback(null, result);
});
@@ -170,11 +183,12 @@ function getAll(callback) {
});
}
function update(domain, provider, config, fallbackCertificate, callback) {
function update(domain, provider, config, fallbackCertificate, tlsConfig, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof fallbackCertificate, 'object');
assert.strictEqual(typeof tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
domaindb.get(domain, function (error, result) {
@@ -182,10 +196,14 @@ function update(domain, provider, config, fallbackCertificate, callback) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (fallbackCertificate) {
let error = certificates.validateCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain);
let error = reverseProxy.validateCertificate(`test.${domain}`, fallbackCertificate.cert, fallbackCertificate.key);
if (error) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
}
if (tlsConfig.provider !== 'fallback' && tlsConfig.provider !== 'caas' && tlsConfig.provider.indexOf('letsencrypt-') !== 0) {
return callback(new DomainError(DomainError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback or letsencrypt-*'));
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
@@ -197,15 +215,15 @@ function update(domain, provider, config, fallbackCertificate, callback) {
if (error && error.reason === DomainError.INVALID_PROVIDER) return callback(new DomainError(DomainError.BAD_FIELD, error.message));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
domaindb.update(domain, provider, result, function (error) {
domaindb.update(domain, { provider: provider, config: result, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainError(DomainError.NOT_FOUND));
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
// cert validation already happened above no need to check all errors again
certificates.setFallbackCertificate(fallbackCertificate.cert, fallbackCertificate.key, domain, function (error) {
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
callback();
});
});
@@ -227,7 +245,18 @@ function del(domain, callback) {
});
}
function getDNSRecords(subdomain, domain, type, callback) {
function getName(domain, subdomain) {
// support special caas domains
if (domain.provider === 'caas') return subdomain;
if (domain.domain === domain.zoneName) return subdomain;
var part = domain.domain.slice(0, -domain.zoneName.length - 1);
return subdomain === '' ? part : subdomain + '.' + part;
}
function getDnsRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -236,7 +265,7 @@ function getDNSRecords(subdomain, domain, type, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).get(result.config, result.zoneName, subdomain, type, function (error, values) {
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain), type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -244,7 +273,7 @@ function getDNSRecords(subdomain, domain, type, callback) {
});
}
function upsertDNSRecords(subdomain, domain, type, values, callback) {
function upsertDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -256,7 +285,7 @@ function upsertDNSRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
api(result.provider).upsert(result.config, result.zoneName, subdomain, type, values, function (error, changeId) {
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain), type, values, function (error, changeId) {
if (error) return callback(error);
callback(null, changeId);
@@ -264,7 +293,7 @@ function upsertDNSRecords(subdomain, domain, type, values, callback) {
});
}
function removeDNSRecords(subdomain, domain, type, values, callback) {
function removeDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
@@ -276,7 +305,7 @@ function removeDNSRecords(subdomain, domain, type, values, callback) {
get(domain, function (error, result) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, subdomain, type, values, function (error) {
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain), type, values, function (error) {
if (error && error.reason !== DomainError.NOT_FOUND) return callback(error);
callback(null);
@@ -284,22 +313,18 @@ function removeDNSRecords(subdomain, domain, type, values, callback) {
});
}
function waitForDNSRecord(fqdn, domain, value, type, options, callback) {
// only wait for A record
function waitForDnsRecord(fqdn, domain, value, options, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
// domain can be not found when waiting for altDomain. When we migrate altDomain, this can never happen
if (error && error.reason !== DomainError.NOT_FOUND) return callback(new DomainError(DomainError.INTERNAL_ERROR, error));
if (error) return callback(error);
// hack for lack of provider with altDomain. When we migrate altDomain, this will be automatically "manual"
const provider = result ? result.provider : 'manual';
api(provider).waitForDns(fqdn, result ? result.zoneName : domain, value, type, options, callback);
api(result.provider).waitForDns(fqdn, result ? result.zoneName : domain, value, options, callback);
});
}
@@ -317,10 +342,9 @@ function setAdmin(domain, callback) {
setPtrRecord(domain, function (error) {
if (error) return callback(new DomainError(DomainError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
config.setFqdn(result.domain);
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.provider === 'caas' ? '-' : '.') + result.domain);
config.setZoneName(result.zoneName);
callback();
+49
View File
@@ -0,0 +1,49 @@
'use strict';
exports = module.exports = {
sync: sync
};
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
sysinfo = require('./sysinfo.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// called for dynamic dns setups where we have to update the IP
function sync(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
debug('refreshDNS: current ip %s', ip);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
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();
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
callback();
});
});
});
});
}
-398
View File
@@ -1,398 +0,0 @@
'use strict';
exports = module.exports = {
verifyRelay: verifyRelay,
getStatus: getStatus,
checkRblStatus: checkRblStatus,
EmailError: EmailError
};
var assert = require('assert'),
async = require('async'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
debug = require('debug')('box:email'),
dig = require('./dig.js'),
net = require('net'),
nodemailer = require('nodemailer'),
safe = require('safetydance'),
settings = require('./settings.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
util = require('util'),
_ = require('underscore');
const digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
function EmailError(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(EmailError, Error);
EmailError.INTERNAL_ERROR = 'Internal Error';
EmailError.BAD_FIELD = 'Bad Field';
function checkOutboundPort25(callback) {
assert.strictEqual(typeof callback, 'function');
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'outgoing.verizon.net'
]);
var relay = {
value: 'OK',
status: false
};
var client = new net.Socket();
client.setTimeout(5000);
client.connect(25, smtpServer);
client.on('connect', function () {
relay.status = true;
relay.value = 'OK';
client.destroy(); // do not use end() because it still triggers timeout
callback(null, relay);
});
client.on('timeout', function () {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' timed out';
client.destroy();
callback(new Error('Timeout'), relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
client.destroy();
callback(error, relay);
});
}
function checkSmtpRelay(relay, callback) {
var result = {
value: 'OK',
status: false
};
var transporter = nodemailer.createTransport(smtpTransport({
host: relay.host,
port: relay.port,
auth: {
user: relay.username,
pass: relay.password
}
}));
transporter.verify(function(error) {
result.status = !error;
if (error) {
result.value = error.message;
return callback(error, result);
}
callback(null, result);
});
}
function verifyRelay(relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
var verifier = relay.provider === 'cloudron-smtp' ? checkOutboundPort25 : checkSmtpRelay.bind(null, relay);
verifier(function (error) {
if (error) return callback(new EmailError(EmailError.BAD_FIELD, error.message));
callback();
});
}
function checkDkim(callback) {
var dkim = {
domain: config.dkimSelector() + '._domainkey.' + config.fqdn(),
type: 'TXT',
expected: null,
value: null,
status: false
};
var dkimKey = cloudron.readDkimPublicKeySync();
if (!dkimKey) return callback(new Error('Failed to read dkim public key'), dkim);
dkim.expected = '"v=DKIM1; t=s; p=' + dkimKey + '"';
dig.resolve(dkim.domain, dkim.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dkim); // not setup
if (error) return callback(error, dkim);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dkim.value = txtRecords[0];
dkim.status = (dkim.value === dkim.expected);
}
callback(null, dkim);
});
}
function checkSpf(callback) {
var spf = {
domain: config.fqdn(),
type: 'TXT',
value: null,
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
status: false
};
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
dig.resolve(spf.domain, spf.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, spf); // not setup
if (error) return callback(error, spf);
if (!Array.isArray(txtRecords)) return callback(null, spf);
var i;
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecords[i];
spf.status = spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + spf.value.slice('"v=spf1 '.length);
}
callback(null, spf);
});
}
function checkMx(callback) {
var mx = {
domain: config.fqdn(),
type: 'MX',
value: null,
expected: '10 ' + config.mailFqdn() + '.',
status: false
};
dig.resolve(mx.domain, mx.type, digOptions, function (error, mxRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, mx); // not setup
if (error) return callback(error, mx);
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
}
callback(null, mx);
});
}
function checkDmarc(callback) {
var dmarc = {
domain: '_dmarc.' + config.fqdn(),
type: 'TXT',
value: null,
expected: '"v=DMARC1; p=reject; pct=100"',
status: false
};
dig.resolve(dmarc.domain, dmarc.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, dmarc); // not setup
if (error) return callback(error, dmarc);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
dmarc.value = txtRecords[0];
dmarc.status = (dmarc.value === dmarc.expected);
}
callback(null, dmarc);
});
}
function checkPtr(callback) {
var ptr = {
domain: null,
type: 'PTR',
value: null,
expected: config.mailFqdn() + '.',
status: false
};
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ptr);
ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null, ptr); // not setup
if (error) return callback(error, ptr);
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
ptr.value = ptrRecords.join(' ');
ptr.status = ptrRecords.some(function (v) { return v === ptr.expected; });
}
return callback(null, ptr);
});
});
}
// https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json
const RBL_LIST = [
{
'name': 'Barracuda',
'dns': 'b.barracudacentral.org',
'site': 'http://www.barracudacentral.org/rbl/removal-request'
},
{
'name': 'SpamCop',
'dns': 'bl.spamcop.net',
'site': 'http://spamcop.net'
},
{
'name': 'Sorbs Aggregate Zone',
'dns': 'dnsbl.sorbs.net',
'site': 'http://dnsbl.sorbs.net/'
},
{
'name': 'Sorbs spam.dnsbl Zone',
'dns': 'spam.dnsbl.sorbs.net',
'site': 'http://sorbs.net'
},
{
'name': 'Composite Blocking List',
'dns': 'cbl.abuseat.org',
'site': 'http://www.abuseat.org'
},
{
'name': 'SpamHaus Zen',
'dns': 'zen.spamhaus.org',
'site': 'http://spamhaus.org'
},
{
'name': 'Multi SURBL',
'dns': 'multi.surbl.org',
'site': 'http://www.surbl.org'
},
{
'name': 'Spam Cannibal',
'dns': 'bl.spamcannibal.org',
'site': 'http://www.spamcannibal.org/cannibal.cgi'
},
{
'name': 'dnsbl.abuse.ch',
'dns': 'spam.abuse.ch',
'site': 'http://dnsbl.abuse.ch/'
},
{
'name': 'The Unsubscribe Blacklist(UBL)',
'dns': 'ubl.unsubscore.com ',
'site': 'http://www.lashback.com/blacklist/'
},
{
'name': 'UCEPROTECT Network',
'dns': 'dnsbl-1.uceprotect.net',
'site': 'http://www.uceprotect.net/en'
}
];
function checkRblStatus(callback) {
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error, ip);
var flippedIp = ip.split('.').reverse().join('.');
// https://tools.ietf.org/html/rfc5782
async.map(RBL_LIST, function (rblServer, iteratorDone) {
dig.resolve(flippedIp + '.' + rblServer.dns, 'A', digOptions, function (error, records) {
if (error || !records) return iteratorDone(null, null); // not listed
debug('checkRblStatus: %s (ip: %s) is in the blacklist of %j', config.fqdn(), flippedIp, rblServer);
var result = _.extend({ }, rblServer);
dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', digOptions, function (error, txtRecords) {
result.txtRecords = error || !txtRecords ? 'No txt record' : txtRecords;
debug('checkRblStatus: %s (error: %s) (txtRecords: %j)', config.fqdn(), error, txtRecords);
return iteratorDone(null, result);
});
});
}, function (ignoredError, blacklistedServers) {
blacklistedServers = blacklistedServers.filter(function(b) { return b !== null; });
debug('checkRblStatus: %s (ip: %s) servers: %j', config.fqdn(), ip, blacklistedServers);
return callback(null, { status: blacklistedServers.length === 0, ip: ip, servers: blacklistedServers });
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
var results = {};
function recordResult(what, func) {
return function (callback) {
func(function (error, result) {
if (error) debug('Ignored error - ' + what + ':', error);
safe.set(results, what, result);
callback();
});
};
}
settings.getMailRelay(function (error, relay) {
if (error) return callback(error);
var checks = [
recordResult('dns.mx', checkMx),
recordResult('dns.dmarc', checkDmarc)
];
if (relay.provider === 'cloudron-smtp') {
// these tests currently only make sense when using Cloudron's SMTP server at this point
checks.push(
recordResult('dns.spf', checkSpf),
recordResult('dns.dkim', checkDkim),
recordResult('dns.ptr', checkPtr),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus)
);
} else {
checks.push(recordResult('relay', checkSmtpRelay.bind(null, relay)));
}
async.parallel(checks, function () {
callback(null, results);
});
});
}
+3 -4
View File
@@ -22,7 +22,6 @@ exports = module.exports = {
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_CLI_MODE: 'settings.climode',
ACTION_START: 'cloudron.start',
ACTION_UPDATE: 'cloudron.update',
ACTION_USER_ADD: 'user.add',
@@ -91,14 +90,14 @@ function get(id, callback) {
});
}
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(action, search, page, perPage, function (error, events) {
eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, events);
+9 -8
View File
@@ -40,8 +40,8 @@ function get(eventId, callback) {
});
}
function getAllPaged(action, search, page, perPage, callback) {
assert(typeof action === 'string' || action === null);
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
@@ -50,14 +50,15 @@ function getAllPaged(action, search, page, perPage, callback) {
var data = [];
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
if (action || search) query += ' WHERE';
if (actions.length || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
if (action && search) query += ' AND ';
if (action) {
query += ' action=?';
data.push(action);
}
if (actions.length && search) query += ' AND ( ';
actions.forEach(function (action, i) {
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
if (i < actions.length-1) query += ' OR ';
});
if (actions.length && search) query += ' ) ';
query += ' ORDER BY creationTime DESC LIMIT ?,?';
+3 -10
View File
@@ -24,10 +24,8 @@ exports = module.exports = {
var assert = require('assert'),
constants = require('./constants.js'),
config = require('./config.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js');
DatabaseError = require('./databaseerror');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
@@ -89,13 +87,9 @@ function add(id, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
var queries = [];
queries.push({ query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)', args: [ name, config.fqdn(), id, mailboxdb.TYPE_GROUP ] });
queries.push({ query: 'INSERT INTO groups (id, name) VALUES (?, ?)', args: [ id, name ] });
database.transaction(queries, function (error, result) {
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
@@ -109,7 +103,6 @@ function del(id, callback) {
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+8 -3
View File
@@ -24,6 +24,7 @@ var assert = require('assert'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
mailboxdb = require('./mailboxdb.js'),
util = require('util'),
uuid = require('uuid');
@@ -99,11 +100,15 @@ function remove(id, callback) {
// never allow admin group to be deleted
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
mailboxdb.delByOwnerId(id, function (error) {
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null);
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null);
});
});
}
+7 -7
View File
@@ -7,18 +7,18 @@
exports = module.exports = {
// a major version makes all apps restore from backup. #451 must be fixed before we do this.
// a minor version makes all apps re-configure themselves
'version': '48.8.0',
'version': '48.9.0',
'baseImages': [ 'cloudron/base:0.10.0' ],
// Note that if any of the databases include an upgrade, bump the infra version above
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.18.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.1' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.40.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.12.0' }
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:1.0.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:1.0.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:1.0.1' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:1.0.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:1.2.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:1.0.0' }
}
};
+65 -48
View File
@@ -16,6 +16,8 @@ var assert = require('assert'),
user = require('./user.js'),
UserError = user.UserError,
ldap = require('ldapjs'),
mail = require('./mail.js'),
MailError = mail.MailError,
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance');
@@ -152,7 +154,7 @@ function userSearch(req, res, next) {
cn: entry.id,
uid: entry.id,
mail: entry.email,
mailAlternateAddress: entry.alternateEmail,
mailAlternateAddress: entry.fallbackEmail,
displayname: displayName,
givenName: firstName,
username: entry.username,
@@ -258,14 +260,11 @@ function mailboxSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
// allow login via email
var parts = name.split('@');
if (parts[1] === config.fqdn()) {
name = parts[0];
}
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getMailbox(name, config.fqdn(), function (error, mailbox) {
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -274,9 +273,9 @@ function mailboxSearch(req, res, next) {
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
cn: mailbox.name,
uid: mailbox.name,
mail: mailbox.name + '@' + config.fqdn(),
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
ownerType: mailbox.ownerType
}
};
@@ -298,19 +297,24 @@ function mailAliasSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getAlias(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, alias) {
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getAlias(parts[0], parts[1], function (error, alias) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// https://wiki.debian.org/LDAP/MigrationTools/Examples
// https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html
// member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias',
cn: alias.name,
rfc822MailMember: alias.aliasTarget
cn: `${alias.name}@${alias.domain}`,
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
}
};
@@ -331,19 +335,24 @@ function mailingListSearch(req, res, next) {
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getGroup(req.dn.rdns[0].attrs.cn.value.toLowerCase(), config.fqdn(), function (error, group) {
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
mailboxdb.getGroup(parts[0], parts[1], function (error, group) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
// members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356)
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['mailGroup'],
objectcategory: 'mailGroup',
cn: group.name,
mail: group.name + '@' + config.fqdn(),
mgrpRFC822MailMember: group.members
cn: `${group.name}@${group.domain}`, // fully qualified
mail: `${group.name}@${group.domain}`,
mgrpRFC822MailMember: group.members.map(function (m) { return `${m}@${group.domain}`; })
}
};
@@ -401,7 +410,7 @@ function authorizeUserForApp(req, res, next) {
// we return no such object, to avoid leakage of a users existence
if (!result) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id }, { userId: req.user.id });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: app.id, app: app }, { userId: req.user.id, user: user.removePrivateFields(req.user) });
res.end();
});
@@ -409,43 +418,51 @@ function authorizeUserForApp(req, res, next) {
}
function authenticateMailbox(req, res, next) {
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
if (!req.dn.rdns[0].attrs.cn) return next(new ldap.NoSuchObjectError(req.dn.toString()));
var name = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var email = req.dn.rdns[0].attrs.cn.value.toLowerCase();
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// allow login via email
var parts = name.split('@');
if (parts[1] === config.fqdn()) {
name = parts[0];
}
mailboxdb.getMailbox(name, config.fqdn(), function (error, mailbox) {
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
mail.get(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
authenticateUser(req, res, function (error) {
if (error) return next(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: name }, { userId: req.user.username });
res.end();
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
user.verifyWithUsername(parts[0], req.credentials || '', function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: user.removePrivateFields(result) });
res.end();
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
});
});
}
+1053
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -9,7 +9,7 @@ This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app in the app configuration dialog
* Restore the app to the latest backup
* Contact us via support@cloudron.io or https://chat.cloudron.io
* Contact us via support@cloudron.io or https://forum.cloudron.io
Powered by https://cloudron.io
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
Dear <%= cloudronName %> Admin,
creating a backup of <%= fqdn %> has failed.
creating a backup has failed.
-------------------------------------
+2 -2
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
Version <%= newBoxVersion %> is now available!
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
@@ -27,7 +27,7 @@ Sent at: <%= new Date().toUTCString() %>
<div style="width: 650px; text-align: left;">
<p>
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
Version <b><%= newBoxVersion %></b> is now available!
</p>
<h5>Changelog:</h5>
+1 -1
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
This is a summary of the activities on your Cloudron <%= fqdn %>.
This is a summary of the activities on your Cloudron.
<% if (info.usersAdded.length) { -%>
The following users were added:
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
Dear <%= cloudronName %> Admin,
<%= program %> on <%= fqdn %> exited unexpectedly using too much memory!
<%= program %> exited unexpectedly using too much memory!
The app has been restarted now. Should this message appear repeatedly or
undefined behavior is observed, give the app more memory.
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
Dear <%= cloudronName %> Admin,
<%= fqdn %> is running out of disk space.
your server is running out of disk space.
Disk space logs are attached.
+2 -2
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,
Hi <%= user.displayName || user.username || user.email %>,
Someone, hopefully you, has requested your account's password
be reset. If you did not request this reset, please ignore this message.
@@ -18,7 +18,7 @@ Powered by https://cloudron.io
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,</h3>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<p>
Someone, hopefully you, has requested your account's password be reset.<br/>
+1 -1
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Test email from <%= fqdn %>,
Test email from <%= cloudronName %>,
If you can read this, your Cloudron email settings are good.
-1
View File
@@ -1,7 +1,6 @@
{
"format": "html",
"webadminUrl": "https://my.cloudron.io",
"fqdn": "my.cloudron.io",
"cloudronName": "Smartserver",
"cloudronAvatarUrl": "https://cloudron.io/img/logo.png",
"info": {
+2 -2
View File
@@ -1,8 +1,8 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
Dear <%= cloudronName %> Admin,
Unfortunately <%= program %> on <%= fqdn %> exited unexpectedly!
Unfortunately <%= program %> exited unexpectedly!
Please see some excerpt of the logs below:
+2 -2
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
A new user with email <%= user.alternateEmail || user.email %> was added to <%= cloudronName %>.
A new user with email <%= user.email %> was added to <%= cloudronName %>.
<% if (inviteLink) { %>
As requested, this user has not been sent an invitation email.
@@ -24,7 +24,7 @@ Powered by https://cloudron.io
<h3>Dear <%= cloudronName %> Admin,</h3>
<p>
A new user with email <%= user.alternateEmail || user.email %> was added to <%= cloudronName %>.
A new user with email <%= user.email %> was added to <%= cloudronName %>.
</p>
<% if (inviteLink) { %>
+1 -1
View File
@@ -2,7 +2,7 @@
Dear Cloudron Admin,
User <%= user.username || user.alternateEmail || user.email %> <%= event %>.
User <%= user.username || user.email %> <%= event %>.
Powered by https://cloudron.io
+2 -2
View File
@@ -1,6 +1,6 @@
<%if (format === 'text') { %>
Dear <%= user.displayName || user.username || user.alternateEmail || user.email %>,
Dear <%= user.displayName || user.username || user.email %>,
Welcome to <%= cloudronName %>!
@@ -20,7 +20,7 @@ Powered by https://cloudron.io
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Hi <%= user.displayName || user.username || user.alternateEmail || user.email %>,</h3>
<h3>Hi <%= user.displayName || user.username || user.email %>,</h3>
<h2>Welcome to <%= cloudronName %>!</h2>
+27 -1
View File
@@ -6,6 +6,7 @@ exports = module.exports = {
listAliases: listAliases,
listMailboxes: listMailboxes,
listGroups: listGroups,
getMailbox: getMailbox,
getGroup: getGroup,
@@ -16,6 +17,7 @@ exports = module.exports = {
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
updateName: updateName,
@@ -71,6 +73,18 @@ function del(name, domain, callback) {
});
}
function delByDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function delByOwnerId(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -126,6 +140,18 @@ function listMailboxes(domain, callback) {
});
}
function listGroups(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE domain = ? AND ownerType = ? AND aliasTarget IS NULL', [ domain, exports.TYPE_GROUP ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, results);
});
}
function getGroup(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
@@ -178,7 +204,7 @@ function setAliasesForName(name, domain, aliases, callback) {
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ?', args: [ name, domain ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, domain, aliasTarget, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
args: [ alias, domain, name, results[0].ownerId, results[0].ownerType ] });
});
database.transaction(queries, function (error) {
+123
View File
@@ -0,0 +1,123 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
getAll: getAll,
update: update,
_clear: clear,
TYPE_USER: 'user',
TYPE_APP: 'app',
TYPE_GROUP: 'group'
};
var assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson' ].join(',');
function postProcess(data) {
data.enabled = !!data.enabled; // int to boolean
data.mailFromValidation = !!data.mailFromValidation; // int to boolean
data.catchAll = safe.JSON.parse(data.catchAllJson) || [ ];
delete data.catchAllJson;
data.relay = safe.JSON.parse(data.relayJson) || { provider: 'cloudron-smtp' };
delete data.relayJson;
return data;
}
function add(domain, callback) {
assert.strictEqual(typeof domain, 'string');
database.query('INSERT INTO mail (domain) VALUES (?)', [ domain ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'mail domain already exists'));
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);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE mail', [], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new DatabaseError(DatabaseError.IN_USE));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail WHERE domain = ?', [ domain ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, postProcess(results[0]));
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function update(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ];
var fields = [ ];
for (var k in data) {
if (k === 'catchAll') {
fields.push('catchAllJson = ?');
args.push(JSON.stringify(data[k]));
} else if (k === 'relay') {
fields.push('relayJson = ?');
args.push(JSON.stringify(data[k]));
} else {
fields.push(k + ' = ?');
args.push(data[k]);
}
}
args.push(domain);
database.query('UPDATE mail SET ' + fields.join(', ') + ' WHERE domain=?', args, 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);
});
}
+192 -196
View File
@@ -32,6 +32,7 @@ var assert = require('assert'),
debug = require('debug')('box:mailer'),
docker = require('./docker.js').connection,
ejs = require('ejs'),
mail = require('./mail.js'),
nodemailer = require('nodemailer'),
path = require('path'),
safe = require('safetydance'),
@@ -59,12 +60,38 @@ function splatchError(error) {
return util.inspect(result, { depth: null, showHidden: true });
}
function mailConfig() {
return {
from: '"Cloudron" <no-reply@' + config.fqdn() + '>'
};
// This will collect the most common details required for notification emails
function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
getAdminEmails(function (error, adminEmails) {
if (error) return callback(error);
settings.getCloudronName(function (error, cloudronName) {
// this is not fatal
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
mail.getAll(function (error, domains) {
if (error) return callback(error);
if (domains.length === 0) return callback('No domains configured');
const defaultDomain = domains[0];
callback(null, {
adminEmails: adminEmails,
cloudronName: cloudronName,
notificationDomain: defaultDomain.domain,
notificationFrom: `"${cloudronName}" <no-reply@${defaultDomain.domain}>`
});
});
});
});
}
function processQueue() {
sendMails(gMailQueue);
gMailQueue = [ ];
@@ -137,7 +164,7 @@ function getAdminEmails(callback) {
if (admins.length === 0) return callback(new Error('No admins on this cloudron')); // box not activated yet
var adminEmails = [ ];
if (admins[0].alternateEmail) adminEmails.push(admins[0].alternateEmail);
adminEmails.push(admins[0].fallbackEmail);
admins.forEach(function (admin) { adminEmails.push(admin.email); });
callback(null, adminEmails);
@@ -148,16 +175,16 @@ function mailUserEventToAdmins(user, event) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof event, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
adminEmails = _.difference(adminEmails, [ user.email ]);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
var mailOptions = {
from: mailConfig().from,
from: mailConfig.notificationFrom,
to: adminEmails.join(', '),
subject: util.format('[%s] %s %s', config.fqdn(), user.username || user.alternateEmail || user.email, event),
text: render('user_event.ejs', { fqdn: config.fqdn(), user: user, event: event, format: 'text' }),
subject: util.format('[%s] %s %s', mailConfig.cloudronName, user.username || user.fallbackEmail || user.email, event),
text: render('user_event.ejs', { user: user, event: event, format: 'text' }),
};
enqueue(mailOptions);
@@ -170,19 +197,15 @@ function sendInvite(user, invitor) {
debug('Sending invite mail');
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var templateData = {
user: user,
webadminUrl: config.adminOrigin(),
setupLink: config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
fqdn: config.fqdn(),
invitor: invitor,
cloudronName: cloudronName,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
@@ -193,9 +216,9 @@ function sendInvite(user, invitor) {
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig().from,
to: user.alternateEmail || user.email,
subject: util.format('Welcome to %s', cloudronName),
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('Welcome to %s', mailConfig.cloudronName),
text: render('welcome_user.ejs', templateDataText),
html: render('welcome_user.ejs', templateDataHTML)
};
@@ -210,41 +233,33 @@ function userAdded(user, inviteSent) {
debug('Sending mail for userAdded %s including invite link', inviteSent ? 'not' : '');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
adminEmails = _.difference(adminEmails, [ user.email ]);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
user: user,
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateData = {
fqdn: config.fqdn(),
user: user,
inviteLink: inviteSent ? null : config.adminOrigin() + '/api/v1/session/account/setup.html?reset_token=' + user.resetToken,
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: adminEmails.join(', '),
subject: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
};
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] User %s added', config.fqdn(), user.alternateEmail || user.email),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
enqueue(mailOptions);
});
}
@@ -270,17 +285,13 @@ function passwordReset(user) {
debug('Sending mail for password reset for user %s.', user.email, user.id);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var templateData = {
fqdn: config.fqdn(),
user: user,
resetLink: config.adminOrigin() + '/api/v1/session/password/reset.html?reset_token=' + user.resetToken,
cloudronName: cloudronName,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
@@ -291,9 +302,9 @@ function passwordReset(user) {
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig().from,
to: user.alternateEmail || user.email,
subject: util.format('[%s] Password Reset', config.fqdn()),
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: util.format('[%s] Password Reset', mailConfig.cloudronName),
text: render('password_reset.ejs', templateDataText),
html: render('password_reset.ejs', templateDataHTML)
};
@@ -307,14 +318,14 @@ function appDied(app) {
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] App %s is down', config.fqdn(), app.fqdn),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
};
enqueue(mailOptions);
@@ -326,44 +337,36 @@ function boxUpdateAvailable(hasSubscription, newBoxVersion, changelog) {
assert.strictEqual(typeof newBoxVersion, 'string');
assert(util.isArray(changelog));
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var converter = new showdown.Converter();
var converter = new showdown.Converter();
var templateData = {
webadminUrl: config.adminOrigin(),
newBoxVersion: newBoxVersion,
hasSubscription: hasSubscription,
changelog: changelog,
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
newBoxVersion: newBoxVersion,
hasSubscription: hasSubscription,
changelog: changelog,
changelogHTML: changelog.map(function (e) { return converter.makeHtml(e); }),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('%s has a new update available', mailConfig.cloudronName),
text: render('box_update_available.ejs', templateDataText),
html: render('box_update_available.ejs', templateDataHTML)
};
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('%s has a new update available', config.fqdn()),
text: render('box_update_available.ejs', templateDataText),
html: render('box_update_available.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
enqueue(mailOptions);
});
}
@@ -372,97 +375,81 @@ function appUpdateAvailable(app, hasSubscription, info) {
assert.strictEqual(typeof hasSubscription, 'boolean');
assert.strictEqual(typeof info, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var converter = new showdown.Converter();
var converter = new showdown.Converter();
var templateData = {
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
app: app,
updateInfo: info,
changelogHTML: converter.makeHtml(info.manifest.changelog),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
hasSubscription: hasSubscription,
app: app,
updateInfo: info,
changelogHTML: converter.makeHtml(info.manifest.changelog),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar'
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('App %s has a new update available', app.fqdn),
text: render('app_update_available.ejs', templateDataText),
html: render('app_update_available.ejs', templateDataHTML)
};
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('App %s has a new update available', app.fqdn),
text: render('app_update_available.ejs', templateDataText),
html: render('app_update_available.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
enqueue(mailOptions);
});
}
function sendDigest(info) {
assert.strictEqual(typeof info, 'object');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
settings.getCloudronName(function (error, cloudronName) {
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
var templateData = {
webadminUrl: config.adminOrigin(),
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
var templateData = {
fqdn: config.fqdn(),
webadminUrl: config.adminOrigin(),
cloudronName: cloudronName,
cloudronAvatarUrl: config.adminOrigin() + '/api/v1/cloudron/avatar',
info: info
};
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataText = JSON.parse(JSON.stringify(templateData));
templateDataText.format = 'text';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var templateDataHTML = JSON.parse(JSON.stringify(templateData));
templateDataHTML.format = 'html';
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', mailConfig.cloudronName),
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
var mailOptions = {
from: mailConfig().from,
to: adminEmails.join(', '),
subject: util.format('[%s] Cloudron - Weekly activity digest', config.fqdn()),
text: render('digest.ejs', templateDataText),
html: render('digest.ejs', templateDataHTML)
};
enqueue(mailOptions);
});
enqueue(mailOptions);
});
}
function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Out of disk space alert', config.fqdn()),
text: render('out_of_disk_space.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Out of disk space alert', mailConfig.cloudronName),
text: render('out_of_disk_space.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
};
sendMails([ mailOptions ]);
@@ -472,14 +459,14 @@ function outOfDiskSpace(message) {
function backupFailed(error) {
var message = splatchError(error);
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Failed to backup', config.fqdn()),
text: render('backup_failed.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Failed to backup', mailConfig.cloudronName),
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: message, format: 'text' })
};
enqueue(mailOptions);
@@ -490,12 +477,12 @@ function certificateRenewalError(domain, message) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof message, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] Certificate renewal error', domain),
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
@@ -508,14 +495,14 @@ function oomEvent(program, context) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return debug('Error getting admins', error);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('oom_event.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
};
sendMails([ mailOptions ]);
@@ -523,7 +510,7 @@ function oomEvent(program, context) {
}
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// NOTE: crashnotifier should be able to send mail when there is no db
// NOTE: crashnotifier should ideally be able to send mail when there is no db, however we need the 'from' address domain from the db
function unexpectedExit(program, context, callback) {
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
@@ -531,27 +518,36 @@ function unexpectedExit(program, context, callback) {
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
var mailOptions = {
from: mailConfig().from,
to: 'support@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('unexpected_exit.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
sendMails([ mailOptions ], callback);
var mailOptions = {
from: mailConfig.notificationFrom,
to: 'support@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
};
sendMails([ mailOptions ], callback);
});
}
function sendTestMail(email) {
function sendTestMail(domain, email) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof email, 'string');
var mailOptions = {
from: mailConfig().from,
to: email,
subject: util.format('Test Email from %s', config.fqdn()),
text: render('test.ejs', { fqdn: config.fqdn(), format: 'text'})
};
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
enqueue(mailOptions);
var mailOptions = {
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
to: email,
subject: util.format('Test Email from %s', mailConfig.cloudronName),
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
};
enqueue(mailOptions);
});
}
function _getMailQueue() {
+35
View File
@@ -0,0 +1,35 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
dns = require('dns');
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
// are added for DNS server software to enclose spaces. Such quotes may also be returned
// by the DNS REST API of some providers
function resolve(hostname, rrtype, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof rrtype, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
const resolver = new dns.Resolver();
if (options.server) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
resolver.resolve(hostname, rrtype, function (error, result) {
clearTimeout(timerId);
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
callback(error, result);
});
}
-108
View File
@@ -1,108 +0,0 @@
'use strict';
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:nginx'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js');
exports = module.exports = {
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload,
removeAppConfigs: removeAppConfigs
};
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function configureAdmin(certFilePath, keyFilePath, configFileName, vhost, callback) {
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: certFilePath,
keyFilePath: keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
reload(callback);
}
function configureApp(app, certFilePath, keyFilePath, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof certFilePath, 'string');
assert.strictEqual(typeof keyFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var vhost = app.altDomain || app.intrinsicFqdn;
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: vhost,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: certFilePath,
keyFilePath: keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', vhost, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', vhost, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var vhost = app.altDomain || app.intrinsicFqdn;
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', vhost, safe.error.message);
return callback(null);
}
reload(callback);
}
function reload(callback) {
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function removeAppConfigs() {
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
+1 -1
View File
@@ -33,7 +33,7 @@
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset your password</a>
<a href="/api/v1/session/password/resetRequest.html">Reset password</a>
</div>
</div>
</div>
+1 -1
View File
@@ -5,7 +5,7 @@
<div class="layout-content">
<center>
<h2>Reset your password</h2>
<h2>Reset password</h2>
</center>
<br/>
+14 -120
View File
@@ -4,29 +4,26 @@ exports = module.exports = {
start: start,
stop: stop,
createMailConfig: createMailConfig
handleCertChanged: handleCertChanged
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
certificates = require('./certificates.js'),
debug = require('debug')('box:platform'),
domains = require('./domains.js'),
fs = require('fs'),
hat = require('hat'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
nginx = require('./nginx.js'),
mail = require('./mail.js'),
os = require('os'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
shell = require('./shell.js'),
taskmanager = require('./taskmanager.js'),
user = require('./user.js'),
util = require('util'),
_ = require('underscore');
@@ -41,14 +38,6 @@ function start(callback) {
debug('initializing addon infrastructure');
// restart mail container if any of these keys change
settings.events.on(settings.MAIL_CONFIG_KEY, function () { startMail(NOOP_CALLBACK); });
settings.events.on(settings.MAIL_RELAY_KEY, function () { startMail(NOOP_CALLBACK); });
certificates.events.on(certificates.EVENT_CERT_CHANGED, function (domain) {
if (domain === '*.' + config.fqdn() || domain === config.adminFqdn()) startMail(NOOP_CALLBACK);
});
var existingInfra = { version: 'none' };
if (fs.existsSync(paths.INFRA_VERSION_FILE)) {
existingInfra = safe.JSON.parse(fs.readFileSync(paths.INFRA_VERSION_FILE, 'utf8'));
@@ -240,116 +229,13 @@ function startMongodb(callback) {
setTimeout(callback, 5000);
}
function createMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const fqdn = config.fqdn();
const mailFqdn = config.mailFqdn();
const alertsFrom = 'no-reply@' + config.fqdn();
debug('createMailConfig: generating mail config');
user.getOwner(function (error, owner) {
var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
settings.getAll(function (error, result) {
if (error) return callback(error);
var catchAll = result[settings.CATCH_ALL_ADDRESS_KEY].join(',');
var mailFromValidation = result[settings.MAIL_FROM_VALIDATION_KEY];
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\ndkim_selector=${config.dkimSelector()}\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
var relay = result[settings.MAIL_RELAY_KEY];
const enabled = relay.provider !== 'cloudron-smtp' ? true : false,
host = relay.host || '',
port = relay.port || 25,
username = relay.username || '',
password = relay.password || '';
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`enable_outbound=${enabled}\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=plain\nauth_user=${username}\nauth_pass=${password}`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
callback();
});
});
}
function startMail(callback) {
// mail (note: 2525 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
const tag = infra.images.mail.tag;
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
// admin and mail share the same certificate
certificates.getAdminCertificate(function (error, cert, key) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_cert.pem', cert)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_key.pem', key)) return callback(new Error('Could not create key file:' + safe.error.message));
settings.getMailConfig(function (error, mailConfig) {
if (error) return callback(error);
shell.execSync('startMail', 'docker rm -f mail || true');
createMailConfig(function (error) {
if (error) return callback(error);
var ports = mailConfig.enabled ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
--env ENABLE_MDA=${mailConfig.enabled} \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
--read-only -v /run -v /tmp ${tag}`;
shell.execSync('startMail', cmd);
if (!mailConfig.enabled || process.env.BOX_ENV === 'test') return callback();
// Add MX and DMARC record. Note that DMARC policy depends on DKIM signing and thus works
// only if we use our internal mail server.
var records = [
{ subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] },
{ subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] }
];
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDNSRecords(record.subdomain, config.fqdn(), record.type, record.values, iteratorCallback);
}, NOOP_CALLBACK); // do not crash if DNS creds do not work in startup sequence
callback();
});
});
});
}
function startAddons(existingInfra, callback) {
var startFuncs = [ ];
// always start addons on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
debug('startAddons: no existing infra or infra upgrade. starting all addons');
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, startMail);
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, mail.startMail);
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
@@ -357,7 +243,7 @@ function startAddons(existingInfra, callback) {
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql);
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql);
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb);
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(startMail);
if (infra.images.mail.tag !== existingInfra.images.mail.tag) startFuncs.push(mail.startMail);
debug('startAddons: existing infra. incremental addon create %j', startFuncs.map(function (f) { return f.name; }));
}
@@ -379,7 +265,15 @@ function startApps(existingInfra, callback) {
apps.restoreInstalledApps(callback);
} else {
debug('startApps: reconfiguring installed apps');
nginx.removeAppConfigs(); // should we change the cert location, nginx will not start
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
}
}
function handleCertChanged(cn) {
assert.strictEqual(typeof cn, 'string');
if (cn === '*.' + config.adminDomain() || cn === config.adminFqdn()) {
mail.startMail(NOOP_CALLBACK);
}
}
+435
View File
@@ -0,0 +1,435 @@
'use strict';
exports = module.exports = {
ReverseProxyError: ReverseProxyError,
setFallbackCertificate: setFallbackCertificate,
getFallbackCertificate: getFallbackCertificate,
validateCertificate: validateCertificate,
getCertificate: getCertificate,
renewAll: renewAll,
configureDefaultServer: configureDefaultServer,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
reload: reload,
removeAppConfigs: removeAppConfigs,
// 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'),
crypto = require('crypto'),
debug = require('debug')('box:certificates'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fallback = require('./cert/fallback.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
user = require('./user.js'),
util = require('util');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
NOOP_CALLBACK = function (error) { if (error) debug(error); };
function ReverseProxyError(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(ReverseProxyError, Error);
ReverseProxyError.INTERNAL_ERROR = 'Internal Error';
ReverseProxyError.INVALID_CERT = 'Invalid certificate';
ReverseProxyError.NOT_FOUND = 'Not Found';
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
domains.get(app.domain, function (error, domain) {
if (error) return callback(error);
if (domain.tlsConfig.provider === 'fallback') return callback(null, fallback, {});
var api = domain.tlsConfig.provider === 'caas' ? caas : acme;
var options = { };
if (domain.tlsConfig.provider === 'caas') {
options.prod = true;
} else { // acme
options.prod = domain.tlsConfig.provider.match(/.*-prod/) !== null; // matches 'le-prod' or 'letsencrypt-prod'
}
// 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.fallbackEmail || owner.email); // can error if not activated yet
callback(null, api, options);
});
});
}
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
}
// 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(domain, cert, key) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof cert, 'string');
assert.strictEqual(typeof key, 'string');
function matchesDomain(candidate) {
if (typeof candidate !== 'string') return false;
if (candidate === domain) return true;
if (candidate.indexOf('*') === 0 && candidate.slice(2) === domain.slice(domain.indexOf('.') + 1)) return true;
return false;
}
// check for empty cert and key strings
if (!cert && key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing cert');
if (cert && !key) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'missing key');
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${domain}"`, { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Unable to get certificate subject.');
if (result.indexOf('does match certificate') === -1) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, `Certificate is not valid for this domain. Expecting ${domain}`);
// 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 ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Key does not match the certificate.');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new ReverseProxyError(ReverseProxyError.INVALID_CERT, 'Certificate has expired.');
return null;
}
function reload(callback) {
if (process.env.BOX_ENV === 'test') return callback();
shell.sudo('reload', [ RELOAD_NGINX_CMD ], callback);
}
function setFallbackCertificate(domain, fallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof fallback, 'object');
assert.strictEqual(typeof callback, 'function');
const certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
const keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
if (fallback) {
// backup the cert
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`), fallback.cert)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, `${domain}.host.key`), fallback.key)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
} else if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
// SAN must contain all the domains since CN check is based on implementation if SAN is found. -checkhost also checks only SAN if present!
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain},DNS:*.${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
let certCommand = util.format(`openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`);
if (!safe.child_process.execSync(certCommand)) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, safe.error.message));
safe.fs.unlinkSync(configFile);
}
platform.handleCertChanged('*.' + domain);
reload(function (error) {
if (error) return callback(new ReverseProxyError(ReverseProxyError.INTERNAL_ERROR, error));
return callback(null);
});
}
function getFallbackCertificate(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// check for any pre-provisioned (caas) certs. they get first priority
var certFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.cert`);
var keyFilePath = path.join(paths.NGINX_CERT_DIR, `${domain}.host.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
// check for auto-generated or user set fallback certs
certFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${domain}.host.key`);
callback(null, { certFilePath, keyFilePath });
}
function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.cert`);
var keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.user.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
certFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.cert`);
keyFilePath = path.join(paths.APP_CERTS_DIR, `${app.fqdn}.key`);
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, { certFilePath, keyFilePath });
return getFallbackCertificate(app.domain, callback);
}
function ensureCertificate(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const vhost = app.fqdn;
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)) {
debug('ensureCertificate: %s. user certificate already exists at %s', vhost, keyFilePath);
return callback(null, { certFilePath, keyFilePath, reason: 'user' });
}
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)) {
debug('ensureCertificate: %s. certificate already exists at %s', vhost, keyFilePath);
if (!isExpiringSync(certFilePath, 24 * 30)) return callback(null, { certFilePath, keyFilePath, reason: 'existing-le' });
debug('ensureCertificate: %s cert require renewal', vhost);
} else {
debug('ensureCertificate: %s cert does not exist', vhost);
}
getApi(app, function (error, api, apiOptions) {
if (error) return callback(error);
debug('ensureCertificate: getting certificate for %s with options %j', vhost, apiOptions);
api.getCertificate(vhost, apiOptions, function (error, certFilePath, keyFilePath) {
var errorMessage = error ? error.message : '';
if (error) {
debug('ensureCertificate: could not get certificate. using fallback certs', error);
mailer.certificateRenewalError(vhost, errorMessage);
}
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: vhost, errorMessage: errorMessage });
// if no cert was returned use fallback. the fallback/caas provider will not provide any for example
if (!certFilePath || !keyFilePath) return getFallbackCertificate(app.domain, callback);
callback(null, { certFilePath, keyFilePath, reason: 'new-le' });
});
});
}
function configureAdminInternal(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
sourceDir: path.resolve(__dirname, '..'),
adminOrigin: config.adminOrigin(),
vhost: vhost, // if vhost is empty it will become the default_server
hasIPv6: config.hasIPv6(),
endpoint: 'admin',
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
xFrameOptions: 'SAMEORIGIN',
robotsTxtQuoted: JSON.stringify('User-agent: *\nDisallow: /\n')
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, configFileName);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
reload(callback);
}
function configureAdmin(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var adminApp = { domain: config.adminDomain(), fqdn: config.adminFqdn() };
ensureCertificate(adminApp, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAdminInternal(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
});
}
function configureAppInternal(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
var sourceDir = path.resolve(__dirname, '..');
var endpoint = 'app';
var data = {
sourceDir: sourceDir,
adminOrigin: config.adminOrigin(),
vhost: app.fqdn,
hasIPv6: config.hasIPv6(),
port: app.httpPort,
endpoint: endpoint,
certFilePath: bundle.certFilePath,
keyFilePath: bundle.keyFilePath,
robotsTxtQuoted: app.robotsTxt ? JSON.stringify(app.robotsTxt) : null,
xFrameOptions: app.xFrameOptions || 'SAMEORIGIN' // once all apps have been updated/
};
var nginxConf = ejs.render(NGINX_APPCONFIG_EJS, data);
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
debug('writing config for "%s" to %s with options %j', app.fqdn, nginxConfigFilename, data);
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) {
debug('Error creating nginx config for "%s" : %s', app.fqdn, safe.error.message);
return callback(safe.error);
}
reload(callback);
}
function configureApp(app, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
ensureCertificate(app, auditSource, function (error, bundle) {
if (error) return callback(error);
configureAppInternal(app, bundle, callback);
});
}
function unconfigureApp(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
var nginxConfigFilename = path.join(paths.NGINX_APPCONFIG_DIR, app.id + '.conf');
if (!safe.fs.unlinkSync(nginxConfigFilename)) {
if (safe.error.code !== 'ENOENT') debug('Error removing nginx configuration of "%s": %s', app.fqdn, safe.error.message);
return callback(null);
}
reload(callback);
}
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({ domain: config.adminDomain(), fqdn: config.adminFqdn() }); // inject fake webadmin app
async.eachSeries(allApps, function (app, iteratorCallback) {
ensureCertificate(app, auditSource, function (error, bundle) {
if (bundle.reason !== 'new-le' && bundle.reason !== 'fallback') return iteratorCallback();
// reconfigure for the case where we got a renewed cert after fallback
var configureFunc = app.fqdn === config.adminFqdn() ?
configureAdminInternal.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn())
: configureAppInternal.bind(null, app, bundle);
configureFunc(function (ignoredError) {
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
platform.handleCertChanged(app.fqdn);
iteratorCallback(); // move to next app
});
});
});
});
}
function removeAppConfigs() {
for (var appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
function configureDefaultServer(callback) {
callback = callback || NOOP_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);
}
configureAdminInternal({ certFilePath, keyFilePath }, 'default.conf', '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
callback(null);
});
}
+5 -6
View File
@@ -58,8 +58,6 @@ function removeInternalAppFields(app) {
iconUrl: app.iconUrl,
fqdn: app.fqdn,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain,
cnameTarget: app.cnameTarget,
xFrameOptions: app.xFrameOptions,
sso: app.sso,
debugMode: app.debugMode,
@@ -134,9 +132,6 @@ function installApp(req, res, next) {
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
// falsy value in altDomain unsets it
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('sso' in data && typeof data.sso !== 'boolean') return next(new HttpError(400, 'sso must be a boolean'));
@@ -149,6 +144,7 @@ function installApp(req, res, next) {
debug('Installing app :%j', data);
apps.install(data, auditSource(req), function (error, app) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
@@ -180,7 +176,6 @@ function configureApp(req, res, next) {
if (!data.cert && data.key) return next(new HttpError(400, 'cert must be provided'));
if ('memoryLimit' in data && typeof data.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
if (data.altDomain && typeof data.altDomain !== 'string') return next(new HttpError(400, 'altDomain must be a string'));
if (data.xFrameOptions && typeof data.xFrameOptions !== 'string') return next(new HttpError(400, 'xFrameOptions must be a string'));
if ('enableBackup' in data && typeof data.enableBackup !== 'boolean') return next(new HttpError(400, 'enableBackup must be a boolean'));
@@ -242,9 +237,13 @@ function cloneApp(req, res, next) {
apps.clone(req.params.id, data, auditSource(req), function (error, result) {
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(404, 'No such app'));
if (error && error.reason === AppsError.PORT_RESERVED) return next(new HttpError(409, 'Port ' + error.message + ' is reserved.'));
if (error && error.reason === AppsError.PORT_CONFLICT) return next(new HttpError(409, 'Port ' + error.message + ' is already in use.'));
if (error && error.reason === AppsError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === AppsError.BILLING_REQUIRED) return next(new HttpError(402, 'Billing required'));
if (error && error.reason === AppsError.BAD_CERTIFICATE) return next(new HttpError(400, error.message));
if (error && error.reason === AppsError.EXTERNAL_ERROR) return next(new HttpError(424, error.message));
if (error) return next(new HttpError(500, error));
+3 -153
View File
@@ -1,12 +1,6 @@
'use strict';
exports = module.exports = {
activate: activate,
dnsSetup: dnsSetup,
setupTokenAuth: setupTokenAuth,
providerTokenAuth: providerTokenAuth,
getStatus: getStatus,
restore: restore,
reboot: reboot,
getProgress: getProgress,
getConfig: getConfig,
@@ -15,8 +9,7 @@ exports = module.exports = {
feedback: feedback,
checkForUpdates: checkForUpdates,
getLogs: getLogs,
getLogStream: getLogStream,
sendTestMail: sendTestMail
getLogStream: getLogStream
};
var appstore = require('../appstore.js'),
@@ -25,13 +18,9 @@ var appstore = require('../appstore.js'),
async = require('async'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
config = require('../config.js'),
debug = require('debug')('box:routes/cloudron'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
mailer = require('../mailer.js'),
superagent = require('superagent'),
updateChecker = require('../updatechecker.js'),
_ = require('underscore');
@@ -40,136 +29,6 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = req.body.password;
var email = req.body.email;
var displayName = req.body.displayName || '';
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
cloudron.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === CloudronError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
// Now let the api server know we got activated
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/done').query({ setupToken: req.query.setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 201) return next(new HttpError(500, result.text || 'Internal error'));
next(new HttpSuccess(201, info));
});
});
}
function restore(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.backupConfig || typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig is required'));
var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
cloudron.restore(backupConfig, req.body.backupId, req.body.version, function (error) {
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function dnsSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string' || !req.body.provider) return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.adminFqdn !== 'string' || !req.body.domain) return next(new HttpError(400, 'adminFqdn is required'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
cloudron.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, function (error) {
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (config.provider() === 'caas') {
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
next();
});
} else {
next();
}
}
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (config.provider() === 'ami') {
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode !== 200) return next(new HttpError(500, 'Unable to get meta data'));
if (result.text !== req.body.providerToken) return next(new HttpError(403, 'Invalid providerToken'));
next();
});
} else {
next();
}
}
function getStatus(req, res, next) {
cloudron.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
function getProgress(req, res, next) {
return next(new HttpSuccess(200, progress.getAll()));
}
@@ -230,8 +89,9 @@ function feedback(req, res, next) {
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.sendFeedback(_.extend(req.body, { email: req.user.alternateEmail || req.user.email, displayName: req.user.displayName }), function (error) {
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
@@ -305,13 +165,3 @@ function getLogStream(req, res, next) {
logStream.on('error', res.end.bind(res, null));
});
}
function sendTestMail(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
mailer.sendTestMail(req.body.email);
next(new HttpSuccess(202));
}
+3 -6
View File
@@ -9,17 +9,14 @@ var developer = require('../developer.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function login(req, res, next) {
passport.authenticate('local', function (error, user) {
if (error) return next(new HttpError(500, error));
if (!user) return next(new HttpError(401, 'Invalid credentials'));
developer.issueDeveloperToken(user, auditSource(req), function (error, result) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
developer.issueDeveloperToken(user, ip, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { token: result.token, expiresAt: result.expiresAt }));
+16 -4
View File
@@ -26,8 +26,13 @@ function add(req, res, next) {
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.cert || typeof req.body.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.key || typeof req.body.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
domains.add(req.body.domain, req.body.zoneName || req.body.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, function (error) {
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.add(req.body.domain, req.body.zoneName || '', req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainError.ALREADY_EXISTS) return next(new HttpError(409, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
@@ -44,6 +49,8 @@ function get(req, res, next) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
next(new HttpSuccess(200, result));
});
}
@@ -65,8 +72,13 @@ function update(req, res, next) {
if ('fallbackCertificate' in req.body && typeof req.body.fallbackCertificate !== 'object') return next(new HttpError(400, 'fallbackCertificate must be a object with cert and key strings'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.cert || typeof req.body.fallbackCertificate.cert !== 'string')) return next(new HttpError(400, 'fallbackCertificate.cert must be a string'));
if (req.body.fallbackCertificate && (!req.body.fallbackCertificate.key || typeof req.body.fallbackCertificate.key !== 'string')) return next(new HttpError(400, 'fallbackCertificate.key must be a string'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be a object with a provider string property'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, function (error) {
// some DNS providers like DigitalOcean take a really long time to verify credentials (https://github.com/expressjs/timeout/issues/26)
req.clearTimeout();
domains.update(req.params.domain, req.body.provider, req.body.config, req.body.fallbackCertificate || null, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === DomainError.INVALID_PROVIDER) return next(new HttpError(400, error.message));
@@ -81,10 +93,10 @@ function del(req, res, next) {
domains.del(req.params.domain, function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use'));
if (error && error.reason === DomainError.IN_USE) return next(new HttpError(409, 'Domain is still in use. Remove all apps using this domain'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, {}));
next(new HttpSuccess(204));
});
}
+5 -1
View File
@@ -15,10 +15,14 @@ function get(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.actions && typeof req.query.actions !== 'string') return next(new HttpError(400, 'actions must be a comma separated string'));
if (req.query.action && typeof req.query.action !== 'string') return next(new HttpError(400, 'action must be a string'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
eventlog.getAllPaged(req.query.action || null, req.query.search || null, page, perPage, function (error, result) {
var actions = req.query.actions ? req.query.actions.split(',').map(function (s) { return s.trim(); }) : [];
if (req.query.action) actions.push(req.query.action);
eventlog.getAllPaged(actions, req.query.search || null, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { eventlogs: result }));
+2
View File
@@ -12,7 +12,9 @@ exports = module.exports = {
graphs: require('./graphs.js'),
groups: require('./groups.js'),
oauth2: require('./oauth2.js'),
mail: require('./mail.js'),
profile: require('./profile.js'),
setup: require('./setup.js'),
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
ssh: require('./ssh.js'),
+311
View File
@@ -0,0 +1,311 @@
'use strict';
exports = module.exports = {
get: get,
add: add,
update: update,
del: del,
getStatus: getStatus,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
getUserMailbox: getUserMailbox,
enableUserMailbox: enableUserMailbox,
disableUserMailbox: disableUserMailbox,
getAliases: getAliases,
setAliases: setAliases,
getLists: getLists,
getList: getList,
addList: addList,
removeList: removeList
};
var assert = require('assert'),
mail = require('../mail.js'),
MailError = mail.MailError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.get(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
}
function add(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
mail.add(req.body.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'domain already exists'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { domain: req.body.domain }));
});
}
function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
mail.update(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function del(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.del(req.params.domain, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.IN_USE) return next(new HttpError(409, 'Mail domain is still in use. Remove existing mailboxes'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
// can take a while to query all the DNS entries
req.clearTimeout();
mail.getStatus(req.params.domain, function (error, records) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));
});
}
function setMailFromValidation(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailFromValidation(req.params.domain, req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required'));
for (var i = 0; i < req.body.address.length; i++) {
if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string'));
}
mail.setCatchAllAddress(req.params.domain, req.body.address, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setMailRelay(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('host' in req.body && typeof req.body.host !== 'string') return next(new HttpError(400, 'host must be a string'));
if ('port' in req.body && typeof req.body.port !== 'number') return next(new HttpError(400, 'port must be a string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
mail.setMailRelay(req.params.domain, req.body, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setMailEnabled(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
mail.setMailEnabled(req.params.domain, !!req.body.enabled, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function sendTestMail(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!req.body.to || typeof req.body.to !== 'string') return next(new HttpError(400, 'to must be a non-empty string'));
mail.sendTestMail(req.params.domain, req.body.to, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailboxes(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getMailboxes(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { mailboxes: result }));
});
}
function getUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.getUserMailbox(req.params.domain, req.params.userId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { mailbox: result }));
});
}
function enableUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.enableUserMailbox(req.params.domain, req.params.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpSuccess(201, {}));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function disableUserMailbox(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.disableUserMailbox(req.params.domain, req.params.userId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpSuccess(201, {}));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function getAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
mail.getAliases(req.params.domain, req.params.userId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { aliases: result }));
});
}
function setAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.userId, 'string');
assert.strictEqual(typeof req.body, 'object');
if (!Array.isArray(req.body.aliases)) return next(new HttpError(400, 'aliases must be an array'));
for (var i = 0; i < req.body.aliases.length; i++) {
if (typeof req.body.aliases[i] !== 'string') return next(new HttpError(400, 'alias must be a string'));
}
mail.setAliases(req.params.domain, req.params.userId, req.body.aliases, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getLists(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.getLists(req.params.domain, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { lists: result }));
});
}
function getList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
mail.getList(req.params.domain, req.params.groupId, function (error, result) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { list: result }));
});
}
function addList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.groupId !== 'string') return next(new HttpError(400, 'groupId must be a string'));
mail.addList(req.params.domain, req.body.groupId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error && error.reason === MailError.ALREADY_EXISTS) return next(new HttpError(409, 'list already exists'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function removeList(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
assert.strictEqual(typeof req.params.groupId, 'string');
mail.removeList(req.params.domain, req.params.groupId, function (error) {
if (error && error.reason === MailError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204));
});
}
+6 -5
View File
@@ -26,9 +26,10 @@ var apps = require('../apps'),
util = require('util'),
_ = require('underscore');
function auditSource(req, appId) {
// appObject is optional here
function auditSource(req, appId, appObject) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { authType: 'oauth', ip: ip, appId: appId };
return { authType: 'oauth', ip: ip, appId: appId, app: appObject };
}
// create OAuth 2.0 server
@@ -239,7 +240,7 @@ function loginForm(req, res) {
apps.get(result.appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.altDomain || result.intrinsicFqdn;
var applicationName = result.fqdn;
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
});
});
@@ -447,7 +448,7 @@ var authorization = [
var type = req.oauth2.client.type;
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id, user: user.removePrivateFields(req.oauth2.user) });
return next();
}
@@ -458,7 +459,7 @@ var authorization = [
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id, appObject), { userId: req.oauth2.user.id, user: user.removePrivateFields(req.oauth2.user) });
next();
});
+3 -2
View File
@@ -25,7 +25,7 @@ function get(req, res, next) {
id: req.user.id,
username: req.user.username,
email: req.user.email,
alternateEmail: req.user.alternateEmail,
fallbackEmail: req.user.fallbackEmail,
admin: req.user.admin,
displayName: req.user.displayName
}));
@@ -36,9 +36,10 @@ function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('fallbackEmail' in req.body && typeof req.body.fallbackEmail !== 'string') return next(new HttpError(400, 'fallbackEmail must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var data = _.pick(req.body, 'email', 'displayName');
var data = _.pick(req.body, 'email', 'fallbackEmail', 'displayName');
user.update(req.user.id, data, auditSource(req), function (error) {
if (error && error.reason === UserError.BAD_FIELD) return next(new HttpError(400, error.message));
+31 -142
View File
@@ -1,8 +1,11 @@
'use strict';
exports = module.exports = {
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
getAppAutoupdatePattern: getAppAutoupdatePattern,
setAppAutoupdatePattern: setAppAutoupdatePattern,
getBoxAutoupdatePattern: getBoxAutoupdatePattern,
setBoxAutoupdatePattern: setBoxAutoupdatePattern,
getCloudronName: getCloudronName,
setCloudronName: setCloudronName,
@@ -10,57 +13,58 @@ exports = module.exports = {
getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar,
getEmailStatus: getEmailStatus,
getBackupConfig: getBackupConfig,
setBackupConfig: setBackupConfig,
getTimeZone: getTimeZone,
setTimeZone: setTimeZone,
getMailConfig: getMailConfig,
setMailConfig: setMailConfig,
getMailRelay: getMailRelay,
setMailRelay: setMailRelay,
getCatchAllAddress: getCatchAllAddress,
setCatchAllAddress: setCatchAllAddress,
getMailFromValidation: getMailFromValidation,
setMailFromValidation: setMailFromValidation,
getAppstoreConfig: getAppstoreConfig,
setAppstoreConfig: setAppstoreConfig,
setAdminCertificate: setAdminCertificate
setAppstoreConfig: setAppstoreConfig
};
var assert = require('assert'),
certificates = require('../certificates.js'),
CertificatesError = require('../certificates.js').CertificatesError,
config = require('../config.js'),
email = require('../email.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError;
function getAutoupdatePattern(req, res, next) {
settings.getAutoupdatePattern(function (error, pattern) {
function getAppAutoupdatePattern(req, res, next) {
settings.getAppAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setAutoupdatePattern(req, res, next) {
function setAppAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setAutoupdatePattern(req.body.pattern, function (error) {
settings.setAppAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getBoxAutoupdatePattern(req, res, next) {
settings.getBoxAutoupdatePattern(function (error, pattern) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { pattern: pattern }));
});
}
function setBoxAutoupdatePattern(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.pattern !== 'string') return next(new HttpError(400, 'pattern is required'));
settings.setBoxAutoupdatePattern(req.body.pattern, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
@@ -110,98 +114,6 @@ function setTimeZone(req, res, next) {
});
}
function getMailConfig(req, res, next) {
settings.getMailConfig(function (error, mail) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, mail));
});
}
function setMailConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
settings.setMailConfig({ enabled: req.body.enabled }, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailFromValidation(req, res, next) {
settings.getMailFromValidation(function (error, enabled) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { enabled: enabled }));
});
}
function setMailFromValidation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
settings.setMailFromValidation(req.body.enabled, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getMailRelay(req, res, next) {
settings.getMailRelay(function (error, mail) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, mail));
});
}
function setMailRelay(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('host' in req.body && typeof req.body.host !== 'string') return next(new HttpError(400, 'host must be a string'));
if ('port' in req.body && typeof req.body.port !== 'number') return next(new HttpError(400, 'port must be a string'));
if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
settings.setMailRelay(req.body, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function getCatchAllAddress(req, res, next) {
settings.getCatchAllAddress(function (error, address) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { address: address }));
});
}
function setCatchAllAddress(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.address || !Array.isArray(req.body.address)) return next(new HttpError(400, 'address array is required'));
for (var i = 0; i < req.body.address.length; i++) {
if (typeof req.body.address[i] !== 'string') return next(new HttpError(400, 'address must be an array of string'));
}
settings.setCatchAllAddress(req.body.address, function (error) {
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
@@ -227,14 +139,6 @@ function getCloudronAvatar(req, res, next) {
});
}
function getEmailStatus(req, res, next) {
email.getStatus(function (error, records) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));
});
}
function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
@@ -292,18 +196,3 @@ function setAppstoreConfig(req, res, next) {
});
});
}
// only webadmin cert, until it can be treated just like a normal app
function setAdminCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.cert || typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (!req.body.key || typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
certificates.setAdminCertificate(req.body.cert, req.body.key, function (error) {
if (error && error.reason === CertificatesError.INVALID_CERT) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+154
View File
@@ -0,0 +1,154 @@
'use strict';
exports = module.exports = {
providerTokenAuth: providerTokenAuth,
setupTokenAuth: setupTokenAuth,
dnsSetup: dnsSetup,
activate: activate,
restore: restore,
getStatus: getStatus,
};
var assert = require('assert'),
caas = require('../caas.js'),
CaasError = require('../caas.js').CaasError,
config = require('../config.js'),
debug = require('debug')('box:routes/setup'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
setup = require('../setup.js'),
SetupError = require('../setup.js').SetupError,
superagent = require('superagent');
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
function providerTokenAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (config.provider() === 'ami') {
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode !== 200) return next(new HttpError(500, 'Unable to get meta data'));
if (result.text !== req.body.providerToken) return next(new HttpError(403, 'Invalid providerToken'));
next();
});
} else {
next();
}
}
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (config.provider() !== 'caas') return next();
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
caas.verifySetupToken(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(403, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next();
});
}
function dnsSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.provider !== 'string' || !req.body.provider) return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
if (typeof req.body.adminFqdn !== 'string' || !req.body.adminFqdn) return next(new HttpError(400, 'adminFqdn is required'));
if ('zoneName' in req.body && typeof req.body.zoneName !== 'string') return next(new HttpError(400, 'zoneName must be a string'));
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config must be an object'));
if ('tlsConfig' in req.body && typeof req.body.tlsConfig !== 'object') return next(new HttpError(400, 'tlsConfig must be an object'));
if (req.body.tlsConfig && (!req.body.tlsConfig.provider || typeof req.body.tlsConfig.provider !== 'string')) return next(new HttpError(400, 'tlsConfig.provider must be a string'));
setup.dnsSetup(req.body.adminFqdn.toLowerCase(), req.body.domain.toLowerCase(), req.body.zoneName || '', req.body.provider, req.body.config, req.body.tlsConfig || { provider: 'letsencrypt-prod' }, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
function getStatus(req, res, next) {
setup.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be string'));
if ('displayName' in req.body && typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be string'));
var username = req.body.username;
var password = req.body.password;
var email = req.body.email;
var displayName = req.body.displayName || '';
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
debug('activate: username:%s ip:%s', username, ip);
setup.activate(username, password, email, displayName, ip, auditSource(req), function (error, info) {
if (error && error.reason === SetupError.ALREADY_PROVISIONED) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
// only in caas case do we have to notify the api server about activation
if (config.provider() !== 'caas') return next(new HttpSuccess(201, info));
caas.setupDone(req.query.setupToken, function (error) {
if (error && error.reason === CaasError.BAD_STATE) return next(new HttpError(409, 'Already setup'));
if (error && error.reason === CaasError.INVALID_TOKEN) return next(new HttpError(403, 'Invalid token'));
if (error && error.reason === CaasError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, info));
});
});
}
function restore(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.backupConfig || typeof req.body.backupConfig !== 'object') return next(new HttpError(400, 'backupConfig is required'));
var backupConfig = req.body.backupConfig;
if (typeof backupConfig.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if ('key' in backupConfig && typeof backupConfig.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (typeof backupConfig.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('acceptSelfSignedCerts' in backupConfig && typeof backupConfig.acceptSelfSignedCerts !== 'boolean') return next(new HttpError(400, 'format must be a boolean'));
if (typeof req.body.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string or null'));
if (typeof req.body.version !== 'string') return next(new HttpError(400, 'version must be a string'));
setup.restore(backupConfig, req.body.backupId, req.body.version, function (error) {
if (error && error.reason === SetupError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === SetupError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === SetupError.EXTERNAL_ERROR) return next(new HttpError(402, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200));
});
}
+116 -120
View File
@@ -9,12 +9,12 @@
var appdb = require('../../appdb.js'),
apps = require('../../apps.js'),
assert = require('assert'),
path = require('path'),
async = require('async'),
child_process = require('child_process'),
clients = require('../../clients.js'),
config = require('../../config.js'),
constants = require('../../constants.js'),
apphealthmonitor = require('../../apphealthmonitor.js'),
database = require('../../database.js'),
docker = require('../../docker.js').connection,
expect = require('expect.js'),
@@ -22,10 +22,10 @@ var appdb = require('../../appdb.js'),
hock = require('hock'),
http = require('http'),
https = require('https'),
js2xml = require('js2xmlparser').parse,
ldap = require('../../ldap.js'),
net = require('net'),
nock = require('nock'),
path = require('path'),
paths = require('../../paths.js'),
safe = require('safetydance'),
server = require('../../server.js'),
@@ -41,15 +41,23 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '25.2.0';
var TEST_IMAGE_TAG = '25.4.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
const DOMAIN_0 = {
domain: 'example-apps-test.com',
zoneName: 'example-apps-test.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
const CLOUDRON_ID = 'somecloudronid';
var APP_STORE_ID = 'test', APP_ID;
var APP_LOCATION = 'appslocation';
var APP_DOMAIN = 'example-apps-test.com';
var APP_LOCATION_2 = 'appslocationtwo';
var APP_DOMAIN_2 = 'example-apps-test.com';
var APP_LOCATION_NEW = 'appslocationnew';
var APP_MANIFEST = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
@@ -58,12 +66,18 @@ APP_MANIFEST.dockerImage = TEST_IMAGE;
var APP_MANIFEST_1 = JSON.parse(fs.readFileSync(__dirname + '/../../../../test-app/CloudronManifest.json', 'utf8'));
APP_MANIFEST_1.dockerImage = TEST_IMAGE;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='admin@me.com';
var USER_1_ID = null, USERNAME_1 = 'user', EMAIL_1 ='user@me.com';
var token = null; // authentication token
var token_1 = null;
const USERNAME = 'superadmin';
const PASSWORD = 'Foobar?1337';
const EMAIL ='admin@me.com';
var awsHostedZones;
const USER_1_APPSTORE_TOKEN = 'appstoretoken';
const USERNAME_1 = 'user';
const EMAIL_1 ='user@me.com';
var user_1_id = null;
// authentication token
var token = null;
var token_1 = null;
function startDockerProxy(interceptor, callback) {
assert.strictEqual(typeof interceptor, 'function');
@@ -106,6 +120,7 @@ function checkAddons(appEntry, done) {
delete body.recvmail; // unclear why dovecot mail delivery won't work
delete body.stdenv; // cannot access APP_ORIGIN
delete body.email; // sieve will fail not sure why yet
for (var key in body) {
if (body[key] !== 'OK') return callback('Not done yet: ' + JSON.stringify(body));
@@ -151,44 +166,30 @@ function startBox(done) {
safe.fs.unlinkSync(paths.INFRA_VERSION_FILE);
child_process.execSync('docker ps -qa | xargs --no-run-if-empty docker rm -f');
config.setFqdn(APP_DOMAIN);
config.setZoneName('foobar.com');
awsHostedZones = {
HostedZones: [{
Id: '/hostedzone/ZONEID',
Name: config.zoneName() + '.',
CallerReference: '305AFD59-9D73-4502-B020-F4E6F889CB30',
ResourceRecordSetCount: 2,
ChangeInfo: {
Id: '/change/CKRTFJA0ANHXB',
Status: 'INSYNC'
}
}],
IsTruncated: false,
MaxItems: '100'
};
async.series([
// first clear, then start server. otherwise, taskmanager spins up tasks for obsolete appIds
database.initialize,
database._clear,
server.start.bind(server),
server.start,
ldap.start,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/dns_setup')
.send({ provider: 'noop', domain: DOMAIN_0.domain, adminFqdn: 'my.' + DOMAIN_0.domain, config: DOMAIN_0.config, tlsConfig: DOMAIN_0.tlsConfig })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
callback();
});
},
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash for further use
token = result.body.token;
@@ -204,7 +205,7 @@ function startBox(done) {
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
USER_1_ID = res.body.id;
user_1_id = res.body.id;
callback(null);
});
@@ -214,7 +215,7 @@ function startBox(done) {
token_1 = tokendb.generateToken();
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, '*', callback);
tokendb.add(token_1, user_1_id, 'test-client-id', Date.now() + 1000000, '*', callback);
},
function (callback) {
@@ -232,11 +233,7 @@ function startBox(done) {
}
return false;
}, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
}
], function (error) {
if (error) return done(error);
@@ -253,6 +250,7 @@ function stopBox(done) {
// db is not cleaned up here since it's too late to call it after server.stop. if called before server.stop taskmanager apptasks are unhappy :/
async.series([
apphealthmonitor.stop,
taskmanager.stopPendingTasks,
taskmanager.waitForPendingTasks,
appdb._clear,
@@ -321,13 +319,24 @@ describe('App API', function () {
});
});
it('app install fails - invalid location', function (done) {
it('app install fails - missing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: '!awesome', accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('Hostname can only contain alphanumerics and hyphen');
expect(res.body.message).to.eql('domain is required');
done();
});
});
it('app install fails - non-existing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'some', accessRestriction: null, domain: 'doesnotexist.com' })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
expect(res.body.message).to.eql('No such domain');
done();
});
});
@@ -335,7 +344,7 @@ describe('App API', function () {
it('app install fails - invalid location type', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: 42, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('location is required');
@@ -346,7 +355,7 @@ describe('App API', function () {
it('app install fails - reserved admin location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: 'my', accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('my is reserved');
@@ -357,7 +366,7 @@ describe('App API', function () {
it('app install fails - reserved api location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: constants.API_LOCATION, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql(constants.API_LOCATION + ' is reserved');
@@ -368,7 +377,7 @@ describe('App API', function () {
it('app install fails - portBindings must be object', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: 23, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('portBindings must be an object');
@@ -379,7 +388,7 @@ describe('App API', function () {
it('app install fails - accessRestriction is required', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {} })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -390,7 +399,7 @@ describe('App API', function () {
it('app install fails - accessRestriction type is wrong', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '' })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: {}, accessRestriction: '', domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.body.message).to.eql('accessRestriction is required');
@@ -401,7 +410,7 @@ describe('App API', function () {
it('app install fails for non admin', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token_1 })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null, domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(403);
done();
@@ -413,9 +422,9 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, domain: DOMAIN_0.domain, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
expect(res.statusCode).to.equal(404);
expect(fake.isDone()).to.be.ok();
done();
});
@@ -423,40 +432,45 @@ describe('App API', function () {
it('app install fails due to purchase failure', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/test').reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(403);
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(503);
expect(res.statusCode).to.equal(402);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds with purchase', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
var fake2 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return done(error);
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake1.isDone()).to.be.ok();
done();
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: { users: [ 'someuser' ], groups: [] } })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake2.isDone()).to.be.ok();
expect(fake3.isDone()).to.be.ok();
done();
});
});
});
it('app install fails because of conflicting location', function (done) {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
@@ -546,32 +560,33 @@ describe('App API', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(200, { });
var fake3 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(204, { });
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
superagent.post(SERVER_URL + '/api/v1/apps/' + APP_ID + '/uninstall')
.send({ password: PASSWORD })
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
it('app install succeeds again', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, portBindings: null, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION_2, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
APP_ID = res.body.id;
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
done();
});
});
@@ -590,7 +605,7 @@ describe('App API', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, portBindings: null, accessRestriction: null })
.send({ manifest: APP_MANIFEST, location: APP_LOCATION+APP_LOCATION, domain: DOMAIN_0.domain, portBindings: null, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(res.body.id).to.be.a('string');
@@ -614,13 +629,11 @@ describe('App installation', function () {
this.timeout(100000);
var apiHockInstance = hock.createHock({ throwOnUnmatched: false }), apiHockServer;
var awsHockInstance = hock.createHock({ throwOnUnmatched: false }), awsHockServer;
// *.foobar.com
var validCert1, validKey1;
before(function (done) {
child_process.execSync('openssl req -subj "/CN=*.foobar.com/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
child_process.execSync('openssl req -subj "/CN=*.' + DOMAIN_0.domain + '/O=My Company Name LTD./C=US" -new -newkey rsa:2048 -days 365 -nodes -x509 -keyout /tmp/server.key -out /tmp/server.crt');
validKey1 = fs.readFileSync('/tmp/server.key', 'utf8');
validCert1 = fs.readFileSync('/tmp/server.crt', 'utf8');
@@ -628,54 +641,37 @@ describe('App installation', function () {
async.series([
startBox,
apphealthmonitor.start,
function (callback) {
apiHockInstance
.get('/api/v1/apps/' + APP_STORE_ID + '/versions/' + APP_MANIFEST.version + '/icon')
.replyWithFile(200, path.resolve(__dirname, '../../../webadmin/src/img/appicon_fallback.png'));
.replyWithFile(200, path.resolve(__dirname, '../../../assets/avatar.png'));
var port = parseInt(url.parse(config.apiServerOrigin()).port, 10);
apiHockServer = http.createServer(apiHockInstance.handler).listen(port, callback);
},
settings.setDnsConfig.bind(null, { provider: 'route53', accessKeyId: 'accessKeyId', secretAccessKey: 'secretAccessKey', endpoint: 'http://localhost:5353' }, config.fqdn(), config.zoneName()),
settings.setTlsConfig.bind(null, { provider: 'caas' }),
function (callback) {
awsHockInstance
.get('/2013-04-01/hostedzone')
.max(Infinity)
.reply(200, js2xml('ListHostedZonesResponse', awsHostedZones, { wrapHandlers: { HostedZones: () => 'HostedZone'} }), { 'Content-Type': 'application/xml' })
.filteringPathRegEx(/name=[^&]*/, 'name=location')
.get('/2013-04-01/hostedzone/ZONEID/rrset?maxitems=1&name=location&type=A')
.max(Infinity)
.reply(200, js2xml('ListResourceRecordSetsResponse', { ResourceRecordSets: [ ] }, { 'Content-Type': 'application/xml' }))
.filteringRequestBody(function (unusedBody) { return ''; }) // strip out body
.post('/2013-04-01/hostedzone/ZONEID/rrset/')
.max(Infinity)
.reply(200, js2xml('ChangeResourceRecordSetsResponse', { ChangeInfo: { Id: 'dnsrecordid', Status: 'INSYNC' } }), { 'Content-Type': 'application/xml' });
var fake1 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons') >= 0; }, { 'domain': DOMAIN_0.domain }).reply(201, { cloudron: { id: CLOUDRON_ID } });
settings.setAppstoreConfig({ userId: user_1_id, token: USER_1_APPSTORE_TOKEN }, function (error) {
if (error) return callback(error);
awsHockServer = http.createServer(awsHockInstance.handler).listen(5353, callback);
expect(fake1.isDone()).to.be.ok();
callback();
});
}
], done);
});
after(function (done) {
APP_ID = null;
async.series([
apiHockServer.close.bind(apiHockServer),
awsHockServer.close.bind(awsHockServer),
stopBox
], done);
});
after(stopBox);
var appResult = null, appEntry = null;
it('can install test app', function (done) {
var fake1 = nock(config.apiServerOrigin()).get('/api/v1/apps/' + APP_STORE_ID).reply(200, { manifest: APP_MANIFEST });
var fake2 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake3 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var fake2 = nock(config.apiServerOrigin()).post(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }, { 'appstoreId': APP_STORE_ID }).reply(201, { });
var count = 0;
function checkInstallStatus() {
@@ -692,10 +688,11 @@ describe('App installation', function () {
superagent.post(SERVER_URL + '/api/v1/apps/install')
.query({ access_token: token })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.send({ appStoreId: APP_STORE_ID, location: APP_LOCATION, domain: DOMAIN_0.domain, portBindings: { ECHO_SERVER_PORT: 7171 }, accessRestriction: null })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
APP_ID = res.body.id;
checkInstallStatus();
});
@@ -724,8 +721,8 @@ describe('App installation', function () {
expect(data.Config.Env).to.contain('WEBADMIN_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('API_ORIGIN=' + config.adminOrigin());
expect(data.Config.Env).to.contain('CLOUDRON=1');
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + APP_DOMAIN);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + APP_DOMAIN);
expect(data.Config.Env).to.contain('APP_ORIGIN=https://' + APP_LOCATION + '.' + DOMAIN_0.domain);
expect(data.Config.Env).to.contain('APP_DOMAIN=' + APP_LOCATION + '.' + DOMAIN_0.domain);
// Hostname must not be set of app fqdn or app location!
expect(data.Config.Hostname).to.not.contain(APP_LOCATION);
expect(data.Config.Env).to.contain('ECHO_SERVER_PORT=7171');
@@ -1103,9 +1100,8 @@ describe('App installation', function () {
});
it('can uninstall app', function (done) {
var fake1 = nock(config.apiServerOrigin()).post('/api/v1/exchangeBoxTokenWithUserToken?token=APPSTORE_TOKEN').reply(201, { userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' });
var fake2 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(200, { });
var fake3 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/apps/') >= 0; }).reply(204, { });
var fake1 = nock(config.apiServerOrigin()).get(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(200, { });
var fake2 = nock(config.apiServerOrigin()).delete(function (uri) { return uri.indexOf('/api/v1/users/' + user_1_id + '/cloudrons/' + CLOUDRON_ID + '/apps/') >= 0; }).reply(204, { });
var count = 0;
function checkUninstallStatus() {
@@ -1123,6 +1119,10 @@ describe('App installation', function () {
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
expect(fake1.isDone()).to.be.ok();
expect(fake2.isDone()).to.be.ok();
checkUninstallStatus();
});
});
@@ -1148,11 +1148,7 @@ describe('App installation', function () {
it('uninstalled - unregistered subdomain', function (done) {
apiHockInstance.done(function (error) { // checks if all the apiHockServer APIs were called
expect(!error).to.be.ok();
awsHockInstance.done(function (error) {
expect(!error).to.be.ok();
done();
});
done();
});
});
+17 -22
View File
@@ -9,40 +9,44 @@ var appdb = require('../../appdb.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
domains = require('../../domains.js'),
expect = require('expect.js'),
nock = require('nock'),
superagent = require('superagent'),
server = require('../../server.js'),
settings = require('../../settings.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
const SERVER_URL = 'http://localhost:' + config.get('port');
const USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
const DOMAIN_0 = {
domain: 'example-backups-test.com',
zoneName: 'example-backups-test.com',
config: {},
provider: 'noop',
fallbackCertificate: null,
tlsConfig: { provider: 'fallback' }
};
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
config.setVersion('1.2.3');
config.setFqdn('example-backups-test.com');
async.series([
server.start.bind(server),
server.start,
database._clear,
domains.add.bind(null, DOMAIN_0.domain, DOMAIN_0.zoneName, DOMAIN_0.provider, DOMAIN_0.config, DOMAIN_0.fallbackCertificate, DOMAIN_0.tlsConfig),
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// stash token for further use
token = result.body.token;
@@ -53,7 +57,7 @@ function setup(done) {
function addApp(callback) {
var manifest = { version: '0.0.1', manifestVersion: 1, dockerImage: 'foo', healthCheckPath: '/', httpPort: 3, title: 'ok', addons: { } };
appdb.add('appid', 'appStoreId', manifest, 'location', config.fqdn(), [ ] /* portBindings */, { }, callback);
appdb.add('appid', 'appStoreId', manifest, 'location', DOMAIN_0.domain, [ ] /* portBindings */, { }, callback);
},
function createSettings(callback) {
@@ -71,9 +75,6 @@ function cleanup(done) {
}
describe('Backups API', function () {
var scope1 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/awscredentials?token=BACKUP_TOKEN')
.reply(201, { credentials: { AccessKeyId: 'accessKeyId', SecretAccessKey: 'secretAccessKey' } }, { 'Content-Type': 'application/json' });
before(setup);
after(cleanup);
@@ -102,13 +103,7 @@ describe('Backups API', function () {
.end(function (error, result) {
expect(result.statusCode).to.equal(202);
function checkAppstoreServerCalled() {
if (scope1.isDone()) return done();
setTimeout(checkAppstoreServerCalled, 100);
}
checkAppstoreServerCalled();
done();
});
});
});

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