Compare commits

..

249 Commits

Author SHA1 Message Date
Girish Ramakrishnan e8c11f6e15 Fix tests 2019-02-15 14:56:51 -08:00
Girish Ramakrishnan 08bb8e3df9 Make token API id based
we don't return the accessToken anymore
2019-02-15 14:31:43 -08:00
Girish Ramakrishnan d62bf6812e Ensure tokens have a name 2019-02-15 13:45:04 -08:00
Girish Ramakrishnan 422abc205b do not return accessToken when listing tokens 2019-02-15 13:26:33 -08:00
Girish Ramakrishnan 1269104112 rbl.status is only valid if rbl test was run 2019-02-15 12:22:39 -08:00
Girish Ramakrishnan 97d762f01f relay username also needs to be hidden 2019-02-15 11:44:33 -08:00
Girish Ramakrishnan 671b5e29d0 Hide mail relay password 2019-02-15 11:25:51 -08:00
Girish Ramakrishnan c7538a35a2 Do not escape link 2019-02-14 19:36:59 -08:00
Girish Ramakrishnan 458658a71b Email gets encoded in plain text email
Unbuffered code for conditionals etc <% code %>
Escapes html by default with <%= code %>
Unescaped buffering with <%- code %>
2019-02-14 19:30:02 -08:00
Girish Ramakrishnan e348a1d2c5 make the view a link 2019-02-13 15:15:32 -08:00
Girish Ramakrishnan 59ff3998bc do not send up mails immediately on installation 2019-02-13 14:44:02 -08:00
Girish Ramakrishnan 9471dc27e0 App can also be dead/error 2019-02-12 17:01:45 -08:00
Girish Ramakrishnan 4b559a58d1 Fix use of source object 2019-02-12 16:41:46 -08:00
Girish Ramakrishnan 5980ab9b69 Add healthTime in the database
this is currently an internal field and not returned in API
2019-02-12 16:33:28 -08:00
Girish Ramakrishnan 70e5daf8c6 Fix usage of audit source 2019-02-11 14:41:12 -08:00
Girish Ramakrishnan 92e1553eed Fullstop 2019-02-11 12:58:38 -08:00
Girish Ramakrishnan 2236e07722 Send app up notification
Fixes #438
2019-02-11 12:58:33 -08:00
Girish Ramakrishnan 5166cd788b More information 2019-02-11 09:30:46 -08:00
Girish Ramakrishnan de89d41e72 Make the notifications more informative 2019-02-10 21:00:32 -08:00
Girish Ramakrishnan 3dd5526938 More 3.5.1 changes 2019-02-09 21:38:36 -08:00
Girish Ramakrishnan a88893b10a remove/inject backups secret fields
follow same pattern as dns code

fixes #615
2019-02-09 20:44:05 -08:00
Girish Ramakrishnan 51d1794e88 only inject fields if provider matches
atleast, the gcdns backend will crash otherwise
2019-02-09 17:59:32 -08:00
Girish Ramakrishnan 95e8fc73e6 Use black circle 2019-02-09 17:33:52 -08:00
Johannes Zellner 96974ab439 Make secret placeholder just stars 2019-02-09 19:16:56 +01:00
Johannes Zellner 127b22d7ce Add dns interface api to inject hidden files for verification 2019-02-09 19:09:51 +01:00
Johannes Zellner ca962e635e Add provider netcup-image 2019-02-09 18:01:31 +01:00
Johannes Zellner a70cc97b8e namecheap apiKey is now token 2019-02-09 17:43:23 +01:00
Johannes Zellner 79ae75030c move caas certificate key removal to the provider backend 2019-02-09 11:59:37 +01:00
Johannes Zellner 32f8a52c2b add provider specific removePrivateFields to redact tokens and secrets 2019-02-09 11:59:37 +01:00
Johannes Zellner d1a1f7004b Do not send out emails for out of disk
We rely now on notifications. We should hover send emails about critical
new notifications. Lets make the admin go to the dashboard to check the
situation.
2019-02-09 11:57:31 +01:00
Girish Ramakrishnan 52289568bf backups: omit the key and secret fields
part of #615
2019-02-08 22:21:28 -08:00
Girish Ramakrishnan dada79cf65 domains: do not return secret keys in api responses
part of #615
2019-02-08 21:58:38 -08:00
Girish Ramakrishnan 139a2bac1a namecheap: apiKey -> token
all token/secret/credentials will not be returned in upcoming change
2019-02-08 20:48:51 -08:00
Girish Ramakrishnan 3e4eaeab35 namecheap: do not use global object
if we have multiple namecheap, it doesn't work.
2019-02-08 20:21:16 -08:00
Girish Ramakrishnan 484171dd1b namecheap: typo 2019-02-08 19:09:28 -08:00
Girish Ramakrishnan 1c69b1695a lint 2019-02-08 11:24:33 -08:00
Girish Ramakrishnan 7cfba0e176 Fix notification tests 2019-02-08 11:22:15 -08:00
Girish Ramakrishnan ade2b65a94 make mail test pass 2019-02-08 11:08:14 -08:00
Girish Ramakrishnan 950a6d4c5d Add restriction on max password length 2019-02-08 09:57:07 -08:00
Girish Ramakrishnan 19348ef205 Fix links in motd 2019-02-07 14:07:30 -08:00
Girish Ramakrishnan 5662b124e0 Add a digitalocean-mp provider 2019-02-06 16:15:36 -08:00
Girish Ramakrishnan 5c1307f6f2 bump license years 2019-02-06 15:38:07 -08:00
Girish Ramakrishnan 2105b2ecdb Run MX and DMARC checks only if mail is enabled 2019-02-06 15:23:41 -08:00
Girish Ramakrishnan d05bf9396d Periodically check mail status as well
Fixes #612, #575
2019-02-06 14:58:45 -08:00
Girish Ramakrishnan 5b22822ac3 More 3.5.1 changes 2019-02-06 11:49:35 -08:00
Girish Ramakrishnan e08e1418e5 3.5.1 changes 2019-02-06 11:38:36 -08:00
Girish Ramakrishnan 31d0a5c40e run system checks immediately post activation
this will notify about backup configuration
2019-02-06 11:15:46 -08:00
Girish Ramakrishnan 89446d56e0 Fix exports 2019-02-06 11:09:34 -08:00
Johannes Zellner bbcad40fcf Also collect the real box logs from logfile on crash 2019-02-06 17:05:25 +01:00
Johannes Zellner 70db169976 eventId in notifications may be null 2019-02-06 16:28:51 +01:00
Johannes Zellner abc867935b Add backup configuration check together with out of disk check cron job 2019-02-06 15:47:56 +01:00
Johannes Zellner 2bb85dc16c Add out of disk and backup config warning notification handler 2019-02-06 15:47:31 +01:00
Johannes Zellner 00f4bf3d16 Add notificationdb.upsert() which clears ack field and matches by userId and title 2019-02-06 15:46:58 +01:00
Johannes Zellner 0cca838db9 Allow eventId in notifications table to be null 2019-02-06 14:40:09 +01:00
Girish Ramakrishnan abc8e1c377 improve motd a bit more 2019-02-05 16:58:24 -08:00
Girish Ramakrishnan de67b6bc0c better motd 2019-02-05 14:58:44 -08:00
Girish Ramakrishnan 058534af21 rename script 2019-02-05 10:27:05 -08:00
Girish Ramakrishnan ce1b621488 motd: add message to finish setup 2019-02-05 09:57:42 -08:00
Girish Ramakrishnan 4434c7862e Rename the fields variable 2019-02-05 09:24:16 -08:00
Johannes Zellner 86c4246f75 Do not dump the whole app object into a login event 2019-02-05 16:13:20 +01:00
Johannes Zellner 7dc3fb9854 Only upsert login events 2019-02-05 15:27:43 +01:00
Johannes Zellner 71b0226c54 add more eventlog upsert tests 2019-02-05 14:50:59 +01:00
Johannes Zellner a18d5bbe34 Add eventlogdb.upsert() for batching once per day 2019-02-05 14:50:59 +01:00
Girish Ramakrishnan f1352c6ef0 Fix crash 2019-02-04 20:51:26 -08:00
Girish Ramakrishnan 7e6ce1a1ef Add event to track dashboard update 2019-02-04 20:42:28 -08:00
Girish Ramakrishnan 9f5471ee85 Update mail DNS records on dashboard switch
Fixes #613
2019-02-04 20:18:01 -08:00
Girish Ramakrishnan 3bf36d6c93 Add mail.configureMail 2019-02-04 17:10:07 -08:00
Girish Ramakrishnan 38523835fd parameterize the mailFqdn 2019-01-31 15:27:26 -08:00
Johannes Zellner 4cb2a929a5 Remove unused require 2019-01-30 13:17:30 +01:00
Girish Ramakrishnan 1db14c710b always send emails from no-reply@dashboard domain
Fixes #614
2019-01-29 20:42:21 -08:00
Girish Ramakrishnan 13787629b6 suffix 0 when comparing versions
> semver.lte('1.2.3', '1.2.3-1')
false
2019-01-27 14:07:42 -08:00
Girish Ramakrishnan 42c705e362 UPDATE_CONFIG_KEY is unused 2019-01-25 15:59:05 -08:00
Girish Ramakrishnan 4765e4f83c Add locked flag to domains table 2019-01-25 14:45:45 -08:00
Girish Ramakrishnan ddffc8a36e better message 2019-01-25 14:11:38 -08:00
Girish Ramakrishnan 8aec71845b Add missing else 2019-01-25 10:49:00 -08:00
Girish Ramakrishnan c01864ccf5 mention outbound 2019-01-25 10:27:44 -08:00
Girish Ramakrishnan 4f839ae44e better error message for outbound port 25 2019-01-24 15:09:14 -08:00
Girish Ramakrishnan db6404a7c6 SysInfo.EXTERNAL_ERROR is undefined 2019-01-24 14:58:28 -08:00
Johannes Zellner 93e0acc8e9 Only supply the actual namecheap DNS record arguments 2019-01-24 18:46:19 +01:00
Johannes Zellner 9fa7a48b86 Print result not error 2019-01-24 14:13:41 +01:00
Girish Ramakrishnan c0b929035f lint 2019-01-23 21:00:26 -08:00
Johannes Zellner 7612e38695 We do not send out invites on user creation 2019-01-23 17:18:37 +01:00
Johannes Zellner 47329eaebc Add tests for getting a single eventlog item 2019-01-23 17:11:57 +01:00
Johannes Zellner f53a951daf Add route to get single eventlog items 2019-01-23 16:44:45 +01:00
Johannes Zellner 2181137181 Use docker based mysql server for testing with the correct version 2019-01-23 16:18:52 +01:00
Johannes Zellner 6e925f6b99 assert if auditSource is null on user apis 2019-01-23 11:18:31 +01:00
Johannes Zellner 3b5495bf72 The notification rules have changed
We do not send out notifications and emails anymore for the user who
performs the action.
2019-01-23 11:10:30 +01:00
Johannes Zellner 3617432113 Fix broken invite sending on user creation 2019-01-23 10:45:13 +01:00
Girish Ramakrishnan f95beff6d4 Fix the tests 2019-01-22 17:49:53 -08:00
Girish Ramakrishnan 6d365fde14 move datalayout to separate file for tests 2019-01-22 17:35:36 -08:00
Girish Ramakrishnan b16ff33688 more changes 2019-01-22 11:39:19 -08:00
Girish Ramakrishnan 9d8d0bed38 Add mail domain after config is setup 2019-01-22 11:37:18 -08:00
Johannes Zellner f967116087 We do not require sudo to migrate the db 2019-01-22 19:38:18 +01:00
Johannes Zellner 721352c5aa Revert "Check for sudo access of root user in cloudron-setup"
We will remove the sudo requirement instead

This reverts commit e5a04e8d38.
2019-01-22 19:33:36 +01:00
Johannes Zellner 496ba986bf Add missing wait() function for namecheap backend 2019-01-22 12:12:46 +01:00
Johannes Zellner 101a3b24ce Fix property passing for namecheap.del() 2019-01-22 12:04:17 +01:00
Johannes Zellner 201dc570cd Fix namecheap nameserver test 2019-01-22 11:56:56 +01:00
Girish Ramakrishnan ff359c477f acme: Wait for 5mins
often, let's encrypt is failing to get the new DNS. not sure why
2019-01-21 10:45:43 -08:00
Johannes Zellner 74cb8d9655 Bring namecheap dns backend up to speed with the new api layout 2019-01-21 14:36:21 +01:00
Johannes Zellner 91d0710e04 Update package lock file 2019-01-21 14:27:16 +01:00
Johannes Zellner 0cc3f08ae7 Add missing requires for scaleway sysinfo backend 2019-01-21 14:26:56 +01:00
Tomer S ac391bfc17 Added NameCheap as option for DNS 2019-01-21 12:59:08 +00:00
Johannes Zellner e5a04e8d38 Check for sudo access of root user in cloudron-setup 2019-01-21 13:33:19 +01:00
Johannes Zellner 8cc07e51bf Fix up notification tests 2019-01-21 08:51:26 +01:00
Girish Ramakrishnan 4b7090cf7c Be paranoid about the data dir location 2019-01-20 11:40:31 -08:00
Girish Ramakrishnan 8c8cc035ab Generate fsmetadata correctly 2019-01-19 21:45:54 -08:00
Girish Ramakrishnan 4b93d30ec0 Send correct error message for dataDir conflict 2019-01-19 21:24:38 -08:00
Girish Ramakrishnan d8ff2488a3 Make syncer work with a layout 2019-01-19 20:39:49 -08:00
Johannes Zellner b771df88da Ensure we write process crash logs to disk 2019-01-19 15:41:47 +01:00
Johannes Zellner 54e237cec8 Set info string if no crash logs can be found 2019-01-19 15:23:54 +01:00
Johannes Zellner b5c848474b Ensure notifications attached to events are deleted as well 2019-01-19 14:53:58 +01:00
Johannes Zellner dae52089e3 Patch auditSource if owner is creating himself an account 2019-01-19 14:34:49 +01:00
Johannes Zellner 4c4f3d04e9 Fix users tests 2019-01-19 14:25:59 +01:00
Johannes Zellner e8674487f2 Remove . makes it harder to doubleclick select and paste 2019-01-19 13:33:03 +01:00
Johannes Zellner e2fadebf64 Rename notifications.unexpectedExit() to notifications.processCrash() 2019-01-19 13:31:31 +01:00
Johannes Zellner d3331fea7f Send emails for apptask crash 2019-01-19 13:30:24 +01:00
Johannes Zellner bdcd9e035c Add missing eventId arg 2019-01-19 13:27:45 +01:00
Johannes Zellner 7f3453ce5c Crashnotifier is now only used for systemd unit crashes (only box) 2019-01-19 13:23:49 +01:00
Johannes Zellner ed7a7bc879 Use eventlog directly for apptask crashes 2019-01-19 13:23:18 +01:00
Johannes Zellner 5a6b8222df Pass down eventId to notifications 2019-01-19 13:22:29 +01:00
Johannes Zellner 3262486a96 Add eventId to notifications table 2019-01-19 13:21:09 +01:00
Johannes Zellner c73b30556f Remove unused require 2019-01-19 12:36:46 +01:00
Johannes Zellner 2ec89d6a20 Fix typo 2019-01-19 12:24:04 +01:00
Girish Ramakrishnan a0b69df20d Add DataLayout class to help with path xforms 2019-01-18 17:13:25 -08:00
Girish Ramakrishnan 57aa3de9bb typo 2019-01-18 15:18:46 -08:00
Girish Ramakrishnan 38a4c1aede Fixup volume management
Fixes related to removing directory and directory perms
2019-01-18 15:18:42 -08:00
Girish Ramakrishnan fcc77635c2 retry must wrap the download function as well 2019-01-18 14:31:30 -08:00
Girish Ramakrishnan 25be1563e1 Update mail container 2019-01-18 14:31:30 -08:00
Girish Ramakrishnan 4a9b0e8db6 Remove all app containers before removing volume
If volume location changes, we re-create the volume. However, volume
can only be removed if all the containers using it are deleted. For
example, the scheduler might be running a container using it.
2019-01-17 23:56:31 -08:00
Girish Ramakrishnan ab35821b59 saveFsMetadata: make it work with a layout 2019-01-17 14:55:37 -08:00
Girish Ramakrishnan 14439ccf77 mount points cannot be removed 2019-01-17 14:55:37 -08:00
Girish Ramakrishnan 5ddfa989d0 setupLocalStorage should remove old volume 2019-01-17 14:50:43 -08:00
Girish Ramakrishnan a915348b22 Return correct error code when already locked 2019-01-17 10:58:38 -08:00
Girish Ramakrishnan a7fe35513a Ubuntu 16 needs MemoryLimit
systemd[1]: [/etc/systemd/system/box.service:25] Unknown lvalue 'MemoryMax' in section 'Service'
2019-01-17 09:28:35 -08:00
Johannes Zellner 701024cf80 Send app down notification through eventlog 2019-01-17 17:26:58 +01:00
Johannes Zellner 4ecb0d82e7 Handle oom notification through eventlog 2019-01-17 15:31:34 +01:00
Johannes Zellner 5279be64d0 Skip notify performer or user operated on 2019-01-17 13:51:10 +01:00
Johannes Zellner b9c3e85f89 Trigger user notifications through eventlog api only 2019-01-17 13:12:26 +01:00
Girish Ramakrishnan 8aaa671412 Add more changes 2019-01-16 21:52:02 -08:00
Girish Ramakrishnan 873ebddbd0 write admin config on dashboard switch 2019-01-16 21:51:06 -08:00
Girish Ramakrishnan 13c628b58b backups (tgz): work with a layout
this will allow us to place the localstorage directory in an arbitrary
location
2019-01-16 12:52:04 -08:00
Girish Ramakrishnan 3500236d32 sync concurrency cannot be very high 2019-01-15 20:44:09 -08:00
Girish Ramakrishnan 2f881c0c91 log download errors 2019-01-15 12:01:12 -08:00
Girish Ramakrishnan 9d45e4e0ae refactor: make removeVolume not clear 2019-01-15 09:46:24 -08:00
Johannes Zellner 13fac3072d Support username search in user listing api 2019-01-15 17:21:40 +01:00
Girish Ramakrishnan 6d8fdb131f remove unused constant 2019-01-14 14:37:43 -08:00
Girish Ramakrishnan ee65089eb7 s3: make copying and uploading significantly faster 2019-01-14 13:47:07 -08:00
Girish Ramakrishnan 40c7d18382 Fix upload progress message 2019-01-14 12:23:03 -08:00
Girish Ramakrishnan 3236a9a5b7 backup: retry rsync file downloads
fixes #608
2019-01-14 11:57:10 -08:00
Girish Ramakrishnan d0522d7d4f backups: retry tgz downloads
Part of #608
2019-01-14 11:36:11 -08:00
Girish Ramakrishnan aef6b32019 Update mail container with the spf fixes 2019-01-14 10:32:55 -08:00
Girish Ramakrishnan 11b4c886d7 Add changes 2019-01-14 09:58:55 -08:00
Johannes Zellner 3470252768 Add user pagination to rest api 2019-01-14 16:39:20 +01:00
Johannes Zellner 1a3d5d0bdc Fix linter issues 2019-01-14 16:26:27 +01:00
Johannes Zellner 05f07b1f47 Add paginated user listing on the db level 2019-01-14 16:08:55 +01:00
Girish Ramakrishnan 898f1dd151 Make volume logic work with absolute paths 2019-01-13 21:12:22 -08:00
Girish Ramakrishnan 17ac6bb1a4 script is not called from redis addon anymore 2019-01-13 19:04:32 -08:00
Girish Ramakrishnan f05bed594b remove redundant assert 2019-01-13 16:06:54 -08:00
Girish Ramakrishnan e63b67b99e resolve any boxdata symlink 2019-01-13 15:17:02 -08:00
Girish Ramakrishnan efbc045c8a Add event for tracking dyndns changes 2019-01-12 15:24:22 -08:00
Girish Ramakrishnan 172d4b7c5e backup: store cleanup result properly 2019-01-12 15:17:04 -08:00
Girish Ramakrishnan 8b9177b484 disallow downgrade of App Store apps
We hit this interesting case:

1. Dashboard showed update indicator for an app of v1. indicator is saying v2 is available.
2. In the meantime, the cron updated the app from v1 to v2 and then to v3 (i.e updates applied)
3. Dashboard for whatever reason (internet issues/laptop suspend) continues to show v2 update indicator.
   This is despite the update logic clearing the update available notification.
4. Use clicked updated indicator on the updated app. App updates to an old version v2!
2019-01-11 14:19:32 -08:00
Girish Ramakrishnan 2acb065d38 Track what the the backup cleaner removed 2019-01-11 14:09:43 -08:00
Girish Ramakrishnan 0b33b0b6a2 task: result can be json 2019-01-11 14:02:18 -08:00
Girish Ramakrishnan 0390891280 Fix test 2019-01-11 13:36:02 -08:00
Girish Ramakrishnan 9203534f67 get app object in start of update func 2019-01-11 13:28:39 -08:00
Girish Ramakrishnan e15d11a693 eventlog: add the old and new manifest in restore 2019-01-11 12:27:42 -08:00
Girish Ramakrishnan c021d3d9ce backup: add retry only if > 1 2019-01-11 11:07:19 -08:00
Girish Ramakrishnan ea3cc9b153 Fix error message 2019-01-11 10:58:51 -08:00
Girish Ramakrishnan 3612b64dae gpg is in different packages in ubuntu 2019-01-11 10:20:28 -08:00
Girish Ramakrishnan 79f9180f6b run backup cleanup as a task 2019-01-10 16:07:06 -08:00
Girish Ramakrishnan 766ef5f420 remove spurious argument 2019-01-10 16:02:15 -08:00
Girish Ramakrishnan bdbb9acfd0 lint 2019-01-10 10:51:31 -08:00
Johannes Zellner 6bdac3aaec Add missing -y in cloudron-setup 2019-01-10 15:28:56 +01:00
Johannes Zellner 14acdbe7d1 Use notifications api for unexpected process exits 2019-01-10 14:30:00 +01:00
Johannes Zellner 895280fc79 Remove unused function mailUserEventToAdmins() 2019-01-10 13:32:39 +01:00
Johannes Zellner 83ae303b31 Skip notifications for user actions against the same user 2019-01-10 13:21:26 +01:00
Johannes Zellner cc81a10dd2 Add more notification/mailer wrapper 2019-01-10 12:00:04 +01:00
Girish Ramakrishnan 6e3600011b Update mail container sha 2019-01-09 16:31:53 -08:00
Girish Ramakrishnan 2b07b5ba3a Add mail container that logs events 2019-01-09 16:18:53 -08:00
Girish Ramakrishnan 7b64b2a708 do-spaces: Limit download concurrency
https://www.digitalocean.com/community/questions/rate-limiting-on-spaces?answer=40441
2019-01-09 15:09:29 -08:00
Girish Ramakrishnan 810f5e7409 Fix line param parsing
lines is a positive integer or -1 to disable line limiting. The
default value is 10 if no argument is given.

Fixes #604
2019-01-08 13:23:29 -08:00
Girish Ramakrishnan 1affb2517a Protect the updater service from the oom killer
Fixes #576
2019-01-08 10:51:47 -08:00
Johannes Zellner 85ea9b3255 Rework the oom notification 2019-01-08 14:37:58 +01:00
Johannes Zellner 07e052b865 Fix notifications route to return all notifications if nothing specified 2019-01-08 13:46:18 +01:00
Girish Ramakrishnan bc0ea740f1 Add more changes 2019-01-07 09:43:48 -08:00
Johannes Zellner 841b4aa814 Can't pass booleans over query 2019-01-07 17:30:28 +01:00
Johannes Zellner 9989478b91 Add all admins action helper 2019-01-07 14:56:49 +01:00
Johannes Zellner d3227eceff Give better oom notification title 2019-01-07 14:05:42 +01:00
Johannes Zellner 5f71f6987c Create notifications for app down event 2019-01-07 13:01:27 +01:00
Johannes Zellner 86dbb1bdcf Create notification for oom events 2019-01-07 12:57:57 +01:00
Girish Ramakrishnan 77ac8d1e62 Add changes 2019-01-06 19:23:44 -08:00
Girish Ramakrishnan e62d417324 Set OOMScoreAdjust to stop box code from being killed
OOMScoreAdjust can be set between -1000 and +1000. This value is inherited
and systemd has no easy way to control this for children (box code also
runs as non-root, so it cannot easily set it for the children using
/proc/<pid>/oom_score_adj.

When set to -1000 and the process reaches the MemoryMax, it seems the kernel
does not kill any process in the cgroup and it spins up in high memory. In fact,
'systemctl status <service>' stops displaying child process (but ps does), not sure
what is happenning.

Keeping it -999 means that if a child process consumed a lot of memory, the kernel
will kill something in the group. If the main box itself is killed, systemd will
kill it at all because of KillMode=control-group.

Keeping it -999 also saves box service group being killed relative to other docker
processes (apps and addons).

Fixes #605
2019-01-06 19:16:53 -08:00
Girish Ramakrishnan b8f85837fb cloudflare: do not wait for dns if proxied 2019-01-05 18:27:10 -08:00
Girish Ramakrishnan 2237d7ef8a Fix test 2019-01-05 00:45:01 -08:00
Girish Ramakrishnan 65210ea91d rework dns api to take domainObject
the DNS backends require many different params, it's just easier to
pass them all together and have backends do whatever.

For example, route53 API requires the fqdn. Some other backends require just the
"part" to insert.

* location - location in the database (where app is installed)
* zoneName - the dns zone name
* domain - domain in the database (where apps are installed into)
* name/getName() - this returns the name to insert in the DNS based on zoneName/location
* fqdn - the fully resolved location in zoneName

verifyDnsConfig also takes a domain object even if it's not in db just so that we can
test even existing domain objects, if required. The IP param is removed since it's not
required.

for caas, we also don't need the fqdn hack in dnsConfig anymore
2019-01-04 22:38:12 -08:00
Girish Ramakrishnan 16c1622b1f Make domains.fqdn take config and domain separately
This way it can be used in the dns backends which don't have the domain object
2019-01-04 14:11:29 -08:00
Girish Ramakrishnan 635557ca45 Fix failing tests 2019-01-04 10:56:56 -08:00
Johannes Zellner b9daa62ece Add notification tests for business logic 2019-01-04 17:13:52 +01:00
Girish Ramakrishnan 808be96de3 gpg is not installed on gandi 2019-01-03 12:28:30 -08:00
Girish Ramakrishnan 1e93289f23 cloudflare: preserve proxied parameter 2019-01-03 10:42:09 -08:00
Girish Ramakrishnan ccf0f84598 cloudflare: getDnsRecordsByZoneId -> getDnsRecords
This misleading name creates much confusion
2019-01-03 10:39:10 -08:00
Girish Ramakrishnan 3ec4c7501d cloudflare: rename confusing callback param 2019-01-03 10:39:10 -08:00
Girish Ramakrishnan f55034906c Set oldConfig.fqdn
Without this, the re-configure task unregisters the domain since
it thinks the domain has changed
2019-01-03 10:08:55 -08:00
Girish Ramakrishnan cbd3c60c5d Use a relay token for no-reply emails 2018-12-28 13:32:59 -08:00
Girish Ramakrishnan 2037fec878 new mail container does not require default domain 2018-12-28 12:12:34 -08:00
Girish Ramakrishnan 772fd1b563 Add cloudron-support to path 2018-12-26 19:42:45 -08:00
Girish Ramakrishnan d9309cb215 Use a separate event for tarExtract 2018-12-22 21:23:20 -08:00
Girish Ramakrishnan 433c34e4ce better debugs 2018-12-22 21:23:17 -08:00
Girish Ramakrishnan 68a4769f1e Fix typo 2018-12-22 19:53:50 -08:00
Girish Ramakrishnan 248569d0a8 awscli is unused 2018-12-21 12:41:43 -08:00
Girish Ramakrishnan 5146e39023 contabo: fix DNS
we disable the DNS servers in initializeBaseImage. On normal VPS,
unbound seems to start by itself but on contabo it doesn't because
the default unbound config on ubuntu does not work without ip6
2018-12-21 11:44:39 -08:00
Girish Ramakrishnan ecd1d69863 install software-properties-common
on contabo,

root@vmi232343:~# add-apt-repository

Command 'add-apt-repository' not found, but can be installed with:

apt install software-properties-common
2018-12-21 11:28:21 -08:00
Girish Ramakrishnan 06219b0c58 add contabo 2018-12-21 11:09:20 -08:00
Girish Ramakrishnan 0a74bd1718 add note on saveFsMetadata 2018-12-20 15:11:15 -08:00
Girish Ramakrishnan 8a5b24afff Make tarPack and tarExtract have consistent style 2018-12-20 11:49:37 -08:00
Girish Ramakrishnan 6bdd7f7a57 Give more memory to the control group
this allows backups to take more memory as part of the systemd group.
the node box code itself runs under little more constraints using
--max_old_space_size=150
2018-12-20 10:44:42 -08:00
Girish Ramakrishnan 1bb2552384 move feedback test 2018-12-19 14:32:54 -08:00
Girish Ramakrishnan b5b20452cc Fix reverseProxy.getCertificate API 2018-12-19 14:20:48 -08:00
Girish Ramakrishnan 4a34703cd3 rework code to enable/disable remote support
we had a generic ssh key management api. this was causing issues because
the ssh format is more complicated than what we had implemented. currently,
the only use case we have is to add our ssh key.

Fixes #600
2018-12-19 13:35:20 -08:00
Girish Ramakrishnan a8d9b57c47 remove unused tar.js 2018-12-19 11:58:08 -08:00
Girish Ramakrishnan 52bbf3be21 move support to separate file 2018-12-19 10:54:33 -08:00
Girish Ramakrishnan 3bde0666e2 volume -> app data directory
the appdata directory is just a place to "hold" various parts
of an app together for backup purposes
2018-12-18 21:16:25 -08:00
Girish Ramakrishnan b5374a1f90 3.5 changes 2018-12-18 15:33:36 -08:00
Girish Ramakrishnan 18b8d23148 Add progress percent for prepareDashboardDomain 2018-12-18 15:26:37 -08:00
Girish Ramakrishnan f51b1e1b6b installationProgress must contain the percent 2018-12-17 15:42:40 -08:00
Johannes Zellner ffc4f9d930 Fix typo 2018-12-17 17:40:53 +01:00
Johannes Zellner 5680fc839b Send new user notification via notifications api 2018-12-17 17:35:19 +01:00
Johannes Zellner 57d435ccf4 Add basic notification rest api 2018-12-17 16:37:19 +01:00
Johannes Zellner 4b90b8e6d8 Add notificationdb tests 2018-12-17 15:53:00 +01:00
Johannes Zellner fc8dcec2bb Add notificationdb table and db wrapper 2018-12-17 15:52:52 +01:00
Girish Ramakrishnan a5245fda65 3.4.3 changes
(cherry picked from commit fd723cf7eb)
2018-12-16 21:08:07 -08:00
Girish Ramakrishnan 4eec2a6414 Add LDAP_MAILBOXES_BASE_DN
this got removed by mistake in the email refactor assuming this
was unused (but it is used by sogo)

(cherry picked from commit 6589ba0988)
2018-12-16 21:06:52 -08:00
Girish Ramakrishnan a536e9fc4b track last oom time using a global variable
because it was a local variable, we were just sending out oom mails
like crazy

also, fixes an issue that if docker.getEvents gets stuck because
docker does not respond then we do not do any health monitoring.
i guess this can happen if the docker API gets stuck.
2018-12-16 20:52:42 -08:00
Girish Ramakrishnan a961407379 Fix setup and restore to have a task style API 2018-12-16 11:02:49 -08:00
Girish Ramakrishnan 1fd6c363ba 3.4.2 changes
(cherry picked from commit 2d7f0c3ebe)
2018-12-15 09:35:35 -08:00
Girish Ramakrishnan 0a7f1faad1 Better progress message 2018-12-14 23:20:32 -08:00
Girish Ramakrishnan e79d963802 do not append to task log file 2018-12-14 22:22:57 -08:00
Girish Ramakrishnan 1b4bbacd5f 3.4.1 changes
(cherry picked from commit a66bc7192d)
2018-12-14 22:22:47 -08:00
Girish Ramakrishnan 447c6fbb5f cloudron.conf has to writable 2018-12-14 16:32:51 -08:00
Girish Ramakrishnan 78acaccd89 wording 2018-12-14 16:32:51 -08:00
Girish Ramakrishnan bdf9671280 Split dashboard dns setup and db operations
The dns setup is now a task that we can wait on. Once that task
is done, we can do db operations to switch the domain in a separate
route
2018-12-14 09:57:28 -08:00
Girish Ramakrishnan 357e44284d Write nginx config into my.<domain>.conf
This way we can switch the domain as an independent task that does
not affect the existing admin conf
2018-12-14 09:20:10 -08:00
Girish Ramakrishnan 9dced3f596 Add domains.setupAdminDnsRecord 2018-12-14 09:20:10 -08:00
Girish Ramakrishnan 63e3560dd7 on startup, only re-generate the admin config
should not try to get certificates on startup
2018-12-14 09:20:06 -08:00
Girish Ramakrishnan 434525943c move appconfig.ejs 2018-12-13 21:53:31 -08:00
Girish Ramakrishnan f0dbf2fc4d Make reverseProxy.configureAdmin not use config
This way we can set things up before modifying config for dashboard switch
2018-12-13 21:42:48 -08:00
Girish Ramakrishnan 3137dbec33 CONFIG_DIR is not used anymore 2018-12-13 19:55:13 -08:00
Girish Ramakrishnan e71a8fce47 add test list tasks 2018-12-13 13:12:45 -08:00
149 changed files with 5979 additions and 2333 deletions
+63
View File
@@ -1474,3 +1474,66 @@
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.1]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.2]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
[3.4.3]
* Improve error page
* Add system view to manage addons and view their status
* Fix iconset regression for account and Cloudron name edits
* Add server reboot button and warn if reboot is required for security updates
* Backup and update tasks are now cancelable
* Move graphite away from port 3000 (reserved by ESXi)
* Flexible mailbox management
* Automatic updates can be toggled per app
* Fix issue where OOM mails are sent out without a rate limit
[3.5.0]
* Add UI to switch dashboard domain
* Fix remote support button to not remove misparsed ssh keys
* cloudflare: preseve domain proxying status
* Fix issue where oom killer might kill the box code or the updater
* Add contabo and netcup as supported providers
* Allow full logs to be downloaded
* Update Haraka to 2.8.22
* Log events in the mail container
* Fix issue where SpamAssassin and SPF checks were run for outbound email
* Improve various eventlog messages
* Track dyndns change events
* Add new S3 regions - Paris/Stockholm/Osaka
* Retry errored downloads during restore
* Add user pagination UI
* Add namecheap as supported DNS provider
[3.5.1]
* Add dashboard domain change event
* Fix issue where notification email were sent from incorrect domain
* Alert about configuration issues in the notification UI
* Switching dashboard domain now updates MX, SPF records
* Mailbox and lists UI is now always visible (but disabled) when incoming email is disabled
* Fix issue where long passwords were not accepted
* DNS and backup credential secrets are not returned in API calls anymore
* Send notification when an app that went down, came back up
[3.5.2]
* Fix encoding of links in plain text email
* Hide mail relay password
* Do not return API tokens in REST API
+1 -1
View File
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016,2017,2018 Cloudron UG
Copyright (C) 2016-2019 Cloudron UG
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
+9 -1
View File
@@ -27,13 +27,15 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \
acl \
awscli \
build-essential \
cron \
curl \
dmsetup \
$gpg_package \
iptables \
libpython2.7 \
logrotate \
@@ -126,3 +128,9 @@ systemctl disable postfix || true
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
# ubuntu's default config for unbound does not work if ipv6 is disabled. this config is overwritten in start.sh
# we need unbound to work as this is required for installer.sh to do any DNS requests
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl restart unbound
+6 -5
View File
@@ -6,17 +6,18 @@ var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
// This is triggered by systemd with the crashed unit name as argument
function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
var unitName = process.argv[2];
console.log('Started crash notifier for', unitName);
// mailer needs the db
// eventlog api 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 });
sendFailureLogs(unitName);
});
}
@@ -0,0 +1,27 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE notifications(' +
'id int NOT NULL AUTO_INCREMENT,' +
'userId VARCHAR(128) NOT NULL,' +
'eventId VARCHAR(128) NOT NULL,' +
'title VARCHAR(512) NOT NULL,' +
'message TEXT,' +
'action VARCHAR(512) NOT NULL,' +
'acknowledged BOOLEAN DEFAULT false,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'FOREIGN KEY(eventId) REFERENCES eventlog(id),' +
'PRIMARY KEY (id)) 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 notifications', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE result resultJson TEXT', [], function (error) {
if (error) console.error(error);
db.runSql('DELETE FROM tasks', callback); // empty tasks table since we have bad results format
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE resultJson result TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN dataDir VARCHAR(256) UNIQUE', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN dataDir', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE domains ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN locked', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,22 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
// WARNING in the future always give constraints proper names to not rely on automatic ones
db.runSql.bind(db, 'ALTER TABLE notifications DROP FOREIGN KEY notifications_ibfk_1'),
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE notifications ADD FOREIGN KEY(eventId) REFERENCES eventlog(id)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
@@ -0,0 +1,23 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
async.eachSeries(domains, function (domain, iteratorCallback) {
if (domain.provider !== 'namecheap') return iteratorCallback();
let config = JSON.parse(domain.configJson);
config.token = config.apiKey;
delete config.apiKey;
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'ALTER TABLE apps ADD COLUMN healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN healthTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,18 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM tokens WHERE clientId=?', ['cid-sdk'], function (error, tokens) {
if (error) console.error(error);
async.eachSeries(tokens, function (token, iteratorDone) {
if (token.name) return iteratorDone();
db.runSql('UPDATE tokens SET name=? WHERE accessToken=?', [ 'Unnamed-' + token.accessToken.slice(0,8), token.accessToken ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,29 @@
'use strict';
var async = require('async');
var uuid = require('uuid');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE tokens ADD COLUMN id VARCHAR(128)'),
function (done) {
db.runSql('SELECT * FROM tokens', function (error, tokens) {
async.eachSeries(tokens, function (token, iteratorDone) {
db.runSql('UPDATE tokens SET id=? WHERE accessToken=?', [ 'tid-'+uuid.v4(), token.accessToken ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE tokens MODIFY id VARCHAR(128) NOT NULL UNIQUE'),
db.runSql.bind(db, 'COMMIT'),
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE tokens DROP COLUMN id'),
], callback);
};
+16 -2
View File
@@ -41,9 +41,10 @@ CREATE TABLE IF NOT EXISTS groupMembers(
FOREIGN KEY(userId) REFERENCES users(id));
CREATE TABLE IF NOT EXISTS tokens(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(64) DEFAULT "", // description
accessToken VARCHAR(128) NOT NULL UNIQUE,
identifier VARCHAR(128) NOT NULL,
identifier VARCHAR(128) NOT NULL, // resourceId: app id or user id
clientId VARCHAR(128),
scope VARCHAR(512) NOT NULL,
expires BIGINT NOT NULL, // FIXME: make this a timestamp
@@ -65,6 +66,7 @@ CREATE TABLE IF NOT EXISTS apps(
installationProgress TEXT,
runState VARCHAR(512),
health VARCHAR(128),
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
containerId VARCHAR(128),
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
@@ -212,5 +214,17 @@ CREATE TABLE IF NOT EXISTS tasks(
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE IF NOT EXISTS notifications(
id int NOT NULL AUTO_INCREMENT,
userId VARCHAR(128) NOT NULL,
eventId VARCHAR(128),
title VARCHAR(512) NOT NULL,
message TEXT,
action VARCHAR(512) NOT NULL,
acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CHARACTER SET utf8 COLLATE utf8_bin;
+226
View File
@@ -829,6 +829,35 @@
}
}
},
"@sinonjs/commons": {
"version": "1.3.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/commons/-/commons-1.3.0.tgz",
"integrity": "sha1-UKJ1QBa28wqZTO2m2aCow2rdqEk=",
"dev": true,
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/formatio": {
"version": "3.1.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/formatio/-/formatio-3.1.0.tgz",
"integrity": "sha1-asnR6xghmE2ExJlnJuRdFkbYzOU=",
"dev": true,
"requires": {
"@sinonjs/samsam": "^2 || ^3"
}
},
"@sinonjs/samsam": {
"version": "3.0.2",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/samsam/-/samsam-3.0.2.tgz",
"integrity": "sha1-ME+zO9VYWgst+KTIAfy0f6hNjkM=",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.0.2",
"array-from": "^2.1.1",
"lodash.get": "^4.4.2"
}
},
"JSONStream": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
@@ -997,6 +1026,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"array-from": {
"version": "2.1.1",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/array-from/-/array-from-2.1.1.tgz",
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
"dev": true
},
"array-uniq": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
@@ -1208,6 +1243,11 @@
"tweetnacl": "^0.14.3"
}
},
"bindings": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz",
"integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew=="
},
"bl": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
@@ -2460,6 +2500,12 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"diff": {
"version": "3.5.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/diff/-/diff-3.5.0.tgz",
"integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=",
"dev": true
},
"dijkstrajs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
@@ -4293,6 +4339,12 @@
}
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -4510,6 +4562,21 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isemail": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
"requires": {
"punycode": "2.x.x"
},
"dependencies": {
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}
}
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4602,6 +4669,23 @@
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ="
},
"joi": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz",
"integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==",
"requires": {
"hoek": "5.x.x",
"isemail": "3.x.x",
"topo": "3.x.x"
},
"dependencies": {
"hoek": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz",
"integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w=="
}
}
},
"js-base64": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
@@ -4719,6 +4803,12 @@
}
}
},
"just-extend": {
"version": "4.0.2",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/just-extend/-/just-extend-4.0.2.tgz",
"integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw=",
"dev": true
},
"jwa": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz",
@@ -4887,6 +4977,12 @@
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.groupby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
@@ -4913,6 +5009,12 @@
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz",
"integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY="
},
"lolex": {
"version": "3.0.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lolex/-/lolex-3.0.0.tgz",
"integrity": "sha1-8E7hqKoT9g8avXsOj0IT7HLsGT4=",
"dev": true
},
"longest": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
@@ -5532,6 +5634,14 @@
}
}
},
"namecheap": {
"version": "github:joshuakarjala/node-namecheap#464a9528b7ded3ee2520c2688bc98cbffb08e603",
"from": "github:joshuakarjala/node-namecheap#464a952",
"requires": {
"request": "*",
"xml2json": "*"
}
},
"nan": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
@@ -5549,6 +5659,42 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"nise": {
"version": "1.4.8",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/nise/-/nise-1.4.8.tgz",
"integrity": "sha1-zpHDHobPmyxMrEnX/Nf1Z3m/1rA=",
"dev": true,
"requires": {
"@sinonjs/formatio": "^3.1.0",
"just-extend": "^4.0.2",
"lolex": "^2.3.2",
"path-to-regexp": "^1.7.0",
"text-encoding": "^0.6.4"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"lolex": {
"version": "2.7.5",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lolex/-/lolex-2.7.5.tgz",
"integrity": "sha1-ETAB1Wv8fgLVbjYpHMXEE9GqBzM=",
"dev": true
},
"path-to-regexp": {
"version": "1.7.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"dev": true,
"requires": {
"isarray": "0.0.1"
}
}
}
},
"nock": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/nock/-/nock-9.3.2.tgz",
@@ -5583,6 +5729,22 @@
}
}
},
"node-expat": {
"version": "2.3.17",
"resolved": "https://registry.npmjs.org/node-expat/-/node-expat-2.3.17.tgz",
"integrity": "sha512-mNTxY/GMiZGayqdKZXyf6lJR7OM1JqyL0EISjE4XF7Ov7+X4zJjmlnfxCi6Gml90IEOyiYBcyJg9MHDsDp6YHw==",
"requires": {
"bindings": "^1.2.1",
"nan": "^2.10.0"
},
"dependencies": {
"nan": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
"integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw=="
}
}
},
"node-forge": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz",
@@ -7997,6 +8159,32 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"sinon": {
"version": "7.2.2",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/sinon/-/sinon-7.2.2.tgz",
"integrity": "sha1-OI7KvUL6k8WSv8cdNacIlNWgygc=",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.2.0",
"@sinonjs/formatio": "^3.1.0",
"@sinonjs/samsam": "^3.0.2",
"diff": "^3.5.0",
"lolex": "^3.0.0",
"nise": "^1.4.7",
"supports-color": "^5.5.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"sntp": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
@@ -8614,6 +8802,12 @@
}
}
},
"text-encoding": {
"version": "0.6.4",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/text-encoding/-/text-encoding-0.6.4.tgz",
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
"dev": true
},
"through2": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
@@ -8636,6 +8830,21 @@
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
},
"topo": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
"integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
"requires": {
"hoek": "6.x.x"
},
"dependencies": {
"hoek": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz",
"integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q=="
}
}
},
"tough-cookie": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
@@ -8952,6 +9161,23 @@
}
}
},
"xml2json": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.11.2.tgz",
"integrity": "sha512-ZJpHpPOL0T5lOvAHMnWm59iQOPqNtam5t2TMUllWZ1k5Wm8L5YyvQnkeaVnRKCvDwY5EumqXWyOjjMdQVz272A==",
"requires": {
"hoek": "^4.2.1",
"joi": "^13.1.2",
"node-expat": "^2.3.15"
},
"dependencies": {
"hoek": {
"version": "4.2.1",
"resolved": "http://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
}
}
},
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+4 -4
View File
@@ -45,6 +45,7 @@
"morgan": "^1.9.0",
"multiparty": "^4.1.4",
"mysql": "^2.15.0",
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
"nodemailer": "^4.6.5",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
@@ -87,12 +88,11 @@
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"recursive-readdir": "^2.2.2"
"recursive-readdir": "^2.2.2",
"sinon": "^7.2.2"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test",
"test": "src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test/[^a]*js",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
+11 -2
View File
@@ -101,7 +101,9 @@ elif [[ \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
"${provider}" != "digitalocean" && \
"${provider}" != "digitalocean-mp" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
@@ -111,13 +113,14 @@ elif [[ \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "netcup" && \
"${provider}" != "netcup-image" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
echo "--provider must be one of: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -138,6 +141,12 @@ echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Installing software-properties-common"
if ! apt-get install -y software-properties-common &>> "${LOG_FILE}"; then
echo "Could not install software-properties-common (for add-apt-repository below). See ${LOG_FILE}"
exit 1
fi
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
@@ -243,7 +252,7 @@ echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate t
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
read -p "This server has to rebooted to apply all the settings. Reboot now ? [Y/n] " yn
read -p "The server has to be rebooted to apply all the settings. Reboot now ? [Y/n] " yn
yn=${yn:-y}
case $yn in
[Yy]* ) systemctl reboot;;
+9 -17
View File
@@ -13,10 +13,12 @@ readonly BOX_SRC_DIR="${HOME_DIR}/box"
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 script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -88,25 +90,19 @@ systemctl daemon-reload
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
mkdir -p "${CONFIG_DIR}"
# remove old cloudron.conf. Can be removed after 3.4
rm -f "${CONFIG_DIR}/cloudron.conf"
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.version" # remove the version field
chown -R "${USER}" /etc/cloudron
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
# If IP6 is not enabled, dns queries seem to fail on some hosts
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: yes\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# If IP6 is not enabled, dns queries seem to fail on some hosts. -s returns false if file missing or 0 size
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: ${ip6}\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# update the root anchor after a out-of-disk-space situation (see #269)
unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
@@ -183,11 +179,7 @@ mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
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
(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)
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
@@ -198,8 +190,8 @@ else
fi
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
chown -R "${USER}" /etc/cloudron
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
+26 -12
View File
@@ -1,15 +1,29 @@
#!/bin/sh
# motd hook to remind admins about updates
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
#!/bin/bash
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
printf "\t\t\tWELCOME TO CLOUDRON\n"
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://<IP> on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
else
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
fi
fi
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
printf "**********************************************************************\n"
+10 -4
View File
@@ -1,8 +1,14 @@
# sudo logging breaks journalctl output with very long urls (systemd bug)
Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
Defaults!/home/yellowtent/box/src/scripts/clearvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/clearvolume.sh
Defaults!/home/yellowtent/box/src/scripts/mvvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mvvolume.sh
Defaults!/home/yellowtent/box/src/scripts/mkdirvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mkdirvolume.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
@@ -25,8 +31,8 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh
Defaults!/home/yellowtent/box/src/scripts/remotesupport.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remotesupport.sh
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
+4 -1
View File
@@ -17,9 +17,12 @@ ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.lo
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
OOMScoreAdjust=-999
User=yellowtent
Group=yellowtent
MemoryLimit=200M
; OOM killer is invoked in this unit beyond this. The start script replaces this with MemoryLimit for Ubuntu 16
MemoryMax=400M
TimeoutStopSec=5s
StartLimitInterval=1
StartLimitBurst=60
+1 -1
View File
@@ -121,7 +121,7 @@ function validateToken(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
tokendb.getByAccessToken(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error); // this triggers 'internal error' in passport
+25 -13
View File
@@ -33,6 +33,7 @@ exports = module.exports = {
var accesscontrol = require('./accesscontrol.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
@@ -109,7 +110,7 @@ var KNOWN_ADDONS = {
clear: NOOP,
},
localstorage: {
setup: setupLocalStorage, // docker creates the directory for us
setup: setupLocalStorage,
teardown: teardownLocalStorage,
backup: NOOP, // no backup because it's already inside app data
restore: NOOP,
@@ -369,24 +370,25 @@ function getServiceLogs(serviceName, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
debug(`Getting logs for ${serviceName}`);
var lines = options.lines || 100,
var lines = options.lines,
format = options.format || 'json',
follow = !!options.follow;
follow = options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var cmd;
var args = [ '--lines=' + lines ];
let cmd, args = [];
// docker and unbound use journald
if (serviceName === 'docker' || serviceName === 'unbound') {
cmd = 'journalctl';
args.push('--lines=' + (lines === -1 ? 'all' : lines));
args.push(`--unit=${serviceName}`);
args.push('--no-pager');
args.push('--output=short-iso');
@@ -395,6 +397,7 @@ function getServiceLogs(serviceName, options, callback) {
} else {
cmd = '/usr/bin/tail';
args.push('--lines=' + (lines === -1 ? '+1' : lines));
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
}
@@ -726,8 +729,13 @@ function setupLocalStorage(app, options, callback) {
debugApp(app, 'setupLocalStorage');
// if you change the name, you have to change getMountsSync
docker.createVolume(app, `${app.id}-localstorage`, 'data', callback);
const volumeDataDir = apps.getDataDir(app, app.dataDir);
// reomve any existing volume in case it's bound with an old dataDir
async.series([
docker.removeVolume.bind(null, app, `${app.id}-localstorage`),
docker.createVolume.bind(null, app, `${app.id}-localstorage`, volumeDataDir)
], callback);
}
function clearLocalStorage(app, options, callback) {
@@ -737,7 +745,7 @@ function clearLocalStorage(app, options, callback) {
debugApp(app, 'clearLocalStorage');
docker.clearVolume(app, `${app.id}-localstorage`, 'data', callback);
docker.clearVolume(app, `${app.id}-localstorage`, { removeDirectory: false }, callback);
}
function teardownLocalStorage(app, options, callback) {
@@ -747,7 +755,10 @@ function teardownLocalStorage(app, options, callback) {
debugApp(app, 'teardownLocalStorage');
docker.removeVolume(app, `${app.id}-localstorage`, 'data', callback);
async.series([
docker.clearVolume.bind(null, app, `${app.id}-localstorage`, { removeDirectory: true }),
docker.removeVolume.bind(null, app, `${app.id}-localstorage`)
], callback);
}
function setupOauth(app, options, callback) {
@@ -815,7 +826,8 @@ function setupEmail(app, options, callback) {
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
{ name: 'MAIL_DOMAIN', value: app.domain },
{ name: 'MAIL_DOMAINS', value: mailInDomains }
{ name: 'MAIL_DOMAINS', value: mailInDomains },
{ name: 'LDAP_MAILBOXES_BASE_DN', value: 'ou=mailboxes,dc=cloudron' }
];
debugApp(app, 'Setting up Email');
+9 -4
View File
@@ -43,7 +43,7 @@ exports = module.exports = {
RSTATE_RUNNING: 'running',
RSTATE_PENDING_START: 'pending_start',
RSTATE_PENDING_STOP: 'pending_stop',
RSTATE_STOPPED: 'stopped', // app stopped by use
RSTATE_STOPPED: 'stopped', // app stopped by us
// run codes (keep in sync in UI)
HEALTH_HEALTHY: 'healthy',
@@ -69,7 +69,8 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate', 'apps.ts' ].join(',');
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -139,6 +140,9 @@ function postProcess(result) {
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
// in the db, we store dataDir as unique/nullable
result.dataDir = result.dataDir || '';
}
function get(id, callback) {
@@ -472,12 +476,13 @@ function updateWithConstraints(id, app, constraints, callback) {
}
// not sure if health should influence runState
function setHealth(appId, health, callback) {
function setHealth(appId, health, healthTime, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
assert(util.isDate(healthTime));
assert.strictEqual(typeof callback, 'function');
var values = { health: health };
var values = { health, healthTime };
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
+43 -38
View File
@@ -7,7 +7,7 @@ var appdb = require('./appdb.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
eventlog = require('./eventlog.js'),
superagent = require('superagent'),
util = require('util');
@@ -15,11 +15,13 @@ exports = module.exports = {
run: run
};
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
const UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
const NOOP_CALLBACK = function (error) { if (error) console.error(error); };
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
function debugApp(app) {
assert(typeof app === 'object');
@@ -32,27 +34,29 @@ function setHealth(app, health, callback) {
assert.strictEqual(typeof health, 'string');
assert.strictEqual(typeof callback, 'function');
var now = new Date();
if (!(app.id in gHealthInfo)) { // add new apps to list
gHealthInfo[app.id] = { time: now, emailSent: false };
}
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
if (health === appdb.HEALTH_HEALTHY) {
gHealthInfo[app.id].time = now;
} else if (Math.abs(now - gHealthInfo[app.id].time) > UNHEALTHY_THRESHOLD) {
if (gHealthInfo[app.id].emailSent) return callback(null);
healthTime = now;
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
debugApp(app, 'app switched from %s to healthy', curHealth);
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, AUDIT_SOURCE, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (curHealth === appdb.HEALTH_HEALTHY) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
if (!app.debugMode) mailer.appDied(app); // do not send mails for dev apps
gHealthInfo[app.id].emailSent = true;
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, AUDIT_SOURCE, { app: app });
}
} else {
debugApp(app, 'waiting for sometime to update the app health');
debugApp(app, 'waiting for %s seconds to update the app health', (UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000);
return callback(null);
}
appdb.setHealth(app.id, health, function (error) {
appdb.setHealth(app.id, health, healthTime, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
if (error) return callback(error);
@@ -118,13 +122,11 @@ function checkAppHealth(app, callback) {
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents(interval, callback) {
assert.strictEqual(typeof interval, 'number');
function processDockerEvents(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
let lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
const since = ((new Date().getTime() / 1000) - interval).toFixed(0);
const since = ((new Date().getTime() / 1000) - intervalSecs).toFixed(0);
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
@@ -133,18 +135,22 @@ function processDockerEvents(interval, callback) {
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
var now = new Date();
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
var containerId = ev.id;
debug('OOM Context: %s', context);
appdb.getByContainerId(containerId, function (error, app) { // this can error for addons
var program = error || !app.id ? containerId : `app-${app.id}`;
var now = Date.now();
const notifyUser = (!app || !app.debugMode) && (now - gLastOomMailTime > OOM_MAIL_LIMIT);
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now, ev);
// do not send mails for dev apps
if ((!app || !app.debugMode) && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
mailer.oomEvent(program, context); // app can be null if it's an addon crash
lastOomMailTime = now;
if (notifyUser) {
// app can be null for addon containers
eventlog.add(eventlog.ACTION_APP_OOM, AUDIT_SOURCE, { ev: ev, containerId: containerId, app: app || null });
gLastOomMailTime = now;
}
});
});
@@ -181,14 +187,13 @@ function processApp(callback) {
});
}
function run(interval, callback) {
assert.strictEqual(typeof interval, 'number');
callback = callback || NOOP_CALLBACK;
function run(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
async.series([
processDockerEvents.bind(null, interval),
processApp
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs)
], function (error) {
if (error) debug(error);
+82 -34
View File
@@ -38,6 +38,7 @@ exports = module.exports = {
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
getDataDir: getDataDir,
downloadFile: downloadFile,
uploadFile: uploadFile,
@@ -301,26 +302,50 @@ function validateEnv(env) {
return null;
}
function validateDataDir(dataDir) {
if (dataDir === '') return null; // revert back to default dataDir
if (path.resolve(dataDir) !== dataDir) return new AppsError(AppsError.BAD_FIELD, 'dataDir must be an absolute path');
// nfs shares will have the directory mounted already
let stat = safe.fs.lstatSync(dataDir);
if (stat) {
if (!stat.isDirectory()) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not a directory`);
let entries = safe.fs.readdirSync(dataDir);
if (!entries) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} could not be listed`);
if (entries.length !== 0) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not empty`);
}
// backup logic relies on paths not overlapping (because it recurses)
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be inside apps data`);
// if we made it this far, it cannot start with any of these realistically
const fhs = [ '/bin', '/boot', '/etc', '/lib', '/lib32', '/lib64', '/proc', '/run', '/sbin', '/tmp', '/usr' ];
if (fhs.some((p) => dataDir.startsWith(p))) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be placed inside this location`);
return null;
}
function getDuplicateErrorDetails(location, portBindings, error) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key/);
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
debug('Unexpected SQL error message.', error);
return new AppsError(AppsError.INTERNAL_ERROR);
return new AppsError(AppsError.INTERNAL_ERROR, error);
}
// check if the location conflicts
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
// check if any of the port bindings conflict
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
}
return new AppsError(AppsError.ALREADY_EXISTS);
return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
}
// app configs that is useful for 'archival' into the app backup config.json
@@ -329,6 +354,7 @@ function getAppConfig(app) {
manifest: app.manifest,
location: app.location,
domain: app.domain,
fqdn: app.fqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
@@ -336,19 +362,25 @@ function getAppConfig(app) {
robotsTxt: app.robotsTxt,
sso: app.sso,
alternateDomains: app.alternateDomains || [],
env: app.env
env: app.env,
dataDir: app.dataDir
};
}
function getDataDir(app, dataDir) {
return dataDir || path.join(paths.APPS_DATA_DIR, app.id, 'data');
}
function removeInternalFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate');
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
}
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'ownerId',
@@ -746,6 +778,12 @@ function configure(appId, data, user, auditSource, callback) {
if (error) return callback(error);
}
if ('dataDir' in data && data.dataDir !== app.dataDir) {
error = validateDataDir(data.dataDir);
if (error) return callback(error);
values.dataDir = data.dataDir;
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -801,34 +839,22 @@ function update(appId, data, auditSource, callback) {
debug('Will update app with id:%s', appId);
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
get(appId, function (error, app) {
if (error) return callback(error);
var updateConfig = { };
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
updateConfig.manifest = manifest;
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
}
}
get(appId, function (error, app) {
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
var updateConfig = { };
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
updateConfig.manifest = manifest;
// 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
if (app.manifest.id !== updateConfig.manifest.id) {
@@ -837,6 +863,25 @@ function update(appId, data, auditSource, callback) {
updateConfig.appStoreId = '';
}
// suffix '0' if prerelease is missing for semver.lte to work as expected
const currentVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
const updateVersion = semver.prerelease(updateConfig.manifest.version) ? updateConfig.manifest.version : `${updateConfig.manifest.version}-0`;
if (app.appStoreId !== '' && semver.lte(updateVersion, currentVersion)) {
if (!data.force) return callback(new AppsError(AppsError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override'));
}
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
}
}
// do not update apps in debug mode
if (app.debugMode && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'debug mode enabled. force to override'));
@@ -868,16 +913,19 @@ function getLogs(appId, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
debug('Getting logs for %s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
var lines = options.lines || 100,
var lines = options.lines === -1 ? '+1' : options.lines,
format = options.format || 'json',
follow = !!options.follow;
follow = options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var args = [ '--lines=' + lines ];
@@ -952,7 +1000,7 @@ function restore(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest });
callback(null);
});
+39 -30
View File
@@ -52,6 +52,7 @@ var addons = require('./addons.js'),
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'),
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
@@ -135,27 +136,14 @@ function createContainer(app, callback) {
});
}
// Only delete the main container of the app, not destroy any docker addon created ones
function deleteMainContainer(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting main app container');
docker.deleteContainer(app.containerId, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
});
}
function deleteContainers(app, callback) {
function deleteContainers(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting app containers (app, scheduler)');
docker.deleteContainers(app.id, function (error) {
docker.deleteContainers(app.id, options, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
@@ -187,6 +175,7 @@ function deleteAppDir(app, options, callback) {
if (!entries) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`);
// remove only files. directories inside app dir are currently volumes managed by the addons
// we cannot delete those dirs anyway because of perms
entries.forEach(function (entry) {
let stat = safe.fs.statSync(path.join(resolvedAppDataDir, entry));
if (stat && !stat.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, entry));
@@ -469,6 +458,19 @@ function waitForDnsPropagation(app, callback) {
});
}
function migrateDataDir(app, sourceDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof sourceDir, 'string');
assert.strictEqual(typeof callback, 'function');
let resolvedSourceDir = apps.getDataDir(app, sourceDir);
let resolvedTargetDir = apps.getDataDir(app, app.dataDir);
debug(`migrateDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, callback);
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -497,14 +499,14 @@ function install(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked appdata dir
// for restore case
function deleteImageIfChanged(done) {
@@ -527,7 +529,7 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
updateApp.bind(null, app, { installationProgress: '50, Creating app data directory' }),
createAppDir.bind(null, app),
function restoreFromBackup(next) {
@@ -538,10 +540,10 @@ function install(app, callback) {
], next);
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
updateApp.bind(null, app, { installationProgress: '65, Download backup and restoring addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
], next);
}
},
@@ -583,7 +585,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK)),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
// done!
function (callback) {
@@ -605,7 +607,8 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
const locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
const dataDirChanged = app.oldConfig && (app.oldConfig.dataDir !== app.dataDir);
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
@@ -613,7 +616,7 @@ function configure(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
unregisterAlternateDomains.bind(null, app, false /* all */),
function (next) {
if (!locationChanged) return next();
@@ -621,7 +624,6 @@ function configure(app, callback) {
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
},
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
@@ -636,13 +638,20 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
updateApp.bind(null, app, { installationProgress: '45, Ensuring app data directory' }),
createAppDir.bind(null, app),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
// migrate dataDir
function (next) {
if (!dataDirChanged) return next();
migrateDataDir(app, app.oldConfig.dataDir, next);
},
updateApp.bind(null, app, { installationProgress: '60, Creating container' }),
createContainer.bind(null, app),
@@ -696,7 +705,7 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
], function (error) {
if (error) error.backupError = true;
next(error);
@@ -714,7 +723,7 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
@@ -800,12 +809,12 @@ function uninstall(app, callback) {
stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Deleting container' }),
deleteContainers.bind(null, app),
deleteContainers.bind(null, app, {}),
updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons),
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
updateApp.bind(null, app, { installationProgress: '40, Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
+230 -122
View File
@@ -22,13 +22,17 @@ exports = module.exports = {
upload: upload,
startCleanupTask: startCleanupTask,
cleanup: cleanup,
cleanupCacheFilesSync: cleanupCacheFilesSync,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
// for testing
_getBackupFilePath: getBackupFilePath,
_createTarPackStream: createTarPackStream,
_tarExtract: tarExtract,
_restoreFsMetadata: restoreFsMetadata,
_saveFsMetadata: saveFsMetadata
};
@@ -44,6 +48,7 @@ var addons = require('./addons.js'),
crypto = require('crypto'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
DataLayout = require('./datalayout.js'),
debug = require('debug')('box:backups'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
@@ -64,7 +69,6 @@ var addons = require('./addons.js'),
util = require('util'),
zlib = require('zlib');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
function debugApp(app) {
@@ -114,6 +118,17 @@ function api(provider) {
}
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.key === exports.SECRET_PLACEHOLDER) newConfig.key = currentConfig.key;
if (newConfig.provider === currentConfig.provider) api(newConfig.provider).injectPrivateFields(newConfig, currentConfig);
}
function removePrivateFields(backupConfig) {
assert.strictEqual(typeof backupConfig, 'object');
if (backupConfig.key) backupConfig.key = exports.SECRET_PLACEHOLDER;
return api(backupConfig.provider).removePrivateFields(backupConfig);
}
function testConfig(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -228,7 +243,7 @@ function createReadStream(sourceFile, key) {
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
stream.on('error', function (error) {
debug('createReadStream: tar stream error.', error);
debug('createReadStream: read stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
@@ -262,15 +277,16 @@ function createWriteStream(destFile, key) {
}
}
function createTarPackStream(sourceDir, key) {
assert.strictEqual(typeof sourceDir, 'string');
function tarPack(dataLayout, key, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
var pack = tar.pack('/', {
dereference: false, // pack the symlink and not what it points to
entries: [ sourceDir ],
entries: dataLayout.localPaths(),
map: function(header) {
header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative
header.name = dataLayout.toRemotePath(header.name);
return header;
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
@@ -280,35 +296,40 @@ function createTarPackStream(sourceDir, key) {
var ps = progressStream({ time: 10000 }); // emit 'pgoress' every 10 seconds
pack.on('error', function (error) {
debug('createTarPackStream: tar stream error.', error);
debug('tarPack: tar stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
gzip.on('error', function (error) {
debug('createTarPackStream: gzip stream error.', error);
debug('tarPack: gzip stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('createTarPackStream: encrypt stream error.', error);
debug('tarPack: encrypt stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
return pack.pipe(gzip).pipe(encrypt).pipe(ps);
pack.pipe(gzip).pipe(encrypt).pipe(ps);
} else {
return pack.pipe(gzip).pipe(ps);
pack.pipe(gzip).pipe(ps);
}
callback(null, ps);
}
function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
// the number here has to take into account the s3.upload partSize (which is 10MB). So 20=200MB
const concurrency = backupConfig.syncConcurrency || (backupConfig.provider === 's3' ? 20 : 10);
syncer.sync(dataLayout, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
// the empty task.path is special to signify the directory
const destPath = task.path && backupConfig.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
@@ -329,16 +350,18 @@ function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
progressCallback({ message: `${task.operation} ${task.path} try ${retryCount}` });
if (task.operation === 'add') {
progressCallback({ message: `Adding ${task.path}` + (retryCount > 1 ? ` (Try ${retryCount})` : '') });
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig.key || null);
stream.on('error', function (error) {
debug(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
stream.on('progress', function(progress) {
progressCallback({ message: `Uploading ${task.path}: ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}` });
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: `Uploading ${task.path}` }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading ${task.path}: ${transferred}M@${speed}Mbps` }); // 0M@0Mbps looks wrong
});
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
@@ -346,42 +369,52 @@ function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
});
}
}, iteratorCallback);
}, backupConfig.syncConcurrency || 10 /* concurrency */, function (error) {
}, concurrency, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback();
});
}
function saveFsMetadata(appDataDir, callback) {
assert.strictEqual(typeof appDataDir, 'string');
// this is not part of 'snapshotting' because we need root access to traverse
function saveFsMetadata(dataLayout, metadataFile, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof metadataFile, 'string');
assert.strictEqual(typeof callback, 'function');
var emptyDirs = safe.child_process.execSync('find . -type d -empty', { cwd: `${appDataDir}`, encoding: 'utf8' });
if (emptyDirs === null) return callback(safe.error);
var execFiles = safe.child_process.execSync('find . -type f -executable', { cwd: `${appDataDir}`, encoding: 'utf8' });
if (execFiles === null) return callback(safe.error);
var metadata = {
emptyDirs: emptyDirs.length === 0 ? [ ] : emptyDirs.trim().split('\n'),
execFiles: execFiles.length === 0 ? [ ] : execFiles.trim().split('\n')
// contains paths prefixed with './'
let metadata = {
emptyDirs: [],
execFiles: []
};
if (!safe.fs.writeFileSync(`${appDataDir}/fsmetadata.json`, JSON.stringify(metadata, null, 4))) return callback(safe.error);
for (let lp of dataLayout.localPaths()) {
var emptyDirs = safe.child_process.execSync(`find ${lp} -type d -empty\n`, { encoding: 'utf8' });
if (emptyDirs === null) return callback(safe.error);
if (emptyDirs.length) metadata.emptyDirs = metadata.emptyDirs.concat(emptyDirs.trim().split('\n').map((ed) => dataLayout.toRemotePath(ed)));
var execFiles = safe.child_process.execSync(`find ${lp} -type f -executable\n`, { encoding: 'utf8' });
if (execFiles === null) return callback(safe.error);
if (execFiles.length) metadata.execFiles = metadata.execFiles.concat(execFiles.trim().split('\n').map((ef) => dataLayout.toRemotePath(ef)));
}
if (!safe.fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 4))) return callback(safe.error);
callback();
}
// this function is called via backupupload (since it needs root to traverse app's directory)
function upload(backupId, format, dataDir, progressCallback, callback) {
function upload(backupId, format, dataLayoutString, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof dataLayoutString, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`upload: id ${backupId} format ${format} dataDir ${dataDir}`);
debug(`upload: id ${backupId} format ${format} dataLayout ${dataLayoutString}`);
const dataLayout = DataLayout.fromString(dataLayoutString);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -390,87 +423,99 @@ function upload(backupId, format, dataDir, progressCallback, callback) {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('progress', function(progress) {
progressCallback({ message: `Uploading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BackupsError
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
tarStream.on('progress', function(progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
});
}, callback);
} else {
async.series([
saveFsMetadata.bind(null, dataDir),
sync.bind(null, backupConfig, backupId, dataDir, progressCallback)
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
], callback);
}
});
}
function tarExtract(inStream, destination, key, callback) {
function tarExtract(inStream, dataLayout, key, callback) {
assert.strictEqual(typeof inStream, 'object');
assert.strictEqual(typeof destination, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
var gunzip = zlib.createGunzip({});
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
var extract = tar.extract(destination);
var extract = tar.extract('/', {
map: function (header) {
header.name = dataLayout.toLocalPath(header.name);
return header;
}
});
const emitError = once((error) => ps.emit('error', error));
inStream.on('error', function (error) {
debug('tarExtract: input stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
gunzip.on('error', function (error) {
debug('tarExtract: gunzip stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
extract.on('error', function (error) {
debug('tarExtract: extract stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
extract.on('finish', function () {
debug('tarExtract: done.');
callback(null);
// we use a separate event because ps is a through2 stream which emits 'finish' event indicating end of inStream and not extract
ps.emit('done');
});
if (key !== null) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('tarExtract: decrypt stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
});
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
} else {
inStream.pipe(ps).pipe(gunzip).pipe(extract);
}
return ps;
callback(null, ps);
}
function restoreFsMetadata(appDataDir, callback) {
assert.strictEqual(typeof appDataDir, 'string');
function restoreFsMetadata(dataLayout, metadataFile, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof metadataFile, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`Recreating empty directories in ${appDataDir}`);
debug(`Recreating empty directories in ${dataLayout.toString()}`);
var metadataJson = safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8');
var metadataJson = safe.fs.readFileSync(metadataFile, 'utf8');
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message));
var metadata = safe.JSON.parse(metadataJson);
if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error parsing fsmetadata.txt:' + safe.error.message));
async.eachSeries(metadata.emptyDirs, function createPath(emptyDir, iteratorDone) {
mkdirp(path.join(appDataDir, emptyDir), iteratorDone);
mkdirp(dataLayout.toLocalPath(emptyDir), iteratorDone);
}, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to create path: ${error.message}`));
async.eachSeries(metadata.execFiles, function createPath(execFile, iteratorDone) {
fs.chmod(path.join(appDataDir, execFile), parseInt('0755', 8), iteratorDone);
fs.chmod(dataLayout.toLocalPath(execFile), parseInt('0755', 8), iteratorDone);
}, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to chmod: ${error.message}`));
@@ -479,14 +524,14 @@ function restoreFsMetadata(appDataDir, callback) {
});
}
function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, callback) {
function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
assert.strictEqual(typeof destDir, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
debug(`downloadDir: ${backupFilePath} to ${dataLayout.toString()}`);
function downloadFile(entry, callback) {
let relativePath = path.relative(backupFilePath, entry.fullPath);
@@ -494,55 +539,79 @@ function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, ca
relativePath = decryptFilePath(relativePath, backupConfig.key);
if (!relativePath) return callback(new BackupsError(BackupsError.BAD_STATE, 'Unable to decrypt file'));
}
const destFilePath = path.join(destDir, relativePath);
const destFilePath = dataLayout.toLocalPath('./' + relativePath);
mkdirp(path.dirname(destFilePath), function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
if (error) return callback(error);
sourceStream.on('error', callback);
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
destStream.on('error', callback);
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
let closeAndRetry = once((error) => {
if (error) progressCallback({ message: `Download ${entry.fullPath} errored: ${error.message}` });
else progressCallback({ message: `Download ${entry.fullPath} finished` });
destStream.destroy();
retryCallback(error);
});
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
});
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
if (error) return closeAndRetry(error);
sourceStream.on('error', closeAndRetry);
destStream.on('error', closeAndRetry);
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
sourceStream.pipe(destStream, { end: true }).on('finish', closeAndRetry);
});
}, callback);
});
}
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) {
async.each(entries, downloadFile, done);
// https://www.digitalocean.com/community/questions/rate-limiting-on-spaces?answer=40441
const concurrency = backupConfig.downloadConcurrency || (backupConfig.provider === 's3' ? 30 : 10);
async.eachLimit(entries, concurrency, downloadFile, done);
}, callback);
}
function download(backupConfig, backupId, format, dataDir, progressCallback, callback) {
function download(backupConfig, backupId, format, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`download - Downloading ${backupId} of format ${format} to ${dataDir}`);
debug(`download - Downloading ${backupId} of format ${format} to ${dataLayout.toString()}`);
const backupFilePath = getBackupFilePath(backupConfig, backupId, format);
if (format === 'tgz') {
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
if (error) return callback(error);
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
api(backupConfig.provider).download(backupConfig, backupFilePath, function (error, sourceStream) {
if (error) return retryCallback(error);
let ps = tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
ps.on('progress', function (progress) {
progressCallback({ message: `Downloading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
tarExtract(sourceStream, dataLayout, backupConfig.key || null, function (error, ps) {
if (error) return retryCallback(error);
ps.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Downloading' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Downloading ${transferred}M@${speed}Mbps` });
});
ps.on('error', retryCallback);
ps.on('done', retryCallback);
});
});
});
}, callback);
} else {
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, progressCallback, function (error) {
downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
restoreFsMetadata(dataDir, callback);
restoreFsMetadata(dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`, callback);
});
}
}
@@ -553,12 +622,14 @@ function restore(backupConfig, backupId, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
const dataLayout = new DataLayout(paths.BOX_DATA_DIR, []);
download(backupConfig, backupId, backupConfig.format, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
debug('restore: download completed, importing database');
database.importFromFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
database.importFromFile(`${dataLayout.localRoot()}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('restore: database imported');
@@ -575,7 +646,9 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
if (!appDataDir) return callback(safe.error);
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
var startTime = new Date();
@@ -583,7 +656,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.series([
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir, progressCallback),
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback),
addons.restoreAddons.bind(null, app, addonsToRestore)
], function (error) {
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
@@ -593,16 +666,16 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
});
}
function runBackupUpload(backupId, format, dataDir, progressCallback, callback) {
function runBackupUpload(backupId, format, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
let result = '';
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataDir ], { preserveEnv: true, ipc: true }, function (error) {
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataLayout.toString() ], { preserveEnv: true, ipc: true }, function (error) {
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Backuptask crashed'));
} else if (error && error.code === 50) { // exited with error
@@ -662,7 +735,11 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
snapshotBox(progressCallback, function (error) {
if (error) return callback(error);
runBackupUpload('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
if (!boxDataDir) return callback(safe.error);
const dataLayout = new DataLayout(boxDataDir, []);
runBackupUpload('snapshot/box', backupConfig.format, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
@@ -771,7 +848,7 @@ function snapshotApp(app, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
progressCallback({ message: `Snapshotting app ${app.id}` });
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
@@ -834,9 +911,13 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
snapshotApp(app, progressCallback, function (error) {
if (error) return callback(error);
var backupId = util.format('snapshot/app_%s', app.id);
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
runBackupUpload(backupId, backupConfig.format, appDataDir, progressCallback, function (error) {
const backupId = util.format('snapshot/app_%s', app.id);
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
if (!appDataDir) return callback(safe.error);
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
runBackupUpload(backupId, backupConfig.format, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
@@ -924,9 +1005,9 @@ function backupBoxAndApps(progressCallback, callback) {
function startBackupTask(auditSource, callback) {
let error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(error);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, `Cannot backup now: ${error.message}`));
let task = tasks.startTask(tasks.TASK_BACKUP, [], auditSource);
let task = tasks.startTask(tasks.TASK_BACKUP, []);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
@@ -1006,22 +1087,24 @@ function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
assert.strictEqual(typeof callback, 'function');
const now = new Date();
let removedAppBackups = [];
// we clean app backups of any state because the ones to keep are determined by the box cleanup code
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, appBackups) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.eachSeries(appBackups, function iterator(backup, iteratorDone) {
if (referencedAppBackups.indexOf(backup.id) !== -1) return iteratorDone();
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
async.eachSeries(appBackups, function iterator(appBackup, iteratorDone) {
if (referencedAppBackups.indexOf(appBackup.id) !== -1) return iteratorDone();
if ((now - appBackup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
debug('cleanupAppBackups: removing %s', backup.id);
debug('cleanupAppBackups: removing %s', appBackup.id);
cleanupBackup(backupConfig, backup, iteratorDone);
removedAppBackups.push(appBackup.id);
cleanupBackup(backupConfig, appBackup, iteratorDone);
}, function () {
debug('cleanupAppBackups: done');
callback();
callback(null, removedAppBackups);
});
});
}
@@ -1032,12 +1115,12 @@ function cleanupBoxBackups(backupConfig, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
const now = new Date();
var referencedAppBackups = [];
let referencedAppBackups = [], removedBoxBackups = [];
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, boxBackups) {
if (error) return callback(error);
if (boxBackups.length === 0) return callback(null, []);
if (boxBackups.length === 0) return callback(null, { removedBoxBackups, referencedAppBackups });
// search for the first valid backup
var i;
@@ -1054,21 +1137,22 @@ function cleanupBoxBackups(backupConfig, auditSource, callback) {
debug('cleanupBoxBackups: no box backup to preserve');
}
async.eachSeries(boxBackups, function iterator(backup, nextBackup) {
async.eachSeries(boxBackups, function iterator(boxBackup, iteratorNext) {
// TODO: errored backups should probably be cleaned up before retention time, but we will
// have to be careful not to remove any backup currently being created
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) {
referencedAppBackups = referencedAppBackups.concat(backup.dependsOn);
return nextBackup();
if ((now - boxBackup.creationTime) < (backupConfig.retentionSecs * 1000)) {
referencedAppBackups = referencedAppBackups.concat(boxBackup.dependsOn);
return iteratorNext();
}
debug('cleanupBoxBackups: removing %s', backup.id);
debug('cleanupBoxBackups: removing %s', boxBackup.id);
cleanupBackup(backupConfig, backup, nextBackup);
removedBoxBackups.push(boxBackup.id);
cleanupBackup(backupConfig, boxBackup, iteratorNext);
}, function () {
debug('cleanupBoxBackups: done');
return callback(null, referencedAppBackups);
callback(null, { removedBoxBackups, referencedAppBackups });
});
});
}
@@ -1122,29 +1206,53 @@ function cleanupSnapshots(backupConfig, callback) {
});
}
function cleanup(auditSource, callback) {
function cleanup(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
callback = callback || NOOP_CALLBACK;
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
if (backupConfig.retentionSecs < 0) {
debug('cleanup: keeping all backups');
return callback();
return callback(null, {});
}
cleanupBoxBackups(backupConfig, auditSource, function (error, referencedAppBackups) {
progressCallback({ percent: 10, message: 'Cleaning box backups' });
cleanupBoxBackups(backupConfig, auditSource, function (error, result) {
if (error) return callback(error);
cleanupAppBackups(backupConfig, referencedAppBackups, function (error) {
progressCallback({ percent: 40, message: 'Cleaning app backups' });
cleanupAppBackups(backupConfig, result.referencedAppBackups, function (error, removedAppBackups) {
if (error) return callback(error);
cleanupSnapshots(backupConfig, callback);
progressCallback({ percent: 90, message: 'Cleaning snapshots' });
cleanupSnapshots(backupConfig, function (error) {
if (error) return callback(error);
callback(null, { removedBoxBackups: result.removedBoxBackups, removedAppBackups: removedAppBackups });
});
});
});
});
}
function startCleanupTask(auditSource, callback) {
let task = tasks.startTask(tasks.TASK_CLEAN_BACKUPS, [ auditSource ]);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_START, auditSource, { taskId });
callback(null, taskId);
});
task.on('finish', (error, result) => { // result is { removedBoxBackups, removedAppBackups }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
});
});
}
+2 -2
View File
@@ -226,7 +226,7 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
debug(`waitForOrder: ${orderUrl}`);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitForOrder: getting status');
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
@@ -290,7 +290,7 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
+21 -6
View File
@@ -18,6 +18,8 @@ exports = module.exports = {
addDefaultClients: addDefaultClients,
removeTokenPrivateFields: removeTokenPrivateFields,
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
@@ -39,7 +41,8 @@ var apps = require('./apps.js'),
users = require('./users.js'),
UsersError = users.UsersError,
util = require('util'),
uuid = require('uuid');
uuid = require('uuid'),
_ = require('underscore');
function ClientsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -273,16 +276,24 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
accesscontrol.scopesForUser(user, function (error, userScopes) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
var scope = accesscontrol.canonicalScopeString(result.scope);
var authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
const scope = accesscontrol.canonicalScopeString(result.scope);
const authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
var token = tokendb.generateToken();
const token = {
id: 'tid-' + uuid.v4(),
accessToken: hat(8 * 32),
identifier: userId,
clientId: result.id,
expires: expiresAt,
scope: authorizedScopes.join(','),
name: name
};
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
tokendb.add(token, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
accessToken: token.accessToken,
tokenScopes: authorizedScopes,
identifier: userId,
clientId: result.id,
@@ -342,3 +353,7 @@ function addDefaultClients(origin, callback) {
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
], callback);
}
function removeTokenPrivateFields(token) {
return _.pick(token, 'id', 'identifier', 'clientId', 'scope', 'expires', 'name');
}
+106 -110
View File
@@ -8,33 +8,36 @@ exports = module.exports = {
getConfig: getConfig,
getDisks: getDisks,
getLogs: getLogs,
getStatus: getStatus,
reboot: reboot,
isRebootRequired: isRebootRequired,
onActivated: onActivated,
prepareDashboardDomain: prepareDashboardDomain,
setDashboardDomain: setDashboardDomain,
renewCerts: renewCerts,
checkDiskSpace: checkDiskSpace,
runSystemChecks: runSystemChecks,
configureWebadmin: configureWebadmin,
getWebadminStatus: getWebadminStatus
// exposed for testing
_checkDiskSpace: checkDiskSpace
};
var assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
DomainsError = require('./domains.js').DomainsError,
df = require('@sindresorhus/df'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mailer = require('./mailer.js'),
mail = require('./mail.js'),
notifications = require('./notifications.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
@@ -44,7 +47,6 @@ var assert = require('assert'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
util = require('util');
@@ -53,16 +55,6 @@ var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
let gWebadminStatus = {
dns: false,
tls: false,
configuring: false,
restore: {
active: false,
error: null
}
};
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -123,7 +115,7 @@ function runStartupTasks() {
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
configureWebadmin(NOOP_CALLBACK);
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
// check activation state and start the platform
users.isActivated(function (error, activated) {
@@ -194,8 +186,35 @@ function isRebootRequired(callback) {
callback(null, fs.existsSync('/var/run/reboot-required'));
}
// called from cron.js
function runSystemChecks() {
async.parallel([
checkBackupConfiguration,
checkDiskSpace,
checkMailStatus
], function () {
debug('runSystemChecks: done');
});
}
function checkBackupConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking backup configuration');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
if (backupConfig.provider === 'noop') {
notifications.backupConfigWarning('Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://cloudron.io/documentation/backups/#storage-providers for more information.');
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
notifications.backupConfigWarning('Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://cloudron.io/documentation/backups/#storage-providers for storing backups in an external location.');
}
});
}
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
assert.strictEqual(typeof callback, 'function');
debug('Checking disk space');
@@ -225,13 +244,42 @@ function checkDiskSpace(callback) {
debug('Disk space checked. ok: %s', !oos);
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
if (oos) notifications.diskSpaceWarning(JSON.stringify(entries, null, 4));
callback();
}).catch(function (error) {
debug('df error %s', error.message);
mailer.outOfDiskSpace(error.message);
return callback();
if (error) console.error(error);
callback();
});
});
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking mail status');
domains.getAll(function (error, allDomains) {
if (error) return callback(error);
async.filterSeries(allDomains, function (domainObject, iteratorCallback) {
mail.getStatus(config.adminDomain(), function (error, result) {
if (error) return iteratorCallback(null, true);
let mailError = Object.keys(result.dns).some((record) => !result.dns[record].status);
if (result.relay && !result.relay.status) mailError = true;
if (result.rbl && result.rbl.status === false) mailError = true; // rbl is an optional check
iteratorCallback(null, mailError);
});
}, function (error, erroredDomainObjects) {
if (error || erroredDomainObjects.length === 0) return callback(error);
const erroredDomains = erroredDomainObjects.map((d) => d.domain);
debug(`checkMailStatus: ${erroredDomains.join(',')} failed status checks`);
if (erroredDomains.length) notifications.mailStatusWarning(`Email status check of the following domain(s) failed - ${erroredDomains.join(',')}. See the Status tab in the [Email view](/#/email/) for more information.`);
callback();
});
});
}
@@ -241,15 +289,13 @@ function getLogs(unit, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
var lines = options.lines || 100,
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
var lines = options.lines === -1 ? '+1' : options.lines,
format = options.format || 'json',
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
follow = options.follow;
debug('Getting logs for %s as %s', unit, format);
@@ -283,97 +329,47 @@ function getLogs(unit, options, callback) {
return callback(null, transformStream);
}
function configureWebadmin(callback) {
assert.strictEqual(typeof callback, 'function');
debug('configureWebadmin: adminDomain:%s status:%j', config.adminDomain(), gWebadminStatus);
if (process.env.BOX_ENV === 'test' || !config.adminDomain() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function configureReverseProxy(error) {
debug('configureReverseProxy: error %j', error || null);
reverseProxy.configureAdmin({ userId: null, username: 'setup' }, function (error) {
debug('configureWebadmin: done error: %j', error || {});
gWebadminStatus.configuring = false;
if (error) return callback(error);
gWebadminStatus.tls = true;
callback();
});
}
// 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 configureReverseProxy(error);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
debug('addWebadminDnsRecord: updated records with error:', error);
if (error) return configureReverseProxy(error);
domains.waitForDnsRecord(config.adminLocation(), config.adminDomain(), 'A', ip, { interval: 30000, times: 50000 }, function (error) {
if (error) return configureReverseProxy(error);
gWebadminStatus.dns = true;
configureReverseProxy();
});
});
});
}
function getWebadminStatus() {
return gWebadminStatus;
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
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, {
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: activated,
edition: config.edition(),
webadminStatus: gWebadminStatus // only valid when !activated
});
});
});
}
function setDashboardDomain(domain, callback) {
function prepareDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`prepareDashboardDomain: ${domain}`);
let task = tasks.startTask(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
}
function setDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`setDashboardDomain: ${domain}`);
domains.get(domain, function (error, result) {
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such domain'));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
clients.addDefaultClients(config.adminOrigin(), function (error) {
reverseProxy.writeAdminConfig(domain, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null);
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
configureWebadmin(NOOP_CALLBACK); // ## trigger as task
config.setAdminDomain(domain);
config.setAdminLocation(constants.ADMIN_LOCATION);
config.setAdminFqdn(fqdn);
clients.addDefaultClients(config.adminOrigin(), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
mail.setMailFqdn(fqdn, domain, NOOP_CALLBACK);
callback(null);
});
});
});
}
+5 -3
View File
@@ -119,8 +119,9 @@ function initConfig() {
if (exports.TEST) {
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.password = '';
data.database.name = 'boxtest';
// see setupTest script how the mysql-server is run
data.database.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
// overwrite defaults with saved config
@@ -231,7 +232,8 @@ function isManaged() {
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
return fs.existsSync(IPV6_PROC_FILE);
// on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
}
// it has to change with the adminLocation so that multiple cloudrons
+4 -3
View File
@@ -17,9 +17,8 @@ exports = module.exports = {
'admins', 'users' // ldap code uses 'users' pseudo group
],
ADMIN_NAME: 'Settings',
ADMIN_LOCATION: 'my',
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
@@ -30,6 +29,8 @@ exports = module.exports = {
DEMO_USERNAME: 'cloudron',
AUTOUPDATE_PATTERN_NEVER: 'never'
AUTOUPDATE_PATTERN_NEVER: 'never',
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
};
+9 -9
View File
@@ -22,7 +22,6 @@ var appHealthMonitor = require('./apphealthmonitor.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'),
updater = require('./updater.js'),
@@ -36,7 +35,7 @@ var gJobs = {
backup: null,
boxUpdateChecker: null,
caasHeartbeat: null,
checkDiskSpace: null,
systemChecks: null,
certificateRenew: null,
cleanupBackups: null,
cleanupEventlog: null,
@@ -48,7 +47,7 @@ var gJobs = {
appHealthMonitor: null
};
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var AUDIT_SOURCE = { userId: null, username: 'cron' };
// cron format
@@ -116,11 +115,12 @@ function recreateJobs(tz) {
timeZone: tz
});
if (gJobs.checkDiskSpace) gJobs.checkDiskSpace.stop();
gJobs.checkDiskSpace = new CronJob({
cronTime: '00 30 */4 * * *', // every 4 hours
onTick: cloudron.checkDiskSpace,
if (gJobs.systemChecks) gJobs.systemChecks.stop();
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every hour
onTick: cloudron.runSystemChecks,
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
@@ -154,7 +154,7 @@ function recreateJobs(tz) {
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
gJobs.cleanupBackups = new CronJob({
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
onTick: backups.cleanup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: backups.startCleanupTask.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -202,7 +202,7 @@ function recreateJobs(tz) {
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10),
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
start: true,
timeZone: tz
});
+3 -7
View File
@@ -85,7 +85,7 @@ function reconnect(callback) {
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
var cmd = util.format('mysql --host=%s --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host=%s --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
config.database().hostname, config.database().username, config.database().password, config.database().name,
config.database().hostname, config.database().username, config.database().password, config.database().name);
@@ -177,9 +177,7 @@ function importFromFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysql -u ${config.database().username} ${password} ${config.database().name} < ${file}`;
var cmd = `/usr/bin/mysql -h "${config.database().hostname}" -u ${config.database().username} -p${config.database().password} ${config.database().name} < ${file}`;
async.series([
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
@@ -191,9 +189,7 @@ function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysqldump -u root ${password} --single-transaction --routines \
--triggers ${config.database().name} > "${file}"`;
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
child_process.exec(cmd, callback);
}
+55
View File
@@ -0,0 +1,55 @@
'use strict';
let assert = require('assert'),
path = require('path');
class DataLayout {
constructor(localRoot, dirMap) {
assert.strictEqual(typeof localRoot, 'string');
assert(Array.isArray(dirMap), 'Expecting layout to be an array');
this._localRoot = localRoot;
this._dirMap = dirMap;
this._remoteRegexps = dirMap.map((l) => new RegExp('^\\./' + l.remoteDir + '/?'));
this._localRegexps = dirMap.map((l) => new RegExp('^' + l.localDir + '/?'));
}
toLocalPath(remoteName) {
assert.strictEqual(typeof remoteName, 'string');
for (let i = 0; i < this._remoteRegexps.length; i++) {
if (!remoteName.match(this._remoteRegexps[i])) continue;
return remoteName.replace(this._remoteRegexps[i], this._dirMap[i].localDir + '/'); // make paths absolute
}
return remoteName.replace(new RegExp('^\\.'), this._localRoot);
}
toRemotePath(localName) {
assert.strictEqual(typeof localName, 'string');
for (let i = 0; i < this._localRegexps.length; i++) {
if (!localName.match(this._localRegexps[i])) continue;
return localName.replace(this._localRegexps[i], './' + this._dirMap[i].remoteDir + '/'); // make paths relative
}
return localName.replace(new RegExp('^' + this._localRoot + '/?'), './');
}
localRoot() {
return this._localRoot;
}
getBasename() { // used to generate cache file names
return path.basename(this._localRoot);
}
toString() {
return JSON.stringify({ localRoot: this._localRoot, layout: this._dirMap });
}
localPaths() {
return [ this._localRoot ].concat(this._dirMap.map((l) => l.localDir));
}
directoryMap() {
return this._dirMap;
}
static fromString(str) {
const obj = JSON.parse(str);
return new DataLayout(obj.localRoot, obj.layout);
}
}
exports = module.exports = DataLayout;
+1 -2
View File
@@ -1,7 +1,6 @@
'use strict';
var appstore = require('./appstore.js'),
debug = require('debug')('box:digest'),
var debug = require('debug')('box:digest'),
eventlog = require('./eventlog.js'),
updatechecker = require('./updatechecker.js'),
mailer = require('./mailer.js'),
+64 -44
View File
@@ -1,38 +1,56 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
function getFqdn(subdomain, domain) {
assert.strictEqual(typeof subdomain, 'string');
function getFqdn(location, domain) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
return (subdomain === '') ? domain : subdomain + '-' + domain;
return (location === '') ? domain : location + '-' + domain;
}
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
// do not return the 'key'. in caas, this is private
delete domainObject.fallbackCertificate.key;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
const dnsConfig = domainObject.config;
debug('add: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values);
let fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('add: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
@@ -54,16 +72,16 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
const dnsConfig = domainObject.config;
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', dnsConfig.fqdn, subdomain, type, fqdn);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
@@ -77,26 +95,15 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
add(dnsConfig, zoneName, subdomain, type, values, callback);
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('del: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values);
const dnsConfig = domainObject.config;
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
@@ -104,7 +111,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
};
superagent
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(subdomain, dnsConfig.fqdn))
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(location, domainObject.domain))
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
@@ -119,29 +126,42 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token,
fqdn: domain,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
};
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+117 -62
View File
@@ -1,10 +1,12 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
@@ -12,14 +14,25 @@ var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
// we are using latest v4 stable API https://api.cloudflare.com/#getting-started-endpoints
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function translateRequestError(result, callback) {
assert.strictEqual(typeof result, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -53,16 +66,14 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
// gets records filtered by zone, type and fqdn
function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
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)
@@ -78,32 +89,30 @@ function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
getZoneByName(dnsConfig, zoneName, function(error, result){
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
var zoneId = result.id;
let zoneId = result.id;
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
if (error) return callback(error);
var dnsRecords = result;
let i = 0; // // used to track available records to update instead of create
// used to track available records to update instead of create
var i = 0;
async.eachSeries(values, function (value, callback) {
async.eachSeries(values, function (value, iteratorCallback) {
var priority = null;
if (type === 'MX') {
@@ -116,35 +125,41 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
name: fqdn,
content: value,
priority: priority,
proxied: false,
ttl: 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
};
if (i >= dnsRecords.length) {
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
if (i >= dnsRecords.length) { // create a new record
debug(`upsert: Adding new record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: false`);
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);
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
callback(null);
iteratorCallback(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)
} else { // replace existing record
data.proxied = dnsRecords[i].proxied; // preserve proxied parameter
debug(`upsert: Updating existing record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: ${data.proxied}`);
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) {
// increment, as we have consumed the record
++i;
++i; // increment, as we have consumed the record
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
callback(null);
iteratorCallback(null);
});
}
}, callback);
@@ -152,17 +167,20 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getZoneByName(dnsConfig, zoneName, function(error, result){
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDnsRecords(dnsConfig, zone.id, fqdn, type, function (error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
@@ -173,18 +191,21 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
getZoneByName(dnsConfig, zoneName, function(error, result){
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDnsRecords(dnsConfig, zone.id, fqdn, type, function(error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -197,8 +218,8 @@ 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)
.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);
@@ -217,16 +238,50 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('wait: %s for zone %s of type %s', fqdn, zoneName, type);
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
let zoneId = result.id;
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
if (error) return callback(error);
if (dnsRecords.length === 0) return callback(new DomainsError(DomainsError.NOT_FOUND, 'Domain not found'));
if (!dnsRecords[0].proxied) return waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
debug('wait: skipping wait of proxied domain');
callback(null); // maybe we can check for dns to be cloudflare IPs? https://api.cloudflare.com/#cloudflare-ips-cloudflare-ip-details
});
});
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'email must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token,
email: dnsConfig.email
@@ -238,22 +293,22 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
getZoneByName(dnsConfig, zoneName, function(error, result) {
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
if (!_.isEqual(result.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, result.name_servers);
if (!_.isEqual(zone.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, zone.name_servers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+65 -34
View File
@@ -1,10 +1,12 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
@@ -12,10 +14,12 @@ var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
var DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com';
@@ -23,10 +27,19 @@ function formatError(response) {
return util.format('DigitalOcean DNS error [%s] %j', response.statusCode, response.body);
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getInternal(dnsConfig, zoneName, name, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -45,7 +58,7 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
return (record.type === type && record.name === subdomain);
return (record.type === type && record.name === name);
}));
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
@@ -61,19 +74,20 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
// used to track available records to update instead of create
@@ -89,7 +103,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var data = {
type: type,
name: subdomain,
name: name,
data: value,
priority: priority,
ttl: 1
@@ -133,16 +147,17 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
// We only return the value string
@@ -154,17 +169,18 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -193,15 +209,30 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token
};
@@ -217,14 +248,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+63 -32
View File
@@ -1,19 +1,23 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
@@ -21,24 +25,34 @@ function formatError(response) {
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
'rrset_ttl': 300, // this is the minimum allowed
'rrset_values': values // for mx records, value is already of the '<priority> <server>' format
};
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.send(data)
@@ -52,18 +66,19 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -78,19 +93,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -105,19 +121,34 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
var credentials = {
token: dnsConfig.token
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -129,14 +160,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Gandi'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+66 -32
View File
@@ -1,21 +1,34 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
GCDNS = require('@google-cloud/dns'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.credentials.private_key === domains.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
}
function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
@@ -55,22 +68,23 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function (error, oldRecords) {
zone.getRecords({ type: type, name: fqdn + '.' }, function (error, oldRecords) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) {
debug('upsert->zone.getRecords', error);
@@ -78,12 +92,12 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
}
var newRecord = zone.record(type, {
name: domain,
name: fqdn + '.',
data: values,
ttl: 1
});
zone.createChange({ delete: oldRecords, add: newRecord }, function(error, change) {
zone.createChange({ delete: oldRecords, add: newRecord }, function(error /*, change */) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
if (error) {
@@ -97,18 +111,21 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
name: (subdomain ? subdomain + '.' : '') + zoneName + '.',
name: fqdn + '.',
type: type
};
@@ -122,20 +139,21 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function(error, oldRecords) {
zone.getRecords({ type: type, name: fqdn + '.' }, function(error, oldRecords) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) {
debug('del->zone.getRecords', error);
@@ -156,19 +174,35 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (typeof dnsConfig.projectId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'projectId must be a string'));
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials must be an object'));
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.client_email must be a string'));
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.private_key must be a string'));
var credentials = getDnsCredentials(dnsConfig);
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -184,14 +218,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+64 -33
View File
@@ -1,19 +1,23 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
@@ -27,17 +31,27 @@ function formatError(response) {
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function removePrivateFields(domainObject) {
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.apiSecret === domains.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var records = [ ];
values.forEach(function (value) {
@@ -53,7 +67,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
records.push(record);
});
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.send(records)
@@ -68,18 +82,19 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -98,22 +113,23 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`get: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
if (type !== 'A' && type !== 'TXT') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Record deletion is not supported by GoDaddy API')));
// check if the record exists at all so that we don't insert the "Dead" record for no reason
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
get(domainObject, location, type, function (error, values) {
if (error) return callback(error);
if (values.length === 0) return callback();
@@ -123,7 +139,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
data: type === 'A' ? GODADDY_INVALID_IP : GODADDY_INVALID_TXT
}];
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.send(records)
.timeout(30 * 1000)
@@ -140,16 +156,31 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiSecret must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
apiKey: dnsConfig.apiKey,
apiSecret: dnsConfig.apiSecret
@@ -166,14 +197,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+34 -19
View File
@@ -7,21 +7,30 @@
// -------------------------------------------
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
DomainsError = require('../domains.js').DomainsError,
util = require('util');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function removePrivateFields(domainObject) {
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -31,10 +40,9 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
callback(new Error('not implemented'));
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -43,10 +51,9 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
callback(new Error('not implemented'));
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -56,11 +63,19 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
callback(new Error('not implemented'));
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
callback();
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
// Result: dnsConfig object
+41 -20
View File
@@ -1,46 +1,55 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function removePrivateFields(domainObject) {
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -48,13 +57,25 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const zoneName = domainObject.zoneName;
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
+297
View File
@@ -0,0 +1,297 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
verifyDnsConfig: verifyDnsConfig,
wait: wait
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
Namecheap = require('namecheap'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js');
function formatError(response) {
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
// Only send required fields - https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
function mapHosts(hosts) {
return hosts.map(function (host) {
let tmp = {};
tmp.TTL = '300';
tmp.RecordType = host.RecordType || host.Type;
tmp.HostName = host.HostName || host.Name;
tmp.Address = host.Address;
if (tmp.RecordType === 'MX') {
tmp.EmailType = 'MX';
if (host.MXPref) tmp.MXPref = host.MXPref;
}
return tmp;
});
}
function getApi(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
// Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard
let namecheap = new Namecheap(dnsConfig.username, dnsConfig.token, ip);
namecheap.setUsername(dnsConfig.username);
callback(null, namecheap);
});
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getApi(dnsConfig, function (error, namecheap) {
if (error) return callback(error);
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
debug('entire getInternal response: %j', result);
return callback(null, result['DomainDNSGetHostsResult']['host']);
});
});
}
function setInternal(dnsConfig, zoneName, hosts, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert(Array.isArray(hosts));
assert.strictEqual(typeof callback, 'function');
let mappedHosts = mapHosts(hosts);
getApi(dnsConfig, function (error, namecheap) {
if (error) return callback(error);
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
return callback(null, result);
});
});
}
function upsert(domainObject, subdomain, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// Array to keep track of records that need to be inserted
let toInsert = [];
for (var i = 0; i < values.length; i++) {
let curValue = values[i];
let wasUpdate = false;
for (var j = 0; j < result.length; j++) {
let curHost = result[j];
if (curHost.Type === type && curHost.Name === subdomain) {
// Updating an already existing host
wasUpdate = true;
if (type === 'MX') {
curHost.MXPref = curValue.split(' ')[0];
curHost.Address = curValue.split(' ')[1];
} else {
curHost.Address = curValue;
}
}
}
// We don't have this host at all yet, let's push to toInsert array
if (!wasUpdate) {
let newRecord = {
RecordType: type,
HostName: subdomain,
Address: curValue
};
// Special case for MX records
if (type === 'MX') {
newRecord.MXPref = curValue.split(' ')[0];
newRecord.Address = curValue.split(' ')[1];
}
toInsert.push(newRecord);
}
}
let toUpsert = result.concat(toInsert);
setInternal(dnsConfig, zoneName, toUpsert, callback);
});
}
function get(domainObject, subdomain, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// We need to filter hosts to ones with this subdomain and type
let actualHosts = result.filter((host) => host.Type === type && host.Name === subdomain);
// We only return the value string
var tmp = actualHosts.map(function (record) { return record.Address; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function del(domainObject, subdomain, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
let removed = false;
for (var i = 0; i < values.length; i++) {
let curValue = values[i];
for (var j = 0; j < result.length; j++) {
let curHost = result[i];
if (curHost.Type === type && curHost.Name === subdomain && curHost.Address === curValue) {
removed = true;
result.splice(i, 1); // Remove element from result array
}
}
}
// Only set hosts if we actually removed a host
if (removed) return setInternal(dnsConfig, zoneName, result, callback);
callback();
});
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
const ip = '127.0.0.1';
if (!dnsConfig.username || typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a non-empty string'));
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
let credentials = {
username: dnsConfig.username,
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (nameservers.some(function (n) { return n.toLowerCase().indexOf('.registrar-servers.com') === -1; })) {
debug('verifyDnsConfig: %j does not contains NC NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to NameCheap'));
}
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, testSubdomain, 'A', [ip], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, testSubdomain, 'A', [ip], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
function wait(domainObject, subdomain, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(subdomain, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
+81 -49
View File
@@ -1,19 +1,24 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
safe = require('safetydance'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent');
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
const NAMECOM_API = 'https://api.name.com/v4';
@@ -21,18 +26,27 @@ function formatError(response) {
return `Name.com DNS error [${response.statusCode}] ${response.text}`;
}
function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`add: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`add: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: subdomain,
host: name,
type: type,
ttl: 300 // 300 is the lowest
};
@@ -57,19 +71,19 @@ function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof recordId, 'number');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`update:${recordId} on ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`update:${recordId} on ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: subdomain,
host: name,
type: type,
ttl: 300 // 300 is the lowest
};
@@ -94,16 +108,14 @@ function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, ca
});
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
function getInternal(dnsConfig, zoneName, name, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`getInternal: ${subdomain} in zone ${zoneName} of type ${type}`);
debug(`getInternal: ${name} in zone ${zoneName} of type ${type}`);
superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
.auth(dnsConfig.username, dnsConfig.token)
@@ -123,7 +135,7 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
});
var results = result.body.records.filter(function (r) {
return (r.host === subdomain && r.type === type);
return (r.host === name && r.type === type);
});
debug('getInternal: %j', results);
@@ -132,35 +144,39 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return addRecord(dnsConfig, zoneName, subdomain, type, values, callback);
if (result.length === 0) return addRecord(dnsConfig, zoneName, name, type, values, callback);
return updateRecord(dnsConfig, zoneName, result[0].id, subdomain, type, values, callback);
return updateRecord(dnsConfig, zoneName, result[0].id, name, type, values, callback);
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.answer; });
@@ -171,19 +187,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
@@ -201,13 +218,26 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
@@ -216,6 +246,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
token: dnsConfig.token
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -227,14 +259,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Name.com'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+25 -22
View File
@@ -1,10 +1,12 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: waitForDns,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
@@ -12,33 +14,37 @@ var assert = require('assert'),
debug = require('debug')('box:dns/noop'),
util = require('util');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function removePrivateFields(domainObject) {
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -46,9 +52,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, type, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
@@ -57,11 +63,8 @@ function waitForDns(domain, zoneName, type, value, options, callback) {
callback();
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(null, { });
+62 -41
View File
@@ -1,24 +1,34 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig,
// not part of "dns" interface
getHostedZone: getHostedZone
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
AWS = require('aws-sdk'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.secretAccessKey === domains.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
}
function getDnsCredentials(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
@@ -82,20 +92,22 @@ function getHostedZone(dnsConfig, zoneName, callback) {
});
}
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the '<priority> <server>' format
var params = {
@@ -126,31 +138,23 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
add(dnsConfig, zoneName, subdomain, type, values, callback);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
HostedZoneId: zone.Id,
MaxItems: '1',
StartRecordName: (subdomain ? subdomain + '.' : '') + zoneName + '.',
StartRecordName: fqdn + '.',
StartRecordType: type
};
@@ -169,18 +173,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; });
var resourceRecordSet = {
@@ -226,13 +232,26 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'accessKeyId must be a non-empty string'));
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'secretAccessKey must be a non-empty string'));
@@ -244,6 +263,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
listHostedZonesByName: true, // new/updated creds require this perm
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -258,14 +279,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+13 -13
View File
@@ -30,8 +30,8 @@ function resolveIp(hostname, options, callback) {
});
}
function isChangeSynced(domain, type, value, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
function isChangeSynced(hostname, type, value, nameserver, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof nameserver, 'string');
@@ -46,16 +46,16 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
async.every(nsIps, function (nsIp, iteratorCallback) {
const resolveOptions = { server: nsIp, timeout: 5000 };
const resolver = type === 'A' ? resolveIp.bind(null, domain) : dns.resolve.bind(null, domain, 'TXT');
const resolver = type === 'A' ? resolveIp.bind(null, hostname) : dns.resolve.bind(null, hostname, 'TXT');
resolver(resolveOptions, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain} (${type})`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${hostname} (${type})`);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain} (${type}): ${error}`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${hostname} (${type}): ${error}`);
return iteratorCallback(null, false);
}
@@ -66,7 +66,7 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
match = answer.some(function (a) { return value === a.join(''); });
}
debug(`isChangeSynced: ${domain} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
debug(`isChangeSynced: ${hostname} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
iteratorCallback(null, match);
});
@@ -76,26 +76,26 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, type, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
function waitForDns(hostname, zoneName, type, value, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
debug('waitForDns: hostname %s to be %s in zone %s.', hostname, value, zoneName);
var attempt = 0;
async.retry(options, function (retryCallback) {
++attempt;
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
debug(`waitForDns (try ${attempt}): ${hostname} to be ${value} in zone ${zoneName}`);
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, type, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, hostname, type, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', hostname, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
@@ -103,7 +103,7 @@ function waitForDns(domain, zoneName, type, value, options, callback) {
}, function retryDone(error) {
if (error) return callback(error);
debug(`waitForDns: ${domain} has propagated`);
debug(`waitForDns: ${hostname} has propagated`);
callback(null);
});
+43 -22
View File
@@ -1,47 +1,55 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
sysinfo = require('../sysinfo.js'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function removePrivateFields(domainObject) {
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -49,20 +57,33 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const zoneName = domainObject.zoneName;
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
const separator = dnsConfig.hyphenatedSubdomains ? '-' : '.';
const fqdn = `cloudrontest${separator}${domain}`;
const location = 'cloudrontestdns';
const fqdn = domains.fqdn(location, domainObject);
dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, `Unable to resolve ${fqdn}`));
if (error || !result) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`));
+27 -21
View File
@@ -54,17 +54,15 @@ var addons = require('./addons.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker.js'),
mkdirp = require('mkdirp'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
const RMVOLUME_CMD = path.join(__dirname, 'scripts/rmvolume.sh');
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
function DockerError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -373,15 +371,19 @@ function deleteContainer(containerId, callback) {
});
}
function deleteContainers(appId, callback) {
function deleteContainers(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('deleting containers of %s', appId);
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
@@ -517,16 +519,14 @@ function execContainer(containerId, cmd, options, callback) {
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
}
function createVolume(app, name, subdir, callback) {
function createVolume(app, name, volumeDataDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
const volumeDataDir = path.join(paths.APPS_DATA_DIR, app.id, subdir);
const volumeOptions = {
Name: name,
Driver: 'local',
@@ -541,7 +541,8 @@ function createVolume(app, name, subdir, callback) {
},
};
mkdirp(volumeDataDir, function (error) {
// requires sudo because the path can be outside appsdata
shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) {
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
docker.createVolume(volumeOptions, function (error) {
@@ -552,30 +553,35 @@ function createVolume(app, name, subdir, callback) {
});
}
function clearVolume(app, name, subdir, callback) {
function clearVolume(app, name, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
let docker = exports.connection;
let volume = docker.getVolume(name);
volume.inspect(function (error, v) {
if (error && error.statusCode === 404) return callback();
if (error) return callback(error);
const volumeDataDir = v.Options.device;
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, callback);
});
}
function removeVolume(app, name, subdir, callback) {
// this only removes the volume and not the data
function removeVolume(app, name, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
let volume = docker.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) {
debug(`removeVolume: Error removing volume of ${app.id} ${error}`);
callback(error);
}
if (error && error.statusCode !== 404) return callback(new Error(`removeVolume: Error removing volume of ${app.id} ${error.message}`));
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
callback();
});
}
+2 -2
View File
@@ -70,7 +70,7 @@ function attachDockerRequest(req, res, next) {
function containersCreate(req, res, next) {
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
@@ -122,7 +122,7 @@ function start(callback) {
if (config.TEST) {
proxyServer.use(function (req, res, next) {
console.log('Proxying: ' + req.method, req.url);
debug('proxying: ' + req.method, req.url);
next();
});
}
+3 -1
View File
@@ -16,7 +16,7 @@ var assert = require('assert'),
DatabaseError = require('./databaseerror'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'locked' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
@@ -24,6 +24,8 @@ function postProcess(data) {
delete data.configJson;
delete data.tlsConfigJson;
data.locked = !!data.locked; // make it bool
return data;
}
+91 -73
View File
@@ -7,9 +7,9 @@ module.exports = exports = {
update: update,
del: del,
clear: clear,
isLocked: isLocked,
fqdn: fqdn,
getName: getName,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
@@ -26,13 +26,15 @@ module.exports = exports = {
parentDomain: parentDomain,
prepareDashboardDomain: prepareDashboardDomain,
DomainsError: DomainsError,
// exported for testing
_getName: getName
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
};
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
@@ -90,6 +92,7 @@ function api(provider) {
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
case 'wildcard': return require('./dns/wildcard.js');
@@ -102,18 +105,18 @@ function parentDomain(domain) {
return domain.replace(/^\S+?\./, ''); // +? means non-greedy
}
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, callback) {
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var backend = api(provider);
if (!backend) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid provider'));
api(provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) {
const domainObject = { config: dnsConfig, domain: domain, zoneName: zoneName };
api(provider).verifyDnsConfig(domainObject, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Incorrect configuration. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Configuration error: ' + error.message));
@@ -224,32 +227,24 @@ function add(domain, data, auditSource, callback) {
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
callback();
});
callback();
});
});
});
}
function isLocked(domain) {
return domain === config.adminDomain() && config.edition() === 'hostingprovider';
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -259,8 +254,6 @@ function get(domain, callback) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.locked = isLocked(domain);
reverseProxy.getFallbackCertificate(domain, function (error, bundle) {
if (error && error.reason !== ReverseProxyError.NOT_FOUND) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
@@ -282,8 +275,6 @@ function getAll(callback) {
domaindb.getAll(function (error, result) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
result.forEach(function (r) { r.locked = isLocked(r.domain); });
return callback(null, result);
});
}
@@ -318,25 +309,30 @@ function update(domain, data, auditSource, callback) {
error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
if (provider === domainObject.provider) api(provider).injectPrivateFields(config, domainObject.config);
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
if (error) return callback(error);
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
let newData = {
config: sanitizedConfig,
zoneName: zoneName,
provider: provider,
tlsConfig: tlsConfig
};
domaindb.update(domain, newData, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
callback();
});
callback();
});
});
});
@@ -372,39 +368,36 @@ function clear(callback) {
}
// returns the 'name' that needs to be inserted into zone
function getName(domain, subdomain, type) {
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
if (domain.provider === 'caas') return subdomain;
function getName(domain, location, type) {
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
if (subdomain === '') return part;
if (location === '') return part;
if (!domain.config.hyphenatedSubdomains) return part ? `${subdomain}.${part}` : subdomain;
if (!domain.config.hyphenatedSubdomains) return part ? `${location}.${part}` : location;
// hyphenatedSubdomains
if (type !== 'TXT') return `${subdomain}-${part}`;
if (type !== 'TXT') return `${location}-${part}`;
if (subdomain.startsWith('_acme-challenge.')) {
return `${subdomain}-${part}`;
} else if (subdomain === '_acme-challenge') {
if (location.startsWith('_acme-challenge.')) {
return `${location}-${part}`;
} else if (location === '_acme-challenge') {
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
return up ? `${subdomain}.${up}` : subdomain;
return up ? `${location}.${up}` : location;
} else {
return `${subdomain}.${part}`;
return `${location}.${part}`;
}
}
function getDnsRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
function getDnsRecords(location, domain, type, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
get(domain, function (error, domainObject) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
api(domainObject.provider).get(domainObject, location, type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -413,19 +406,19 @@ function getDnsRecords(subdomain, domain, type, callback) {
}
// note: for TXT records the values must be quoted
function upsertDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
function upsertDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsertDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values);
get(domain, function (error, result) {
get(domain, function (error, domainObject) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
api(domainObject.provider).upsert(domainObject, location, type, values, function (error) {
if (error) return callback(error);
callback(null);
@@ -433,19 +426,19 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
});
}
function removeDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
function removeDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('removeDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values);
get(domain, function (error, result) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
api(domainObject.provider).del(domainObject, location, type, values, function (error) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
callback(null);
@@ -453,8 +446,8 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
});
}
function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
assert.strictEqual(typeof subdomain, 'string');
function waitForDnsRecord(location, domain, type, value, options, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
@@ -464,17 +457,14 @@ function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
const hostname = fqdn(subdomain, domainObject);
api(domainObject.provider).waitForDns(hostname, domainObject.zoneName, type, value, options, callback);
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
});
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
if (result.fallbackCertificate) delete result.fallbackCertificate.key; // do not return the 'key'. in caas, this is private
return result;
return api(result.provider).removePrivateFields(result);
}
// removes all fields that are not accessible by a normal user
@@ -494,3 +484,31 @@ function makeWildcard(hostname) {
parts[0] = '*';
return parts.join('.');
}
function prepareDashboardDomain(domain, auditSource, progressCallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
async.series([
(done) => { progressCallback({ percent: 10, message: 'Updating DNS' }); done(); },
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
(done) => { progressCallback({ percent: 40, message: 'Waiting for DNS' }); done(); },
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ percent: 70, message: 'Getting certificate' }); done(); },
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
callback(null);
});
});
});
}
+16 -3
View File
@@ -10,6 +10,9 @@ var appdb = require('./appdb.js'),
config = require('./config.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
sysinfo = require('./sysinfo.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -21,12 +24,18 @@ function sync(callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
debug('refreshDNS: current ip %s', ip);
let info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ip: null };
if (info.ip === ip) {
debug(`refreshDNS: no change in IP ${ip}`);
return callback();
}
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
debug('refreshDNS: updated admin location');
apps.getAll(function (error, result) {
if (error) return callback(error);
@@ -39,7 +48,11 @@ function sync(callback) {
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
debug('refreshDNS: updated apps');
eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, { userId: null, username: 'cron' }, { fromIp: info.ip, toIp: ip });
info.ip = ip;
safe.fs.writeFileSync(paths.DYNDNS_INFO_FILE, JSON.stringify(info), 'utf8');
callback();
});
+37 -4
View File
@@ -18,14 +18,21 @@ exports = module.exports = {
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_APP_LOGIN: 'app.login',
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_TASK_CRASH: 'app.task.crash',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start',
ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish',
ACTION_CERTIFICATE_NEW: 'certificate.new',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
ACTION_DASHBOARD_DOMAIN_UPDATE: 'dashboard.domain.update',
ACTION_DOMAIN_ADD: 'domain.add',
ACTION_DOMAIN_UPDATE: 'domain.update',
ACTION_DOMAIN_REMOVE: 'domain.remove',
@@ -47,12 +54,17 @@ exports = module.exports = {
ACTION_USER_REMOVE: 'user.remove',
ACTION_USER_UPDATE: 'user.update',
ACTION_USER_TRANSFER: 'user.transfer',
ACTION_DYNDNS_UPDATE: 'dyndns.update',
ACTION_PROCESS_CRASH: 'system.crash'
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
notifications = require('./notifications.js'),
util = require('util'),
uuid = require('uuid');
@@ -88,11 +100,32 @@ function add(action, source, data, callback) {
callback = callback || NOOP_CALLBACK;
var id = uuid.v4();
eventlogdb.add(id, action, source, data, function (error) {
// we do only daily upserts for login actions, so they don't spam the db
var api = action === exports.ACTION_USER_LOGIN ? eventlogdb.upsert : eventlogdb.add;
api(uuid.v4(), action, source, data, function (error, id) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
// decide if we want to add notifications as well
if (action === exports.ACTION_USER_ADD) {
notifications.userAdded(source.userId, id, data.user);
} else if (action === exports.ACTION_USER_REMOVE) {
notifications.userRemoved(source.userId, id, data.user);
} else if (action === exports.ACTION_USER_UPDATE && data.adminStatusChanged) {
notifications.adminChanged(source.userId, id, data.user);
} else if (action === exports.ACTION_APP_OOM) {
notifications.oomEvent(id, data.app ? data.app.id : data.containerId, { app: data.app, details: data });
} else if (action === exports.ACTION_APP_DOWN) {
notifications.appDied(id, data.app);
} else if (action === exports.ACTION_APP_UP) {
notifications.appUp(id, data.app);
} else if (action === exports.ACTION_APP_TASK_CRASH) {
notifications.apptaskCrash(id, data.appId, data.crashLogFile);
} else if (action === exports.ACTION_PROCESS_CRASH) {
notifications.processCrash(id, data.processName, data.crashLogFile);
} else {
// no notification
}
callback(null, { id: id });
});
}
+45 -9
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
getAllPaged: getAllPaged,
getByCreationTime: getByCreationTime,
add: add,
upsert: upsert,
count: count,
delByCreationTime: delByCreationTime,
@@ -12,13 +13,14 @@ exports = module.exports = {
};
var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mysql = require('mysql'),
safe = require('safetydance'),
util = require('util');
var EVENTLOGS_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
var EVENTLOG_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
function postProcess(eventLog) {
// usually we have sourceJson and dataJson, however since this used to be the JSON data type, we don't
@@ -32,7 +34,7 @@ function get(eventId, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
database.query('SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -48,7 +50,7 @@ function getAllPaged(actions, search, page, perPage, callback) {
assert.strictEqual(typeof callback, 'function');
var data = [];
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog';
var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog';
if (actions.length || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
@@ -78,7 +80,7 @@ function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT ' + EVENTLOGS_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
database.query(query, [ creationTime ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -99,7 +101,33 @@ function add(id, action, source, data, callback) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
callback(null, id);
});
}
// id is only used if we didn't do an update but insert instead
function upsert(id, action, source, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
// can't do a real sql upsert, for frequent eventlog entries we only have to do 2 queries once a day
var queries = [{
query: 'UPDATE eventlog SET creationTime=NOW(), data=? WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()',
args: [ JSON.stringify(data), action, JSON.stringify(source) ]
}, {
query: 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()',
args: [ action, JSON.stringify(source) ]
}];
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result[0].affectedRows >= 1) return callback(null, result[1][0].id);
// no existing eventlog found, create one
add(id, action, source, data, callback);
});
}
@@ -125,11 +153,19 @@ function delByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
var query = 'DELETE FROM eventlog WHERE creationTime < ?';
database.query(query, [ creationTime ], function (error) {
// since notifications reference eventlog items, we have to clean them up as well
database.query('SELECT * FROM eventlog WHERE creationTime < ?', [ creationTime ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
async.eachSeries(result, function (item, callback) {
database.query('DELETE FROM notifications WHERE eventId=?', [ item.id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM eventlog WHERE id=?', [ item.id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback();
});
});
}, callback);
});
}
+1 -1
View File
@@ -19,7 +19,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.1@sha256:deee3739011670d45abd8997a8a0b8d3c4cd577a93f235417614dea58338e0f9' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.1.0@sha256:131db42dcb90111f679ab1f0f37c552f93f797d9b803b2346c7c202daf86ac36' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
}
};
+41 -9
View File
@@ -47,7 +47,7 @@ function getUsersWithAccessToApp(req, callback) {
assert.strictEqual(typeof req.app, 'object');
assert.strictEqual(typeof callback, 'function');
users.list(function (error, result) {
users.getAll(function (error, result) {
if (error) return callback(new ldap.OperationsError(error.toString()));
async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) {
@@ -444,14 +444,45 @@ 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: req.app.id, app: req.app }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
res.end();
});
}
function authenticateMailbox(req, res, next) {
debug('mailbox auth: %s (from %s)', req.dn.toString(), req.connection.ldap.id);
function authenticateUserMailbox(req, res, next) {
debug('user 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 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()));
mail.getDomain(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));
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
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));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
if (error && error.reason === UsersError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === UsersError.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: users.removePrivateFields(result) });
res.end();
});
});
});
}
function authenticateMailAddon(req, res, next) {
debug('mail addon 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()));
@@ -516,12 +547,13 @@ function start(callback) {
gServer.bind('ou=users,dc=cloudron', authenticateApp, authenticateUser, authorizeUserForApp);
// http://www.ietf.org/proceedings/43/I-D/draft-srivastava-ldap-mail-00.txt
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch);
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
gServer.search('ou=mailboxes,dc=cloudron', mailboxSearch); // haraka, dovecot
gServer.bind('ou=mailboxes,dc=cloudron', authenticateUserMailbox); // apps like sogo can use domain=${domain} to authenticate a mailbox
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch); // haraka
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch); // haraka
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox); // haraka
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailAddon); // dovecot
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailAddon); // haraka
gServer.compare('cn=users,ou=groups,dc=cloudron', authenticateApp, groupUsersCompare);
gServer.compare('cn=admins,ou=groups,dc=cloudron', authenticateApp, groupAdminsCompare);
+19 -30
View File
@@ -5,9 +5,10 @@ exports = module.exports = {
};
var assert = require('assert'),
mailer = require('./mailer.js'),
eventlog = require('./eventlog.js'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
util = require('util');
var COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
@@ -15,7 +16,8 @@ var COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
var CRASH_LOG_TIMESTAMP_OFFSET = 1000 * 60 * 60; // 60 min
var CRASH_LOG_TIMESTAMP_FILE = '/tmp/crashlog.timestamp';
var CRASH_LOG_STASH_FILE = '/tmp/crashlog';
var CRASH_LOG_FILE_LIMIT = 2 * 1024 * 1024; // 2mb
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
function collectLogs(unitName, callback) {
assert.strictEqual(typeof unitName, 'string');
@@ -26,53 +28,40 @@ function collectLogs(unitName, callback) {
logs = logs + '\n\n=====================================\n\n';
// special case for box since the real logs are at path.join(paths.LOG_DIR, 'box.log')
if (unitName === 'box.service') {
logs += safe.child_process.execSync('tail --lines=500 ' + path.join(paths.LOG_DIR, 'box.log'), { encoding: 'utf8' });
}
callback(null, logs);
}
function stashLogs(logs) {
var stat = safe.fs.statSync(CRASH_LOG_STASH_FILE);
if (stat && (stat.size > CRASH_LOG_FILE_LIMIT)) {
console.error('Dropping logs since crash file has become too big');
return;
}
function sendFailureLogs(unitName) {
assert.strictEqual(typeof unitName, 'string');
// append here
safe.fs.writeFileSync(CRASH_LOG_STASH_FILE, logs, { flag: 'a' });
}
function sendFailureLogs(processName, options) {
assert.strictEqual(typeof processName, 'string');
assert.strictEqual(typeof options, 'object');
collectLogs(options.unit || processName, function (error, newLogs) {
collectLogs(unitName, function (error, logs) {
if (error) {
console.error('Failed to collect logs.', error);
newLogs = util.format('Failed to collect logs.', error);
logs = util.format('Failed to collect logs.', error);
}
console.log('Sending failure logs for', processName);
console.log('Sending failure logs for', unitName);
if (!safe.fs.writeFileSync(CRASH_LOG_STASH_FILE, logs)) console.log(`Failed to stash logs to ${CRASH_LOG_STASH_FILE}`);
var timestamp = safe.fs.readFileSync(CRASH_LOG_TIMESTAMP_FILE, 'utf8');
// check if we already sent a mail in the last CRASH_LOG_TIME_OFFSET window
if (timestamp && (parseInt(timestamp) + CRASH_LOG_TIMESTAMP_OFFSET) > Date.now()) {
console.log('Crash log already sent within window. Stashing logs.');
return stashLogs(newLogs);
return;
}
var stashedLogs = safe.fs.readFileSync(CRASH_LOG_STASH_FILE, 'utf8');
var compiledLogs = stashedLogs ? (stashedLogs + newLogs) : newLogs;
var mailSubject = processName + (stashedLogs ? ' and others' : '');
mailer.unexpectedExit(mailSubject, compiledLogs, function (error) {
if (error) {
console.log('Error sending crashlog. Stashing logs.');
return stashLogs(newLogs);
}
eventlog.add(eventlog.ACTION_PROCESS_CRASH, AUDIT_SOURCE, { processName: unitName, crashLogFile: CRASH_LOG_STASH_FILE }, function (error) {
if (error) console.log(`Error sending crashlog. Logs stashed at ${CRASH_LOG_STASH_FILE}`);
// write the new timestamp file and delete stash file
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
safe.fs.unlinkSync(CRASH_LOG_STASH_FILE);
});
});
}
+121 -45
View File
@@ -10,7 +10,10 @@ exports = module.exports = {
removeDomain: removeDomain,
clearDomains: clearDomains,
removePrivateFields: removePrivateFields,
setDnsRecords: setDnsRecords,
setMailFqdn: setMailFqdn,
validateName: validateName,
@@ -48,6 +51,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
@@ -141,13 +145,13 @@ function checkOutboundPort25(callback) {
});
client.on('timeout', function () {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' timed out';
relay.value = `Connect to ${smtpServer} timed out. Check if port 25 (outbound) is blocked`;
client.destroy();
callback(new Error('Timeout'), relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
relay.value = `Connect to ${smtpServer} failed: ${error.message}. Check if port 25 (outbound) is blocked`;
client.destroy();
callback(error, relay);
});
@@ -223,13 +227,17 @@ function checkDkim(domain, callback) {
});
}
function checkSpf(domain, callback) {
function checkSpf(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var spf = {
domain: domain,
name: '@',
type: 'TXT',
value: null,
expected: 'v=spf1 a:' + config.mailFqdn() + ' ~all',
expected: 'v=spf1 a:' + mailFqdn + ' ~all',
status: false
};
@@ -255,13 +263,17 @@ function checkSpf(domain, callback) {
});
}
function checkMx(domain, callback) {
function checkMx(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var mx = {
domain: domain,
name: '@',
type: 'MX',
value: null,
expected: '10 ' + config.mailFqdn() + '.',
expected: '10 ' + mailFqdn + '.',
status: false
};
@@ -269,7 +281,7 @@ function checkMx(domain, callback) {
if (error) return callback(error, mx);
if (mxRecords.length !== 0) {
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === config.mailFqdn();
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
}
@@ -310,12 +322,15 @@ function checkDmarc(domain, callback) {
});
}
function checkPtr(callback) {
function checkPtr(mailFqdn, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
var ptr = {
domain: null,
type: 'PTR',
value: null,
expected: config.mailFqdn(), // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
expected: mailFqdn, // any trailing '.' is added by client software (https://lists.gt.net/spf/devel/7918)
status: false
};
@@ -456,20 +471,25 @@ function getStatus(domain, callback) {
};
}
const mailFqdn = config.mailFqdn();
getDomain(domain, function (error, result) {
if (error) return callback(error);
var checks = [
recordResult('dns.mx', checkMx.bind(null, domain)),
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
];
let checks = [];
if (result.enabled) {
checks.push(
recordResult('dns.mx', checkMx.bind(null, domain, mailFqdn)),
recordResult('dns.dmarc', checkDmarc.bind(null, domain))
);
}
if (result.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.bind(null, domain)),
recordResult('dns.spf', checkSpf.bind(null, domain, mailFqdn)),
recordResult('dns.dkim', checkDkim.bind(null, domain)),
recordResult('dns.ptr', checkPtr),
recordResult('dns.ptr', checkPtr.bind(null, mailFqdn)),
recordResult('relay', checkOutboundPort25),
recordResult('rbl', checkRblStatus.bind(null, domain))
);
@@ -483,7 +503,8 @@ function getStatus(domain, callback) {
});
}
function createMailConfig(callback) {
function createMailConfig(mailFqdn, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
debug('createMailConfig: generating mail config');
@@ -492,9 +513,7 @@ function createMailConfig(callback) {
if (error) return callback(error);
users.getOwner(function (error, owner) {
const mailFqdn = config.mailFqdn();
const defaultDomain = config.adminDomain();
const alertsFrom = `no-reply@${defaultDomain}`;
const alertsFrom = `no-reply@${config.adminDomain()}`;
const alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
@@ -503,7 +522,7 @@ function createMailConfig(callback) {
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_default_domain=${defaultDomain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
@@ -543,20 +562,21 @@ function createMailConfig(callback) {
});
}
function restartMail(callback) {
function configureMail(mailFqdn, mailDomain, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
// 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
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
const tag = infra.images.mail.tag;
const memoryLimit = 4 * 256;
const cloudronToken = hat(8 * 128);
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
// admin and mail share the same certificate
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
reverseProxy.getCertificate(mailFqdn, mailDomain, function (error, bundle) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
@@ -569,7 +589,7 @@ function restartMail(callback) {
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
if (error) return callback(error);
createMailConfig(function (error, allowInbound) {
createMailConfig(mailFqdn, function (error, allowInbound) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
@@ -586,6 +606,7 @@ function restartMail(callback) {
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
-e CLOUDRON_RELAY_TOKEN="${relayToken}" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
@@ -599,6 +620,14 @@ function restartMail(callback) {
});
}
function restartMail(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
configureMail(config.mailFqdn(), config.adminDomain(), callback);
}
function restartMailIfActivated(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -636,8 +665,9 @@ function getDomains(callback) {
}
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(domain, callback) {
function txtRecordsWithSpf(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
domains.getDnsRecords('', domain, 'TXT', function (error, txtRecords) {
@@ -652,17 +682,17 @@ function txtRecordsWithSpf(domain, callback) {
if (matches === null) continue;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + config.mailFqdn()) !== -1;
validSpf = txtRecords[i].indexOf('a:' + mailFqdn) !== -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.mailFqdn() + ' ~all"');
txtRecords.push('"v=spf1 a:' + mailFqdn + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.mailFqdn() + txtRecords[i].slice(matches[1].length);
txtRecords[i] = matches[1] + ' a:' + mailFqdn + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
@@ -719,8 +749,9 @@ function readDkimPublicKeySync(domain) {
return publicKey;
}
function setDnsRecords(domain, callback) {
function upsertDnsRecords(domain, mailFqdn, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
maildb.get(domain, function (error, result) {
@@ -742,27 +773,27 @@ function setDnsRecords(domain, callback) {
records.push(dkimRecord);
if (result.enabled) {
records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] });
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
}
debug('setDnsRecords: %j', records);
debug('upsertDnsRecords: %j', records);
txtRecordsWithSpf(domain, function (error, txtRecords) {
txtRecordsWithSpf(domain, mailFqdn, function (error, txtRecords) {
if (error) return callback(error);
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
debug('setDnsRecords: will update %j', records);
debug('upsertDnsRecords: 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(`setDnsRecords: failed to update: ${error}`);
debug(`upsertDnsRecords: failed to update: ${error}`);
return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
}
debug('setDnsRecords: records %j added with changeIds %j', records, changeIds);
debug('upsertDnsRecords: records %j added with changeIds %j', records, changeIds);
callback(null);
});
@@ -770,6 +801,31 @@ function setDnsRecords(domain, callback) {
});
}
function setDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
upsertDnsRecords(domain, config.mailFqdn(), callback);
}
function setMailFqdn(mailFqdn, mailDomain, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof mailDomain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.getAll(function (error, allDomains) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
upsertDnsRecords(domainObject.domain, mailFqdn, iteratorDone);
}, function (error) {
if (error) return callback(new MailError(MailError.EXTERNAL_ERROR, error.message));
configureMail(mailFqdn, mailDomain, callback);
});
});
}
function addDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -780,7 +836,7 @@ function addDomain(domain, callback) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
async.series([
setDnsRecords.bind(null, domain), // do this first to ensure DKIM keys
upsertDnsRecords.bind(null, domain, config.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
@@ -815,6 +871,16 @@ function clearDomains(callback) {
});
}
// remove all fields that should never be sent out via REST API
function removePrivateFields(domain) {
let result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay');
if (result.relay.provider !== 'cloudron-smtp') {
if (result.relay.username === result.relay.password) result.relay.username = constants.SECRET_PLACEHOLDER;
result.relay.password = constants.SECRET_PLACEHOLDER;
}
return result;
}
function setMailFromValidation(domain, enabled, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof enabled, 'boolean');
@@ -850,16 +916,26 @@ function setMailRelay(domain, relay, callback) {
assert.strictEqual(typeof relay, 'object');
assert.strictEqual(typeof callback, 'function');
verifyRelay(relay, function (error) {
getDomain(domain, function (error, result) {
if (error) return callback(error);
maildb.update(domain, { relay: relay }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
// inject current username/password
if (result.relay.provider === relay.provider) {
if (relay.username === constants.SECRET_PLACEHOLDER) relay.username = result.relay.username;
if (relay.password === constants.SECRET_PLACEHOLDER) relay.password = result.relay.password;
}
restartMail(NOOP_CALLBACK);
verifyRelay(relay, function (error) {
if (error) return callback(error);
callback(null);
maildb.update(domain, { relay: relay }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
restartMail(NOOP_CALLBACK);
callback(null);
});
});
});
}
+14
View File
@@ -0,0 +1,14 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
The application '<%= title %>' installed at <%= appFqdn %> is back online
and responding to health checks.
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<% } %>
-22
View File
@@ -1,22 +0,0 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
your server is running out of disk space.
Disk space logs are attached.
-------------------------------------
<%- message %>
-------------------------------------
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<% } %>
+1 -1
View File
@@ -6,7 +6,7 @@ Someone, hopefully you, has requested your account's password
be reset. If you did not request this reset, please ignore this message.
To reset your password, please visit the following page:
<%= resetLink %>
<%- resetLink %>
+1 -1
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
Unfortunately <%= program %> exited unexpectedly!
<%= subject %>
Please see some excerpt of the logs below:
+1 -1
View File
@@ -5,7 +5,7 @@ Dear <%= user.displayName || user.username || user.email %>,
Welcome to <%= cloudronName %>!
Follow the link to get started.
<%= setupLink %>
<%- setupLink %>
<% if (invitor && invitor.email) { %>
You are receiving this email because you were invited by <%= invitor.email %>.
+67 -62
View File
@@ -12,10 +12,10 @@ exports = module.exports = {
sendInvite: sendInvite,
unexpectedExit: unexpectedExit,
appUp: appUp,
appDied: appDied,
oomEvent: oomEvent,
outOfDiskSpace: outOfDiskSpace,
backupFailed: backupFailed,
certificateRenewalError: certificateRenewalError,
@@ -32,7 +32,6 @@ 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'),
@@ -40,8 +39,7 @@ var assert = require('assert'),
showdown = require('showdown'),
smtpTransport = require('nodemailer-smtp-transport'),
users = require('./users.js'),
util = require('util'),
_ = require('underscore');
util = require('util');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -87,18 +85,10 @@ function getMailConfig(callback) {
cloudronName = 'Cloudron';
}
mail.getDomains(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}>`
});
callback(null, {
adminEmails: adminEmails,
cloudronName: cloudronName,
notificationFrom: `"${cloudronName}" <no-reply@${config.adminDomain()}>`
});
});
});
@@ -122,9 +112,21 @@ function sendMails(queue, callback) {
var mailServerIp = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
if (!mailServerIp) return callback('Error querying mail server IP');
// extract the relay token for auth
const env = safe.query(data, 'Config.Env', null);
if (!env) return callback(new Error('Error getting mail env'));
const tmp = env.find(function (e) { return e.indexOf('CLOUDRON_RELAY_TOKEN') === 0; });
if (!tmp) return callback(new Error('Error getting CLOUDRON_RELAY_TOKEN env var'));
const relayToken = tmp.slice('CLOUDRON_RELAY_TOKEN'.length + 1); // +1 for the = sign
if (!relayToken) return callback(new Error('Error parsing CLOUDRON_RELAY_TOKEN'));
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: config.get('smtpPort')
port: config.get('smtpPort'),
auth: {
user: `no-reply@${config.adminDomain()}`,
pass: relayToken
}
}));
debug('Processing mail queue of size %d (through %s:2525)', queue.length, mailServerIp);
@@ -170,18 +172,17 @@ function render(templateFile, params) {
return content;
}
function mailUserEventToAdmins(user, event) {
function mailUserEvent(mailTo, user, event) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof event, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
var mailOptions = {
from: mailConfig.notificationFrom,
to: adminEmails.join(', '),
to: mailTo,
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' }),
};
@@ -226,16 +227,15 @@ function sendInvite(user, invitor) {
});
}
function userAdded(user) {
function userAdded(mailTo, user) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
debug('Sending mail for userAdded');
debug(`userAdded: Sending mail for added users ${user.fallbackEmail} to ${mailTo}`);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
var templateData = {
user: user,
cloudronName: mailConfig.cloudronName,
@@ -250,7 +250,7 @@ function userAdded(user) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: adminEmails.join(', '),
to: mailTo,
subject: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
@@ -260,21 +260,23 @@ function userAdded(user) {
});
}
function userRemoved(user) {
function userRemoved(mailTo, user) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
debug('Sending mail for userRemoved.', user.id, user.email);
debug('Sending mail for userRemoved.', user.id, user.username, user.email);
mailUserEventToAdmins(user, 'was removed');
mailUserEvent(mailTo, user, 'was removed');
}
function adminChanged(user, admin) {
function adminChanged(mailTo, user, isAdmin) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof admin, 'boolean');
assert.strictEqual(typeof isAdmin, 'boolean');
debug('Sending mail for adminChanged');
mailUserEventToAdmins(user, admin ? 'is now an admin' : 'is no more an admin');
mailUserEvent(mailTo, user, isAdmin ? 'is now an admin' : 'is no more an admin');
}
function passwordReset(user) {
@@ -310,7 +312,28 @@ function passwordReset(user) {
});
}
function appDied(app) {
function appUp(mailTo, app) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof app, 'object');
debug('Sending mail for app %s @ %s up', app.id, app.fqdn);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: util.format('[%s] App %s is back online', mailConfig.cloudronName, app.fqdn),
text: render('app_up.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
};
enqueue(mailOptions);
});
}
function appDied(mailTo, app) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof app, 'object');
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
@@ -320,7 +343,7 @@ function appDied(app) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
to: mailTo,
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' })
};
@@ -436,23 +459,6 @@ function sendDigest(info) {
});
}
function outOfDiskSpace(message) {
assert.strictEqual(typeof message, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
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 ]);
});
}
function backupFailed(error) {
var message = splatchError(error);
@@ -488,7 +494,8 @@ function certificateRenewalError(domain, message) {
});
}
function oomEvent(program, context) {
function oomEvent(mailTo, program, context) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
@@ -497,7 +504,7 @@ function oomEvent(program, context) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
to: mailTo,
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
};
@@ -508,24 +515,22 @@ function oomEvent(program, context) {
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// 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');
function unexpectedExit(mailTo, subject, context) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof subject, 'string');
assert.strictEqual(typeof context, 'string');
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
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' })
to: mailTo,
subject: `[${mailConfig.cloudronName}] ${subject}`,
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, subject: subject, context: context, format: 'text' })
};
sendMails([ mailOptions ], callback);
sendMails([ mailOptions ]);
});
}
+133
View File
@@ -0,0 +1,133 @@
'use strict';
exports = module.exports = {
get: get,
add: add,
upsert: upsert,
update: update,
del: del,
listByUserIdPaged: listByUserIdPaged
};
let assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
const NOTIFICATION_FIELDS = [ 'id', 'userId', 'eventId', 'title', 'message', 'action', 'creationTime', 'acknowledged' ];
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.id = String(result.id);
// convert to boolean
result.acknowledged = !!result.acknowledged;
}
function add(notification, callback) {
assert.strictEqual(typeof notification, 'object');
assert.strictEqual(typeof callback, 'function');
const query = 'INSERT INTO notifications (userId, eventId, title, message, action) VALUES (?, ?, ?, ?, ?)';
const args = [ notification.userId, notification.eventId, notification.title, notification.message, notification.action ];
database.query(query, args, function (error, result) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such eventlog entry'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, String(result.insertId));
});
}
// will clear the ack flag
// matches by userId and title
function upsert(notification, callback) {
assert.strictEqual(typeof notification, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT * from notifications WHERE userId = ? AND title = ?', [ notification.userId, notification.title ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return add(notification, callback);
postProcess(result[0]);
var data = {
acknowledged: false,
eventId: notification.eventId,
message: notification.message,
action: notification.action,
creationTime: new Date()
};
update(result[0].id, data, callback);
});
}
function update(id, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
let args = [ ];
let fields = [ ];
for (let k in data) {
fields.push(k + ' = ?');
args.push(data[k]);
}
args.push(id);
database.query('UPDATE notifications SET ' + fields.join(', ') + ' WHERE id = ?', 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));
return callback(null, id);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM notifications WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function listByUserIdPaged(userId, page, perPage, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var data = [ userId ];
var query = 'SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE userId=?';
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
+319
View File
@@ -0,0 +1,319 @@
'use strict';
exports = module.exports = {
NotificationsError: NotificationsError,
add: add,
upsert: upsert,
get: get,
ack: ack,
getAllPaged: getAllPaged,
// specialized notifications
userAdded: userAdded,
userRemoved: userRemoved,
adminChanged: adminChanged,
oomEvent: oomEvent,
appUp: appUp,
appDied: appDied,
processCrash: processCrash,
apptaskCrash: apptaskCrash,
backupConfigWarning: backupConfigWarning,
diskSpaceWarning: diskSpaceWarning,
mailStatusWarning: mailStatusWarning
};
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:notifications'),
mailer = require('./mailer.js'),
notificationdb = require('./notificationdb.js'),
safe = require('safetydance'),
users = require('./users.js'),
util = require('util');
function NotificationsError(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(NotificationsError, Error);
NotificationsError.INTERNAL_ERROR = 'Internal Error';
NotificationsError.NOT_FOUND = 'Not Found';
function add(userId, eventId, title, message, action, callback) {
assert.strictEqual(typeof userId, 'string');
assert(typeof eventId === 'string' || eventId === null);
assert.strictEqual(typeof title, 'string');
assert.strictEqual(typeof message, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof callback, 'function');
debug('add: ', userId, title, action);
notificationdb.add({
userId: userId,
eventId: eventId,
title: title,
message: message,
action: action
}, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
callback(null, { id: result });
});
}
function upsert(userId, eventId, title, message, action, callback) {
assert.strictEqual(typeof userId, 'string');
assert(typeof eventId === 'string' || eventId === null);
assert.strictEqual(typeof title, 'string');
assert.strictEqual(typeof message, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof callback, 'function');
debug('upsert: ', userId, title, action);
notificationdb.upsert({
userId: userId,
eventId: eventId,
title: title,
message: message,
action: action
}, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
callback(null, { id: result });
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
notificationdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function ack(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
notificationdb.update(id, { acknowledged: true }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
callback(null);
});
}
// if acknowledged === null we return all, otherwise yes or no based on acknowledged as a boolean
function getAllPaged(userId, acknowledged, page, perPage, callback) {
assert.strictEqual(typeof userId, 'string');
assert(acknowledged === null || typeof acknowledged === 'boolean');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
notificationdb.listByUserIdPaged(userId, page, perPage, function (error, result) {
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (acknowledged === null) return callback(null, result);
callback(null, result.filter(function (r) { return r.acknowledged === acknowledged; }));
});
}
// Calls iterator with (admin, callback)
function actionForAllAdmins(skippingUserIds, iterator, callback) {
assert(Array.isArray(skippingUserIds));
assert.strictEqual(typeof iterator, 'function');
assert.strictEqual(typeof callback, 'function');
users.getAllAdmins(function (error, result) {
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
// filter out users we want to skip (like the user who did the action or the user the action was performed on)
result = result.filter(function (r) { return skippingUserIds.indexOf(r.id) === -1; });
async.each(result, iterator, callback);
});
}
function userAdded(performedBy, eventId, user) {
assert.strictEqual(typeof performedBy, 'string');
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof user, 'object');
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
mailer.userAdded(admin.email, user);
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, '/#/users', callback);
}, function (error) {
if (error) console.error(error);
});
}
function userRemoved(performedBy, eventId, user) {
assert.strictEqual(typeof performedBy, 'string');
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof user, 'object');
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
mailer.userRemoved(admin.email, user);
add(admin.id, eventId, 'User removed', `User ${user.username || user.email || user.fallbackEmail} was removed`, '/#/users', callback);
}, function (error) {
if (error) console.error(error);
});
}
function adminChanged(performedBy, eventId, user) {
assert.strictEqual(typeof performedBy, 'string');
assert.strictEqual(typeof user, 'object');
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
mailer.adminChanged(admin.email, user, user.admin);
add(admin.id, eventId, 'Admin status change', `User ${user.username || user.email || user.fallbackEmail} ${user.admin ? 'is now an admin' : 'is no more an admin'}`, '/#/users', callback);
}, function (error) {
if (error) console.error(error);
});
}
function oomEvent(eventId, program, context) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'object');
// also send us a notification mail
if (config.provider() === 'caas') mailer.oomEvent('support@cloudron.io', program, JSON.stringify(context, null, 4));
actionForAllAdmins([], function (admin, callback) {
mailer.oomEvent(admin.email, program, JSON.stringify(context, null, 4));
var message;
if (context.app) message = `The application ${context.app.manifest.title} with id ${context.app.id} ran out of memory.`;
else message = `The container with id ${context.details.id} ran out of memory`;
add(admin.id, eventId, 'Process died out-of-memory', message, context.app ? '/#/apps' : '', callback);
}, function (error) {
if (error) console.error(error);
});
}
function appUp(eventId, app) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof app, 'object');
// also send us a notification mail
if (config.provider() === 'caas') mailer.appDied('support@cloudron.io', app);
actionForAllAdmins([], function (admin, callback) {
mailer.appUp(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application ${app.manifest.title} installed at ${app.fqdn} is back online.`, '/#/apps', callback);
}, function (error) {
if (error) console.error(error);
});
}
function appDied(eventId, app) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof app, 'object');
// also send us a notification mail
if (config.provider() === 'caas') mailer.appDied('support@cloudron.io', app);
actionForAllAdmins([], function (admin, callback) {
mailer.appDied(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, '/#/apps', callback);
}, function (error) {
if (error) console.error(error);
});
}
function processCrash(eventId, processName, crashLogFile) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof processName, 'string');
assert.strictEqual(typeof crashLogFile, 'string');
var subject = `${processName} exited unexpectedly`;
var crashLogs = safe.fs.readFileSync(crashLogFile, 'utf8') || `No logs found at ${crashLogFile}`;
// also send us a notification mail
if (config.provider() === 'caas') mailer.unexpectedExit('support@cloudron.io', subject, crashLogs);
actionForAllAdmins([], function (admin, callback) {
mailer.unexpectedExit(admin.email, subject, crashLogs);
add(admin.id, eventId, subject, 'Detailed logs have been sent to your email address.', '/#/system', callback);
}, function (error) {
if (error) console.error(error);
});
}
function apptaskCrash(eventId, appId, crashLogFile) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof crashLogFile, 'string');
var subject = `Apptask for ${appId} crashed`;
var crashLogs = safe.fs.readFileSync(crashLogFile, 'utf8') || `No logs found at ${crashLogFile}`;
// also send us a notification mail
if (config.provider() === 'caas') mailer.unexpectedExit('support@cloudron.io', subject, crashLogs);
actionForAllAdmins([], function (admin, callback) {
mailer.unexpectedExit(admin.email, subject, crashLogs);
add(admin.id, eventId, subject, 'Detailed logs have been sent to your email address.', '/#/system', callback);
}, function (error) {
if (error) console.error(error);
});
}
function backupConfigWarning(message) {
assert.strictEqual(typeof message, 'string');
actionForAllAdmins([], function (admin, callback) {
upsert(admin.id, null, 'Backup configuration is unsafe', message, '/#/backups', callback);
}, function (error) {
if (error) console.error(error);
});
}
function mailStatusWarning(message) {
assert.strictEqual(typeof message, 'string');
actionForAllAdmins([], function (admin, callback) {
upsert(admin.id, null, 'Email is not configured properly', message, '/#/email', callback);
}, function (error) {
if (error) console.error(error);
});
}
function diskSpaceWarning(message) {
assert.strictEqual(typeof message, 'string');
actionForAllAdmins([], function (admin, callback) {
upsert(admin.id, null, 'Out of Disk Space', message, '/#/graphs', callback);
}, function (error) {
if (error) console.error(error);
});
}
+1 -1
View File
@@ -1,6 +1,6 @@
<footer class="text-center">
<span class="text-muted">&copy; 2016-18 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted">&copy; 2016-19 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
<span class="text-muted"><a href="https://twitter.com/cloudron_io" target="_blank">Twitter <i class="fa fa-twitter"></i></a></span>
<span class="text-muted"><a href="https://chat.cloudron.io" target="_blank">Chat <i class="fa fa-comments"></i></a></span>
</footer>
+1 -1
View File
@@ -8,7 +8,6 @@ exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(config.baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(config.baseDir(), 'boxdata'),
@@ -23,6 +22,7 @@ exports = module.exports = {
BACKUP_INFO_DIR: path.join(config.baseDir(), 'platformdata/backup'),
UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'),
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(config.baseDir(), 'platformdata/dyndns-info.json'),
// this is not part of appdata because an icon may be set before install
APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'),
+97 -40
View File
@@ -4,6 +4,7 @@ exports = module.exports = {
setup: setup,
restore: restore,
activate: activate,
getStatus: getStatus,
ProvisionError: ProvisionError
};
@@ -21,21 +22,32 @@ var assert = require('assert'),
DomainsError = domains.DomainsError,
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
path = require('path'),
safe = require('safetydance'),
semver = require('semver'),
settingsdb = require('./settingsdb.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
users = require('./users.js'),
UsersError = users.UsersError,
tld = require('tldjs'),
util = require('util');
util = require('util'),
_ = require('underscore');
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// we cannot use tasks since the tasks table gets overwritten when db is imported
let gProvisionStatus = {
setup: {
active: false,
message: '',
errorMessage: null
},
restore: {
active: false,
message: '',
errorMessage: null
}
};
function ProvisionError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -63,6 +75,12 @@ ProvisionError.INTERNAL_ERROR = 'Internal Error';
ProvisionError.EXTERNAL_ERROR = 'External Error';
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
function setProgress(task, message, callback) {
debug(`setProgress: ${task} - ${message}`);
gProvisionStatus[task].message = message;
callback();
}
function autoprovision(autoconf, callback) {
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -102,7 +120,7 @@ function unprovision(callback) {
config.setAdminDomain('');
config.setAdminFqdn('');
config.setAdminLocation('my');
config.setAdminLocation(constants.ADMIN_LOCATION);
// TODO: also cancel any existing configureWebadmin task
async.series([
@@ -117,23 +135,27 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
gProvisionStatus.setup = { active: true, errorMessage: '', message: 'Adding domain' };
function done(error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
callback(error);
}
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_SETUP));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_SETUP));
unprovision(function (error) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
let webadminStatus = cloudron.getWebadminStatus();
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName}`);
let data = {
zoneName: zoneName,
@@ -144,16 +166,24 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
};
domains.add(domain, data, auditSource, function (error) {
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error && error.reason === DomainsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(); // now that args are validated run the task in the background
async.series([
mail.addDomain.bind(null, domain),
cloudron.setDashboardDomain.bind(null, domain), // triggers task to setup my. dns/cert/reverseproxy
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource), // this sets up the config.fqdn()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
autoprovision.bind(null, autoconf),
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], callback);
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
});
});
});
});
@@ -229,40 +259,67 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
if (!semver.valid(version)) return callback(new ProvisionError(ProvisionError.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 ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
let webadminStatus = cloudron.getWebadminStatus();
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
gProvisionStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config' };
function done(error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
callback(error);
}
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error && error.reason === BackupsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return done(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
webadminStatus.restore.active = true;
webadminStatus.restore.error = null;
callback(null); // do no block
callback(); // now that the fields are validated, continue task in the background
async.series([
backups.restore.bind(null, backupConfig, backupId, (progress) => debug(`restore: ${progress}`)),
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
autoprovision.bind(null, autoconf),
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
// Once we have a 100% IP based restore, we can skip this
mail.setDnsRecords.bind(null, config.adminDomain()),
shell.sudo.bind(null, 'restart', [ RESTART_CMD ], {})
mail.setDnsRecords.bind(null, config.adminDomain(), config.mailFqdn()),
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
debug('restore:', error);
if (error) webadminStatus.restore.error = error.message;
webadminStatus.restore.active = false;
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
if (!error) cloudron.onActivated(NOOP_CALLBACK);
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(null, _.extend({
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: activated,
edition: config.edition()
}, gProvisionStatus));
});
});
}
+51 -21
View File
@@ -12,16 +12,19 @@ exports = module.exports = {
validateCertificate: validateCertificate,
getCertificate: getCertificate,
ensureCertificate: ensureCertificate,
renewAll: renewAll,
renewCerts: renewCerts,
// the 'configure' functions always ensure a certificate
configureDefaultServer: configureDefaultServer,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
writeAdminConfig: writeAdminConfig,
reload: reload,
removeAppConfigs: removeAppConfigs,
@@ -54,7 +57,7 @@ var acme2 = require('./cert/acme2.js'),
users = require('./users.js'),
util = require('util');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function ReverseProxyError(reason, errorOrMessage) {
@@ -313,17 +316,18 @@ function getCertificateByHostname(hostname, domainObject, callback) {
callback(null);
}
function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
function getCertificate(fqdn, domain, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.get(app.domain, function (error, domainObject) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
getCertificateByHostname(app.fqdn, domainObject, function (error, result) {
getCertificateByHostname(fqdn, domainObject, function (error, result) {
if (error || result) return callback(error, result);
return getFallbackCertificate(app.domain, callback);
return getFallbackCertificate(domain, callback);
});
});
}
@@ -373,7 +377,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
});
}
function writeAdminConfig(bundle, configFileName, vhost, callback) {
function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -395,21 +399,47 @@ function writeAdminConfig(bundle, configFileName, vhost, callback) {
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
if (vhost) safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf')); // remove legacy admin.conf. remove after 3.5
reload(callback);
}
function configureAdmin(auditSource, callback) {
function configureAdmin(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
ensureCertificate(config.adminFqdn(), config.adminDomain(), auditSource, function (error, bundle) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
writeAdminConfig(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
ensureCertificate(adminFqdn, domainObject.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
});
});
}
function writeAppConfig(app, bundle, callback) {
function writeAdminConfig(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
getCertificate(adminFqdn, domainObject.domain, function (error, bundle) {
if (error) return callback(error);
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
});
});
}
function writeAppNginxConfig(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -442,7 +472,7 @@ function writeAppConfig(app, bundle, callback) {
reload(callback);
}
function writeAppRedirectConfig(app, fqdn, bundle, callback) {
function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof bundle, 'object');
@@ -481,14 +511,14 @@ function configureApp(app, auditSource, callback) {
ensureCertificate(app.fqdn, app.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAppConfig(app, bundle, function (error) {
writeAppNginxConfig(app, bundle, function (error) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAppRedirectConfig(app, alternateDomain.fqdn, bundle, callback);
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, callback);
});
}, callback);
});
@@ -519,7 +549,7 @@ function renewCerts(options, auditSource, progressCallback, callback) {
var appDomains = [];
// add webadmin domain
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_ADMIN_CONFIG_FILE_NAME) });
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${config.adminFqdn()}.conf`) });
// add app main
allApps.forEach(function (app) {
@@ -549,9 +579,9 @@ function renewCerts(options, auditSource, progressCallback, callback) {
// reconfigure since the cert changed
var configureFunc;
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${config.adminFqdn()}.conf`, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(function (ignoredError) {
@@ -575,7 +605,7 @@ function renewAll(auditSource, callback) {
function removeAppConfigs() {
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && appConfigFile !== constants.NGINX_ADMIN_CONFIG_FILE_NAME) {
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && !appConfigFile.startsWith(constants.ADMIN_LOCATION)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
@@ -597,7 +627,7 @@ function configureDefaultServer(callback) {
}
}
writeAdminConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
+7 -4
View File
@@ -210,6 +210,8 @@ function configureApp(req, res, next) {
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
}
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data, req.user, auditSource(req), function (error) {
@@ -367,7 +369,7 @@ function getLogStream(req, res, next) {
debug('Getting logstream of app id:%s', req.params.id);
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
@@ -376,7 +378,8 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true
follow: true,
format: 'json'
};
apps.getLogs(req.params.id, options, function (error, logStream) {
@@ -405,7 +408,7 @@ function getLogStream(req, res, next) {
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug('Getting logs of app id:%s', req.params.id);
@@ -413,7 +416,7 @@ function getLogs(req, res, next) {
var options = {
lines: lines,
follow: false,
format: req.query.format
format: req.query.format || 'json'
};
apps.getLogs(req.params.id, options, function (error, logStream) {
+3
View File
@@ -96,6 +96,9 @@ function getTokens(req, res, next) {
clients.getTokensByUserId(req.params.clientId, req.user.id, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
result = result.map(clients.removeTokenPrivateFields);
next(new HttpSuccess(200, { tokens: result }));
});
}
+14 -35
View File
@@ -7,18 +7,15 @@ exports = module.exports = {
getDisks: getDisks,
getUpdateInfo: getUpdateInfo,
update: update,
feedback: feedback,
checkForUpdates: checkForUpdates,
getLogs: getLogs,
getLogStream: getLogStream,
getStatus: getStatus,
setDashboardDomain: setDashboardDomain,
prepareDashboardDomain: prepareDashboardDomain,
renewCerts: renewCerts
};
var appstore = require('../appstore.js'),
AppstoreError = require('../appstore.js').AppstoreError,
assert = require('assert'),
let assert = require('assert'),
async = require('async'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
@@ -26,8 +23,7 @@ var appstore = require('../appstore.js'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
updater = require('../updater.js'),
updateChecker = require('../updatechecker.js'),
UpdaterError = require('../updater.js').UpdaterError,
_ = require('underscore');
UpdaterError = require('../updater.js').UpdaterError;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -91,36 +87,16 @@ function checkForUpdates(req, res, next) {
});
}
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
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.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'));
next(new HttpSuccess(201, {}));
});
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.unit, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format
format: req.query.format || 'json'
};
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
@@ -140,7 +116,7 @@ function getLogs(req, res, next) {
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.unit, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
@@ -150,7 +126,7 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true,
format: req.query.format
format: req.query.format || 'json'
};
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
@@ -178,7 +154,7 @@ function getLogStream(req, res, next) {
function setDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.setDashboardDomain(req.body.domain, function (error) {
cloudron.setDashboardDomain(req.body.domain, auditSource(req), function (error) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
@@ -186,11 +162,14 @@ function setDashboardDomain(req, res, next) {
});
}
function getStatus(req, res, next) {
cloudron.getStatus(function (error, status) {
function prepareDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.prepareDashboardDomain(req.body.domain, auditSource(req), function (error, taskId) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
next(new HttpSuccess(202, { taskId }));
});
}
+7 -3
View File
@@ -21,13 +21,17 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
// this code exists for the hosting provider edition
function verifyDomainLock(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
if (domains.isLocked(req.params.domain)) return next(new HttpError(423, 'This domain is locked'));
domains.get(req.params.domain, function (error, domain) {
if (error && error.reason === DomainsError.NOT_FOUND) return next(new HttpError(404, 'No such domain'));
if (error) return next(new HttpError(500, error));
next();
if (domain.locked) return next(new HttpError(423, 'This domain is locked'));
next();
});
}
function add(req, res, next) {
+12 -1
View File
@@ -1,14 +1,25 @@
'use strict';
exports = module.exports = {
get: get
get: get,
list: list
};
var eventlog = require('../eventlog.js'),
EventLogError = eventlog.EventLogError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
eventlog.get(req.params.eventId, function (error, result) {
if (error && error.reason === EventLogError.NOT_FOUND) return next(new HttpError(404, 'no such eventlog'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { event: result }));
});
}
function list(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
+2 -1
View File
@@ -13,12 +13,13 @@ exports = module.exports = {
groups: require('./groups.js'),
oauth2: require('./oauth2.js'),
mail: require('./mail.js'),
notifications: require('./notifications.js'),
profile: require('./profile.js'),
provision: require('./provision.js'),
services: require('./services.js'),
settings: require('./settings.js'),
support: require('./support.js'),
sysadmin: require('./sysadmin.js'),
ssh: require('./ssh.js'),
tasks: require('./tasks.js'),
users: require('./users.js')
};
+1 -1
View File
@@ -56,7 +56,7 @@ function getDomain(req, res, next) {
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));
next(new HttpSuccess(200, mail.removePrivateFields(result)));
});
}
+64
View File
@@ -0,0 +1,64 @@
'use strict';
exports = module.exports = {
verifyOwnership: verifyOwnership,
get: get,
list: list,
ack: ack
};
let assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
notifications = require('../notifications.js'),
NotificationsError = notifications.NotificationsError;
function verifyOwnership(req, res, next) {
if (!req.params.notificationId) return next(); // skip for listing
notifications.get(req.params.notificationId, function (error, result) {
if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification'));
if (error) return next(new HttpError(500, error));
if (result.userId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
req.notification = result;
next();
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.notification, 'object');
next(new HttpSuccess(200, { notification: req.notification }));
}
function list(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
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'));
var acknowledged = null;
if (req.query.acknowledged && !(req.query.acknowledged === 'true' || req.query.acknowledged === 'false')) return next(new HttpError(400, 'acknowledged must be a true or false'));
else if (req.query.acknowledged) acknowledged = req.query.acknowledged === 'true' ? true : false;
notifications.getAllPaged(req.user.id, acknowledged, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { notifications: result }));
});
}
function ack(req, res, next) {
assert.strictEqual(typeof req.params.notificationId, 'string');
notifications.ack(req.params.notificationId, function (error) {
if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, {}));
});
}
+11 -5
View File
@@ -44,10 +44,16 @@ var apps = require('../apps.js'),
util = require('util'),
_ = require('underscore');
// 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, app: appObject };
// appId is optional here
function auditSource(req, appId) {
var tmp = {
authType: 'oauth',
ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress || null
};
if (appId) tmp.appId = appId;
return tmp;
}
var gServer = null;
@@ -484,7 +490,7 @@ function 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, appObject), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
next();
});
+10 -1
View File
@@ -5,7 +5,8 @@ exports = module.exports = {
setupTokenAuth: setupTokenAuth,
setup: setup,
activate: activate,
restore: restore
restore: restore,
getStatus: getStatus
};
var assert = require('assert'),
@@ -156,3 +157,11 @@ function restore(req, res, next) {
next(new HttpSuccess(200));
});
}
function getStatus(req, res, next) {
provision.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
+5 -4
View File
@@ -56,7 +56,7 @@ function configure(req, res, next) {
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug(`Getting logs of service ${req.params.service}`);
@@ -64,7 +64,7 @@ function getLogs(req, res, next) {
var options = {
lines: lines,
follow: false,
format: req.query.format
format: req.query.format || 'json'
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
@@ -87,7 +87,7 @@ function getLogStream(req, res, next) {
debug(`Getting logstream of service ${req.params.service}`);
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
@@ -96,7 +96,8 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true
follow: true,
format: 'json'
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
+2 -1
View File
@@ -32,6 +32,7 @@ exports = module.exports = {
};
var assert = require('assert'),
backups = require('../backups.js'),
docker = require('../docker.js'),
DockerError = docker.DockerError,
HttpError = require('connect-lastmile').HttpError,
@@ -153,7 +154,7 @@ function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, config) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, config));
next(new HttpSuccess(200, backups.removePrivateFields(config)));
});
}
-54
View File
@@ -1,54 +0,0 @@
'use strict';
exports = module.exports = {
getAuthorizedKeys: getAuthorizedKeys,
getAuthorizedKey: getAuthorizedKey,
addAuthorizedKey: addAuthorizedKey,
delAuthorizedKey: delAuthorizedKey
};
var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
ssh = require('../ssh.js'),
SshError = ssh.SshError;
function getAuthorizedKeys(req, res, next) {
ssh.getAuthorizedKeys(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { keys: result }));
});
}
function getAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.params.identifier, 'string');
ssh.getAuthorizedKey(req.params.identifier, function (error, result) {
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { identifier: result.identifier, key: result.key }));
});
}
function addAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.key !== 'string' || !req.body.key) return next(new HttpError(400, 'key must be a non empty'));
ssh.addAuthorizedKey(req.body.key, function (error) {
if (error && error.reason === SshError.INVALID_KEY) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function delAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.params.identifier, 'string');
ssh.delAuthorizedKey(req.params.identifier, function (error) {
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+55
View File
@@ -0,0 +1,55 @@
'use strict';
exports = module.exports = {
feedback: feedback,
getRemoteSupport: getRemoteSupport,
enableRemoteSupport: enableRemoteSupport
};
var appstore = require('../appstore.js'),
AppstoreError = require('../appstore.js').AppstoreError,
assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
support = require('../support.js'),
_ = require('underscore');
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
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.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'));
next(new HttpSuccess(201, {}));
});
}
function enableRemoteSupport(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
support.enableRemoteSupport(req.body.enable, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function getRemoteSupport(req, res, next) {
support.getRemoteSupport(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
+5 -4
View File
@@ -59,13 +59,13 @@ function list(req, res, next) {
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format
format: req.query.format || 'json'
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
@@ -86,7 +86,7 @@ function getLogs(req, res, next) {
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
@@ -95,7 +95,8 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true
follow: true,
format: 'json'
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
+4 -139
View File
@@ -5,11 +5,11 @@
/* global before:false */
/* global after:false */
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
let async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
http = require('http'),
nock = require('nock'),
os = require('os'),
@@ -163,11 +163,11 @@ describe('Cloudron', function () {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
token_1 = tokendb.generateToken();
token_1 = hat(8 * 32);
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, 'cloudron', '', callback);
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: userId_1, clientId: 'test-client-id', expires: Date.now() + 100000, scope: 'cloudron', name: '' }, callback);
});
}
], done);
@@ -209,141 +209,6 @@ describe('Cloudron', function () {
});
});
describe('feedback', function () {
before(function (done) {
async.series([
setup,
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();
// stash token for further use
token = result.body.token;
callback();
});
},
], done);
});
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: '', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unknown type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without description', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: '', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty description', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: '' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with ticket type', function (done) {
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
.reply(201, { });
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope2.isDone()).to.be.ok();
done();
});
});
it('succeeds with app type', function (done) {
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
.reply(201, { });
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
describe('logs', function () {
before(function (done) {
async.series([
+47 -2
View File
@@ -9,6 +9,7 @@ var async = require('async'),
child_process = require('child_process'),
config = require('../../config.js'),
database = require('../../database.js'),
domaindb = require('../../domaindb.js'),
expect = require('expect.js'),
fs = require('fs'),
path = require('path'),
@@ -216,9 +217,53 @@ describe('Domains API', function () {
});
});
describe('locked', function () {
before(function (done) {
domaindb.update(DOMAIN_0.domain, { locked: true }, done);
});
after(function (done) {
domaindb.update(DOMAIN_0.domain, { locked: false }, done);
});
it('can list the domains', function (done) {
superagent.get(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.domains).to.be.an(Array);
expect(result.body.domains.length).to.equal(2);
expect(result.body.domains[0].domain).to.equal(DOMAIN_0.domain);
expect(result.body.domains[1].domain).to.equal(DOMAIN_1.domain);
done();
});
});
it('cannot get locked domain', function (done) {
superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(423);
done();
});
});
it('cannot delete locked domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(423);
done();
});
});
});
describe('delete', function () {
it('fails without password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
@@ -228,7 +273,7 @@ describe('Domains API', function () {
});
it('fails with wrong password', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.send({ password: PASSWORD + PASSWORD })
.end(function (error, result) {
+66 -2
View File
@@ -10,7 +10,9 @@ var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
eventlogdb = require('../../eventlogdb.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
superagent = require('superagent'),
server = require('../../server.js'),
tokendb = require('../../tokendb.js');
@@ -22,6 +24,17 @@ var token = null;
var USER_1_ID = null, token_1;
var EVENT_0 = {
id: 'event_0',
action: 'foobaraction',
source: {
ip: '127.0.0.1'
},
data: {
something: 'is there'
}
};
function setup(done) {
config._reset();
config.setFqdn('example-eventlog-test.com');
@@ -60,10 +73,14 @@ function setup(done) {
},
function (callback) {
token_1 = tokendb.generateToken();
token_1 = hat(8 * 32);
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
tokendb.add({ id: 'tid-0', accessToken: token_1, identifier: USER_1_ID, clientId: 'test-client-id', expires: Date.now() + 100000, scope: accesscontrol.SCOPE_PROFILE, name: '' }, callback);
},
function (callback) {
eventlogdb.add(EVENT_0.id, EVENT_0.action, EVENT_0.source, EVENT_0.data, callback);
}
], done);
@@ -82,6 +99,53 @@ describe('Eventlog API', function () {
after(cleanup);
describe('get', function () {
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for non-admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('fails if not exists', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/doesnotexist')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds for admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.event).to.be.an('object');
expect(result.body.event.creationTime).to.be.a('string');
delete result.body.event.creationTime;
expect(result.body.event).to.eql(EVENT_0);
done();
});
});
});
describe('list', function () {
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token.toUpperCase() })
+4 -4
View File
@@ -11,9 +11,9 @@ var accesscontrol = require('../../accesscontrol.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
groups = require('../../groups.js'),
superagent = require('superagent'),
hat = require('../../hat.js'),
server = require('../../server.js'),
superagent = require('superagent'),
tokendb = require('../../tokendb.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -66,11 +66,11 @@ function setup(done) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
token_1 = tokendb.generateToken();
token_1 = hat(8 * 32);
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, userId_1, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: userId_1, clientId: 'test-client-id', expires: Date.now() + 100000, scope: accesscontrol.SCOPE_PROFILE, name: '' }, callback);
});
}
], done);
+28 -4
View File
@@ -13,7 +13,8 @@ var async = require('async'),
maildb = require('../../maildb.js'),
server = require('../../server.js'),
superagent = require('superagent'),
userdb = require('../../userdb.js');
userdb = require('../../userdb.js'),
_ = require('underscore');
var SERVER_URL = 'http://localhost:' + config.get('port');
@@ -48,7 +49,7 @@ function setup(done) {
function dnsSetup(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config } })
.send({ dnsConfig: { provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config, tlsConfig: { provider: 'fallback' } } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
@@ -57,6 +58,21 @@ function setup(done) {
});
},
function waitForSetup(done) {
async.retry({ times: 5, interval: 4000 }, function (retryCallback) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
.end(function (error, result) {
if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result'));
console.dir(result.body);
if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback();
retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body)));
});
}, done);
},
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
@@ -255,7 +271,15 @@ describe('Mail API', function () {
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
.query({ access_token: token })
.send({ enabled: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
});
@@ -696,7 +720,7 @@ describe('Mail API', function () {
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.relay).to.eql(relay);
expect(_.omit(res.body.relay, 'password')).to.eql(_.omit(relay, 'password'));
done();
});
});
+2 -2
View File
@@ -30,7 +30,7 @@ var accesscontrol = require('../../accesscontrol.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
let AUDIT_SOURCE = { ip: '1.2.3.4' };
let AUDIT_SOURCE = { ip: '1.2.3.4', userId: 'someuserid' };
describe('OAuth2', function () {
@@ -221,7 +221,7 @@ describe('OAuth2', function () {
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope),
function (callback) {
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, { }, null /* source */, function (error, userObject) {
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, { }, AUDIT_SOURCE, function (error, userObject) {
expect(error).to.not.be.ok();
// update the global objects to reflect the new user id
+5 -3
View File
@@ -6,9 +6,11 @@
'use strict';
var config = require('../../config.js'),
var accesscontrol = require('../../accesscontrol.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
hat = require('../../hat.js'),
mailer = require('../../mailer.js'),
superagent = require('superagent'),
server = require('../../server.js'),
@@ -110,10 +112,10 @@ describe('Profile API', function () {
});
it('fails with expired token', function (done) {
var token = tokendb.generateToken();
var token = hat(8 * 32);
var expires = Date.now() - 2000; // 1 sec
tokendb.add(token, user_0.id, null, expires, 'profile', 'tokenname', function (error) {
tokendb.add({ id: 'tid-3', accessToken: token, identifier: user_0.id, clientId: null, expires: expires, scope: accesscontrol.SCOPE_PROFILE, name: 'fromtest' }, function (error) {
expect(error).to.not.be.ok();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: token }).end(function (error, result) {
+17 -4
View File
@@ -34,6 +34,19 @@ function cleanup(done) {
], done);
}
function waitForSetup(done) {
async.retry({ times: 5, interval: 4000 }, function (retryCallback) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
.end(function (error, result) {
if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result'));
if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback();
retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body)));
});
}, done);
}
describe('REST API', function () {
before(setup);
after(cleanup);
@@ -128,23 +141,23 @@ describe('REST API', function () {
it('dns setup succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} } })
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {}, tlsConfig: { provider: 'fallback' } } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
done();
waitForSetup(done);
});
});
it('dns setup twice succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} }, tlsConfig: { provider: 'fallback' } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
done();
waitForSetup(done);
});
});
-232
View File
@@ -1,232 +0,0 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var ssh = require('../../ssh.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var INVALID_KEY_TYPE = 'ssh-foobar AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
var INVALID_KEY_VALUE = 'ssh-rsa foobar nebulon@nebulon';
var INVALID_KEY_IDENTIFIER = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N';
var VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
var VALID_KEY_1 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N muchmore';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-ssh-test.com');
async.series([
server.start.bind(server),
ssh._clear,
database._clear,
function createAdmin(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);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('SSH API', function () {
before(setup);
after(cleanup);
describe('add authorized_keys', function () {
it('fails due to missing key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to empty key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: 'foobar' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key type', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_TYPE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key value', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_VALUE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key identifier', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_IDENTIFIER })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
describe('get authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.identifier).to.be.a('string');
expect(res.body.identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.key).to.equal(VALID_KEY);
done();
});
});
});
describe('list authorized_keys', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(1);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY);
done();
});
});
it('succeeds with two keys', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(2);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY_1.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY_1);
expect(res.body.keys[1]).to.be.an('object');
expect(res.body.keys[1].identifier).to.be.a('string');
expect(res.body.keys[1].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[1].key).to.equal(VALID_KEY);
done();
});
});
});
});
describe('delete authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
});
});
});

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