Compare commits

...

504 Commits

Author SHA1 Message Date
Girish Ramakrishnan
66b02b58b6 make notifications.alert take a title
the title is better when it's a bit more dynamic
2019-03-08 16:59:48 -08:00
Girish Ramakrishnan
4428c3d7d8 Move docker config file generation to installer logic
the new version of docker does not support devicemapper on ubuntu 16.
so, we have to first enable overlay2 and then install the latest docker
2019-03-08 16:41:39 -08:00
Girish Ramakrishnan
2d4b9786fa box update is now an alert notification 2019-03-07 14:40:46 -08:00
Girish Ramakrishnan
d2d9c4be6f update notification 2019-03-07 14:27:43 -08:00
Girish Ramakrishnan
a9d6ac29f1 make funcs take proper callbacks 2019-03-07 14:27:23 -08:00
Girish Ramakrishnan
4d50bd5c78 3.5.4 changes 2019-03-07 13:40:20 -08:00
Girish Ramakrishnan
fdd651b9cc Only append markdownMessage is not-empty 2019-03-07 11:50:49 -08:00
Girish Ramakrishnan
7b56f102cc relay check is always performed 2019-03-06 19:48:18 -08:00
Girish Ramakrishnan
e329360daa backup notification now has a doc link and log link 2019-03-06 16:23:25 -08:00
Girish Ramakrishnan
5e8a431a92 Add doc link for cert renweal error 2019-03-06 16:17:56 -08:00
Girish Ramakrishnan
cd3f21a92e Add doc links for the app down mail 2019-03-06 16:14:24 -08:00
Girish Ramakrishnan
03d3ae3eb4 Qualify the name in the email subject 2019-03-06 16:08:12 -08:00
Girish Ramakrishnan
0c350dcf6e add a note 2019-03-06 16:05:50 -08:00
Girish Ramakrishnan
c6b3d15d72 Fix some typos 2019-03-06 16:02:51 -08:00
Girish Ramakrishnan
8d7f7cb438 rename the constant 2019-03-06 15:55:07 -08:00
Girish Ramakrishnan
b5a4121574 Better OOM notification messages 2019-03-06 14:47:24 -08:00
Girish Ramakrishnan
916ca87db4 Expose apps.getByContainerId 2019-03-06 11:15:12 -08:00
Girish Ramakrishnan
bfea97f14e refactor apps.postProcess 2019-03-06 11:12:39 -08:00
Girish Ramakrishnan
f98657aca8 Remove double query of domains 2019-03-06 11:08:14 -08:00
Girish Ramakrishnan
45c5e770fa injectPrivateFields already merges fields 2019-03-05 19:38:56 -08:00
Girish Ramakrishnan
f4ce7ecf4b do not add acked alerts 2019-03-04 21:04:31 -08:00
Girish Ramakrishnan
8dfe1fe97f notifications: add ack flag in db logic 2019-03-04 20:44:41 -08:00
Girish Ramakrishnan
4bf165efaf Fix misplaced callback 2019-03-04 20:22:25 -08:00
Girish Ramakrishnan
c7f6ae5be9 remove unused require 2019-03-04 19:49:25 -08:00
Girish Ramakrishnan
d83d2d5f4e Do not restart mail container when setting fallback certs 2019-03-04 19:35:22 -08:00
Girish Ramakrishnan
2362b2a5a0 Make the version 3.5.3 2019-03-04 18:18:23 -08:00
Girish Ramakrishnan
fb08a17ec8 Add some debugs 2019-03-04 18:11:07 -08:00
Girish Ramakrishnan
1bcc2d544a link to logs instead of spatch'ed error 2019-03-04 18:03:51 -08:00
Girish Ramakrishnan
6fd1205681 settings value is a variant 2019-03-04 15:45:04 -08:00
Girish Ramakrishnan
da2b00c9cf Move cert change notification into ensureCertificate()
When ensureCertificate renews the cert, the filename will match the
nginx config cert file. The current code detects that this implies
that the cert has not changed and thus does not update mail container.

Move the notification into ensureCertificate() itself. If we have a wildcard
cert and it gets renewed when installing a new app, then mail container will
still get it.
2019-03-04 15:24:09 -08:00
Girish Ramakrishnan
f6213595d1 move mailer notification of failed backups
this also removes the splatchError which is causing a crash because
of infinite recursion when backups fail (not sure why)
2019-03-04 15:01:30 -08:00
Girish Ramakrishnan
b1b2bd5b97 move cert renewal notification to notification logic 2019-03-04 14:53:19 -08:00
Girish Ramakrishnan
aa19cbbfc7 Properly escape filename when downloading files 2019-03-04 13:50:17 -08:00
Girish Ramakrishnan
8d39faddc9 cleanup temporary file after upload
also, wait for finish event for the http response. this should be
quick because the file has already been upload and we just have to
copy it to the container
2019-03-04 12:28:27 -08:00
Girish Ramakrishnan
52714dbcc9 Update changelog 2019-03-04 12:17:38 -08:00
Girish Ramakrishnan
be92d3a0bc Properly escape the filename when uploading files
tested with filename:
Fancy - +!"#$&'\''()*+,:;=?@ - Filename

(in the e2e repo)
2019-03-04 12:16:04 -08:00
Girish Ramakrishnan
f3189f72fd Use mail.restartMail alias 2019-03-04 10:42:42 -08:00
Girish Ramakrishnan
144c1d4e2f remove eventemitter pattern
the main issue is that ee pattern does not work across processes.
with task logic, this complicates things
2019-03-04 10:25:18 -08:00
Girish Ramakrishnan
e5964f9d93 Remove unused function 2019-03-02 19:31:19 -08:00
Girish Ramakrishnan
ea30cbe117 Fix tests 2019-03-02 19:23:39 -08:00
Girish Ramakrishnan
598a9664a7 Fix crash because mailboxName is null
mailboxName is always a non-empty string. even for apps that don't use
it, we allocate a mailbox with .app suffix
2019-03-02 19:18:38 -08:00
Girish Ramakrishnan
d04628a42d Suffix .log 2019-03-01 16:55:26 -08:00
Girish Ramakrishnan
7bce63d74e Add API to get crash logs 2019-03-01 16:33:35 -08:00
Girish Ramakrishnan
452fe9f76d add callback to notification code
the crashnotifier prematurely exits otherwise
2019-03-01 15:17:38 -08:00
Girish Ramakrishnan
7983ff5db2 Stash logs in crash log directory 2019-03-01 15:07:06 -08:00
Girish Ramakrishnan
c361ab954d Indicate how often status check is run 2019-03-01 11:30:51 -08:00
Girish Ramakrishnan
a8735a6465 Generate detailed mail configuration error notification 2019-03-01 11:15:05 -08:00
Girish Ramakrishnan
76255c0dd4 Typo 2019-02-28 15:22:55 -08:00
Girish Ramakrishnan
87655ff3cd remove action field from notifications table
it is mostly unused
2019-02-28 15:13:51 -08:00
Girish Ramakrishnan
fc7be2ac1a Clear notifications if mail/backup/disk space situation changes 2019-02-28 15:13:47 -08:00
Girish Ramakrishnan
e93b95bee8 move the switch case to notifications
this way we don't need to export all the functions
2019-02-28 11:38:16 -08:00
Girish Ramakrishnan
6a18d6918e restart mail now restart the service
... and not just the container

Fixes #617
2019-02-28 10:44:26 -08:00
Girish Ramakrishnan
578ce09b5e Fix digest test 2019-02-27 11:56:16 -08:00
Girish Ramakrishnan
27f6177fc9 do not restart mail container when not activated
provision code is calling setDashboardDomain() which is restarting
the mail server
2019-02-27 11:46:03 -08:00
Johannes Zellner
20c0deeac4 Improve digest email 2019-02-27 12:06:01 +01:00
Johannes Zellner
f1f8cdb6e9 Add admin route to test digest 2019-02-27 12:06:01 +01:00
Girish Ramakrishnan
345e4e846c Copy/paste error 2019-02-26 15:03:14 -08:00
Girish Ramakrishnan
6f57b36158 make it executable 2019-02-26 15:03:14 -08:00
Girish Ramakrishnan
0264e10e69 Update license file 2019-02-26 15:03:14 -08:00
Girish Ramakrishnan
067f5bf5a3 auto register cloudron based on license file 2019-02-26 15:03:11 -08:00
Girish Ramakrishnan
c81b643cdf cloudron-setup: copy edition license 2019-02-26 12:06:32 -08:00
Girish Ramakrishnan
388ad077d6 cloudron-setup: baseDataDir is not used 2019-02-26 12:04:31 -08:00
Girish Ramakrishnan
db93cdd95f Make settings logic use the appstore model code 2019-02-25 18:19:25 -08:00
Girish Ramakrishnan
68304a3fc1 Add registerCloudron and getCloudron 2019-02-25 17:29:42 -08:00
Girish Ramakrishnan
13259c114a block updates if app has a maxBoxVersion less than incoming version 2019-02-25 10:03:50 -08:00
Girish Ramakrishnan
5131ba453d Add another change 2019-02-25 10:03:31 -08:00
Girish Ramakrishnan
8fdc9939cd Add locked flag to settings table 2019-02-22 10:08:02 -08:00
Girish Ramakrishnan
c15449492a settings: remove appstore scope 2019-02-22 09:43:26 -08:00
Girish Ramakrishnan
1cab1e06d9 aggregate the settings get/set handlers
this makes it easy to check for a settings lock
2019-02-22 09:37:36 -08:00
Girish Ramakrishnan
4831926869 settings: select fields explicitly 2019-02-22 09:31:09 -08:00
Girish Ramakrishnan
4fcf25077b Update docker to 18.09 2019-02-21 15:30:26 -08:00
Girish Ramakrishnan
c32461f322 Update node modules 2019-02-21 14:09:28 -08:00
Girish Ramakrishnan
0abe6fc0b4 Fixup node version 2019-02-21 13:41:15 -08:00
Girish Ramakrishnan
edc3d53f94 validate fields in the update response 2019-02-20 16:18:47 -08:00
Girish Ramakrishnan
bb5fbbe746 Add boxUpdateInfo to the eventlog
this is used by the email digest code
2019-02-20 16:18:38 -08:00
Girish Ramakrishnan
36f3e3fe50 Enable gzip compression for large objects
This doesn't trigger a re-configure (since it's not a big deal)
2019-02-20 16:03:13 -08:00
Girish Ramakrishnan
65c8000f66 rename function to just "send" 2019-02-20 09:11:45 -08:00
Girish Ramakrishnan
2d45f8bc40 Update node to 10.15.1 2019-02-19 10:46:59 -08:00
Girish Ramakrishnan
7a0d4ad508 Make reboot required check server side 2019-02-19 09:20:20 -08:00
Johannes Zellner
5ae93bb569 Clear connect-timeout handling for graphs 2019-02-18 13:13:29 +01:00
Girish Ramakrishnan
aa6ca46792 Add linode-stackscript as a provider 2019-02-16 13:59:35 -08:00
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
Girish Ramakrishnan
ff550e897a caas: remove box plan change routes 2018-12-13 09:41:52 -08:00
Johannes Zellner
10034fcbba Function is called tasks.removePrivateFields 2018-12-13 13:50:53 +01:00
Girish Ramakrishnan
36f8ce453f No need to reserve .app suffix 2018-12-12 15:07:49 -08:00
Girish Ramakrishnan
c2e40acb2c Fixup configuration and validation of mailboxName 2018-12-12 14:40:26 -08:00
Girish Ramakrishnan
82b1bb668d Fix tests 2018-12-11 16:26:19 -08:00
Girish Ramakrishnan
935a8258a6 task API: remove arg from response
it can contain sensitive information
2018-12-11 16:22:53 -08:00
Girish Ramakrishnan
fa483e5806 rename to listByTypePaged 2018-12-11 16:14:07 -08:00
Girish Ramakrishnan
e0c9658cb9 setup -> provision 2018-12-11 15:29:47 -08:00
Girish Ramakrishnan
0266a46b32 Ensure that location and domain are provided together
in our db, {location,domain} is unique. If we replace them one
by one in the database, it will cause conflicts.
2018-12-11 12:10:22 -08:00
Girish Ramakrishnan
e7294f2950 Make handleCertChanged take a callback 2018-12-11 11:02:32 -08:00
Girish Ramakrishnan
c9f325e75d renewCerts does not call callback 2018-12-11 10:49:04 -08:00
Girish Ramakrishnan
0fa353c2e2 return taskId for renewCerts 2018-12-11 10:16:38 -08:00
Girish Ramakrishnan
c7da090882 tasks: remove auditSource from start/stop 2018-12-11 09:22:13 -08:00
Johannes Zellner
ee609c8ef0 renew certs task also needs an auditSource 2018-12-11 13:10:32 +01:00
Johannes Zellner
6891ce2bc8 Fix typos 2018-12-11 12:33:35 +01:00
Johannes Zellner
94f5adba04 Renew certs is a POST route so lets use the body 2018-12-11 11:55:59 +01:00
Johannes Zellner
b8f843993a Fix typo 2018-12-11 11:26:45 +01:00
Girish Ramakrishnan
f9add21899 Add task route tests 2018-12-10 21:42:03 -08:00
Girish Ramakrishnan
1277da8bfe Add tasks test 2018-12-10 21:17:30 -08:00
Girish Ramakrishnan
55650fb734 Fix crash when db got cleared 2018-12-10 20:56:38 -08:00
Girish Ramakrishnan
d2f4b68c9f Make certificate renewal a task 2018-12-10 20:48:10 -08:00
Girish Ramakrishnan
a76731a991 remove old cloudron.conf and version field 2018-12-10 15:36:00 -08:00
Girish Ramakrishnan
536b8166ce typo 2018-12-09 12:04:51 -08:00
Girish Ramakrishnan
d43106b0af Add taskworker that runs funcs out of process 2018-12-09 10:06:22 -08:00
Girish Ramakrishnan
3688371ce8 Make tasks log to their own log file 2018-12-08 21:59:20 -08:00
Girish Ramakrishnan
6d66eb7759 Various fixes 2018-12-08 21:12:49 -08:00
Girish Ramakrishnan
8502bf4bfa tasks: add route to list tasks 2018-12-08 20:18:20 -08:00
Girish Ramakrishnan
d8225ad653 Make tasks indexed by id instead of type
The caas migrate logic is broken at this point until it uses new
task framework
2018-12-08 19:41:53 -08:00
Girish Ramakrishnan
cfb68a0511 setAdmin -> setDashboardDomain 2018-12-08 18:19:49 -08:00
Girish Ramakrishnan
76677e0aea Fix response 2018-12-07 16:39:22 -08:00
Girish Ramakrishnan
515ee891d3 refactor code to allow admin domain to be set 2018-12-07 16:15:21 -08:00
Girish Ramakrishnan
3aea1f3c9d Fix error message 2018-12-07 15:41:02 -08:00
Girish Ramakrishnan
8d944f9a4a Allow setup to be run again as long as not-activated
this is useful if the activation fails somewhere mid-way and we don't
need to re-setup the cloudron all over
2018-12-07 15:11:25 -08:00
Girish Ramakrishnan
331c8ae247 Add a new line 2018-12-07 11:28:23 -08:00
Girish Ramakrishnan
c71a429f61 Ask user before reboot
this is useful when installing on bare metal, don't want to reboot
server since the instructions get lost
2018-12-07 11:12:45 -08:00
Girish Ramakrishnan
3bad9e523c Add option to toggle app automatic updates 2018-12-07 10:02:11 -08:00
Girish Ramakrishnan
dfa61f1b2d rework how app mailboxes are allocated
Our current setup had a mailbox allocated for an app during app
install (into the mailboxes table). This has many issues:

* When set to a custom mailbox location, there was no way to access
  this mailbox even via IMAP. Even when using app credentials, we
  cannot use IMAP since the ldap logic was testing on the addon type
  (most of our apps only use sendmail addon and thus cannot recvmail).

* The mailboxes table was being used to add hidden 'app' type entries.
  This made it very hard for the user to understand why a mailbox conflicts.
  For example, if you set an app to use custom mailbox 'blog', this is
  hidden from all views.

The solution is to let an app send email as whatever mailbox name is
allocated to it (which we now track in the apps table. the default is in the
db already so that REST response contains it). When not using
Cloudron email, it will just send mail as that mailbox and the auth
checks the "app password" in the addons table. Any replies to that
mailbox will end up in the domain's mail server (not our problem).

When using cloudron email, the app can send mail like above. Any responses
will not end anywhere and bounce since there is no 'mailbox'. This is the
expected behavior. If user wants to access this mailbox name, he can
create a concrete mailbox and set himself as owner OR set this as
an alias.

For apps using the recvmail addon, the workflow is to actually create
a mailbox at some point. Currently, we have no UI for this 'flow'.
It's fine because we have only meemo using it.

Intuitive much!
2018-12-06 22:13:32 -08:00
Girish Ramakrishnan
6331fa5ced Update mail addon (does not use ownerType) 2018-12-06 20:34:18 -08:00
Girish Ramakrishnan
707b03b8c8 mailbox: ownerType is now purely internal 2018-12-06 20:25:24 -08:00
Girish Ramakrishnan
f2f93ed141 ldap: nobody binds to mailboxes 2018-12-06 19:34:25 -08:00
Girish Ramakrishnan
37e16c7a4c Force owner type to be user 2018-12-06 13:00:57 -08:00
Girish Ramakrishnan
41b0c3242e Make function name clearer 2018-12-06 12:47:50 -08:00
Girish Ramakrishnan
48ed051edf Use list verb 2018-12-06 10:23:10 -08:00
Girish Ramakrishnan
502642fd25 Add note on why we need universe 2018-12-06 09:32:02 -08:00
Johannes Zellner
4abe6a7a00 Ensure we have universe apt sources enabled 2018-12-06 13:23:30 +01:00
Girish Ramakrishnan
3f8fa64b98 wildcard must be outside the quotes 2018-12-05 16:39:29 -08:00
Johannes Zellner
527ff1b1fb Support docker and unbound service logs through journalctl 2018-12-05 16:19:10 +01:00
Girish Ramakrishnan
804467dce2 StandardOutput=file does not work in ubuntu 16
ubuntu 16 has systemd 229 which does not support this. ubuntu 18
has systemd 239 which works ok
2018-12-04 15:02:48 -08:00
Girish Ramakrishnan
4d7f308821 translate TaskError to UpdateError 2018-12-04 14:04:43 -08:00
Girish Ramakrishnan
a5b8418845 Remove spurious success return 2018-12-04 09:34:00 -08:00
Johannes Zellner
93d428b8c5 Update cron node module 2018-12-04 14:53:08 +01:00
Johannes Zellner
7c424ad60c Add netcup as provider 2018-12-04 09:51:40 +01:00
Girish Ramakrishnan
5b29a8680d Add missing callback 2018-12-03 15:23:26 -08:00
Girish Ramakrishnan
8f57c44837 provision: force http1.1
for unknown reasons, we get a http2 framing error from curl at times.
2018-12-03 13:29:43 -08:00
Johannes Zellner
b23939127b Ensure we always set the correct default value for TIMESTAMP types
So far it we rely on 0 default values in some case like the eventlog
This will not work in mysql strict mode with https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_no_zero_date
2018-12-03 14:09:57 +01:00
Johannes Zellner
3196322063 Add migration script to rename groups table to userGroups 2018-12-03 12:24:04 +01:00
Johannes Zellner
54c96d98d1 Remove all individual test timeouts and ignore timeouts toplevel 2018-12-03 12:24:04 +01:00
Johannes Zellner
f5f92fbb03 Rename groups table to userGroups 2018-12-03 12:24:04 +01:00
Girish Ramakrishnan
be0876603c ADDON_STATUS_* -> SERVICE_STATUS_* 2018-12-02 19:40:27 -08:00
Girish Ramakrishnan
7c1ef143f9 Add unbound service 2018-12-02 19:38:34 -08:00
Girish Ramakrishnan
6d128595e7 all container related functionality is 'service' 2018-12-02 19:12:43 -08:00
Girish Ramakrishnan
2f55abfc60 Fixup logs routes as well 2018-12-02 19:07:12 -08:00
Girish Ramakrishnan
f93044ac3b move routes to services.js 2018-12-02 18:46:34 -08:00
Girish Ramakrishnan
7ed422a3c1 Split out service related functions 2018-12-02 18:45:39 -08:00
Girish Ramakrishnan
823b3b8aa8 Rename addon route to service
service are the containers and other things like unbound, nginx.
addons are app development hooks.
2018-12-02 17:56:11 -08:00
Girish Ramakrishnan
9a701560f4 Fix email status 2018-12-01 21:50:28 -08:00
Girish Ramakrishnan
9800154d01 Use latest mail container (healthcheck route) 2018-12-01 21:50:24 -08:00
Girish Ramakrishnan
4b3f18ccdb Fix messages in update.sh 2018-11-30 21:18:36 -08:00
Girish Ramakrishnan
840d78b2f4 run update as a task, so it is cancelable 2018-11-30 21:04:24 -08:00
Girish Ramakrishnan
b409fd775d Revert "Fix crash"
This reverts commit 12a5965740.

Really fix crash
2018-11-30 19:10:44 -08:00
Girish Ramakrishnan
dbcfb20fab Remove shebang hack, this does not apply anymore 2018-11-30 17:10:28 -08:00
Girish Ramakrishnan
12a5965740 Fix crash 2018-11-30 17:10:28 -08:00
Girish Ramakrishnan
006ab75433 More changes 2018-11-30 17:10:28 -08:00
Girish Ramakrishnan
c72ea91743 make task args an object and put it in eventlog 2018-11-30 14:57:24 -08:00
Girish Ramakrishnan
f39ce20580 better name for task functions 2018-11-30 14:41:11 -08:00
Girish Ramakrishnan
b5c59e6b7d Add args to tasks table 2018-11-30 14:12:24 -08:00
Girish Ramakrishnan
b0ecdcc8b6 Fix another crash 2018-11-30 10:03:16 -08:00
Girish Ramakrishnan
8e1560f412 Fix crash in apptask 2018-11-30 09:48:26 -08:00
Girish Ramakrishnan
df927eae74 move backupupload.js to scripts
it is just a sudo helper
2018-11-29 23:30:56 -08:00
Girish Ramakrishnan
30aea047e3 startTask now takes args 2018-11-29 23:28:26 -08:00
Girish Ramakrishnan
cbcadaa449 Return an active field to indicate if task is actually running 2018-11-29 23:12:03 -08:00
Girish Ramakrishnan
9f4226093b Handle bad state if task is already stopped 2018-11-29 23:10:15 -08:00
Girish Ramakrishnan
fca0e897b2 Add tasks.startTask 2018-11-29 16:21:04 -08:00
Girish Ramakrishnan
2f729b56fa Use result in db instead of ipc 2018-11-29 15:34:05 -08:00
Girish Ramakrishnan
d9f3f64c76 Store result and error message separately 2018-11-29 14:45:28 -08:00
Girish Ramakrishnan
e8fa909c2f Bump graphite since the container port changed 2018-11-29 09:33:46 -08:00
Johannes Zellner
44f6636653 Add more changes 2018-11-29 16:44:44 +01:00
Johannes Zellner
148a0d0fc6 Add memory usage for addon status api 2018-11-28 12:33:24 +01:00
Girish Ramakrishnan
632ba69663 Make restore/download logic have progress callbacks 2018-11-27 12:13:16 -08:00
Girish Ramakrishnan
b2465dd2ee Set the backup task progress in the task only 2018-11-27 12:12:27 -08:00
Girish Ramakrishnan
e56b87766b backupApp should not set backup task progress 2018-11-27 11:06:03 -08:00
Girish Ramakrishnan
f7ca2e416a Add progress callback for snapshotting 2018-11-27 11:03:58 -08:00
Girish Ramakrishnan
002f68b0a1 Add progress info during backup rotation 2018-11-27 10:51:35 -08:00
Girish Ramakrishnan
aa31be5c5a backup: make upload progress available with callback+ipc 2018-11-27 10:42:34 -08:00
Johannes Zellner
6c0b7017bd Add some 3.4 changes 2018-11-27 14:41:03 +01:00
Girish Ramakrishnan
581774e001 Use ipc to pass back the result 2018-11-26 20:39:52 -08:00
Girish Ramakrishnan
3847a6616e No need to track backup time since it is killable now 2018-11-26 20:39:20 -08:00
Girish Ramakrishnan
48fbe28355 Make upload task abort when parent dies 2018-11-26 19:11:30 -08:00
Girish Ramakrishnan
e3ee5bc1d5 lint 2018-11-26 15:55:00 -08:00
Girish Ramakrishnan
a2da9bea58 backup: use ipc for communicating with upload process 2018-11-26 15:21:48 -08:00
Johannes Zellner
e4512e12c5 Add --help for cloudron-support and make ssh key enabling optional 2018-11-26 15:28:11 +01:00
Johannes Zellner
114f48fb17 Remove leftover console.log() 2018-11-26 14:50:10 +01:00
Johannes Zellner
289e018160 Fix comment text 2018-11-26 08:19:24 +01:00
Johannes Zellner
cb6699eeed Add api to check if the server needs a reboot 2018-11-26 08:19:24 +01:00
Girish Ramakrishnan
802011bb7e Fix args to shell.sudo 2018-11-25 14:57:51 -08:00
Girish Ramakrishnan
6cd8e769be remove all uses of sudoSync 2018-11-25 14:43:29 -08:00
Girish Ramakrishnan
9f6f67d331 ssh: remove use of shell.sudoSync 2018-11-25 14:27:06 -08:00
Johannes Zellner
161a8fe2bf Make docker restartable 2018-11-24 22:12:28 +01:00
Girish Ramakrishnan
b9c9839bb7 apparmor is always enabled on all ubuntu 2018-11-23 13:11:31 -08:00
Girish Ramakrishnan
76edbee48c Better error message 2018-11-23 13:11:15 -08:00
Girish Ramakrishnan
4142d7a050 Fix error handling of all the execSync usage 2018-11-23 13:11:15 -08:00
Girish Ramakrishnan
a0306c69e1 remove unused acme1.js
it got merged to acme2.js
2018-11-23 13:11:15 -08:00
Girish Ramakrishnan
31823f6282 Check if result is not null 2018-11-23 13:11:15 -08:00
Girish Ramakrishnan
9b4fffde29 Use shell.exec instead of shell.execSync 2018-11-23 11:18:45 -08:00
Girish Ramakrishnan
cce03e250d these can just be debugs 2018-11-22 18:05:41 -08:00
Girish Ramakrishnan
9b32cad946 typoe 2018-11-22 17:26:06 -08:00
Girish Ramakrishnan
2877a1057e Add CLOUDRON_PROXY_IP 2018-11-22 16:50:02 -08:00
Johannes Zellner
e2debe3c39 Add starting addon state when we wait for healthcheck 2018-11-22 21:54:31 +01:00
Johannes Zellner
f54ab11f18 Handle mail related addon logs 2018-11-22 12:38:50 +01:00
Johannes Zellner
b560e281d0 share container addon status code 2018-11-22 12:38:50 +01:00
Girish Ramakrishnan
3bb4ef5727 Some servers can be very slow starting databases 2018-11-21 09:49:08 -08:00
Johannes Zellner
900c008d20 Only allow addon restarts, no start+stop 2018-11-21 16:06:58 +01:00
Johannes Zellner
c1183a09a8 Add addon configure route 2018-11-21 15:47:41 +01:00
Girish Ramakrishnan
e04b7b55b0 Remove upgrade flag
This is not used since ages since we changed our update methodology
2018-11-20 10:29:54 -08:00
Johannes Zellner
329cc80933 Send memory settings on addon status 2018-11-20 16:53:27 +01:00
Johannes Zellner
a13f0706b4 Report addon state detail for further use 2018-11-20 13:11:11 +01:00
Johannes Zellner
55811de4b8 Check for docker ping result 2018-11-20 13:07:41 +01:00
Johannes Zellner
ab456f179e Check for healthcheck in addon status code 2018-11-20 12:26:33 +01:00
Johannes Zellner
f9d5bcd352 Make getAddonDetails() return AddonsError 2018-11-20 12:12:25 +01:00
Johannes Zellner
6a337884b5 Add start/stop addon code 2018-11-20 11:09:09 +01:00
Johannes Zellner
f953d115da Fix missing require 2018-11-20 11:09:09 +01:00
Johannes Zellner
88e8fc840f Fetch basic addon status from docker 2018-11-20 11:09:09 +01:00
Johannes Zellner
d1818e31b0 Add addon status hooks 2018-11-20 11:09:09 +01:00
Johannes Zellner
3f4bf647e8 Allow console.* in the linter config 2018-11-20 11:09:09 +01:00
Johannes Zellner
725a7e6dec Add missing requires 2018-11-20 11:09:09 +01:00
Johannes Zellner
e08b210001 Add initial addons rest apis 2018-11-20 11:09:09 +01:00
Girish Ramakrishnan
ec08ccb996 move tasks to separate directory
these are just wrappers that we use to launch as processes
2018-11-19 21:17:04 -08:00
Girish Ramakrishnan
b47a146c2b unlock on log failure 2018-11-19 21:12:58 -08:00
Girish Ramakrishnan
14dff27d45 boxUpdateInfo cannot be null here 2018-11-19 21:09:33 -08:00
Girish Ramakrishnan
305a3c94d0 Rework the progress API to use the task progress 2018-11-19 21:06:44 -08:00
Girish Ramakrishnan
218739a6b5 Add tasks table and API
progress will be tracked with this table instead of being in-process
like progress.js
2018-11-19 17:37:42 -08:00
Girish Ramakrishnan
390e69c01c Fix backup API routes 2018-11-19 14:41:16 -08:00
Johannes Zellner
4ef274acf0 Box logs are now in a log file 2018-11-19 19:47:19 +01:00
Girish Ramakrishnan
8267279779 backup can now be stopped
track the backup process and killable
2018-11-17 20:31:07 -08:00
Girish Ramakrishnan
6d971b9235 backupupload also logs to stdout now 2018-11-17 19:38:02 -08:00
Girish Ramakrishnan
98dc160886 remove the progress and watchdog timers
not needed since we will track these processes now and can be killed
by the user.
2018-11-17 19:38:02 -08:00
Girish Ramakrishnan
a869c88b43 Remove setBackupProgress
the upload task can just output to stdout. for progress status, we
can just use a tailstream on the ui side
2018-11-17 19:38:02 -08:00
Girish Ramakrishnan
0b86070fe9 run backuptask as separate process 2018-11-17 19:38:02 -08:00
Girish Ramakrishnan
5c9b6736f0 Remove pruneInfraImages out of platform startup
If it fails, it's really OK. Maybe we will have a button to remove
images.

This goes further into step of webadmin always staying up and not
crashing/erroring for cosmetic issues.
2018-11-17 19:35:39 -08:00
Girish Ramakrishnan
fd4057df94 shell.exec -> shell.spawn 2018-11-17 19:26:19 -08:00
Girish Ramakrishnan
1b1945e1f5 Move out graphite from port 8000
Port 8000 is used by esxi management service (!)
2018-11-17 19:14:21 -08:00
Girish Ramakrishnan
ebb053b900 add back timestamps to debug output 2018-11-16 14:16:11 -08:00
Girish Ramakrishnan
3381d9b595 Add format to the default backup config 2018-11-16 14:15:20 -08:00
Girish Ramakrishnan
d7a11ef394 runBackupTask -> runBackupUpload 2018-11-16 09:21:07 -08:00
Johannes Zellner
9d40cffabe Add logrotate config for box logs 2018-11-16 13:14:59 +01:00
Johannes Zellner
de44c63557 box now also logs to a a file instead of journald 2018-11-16 13:05:11 +01:00
Johannes Zellner
ac25477cd7 Ensure we prefix all output with a timestamp
note that debug() already does this now on its own in the same format
it does not use console.log but process.stderr
2018-11-16 13:02:37 +01:00
Johannes Zellner
59b86aa090 Stop logging box to journald 2018-11-16 12:15:38 +01:00
Girish Ramakrishnan
6abd48d480 rename backuptask to backupupload 2018-11-15 12:00:51 -08:00
Girish Ramakrishnan
72fc6b8c5a Fix tests 2018-11-15 12:00:51 -08:00
Girish Ramakrishnan
fcce4a6853 Add note on StandardError 2018-11-15 10:52:31 -08:00
Girish Ramakrishnan
a3b1a2c781 Get the domain correctly from subject 2018-11-15 10:47:20 -08:00
Johannes Zellner
a838a1706f Fix indentation 2018-11-15 19:26:50 +01:00
Johannes Zellner
a24c9fbafb Put cloudron-updater (installer.sh) logs persistently to /var/log/ 2018-11-15 14:54:12 +01:00
Johannes Zellner
ab255e78c5 Make the cert subject match 2018-11-15 14:18:34 +01:00
Girish Ramakrishnan
2628678d82 3.3.4 changes 2018-11-14 22:11:46 -08:00
Girish Ramakrishnan
eaf9b7f049 Add certificate.new event 2018-11-14 20:47:18 -08:00
Girish Ramakrishnan
b8df4d0b79 bare domains are getting continuously renewed
the code is not handling the case where bare domain is not part
of the wildcard SAN.
2018-11-14 20:47:15 -08:00
Girish Ramakrishnan
eb315f34dc Pass around domainObject 2018-11-14 20:32:14 -08:00
Girish Ramakrishnan
af535757a8 Fix issue where request module was buffering all data
when request is give a callback, it will save all the data in memory
to give the data in the response callback
2018-11-14 19:03:27 -08:00
Girish Ramakrishnan
600e030c6d add a note what the script is about 2018-11-14 10:25:43 -08:00
Girish Ramakrishnan
e86b813551 cloudron-support: use timeout command for docker ps output 2018-11-14 10:22:10 -08:00
Girish Ramakrishnan
af6653dfeb do not timeout when checking for updates 2018-11-13 10:38:18 -08:00
Johannes Zellner
f93e0c868c Warn the user if / or /tmp is full 2018-11-13 13:31:49 +01:00
215 changed files with 11557 additions and 9301 deletions

View File

@@ -23,6 +23,7 @@
"semi": [
"error",
"always"
]
],
"no-console": "off"
}
}

112
CHANGES
View File

@@ -1451,3 +1451,115 @@
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.3.4]
* Use new addons with REST APIs
* Ubuntu 18.04 LTS support
* Custom env vars can be set per application
* Add a button to renew certs
* Add better support for private builds
* cloudflare: Fix crash when using bad email
* cloudflare: HTTP proxying works now
* add new exoscale-sos regions
* Add UI to toggle dynamic DNS
* Add support for hyphenated subdomains
* Add domain, mail events to eventlog
[3.4.0]
* 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.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
[3.5.3]
* Make reboot required check server side
* Update node to 10.15.1
* Enable gzip compression for large objects
* Update docker to 18.09
* Add a way to lock specific settings
* Add UI to copy app's backup id
* Block platform updates based on app manifest constraints
* Make crash logs viewable via the dashboard
* Fix issue where uploading of filenames with brackets and plus was not working
* Add notification for cert renewal and backup failures
* Fix issue where mail container was not updated with the latest certificate
[3.5.4]
* Make reboot required check server side
* Update node to 10.15.1
* Enable gzip compression for large objects
* Update docker to 18.09
* Add a way to lock specific settings
* Add UI to copy app's backup id
* Block platform updates based on app manifest constraints
* Make crash logs viewable via the dashboard
* Fix issue where uploading of filenames with brackets and plus was not working
* Add notification for cert renewal and backup failures
* Fix issue where mail container was not updated with the latest certificate

694
LICENSE
View File

@@ -1,661 +1,35 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
The Cloudron Subscription license
Copyright (c) 2019 Cloudron UG
With regard to the Cloudron Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Cloudron Subscription Terms of Service, available
at https://cloudron.io/legal/terms.html (the “Subscription Terms”), or other
agreement governing the use of the Software, as agreed by you and Cloudron,
and otherwise have a valid Cloudron Subscription. Subject to the foregoing sentence,
you are free to modify this Software and publish patches to the Software. You agree
that Subscription and/or its licensors (as applicable) retain all right, title and
interest in and to all such modifications and/or patches, and all such modifications
and/or patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Cloudron subscription. Notwithstanding the foregoing, you may copy
and modify the Software for development and testing purposes, without requiring a
subscription. You agree that Cloudron and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not
granted any other rights beyond what is expressly stated herein. Subject to the
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
For all third party components incorporated into the Cloudron Software, those
components are licensed under the original license provided by the owner of the
applicable component.
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
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
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
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

30
baseimage/initializeBaseUbuntuImage.sh Normal file → Executable file
View File

@@ -27,13 +27,16 @@ 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)
ubuntu_codename=$(lsb_release -cs)
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 \
@@ -53,24 +56,27 @@ apt-get -y install \
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
echo "==> Installing node.js"
mkdir -p /usr/local/node-8.9.3
curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
mkdir -p /usr/local/node-10.15.1
curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
echo "==> Installing Docker"
# create systemd drop-in file
# create systemd drop-in file. if you channge options here, be sure to fixup installer.sh as well
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# there are 3 packages for docker - containerd, CLI and the daemon
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/docker.deb
rm /tmp/docker.deb
apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
if [[ "${storage_driver}" != "overlay2" ]]; then
@@ -126,3 +132,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

14
box.js
View File

@@ -2,12 +2,16 @@
'use strict';
require('supererror')({ splatchError: true });
// prefix all output with a timestamp
// debug() already prefixes and uses process.stderr NOT console.*
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
var orig = console[log];
console[log] = function () {
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
};
});
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
require('supererror')({ splatchError: true });
let async = require('async'),
config = require('./src/config.js'),

View File

@@ -4,19 +4,24 @@
var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
var crashNotifier = require('./src/crashnotifier.js');
// 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 });
crashNotifier.sendFailureLogs(unitName, function (error) {
if (error) console.error(error);
process.exit();
});
});
}

View File

@@ -1,7 +1,7 @@
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE groups(" +
var cmd = "CREATE TABLE userGroups(" +
"id VARCHAR(128) NOT NULL UNIQUE," +
"name VARCHAR(128) NOT NULL UNIQUE," +
"PRIMARY KEY(id))";
@@ -13,7 +13,7 @@ exports.up = function(db, callback) {
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE groups', function (error) {
db.runSql('DROP TABLE userGroups', function (error) {
if (error) console.error(error);
callback(error);
});

View File

@@ -4,7 +4,7 @@ exports.up = function(db, callback) {
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
"groupId VARCHAR(128) NOT NULL," +
"userId VARCHAR(128) NOT NULL," +
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
"FOREIGN KEY(groupId) REFERENCES userGroups(id)," +
"FOREIGN KEY(userId) REFERENCES users(id));";
db.runSql(cmd, function (error) {

View File

@@ -7,7 +7,7 @@ var ADMIN_GROUP_ID = 'admin'; // see constants.js
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
db.runSql.bind(db, 'INSERT INTO userGroups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
function migrateAdminFlag(done) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);

View File

@@ -10,7 +10,7 @@ exports.up = function(db, callback) {
function addGroupMailboxes(done) {
console.log('Importing group mailboxes');
db.all('SELECT id, name FROM groups', function (error, results) {
db.all('SELECT id, name FROM userGroups', function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (g, next) {

View File

@@ -16,7 +16,7 @@ exports.up = function(db, callback) {
db.runSql.bind(db, 'ALTER TABLE clients CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE eventlog CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groupMembers CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE groups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE userGroups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE migrations CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
db.runSql.bind(db, 'ALTER TABLE settings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),

View File

@@ -29,7 +29,7 @@ exports.up = function(db, callback) {
// this will be finally created once we have a domain when we create the owner in user.js
const ADMIN_GROUP_ID = 'admin'; // see constants.js
db.runSql('DELETE FROM groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
db.runSql('DELETE FROM userGroups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
if (error) return done(error);
db.runSql('DELETE FROM mailboxes WHERE ownerId = ?', [ ADMIN_GROUP_ID ], done);

View File

@@ -19,8 +19,8 @@ exports.up = function(db, callback) {
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', [ ], function (error, results) {
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) {

View File

@@ -18,7 +18,7 @@ exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
], callback);
});
});

View File

@@ -0,0 +1,27 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE tasks(' +
'id int NOT NULL AUTO_INCREMENT,' +
'type VARCHAR(32) NOT NULL,' +
'argsJson TEXT,' +
'percent INTEGER DEFAULT 0,' +
'message TEXT,' +
'errorMessage TEXT,' +
'result TEXT,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,' +
'PRIMARY KEY (id))';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE tasks', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('SELECT 1 FROM groups LIMIT 1', function (error) {
if (error) return callback(); // groups table does not exist
db.runSql('RENAME TABLE groups TO userGroups', function (error) {
if (error) console.error(error);
callback(error);
});
});
};
exports.down = function(db, callback) {
// this is a one way renaming since the previous migration steps have been already updated to match the new name
callback();
};

View File

@@ -0,0 +1,17 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE apps MODIFY updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE eventlog MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE backups MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
], callback);
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN mailboxName VARCHAR(128)'),
db.runSql.bind(db, 'START TRANSACTION;'),
function migrateMailboxNames(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
if (error) return done(error);
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (mailbox.ownerType !== 'app') return iteratorDone();
db.runSql('UPDATE apps SET mailboxName = ? WHERE id = ?', [ mailbox.name, mailbox.ownerId ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
function migrateMailboxNames(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
if (error) return done(error);
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (mailbox.ownerType !== 'app') return iteratorDone();
db.runSql('DELETE FROM mailboxes WHERE name = ?', [ mailbox.name ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'COMMIT'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN ownerType')
], callback);
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN enableAutomaticUpdate BOOLEAN DEFAULT 1', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN enableAutomaticUpdate', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -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);
});
};

View File

@@ -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);
});
};

View File

@@ -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);
});
};

View File

@@ -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);
});
};

View File

@@ -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);
};

View File

@@ -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();
};

View File

@@ -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);
});
};

View File

@@ -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();
};

View File

@@ -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);
};

View File

@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE settings ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE settings DROP COLUMN locked', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,14 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE notifications DROP COLUMN action', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE notifications ADD COLUMN action VARCHAR(512) NOT NULL', callback);
};

View File

@@ -0,0 +1,27 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
if (app.mailboxName) return iteratorDone();
const mailboxName = (app.subdomain ? app.subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
db.runSql('UPDATE apps SET mailboxName=? WHERE id=?', [ mailboxName, app.id ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS users(
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
CREATE TABLE IF NOT EXISTS userGroups(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
@@ -37,13 +37,14 @@ CREATE TABLE IF NOT EXISTS groups(
CREATE TABLE IF NOT EXISTS groupMembers(
groupId VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
FOREIGN KEY(groupId) REFERENCES groups(id),
FOREIGN KEY(groupId) REFERENCES userGroups(id),
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,14 +66,15 @@ 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
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
creationTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
memoryLimit BIGINT DEFAULT 0,
xFrameOptions VARCHAR(512),
@@ -80,6 +82,8 @@ CREATE TABLE IF NOT EXISTS apps(
debugModeJson TEXT, // options for development mode
robotsTxt TEXT,
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
// the following fields do not belong here, they can be removed when we use a queue for apptask
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
@@ -109,6 +113,7 @@ CREATE TABLE IF NOT EXISTS authcodes(
CREATE TABLE IF NOT EXISTS settings(
name VARCHAR(128) NOT NULL UNIQUE,
value TEXT,
locked BOOLEAN,
PRIMARY KEY(name));
CREATE TABLE IF NOT EXISTS appAddonConfigs(
@@ -126,7 +131,7 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
@@ -141,7 +146,7 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data TEXT, /* free flowing json based on action */
createdAt TIMESTAMP(2) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id));
@@ -151,6 +156,7 @@ CREATE TABLE IF NOT EXISTS domains(
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
tlsConfigJson TEXT, /* JSON containing the tls provider config */
locked BOOLEAN,
PRIMARY KEY (domain))
@@ -173,15 +179,17 @@ CREATE TABLE IF NOT EXISTS mail(
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
NOTE: this table exists only real mailboxes. And has unique constraint to handle
conflict with aliases and mailbox names
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
ownerId VARCHAR(128) NOT NULL, /* user id */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
@@ -195,6 +203,29 @@ CREATE TABLE IF NOT EXISTS subdomains(
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, domain))
UNIQUE (subdomain, domain));
CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE IF NOT EXISTS tasks(
id int NOT NULL AUTO_INCREMENT,
type VARCHAR(32) NOT NULL,
percent INTEGER DEFAULT 0,
message TEXT,
errorMessage TEXT,
result TEXT,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS notifications(
id int NOT NULL AUTO_INCREMENT,
userId VARCHAR(128) NOT NULL,
eventId VARCHAR(128), // reference to eventlog. can be null
title VARCHAR(512) NOT NULL,
message TEXT,
acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CHARACTER SET utf8 COLLATE utf8_bin;

5840
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,34 +17,35 @@
"@google-cloud/dns": "^0.7.2",
"@google-cloud/storage": "^1.7.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"async": "^2.6.2",
"aws-sdk": "^2.408.0",
"body-parser": "^1.18.3",
"cloudron-manifestformat": "^2.14.2",
"connect": "^3.6.6",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.0.2",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.3.5",
"cookie-parser": "^1.4.4",
"cookie-session": "^1.3.2",
"cron": "^1.3.0",
"cron": "^1.6.0",
"csurf": "^1.6.6",
"db-migrate": "^0.11.1",
"db-migrate": "^0.11.5",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.5",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.16.3",
"express": "^4.16.4",
"express-session": "^1.15.6",
"json": "^9.0.3",
"ldapjs": "^1.0.2",
"lodash.chunk": "^4.2.0",
"mime": "^2.3.1",
"moment-timezone": "^0.5.17",
"morgan": "^1.9.0",
"morgan": "^1.9.1",
"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",
@@ -86,13 +87,12 @@
"mocha": "^5.2.0",
"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"
"node-sass": "^4.11.0",
"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 -- --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",

View File

@@ -2,7 +2,7 @@
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
ip=""
dns_config=""

View File

@@ -49,7 +49,6 @@ apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
@@ -94,14 +93,16 @@ echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
"${provider}" != "digitalocean" && \
"${provider}" != "digitalocean-mp" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
@@ -110,18 +111,21 @@ elif [[ \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "linode-stackscript" && \
"${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, 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
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
echo "${baseDataDir} does not exist"
if [[ -n "${edition}" && ! -f "LICENSE" ]]; then
echo "A LICENSE is required to use this edition. Please contact support@cloudron.io"
exit 1
fi
@@ -137,6 +141,18 @@ 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}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
@@ -222,6 +238,8 @@ CONF_END
fi
fi
[[ -f LICENSE ]] && cp LICENSE /etc/cloudron/LICENSE
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
@@ -231,10 +249,15 @@ while true; do
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
systemctl reboot
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
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;;
* ) exit;;
esac
fi

View File

@@ -1,9 +1,19 @@
#!/bin/bash
# This script collects diagnostic information to help debug server related issues
# It also enables SSH access for the cloudron support team
PASTEBIN="https://paste.cloudron.io"
OUT="/tmp/cloudron-support.log"
LINE="\n========================================================\n"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
# We require root
if [[ ${EUID} -ne 0 ]]; then
@@ -11,13 +21,45 @@ if [[ ${EUID} -ne 0 ]]; then
exit 1
fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# check if at least 10mb root partition space is available
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo "No more space left on /"
echo "This is likely the root case of the issue. Free up some space and also check other partitions below:"
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/server/#recovery-after-disk-full"
exit 1
fi
# check for at least 5mb free /tmp space for the log file
if [[ "`df --output="avail" /tmp | sed -n 2p`" -lt "5120" ]]; then
echo "Not enough space left on /tmp"
echo "Free up some space first by deleting files from /tmp"
exit 1
fi
echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
if [[ $SUDO_USER === "" ]]; then
if [[ $SUDO_USER == "" ]]; then
ssh_user="root"
ssh_folder="/root/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
@@ -35,7 +77,9 @@ echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
echo -e $LINE"Docker container"$LINE >> $OUT
docker ps -a &>> $OUT
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
echo -e "Docker is not responding" >> $OUT
fi
echo -e $LINE"Filesystem stats"$LINE >> $OUT
df -h &>> $OUT
@@ -43,6 +87,9 @@ df -h &>> $OUT
echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
iptables -L &>> $OUT
@@ -53,14 +100,16 @@ echo -n "Uploading information..."
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p "${ssh_folder}"
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
chown -R ${ssh_user} "${ssh_folder}"
chmod 600 "${authorized_key_file}"
echo "Done"
fi
echo ""
echo "Please send the following link to support@cloudron.io"
echo "Please email the following link to support@cloudron.io"
echo ""
echo "${PASTEBIN}/${paste_key}"

View File

@@ -41,8 +41,8 @@ if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
exit 1
fi
if [[ "$(node --version)" != "v8.11.2" ]]; then
echo "This script requires node 8.11.2"
if [[ "$(node --version)" != "v10.15.1" ]]; then
echo "This script requires node 10.15.1"
exit 1
fi

View File

@@ -19,17 +19,30 @@ readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
readonly ubuntu_version=$(lsb_release -rs)
readonly ubuntu_codename=$(lsb_release -cs)
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
echo "==> installer: docker binary download is corrupt"
exit 5
fi
# verify that existing docker installation uses overlay2. devicemapper does not works with ubuntu 16/docker 18.09
# if you change the drop-in file below be sure to change initializeUbuntuBaseImage script as well
if ! grep -q "storage-driver=overlay2" /etc/systemd/system/docker.service.d/cloudron.conf; then
echo "==> installer: restarting docker with overlay2 backend"
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
systemctl daemon-reload
systemctl restart docker
# trigger a re-configure after the box starts up, so that all images are repulled with the new graphdriver
sed -e "s/48.12.1/48.12.0/" -i /home/yellowtent/platformdata/INFRA_VERSION
fi
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
# there are 3 packages for docker - containerd, CLI and the daemon
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
echo "==> installer: Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
@@ -47,21 +60,21 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
while ! apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb; do
echo "==> installer: Failed to install docker. Retry"
sleep 1
done
rm /tmp/docker.deb
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v8.11.2" ]]; then
mkdir -p /usr/local/node-8.11.2
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.5
if [[ "$(node --version)" != "v10.15.1" ]]; then
mkdir -p /usr/local/node-10.15.1
$curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
rm -rf /usr/local/node-8.11.2 /usr/local/node-8.9.3
fi
# this is here (and not in updater.js) because rebuild requires the above node
@@ -86,8 +99,14 @@ images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); co
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
docker pull "${image}" # this pulls the image using the sha256
docker pull "${image%@sha256:*}" # this will tag the image for readability
if ! docker pull "${image}"; then # this pulls the image using the sha256
echo "==> installer: Could not pull ${image}"
exit 5
fi
if ! docker pull "${image%@sha256:*}"; then # this will tag the image for readability
echo "==> installer: Could not pull ${image%@sha256:*}"
exit 6
fi
done
echo "==> installer: update cloudron-syslog"

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 get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
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
@@ -24,22 +26,6 @@ systemctl enable apparmor
systemctl restart apparmor
usermod ${USER} -a -G docker
# preserve the existing storage driver (user might be using overlay2)
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
[[ -n "${storage_driver}" ]] || storage_driver="overlay2" # if the above command fails
temp_file=$(mktemp)
# create systemd drop-in. some apps do not work with aufs
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=${storage_driver}" > "${temp_file}"
systemctl enable docker
# restart docker if options changed
if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /etc/systemd/system/docker.service.d/cloudron.conf "${temp_file}" >/dev/null; then
mkdir -p /etc/systemd/system/docker.service.d
mv "${temp_file}" /etc/systemd/system/docker.service.d/cloudron.conf
systemctl daemon-reload
systemctl restart docker
fi
docker network create --subnet=172.18.0.0/16 cloudron || true
mkdir -p "${BOX_DATA_DIR}"
@@ -58,7 +44,10 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
mkdir -p "${PLATFORM_DATA_DIR}/backup"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
"${PLATFORM_DATA_DIR}/logs/updater" \
"${PLATFORM_DATA_DIR}/logs/tasks" \
"${PLATFORM_DATA_DIR}/logs/crash"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
@@ -88,28 +77,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}"
# migration for cloudron.conf file. Can be removed after 3.3
if [[ ! -d /etc/cloudron ]]; then
echo "==> Migrating existing cloudron.conf to new location"
mkdir -p /etc/cloudron
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
fi
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
@@ -142,8 +122,9 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate" # remove pre 3.6 config files
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/"
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
@@ -186,11 +167,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)"
@@ -201,8 +178,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}"

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"

View File

@@ -1,4 +1,4 @@
# logrotate config for app logs
# logrotate config for app and crash logs
/home/yellowtent/platformdata/logs/*/*.log {
# only keep one rotated file, we currently do not send that over the api

View File

@@ -0,0 +1,9 @@
# logrotate config for box logs
/home/yellowtent/platformdata/logs/box.log {
rotate 10
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}

View File

@@ -1,11 +1,17 @@
# 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/rmaddon.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.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
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
@@ -25,15 +31,21 @@ 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
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
Defaults!/home/yellowtent/box/src/scripts/backupupload.js env_keep="HOME BOX_ENV"
Defaults!/home/yellowtent/box/src/scripts/backupupload.js closefrom_override
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/backupupload.js
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
Defaults!/home/yellowtent/box/src/scripts/restartdocker.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.sh
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh

View File

@@ -12,13 +12,17 @@ Wants=cloudron-resize-fs.service
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
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

View File

@@ -12,8 +12,7 @@ exports = module.exports = {
SCOPE_SETTINGS: 'settings',
SCOPE_USERS_READ: 'users:read',
SCOPE_USERS_MANAGE: 'users:manage',
SCOPE_APPSTORE: 'appstore',
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
VALID_SCOPES: [ 'apps', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
SCOPE_ANY: '*',
@@ -121,7 +120,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

File diff suppressed because it is too large Load Diff

View File

@@ -90,6 +90,14 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
# gzip responses that are > 50k and not images
gzip on;
gzip_min_length 50k;
gzip_types text/css text/javascript text/xml text/plain application/javascript application/x-javascript application/json;
# enable for proxied requests as well
gzip_proxied any;
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
@@ -162,7 +170,7 @@ server {
# graphite paths (uncomment block below and visit /graphite/index.html)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8000;
# proxy_pass http://127.0.0.1:8417;
# client_max_body_size 1m;
# }

View File

@@ -18,6 +18,7 @@ exports = module.exports = {
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setInstallationCommand: setInstallationCommand,
@@ -42,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',
@@ -61,7 +62,6 @@ var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
@@ -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.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(',');
@@ -120,6 +121,7 @@ function postProcess(result) {
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
@@ -138,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) {
@@ -260,6 +265,7 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
assert(typeof data.mailboxName === 'string' && data.mailboxName); // non-empty string
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
@@ -276,13 +282,14 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var env = data.env || {};
const mailboxName = data.mailboxName;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName) ' +
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId, mailboxName ]
});
queries.push({
@@ -304,14 +311,6 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
});
});
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
});
}
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
@@ -376,7 +375,6 @@ function del(id, callback) {
var queries = [
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
@@ -384,7 +382,7 @@ function del(id, callback) {
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -442,12 +440,8 @@ function updateWithConstraints(id, app, constraints, callback) {
});
}
if ('location' in app) {
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('domain' in app) {
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
}
if ('alternateDomains' in app) {
@@ -483,12 +477,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"';
@@ -621,6 +616,20 @@ function getAddonConfigByAppId(appId, callback) {
});
}
function getAppIdByAddonConfigValue(addonId, name, value, callback) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name = ? AND value = ?', [ addonId, name, value ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null, results[0].appId);
});
}
function getAddonConfigByName(appId, addonId, name, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');

View File

@@ -6,8 +6,9 @@ var appdb = require('./appdb.js'),
async = require('async'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
docker = require('./docker.js'),
eventlog = require('./eventlog.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util');
@@ -15,11 +16,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_EVENT_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 +35,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);
@@ -73,11 +78,10 @@ function checkAppHealth(app, callback) {
return callback(null);
}
var container = docker.getContainer(app.containerId),
manifest = app.manifest;
const manifest = app.manifest;
container.inspect(function (err, data) {
if (err || !data || !data.State) {
docker.inspect(app.containerId, function (error, data) {
if (error || !data || !data.State) {
debugApp(app, 'Error inspecting container');
return setHealth(app, appdb.HEALTH_ERROR, callback);
}
@@ -112,19 +116,29 @@ function checkAppHealth(app, callback) {
});
}
function getContainerInfo(containerId, callback) {
docker.inspect(containerId, function (error, result) {
if (error) return callback(error);
const appId = safe.query(result, 'Config.Labels.appId', null);
if (!appId) return callback(null, null /* app */, { name: result.Name }); // addon
apps.get(appId, callback); // don't get by container id as this can be an exec container
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents(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) {
@@ -132,19 +146,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';
const event = JSON.parse(data);
const containerId = String(event.id);
debug('OOM Context: %s', context);
getContainerInfo(containerId, function (error, app, addon) {
const program = error ? containerId : (app ? app.fqdn : addon.name);
const now = Date.now();
const notifyUser = !(app && app.debugMode) && ((now - gLastOomMailTime) > OOM_EVENT_LIMIT);
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now);
// 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, { event: event, containerId: containerId, addon: addon || null, app: app || null });
gLastOomMailTime = now;
}
});
});
@@ -181,14 +198,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);

View File

@@ -8,6 +8,7 @@ exports = module.exports = {
removeRestrictedFields: removeRestrictedFields,
get: get,
getByContainerId: getByContainerId,
getByIpAddress: getByIpAddress,
getAll: getAll,
getAllByUser: getAllByUser,
@@ -38,6 +39,7 @@ exports = module.exports = {
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
getDataDir: getDataDir,
downloadFile: downloadFile,
uploadFile: uploadFile,
@@ -72,8 +74,8 @@ var appdb = require('./appdb.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
mailboxdb = require('./mailboxdb.js'),
manifestFormat = require('cloudron-manifestformat'),
once = require('once'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
@@ -154,7 +156,8 @@ function validatePortBindings(portBindings, manifest) {
config.get('ldapPort'), /* ldap server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000, /* graphite (lo) */
8000, /* ESXi monitoring */
8417, /* graphite (lo) */
];
if (!portBindings) return null;
@@ -196,14 +199,6 @@ function translatePortBindings(portBindings, manifest) {
return result;
}
function postProcess(app) {
let result = {};
for (let portName in app.portBindings) {
result[portName] = app.portBindings[portName].hostPort;
}
app.portBindings = result;
}
function addSpacesSuffix(location, user) {
if (user.admin || !config.isSpacesEnabled()) return location;
@@ -301,26 +296,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 +348,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 +356,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');
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
}
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'ownerId',
@@ -360,6 +386,18 @@ function getIconUrlSync(app) {
return fs.existsSync(iconPath) ? '/api/v1/apps/' + app.id + '/icon' : null;
}
function postProcess(app, domainObjectMap) {
let result = {};
for (let portName in app.portBindings) {
result[portName] = app.portBindings[portName].hostPort;
}
app.portBindings = result;
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
}
function hasAccessTo(app, user, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof user, 'object');
@@ -379,33 +417,51 @@ function hasAccessTo(app, user, callback) {
callback(null, false);
}
function get(appId, callback) {
assert.strictEqual(typeof appId, 'string');
function getDomainObjectMap(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
callback(null, domainObjectMap);
});
}
function get(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
appdb.get(appId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
postProcess(app);
postProcess(app, domainObjectMap);
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
callback(null, app);
});
});
}
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
function getByContainerId(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
if (!error) app.mailboxName = mailboxes[0].name;
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
callback(null, app);
});
postProcess(app, domainObjectMap);
callback(null, app);
});
});
}
@@ -414,75 +470,25 @@ function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
docker.getContainerIdByIp(ip, function (error, containerId) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.getByContainerId(containerId, function (error, app) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
postProcess(app);
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
callback(null, app);
});
});
});
});
getByContainerId(containerId, callback);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, domainObjects) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
let domainObjectMap = {};
for (let d of domainObjects) { domainObjectMap[d.domain] = d; }
getDomainObjectMap(function (error, domainObjectMap) {
if (error) return callback(error);
appdb.getAll(function (error, apps) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
apps.forEach(postProcess);
apps.forEach((app) => postProcess(app, domainObjectMap));
async.eachSeries(apps, function (app, iteratorDone) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
mailboxdb.getByOwnerId(app.id, function (error, mailboxes) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return iteratorDone(new AppsError(AppsError.INTERNAL_ERROR, error));
if (!error) app.mailboxName = mailboxes[0].name;
iteratorDone(null, app);
});
}, function (error) {
if (error) return callback(error);
callback(null, apps);
});
callback(null, apps);
});
});
}
@@ -543,11 +549,13 @@ function install(data, user, auditSource, callback) {
debugMode = data.debugMode || null,
robotsTxt = data.robotsTxt || null,
enableBackup = 'enableBackup' in data ? data.enableBackup : true,
enableAutomaticUpdate = 'enableAutomaticUpdate' in data ? data.enableAutomaticUpdate : true,
backupId = data.backupId || null,
backupFormat = data.backupFormat || 'tgz',
ownerId = data.ownerId,
alternateDomains = data.alternateDomains || [],
env = data.env || {};
env = data.env || {},
mailboxName = data.mailboxName || '';
assert(data.appStoreId || data.manifest); // atleast one of them is required
@@ -588,6 +596,13 @@ function install(data, user, auditSource, callback) {
error = validateEnv(env);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
var appId = uuid.v4();
if (icon) {
@@ -621,9 +636,10 @@ function install(data, user, auditSource, callback) {
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode,
mailboxName: mailboxNameForLocation(location, manifest),
mailboxName: mailboxName,
restoreConfig: backupId ? { backupId: backupId, backupFormat: backupFormat } : null,
enableBackup: enableBackup,
enableAutomaticUpdate: enableAutomaticUpdate,
robotsTxt: robotsTxt,
alternateDomains: alternateDomains,
env: env
@@ -682,12 +698,14 @@ function configure(appId, data, user, auditSource, callback) {
get(appId, function (error, app) {
if (error) return callback(error);
let domain, location, portBindings, values = { }, mailboxName;
if ('location' in data) location = values.location = data.location.toLowerCase();
else location = app.location;
if ('domain' in data) domain = values.domain = data.domain.toLowerCase();
else domain = app.domain;
let domain, location, portBindings, values = { };
if ('location' in data && 'domain' in data) {
location = values.location = data.location.toLowerCase();
domain = values.domain = data.domain.toLowerCase();
} else {
location = app.location;
domain = app.domain;
}
if ('accessRestriction' in data) {
values.accessRestriction = data.accessRestriction;
@@ -729,15 +747,15 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('mailboxName' in data) {
if (data.mailboxName === '') { // special case to reset back to .app
mailboxName = mailboxNameForLocation(location, app.manifest);
} else {
if (data.mailboxName) {
error = mail.validateName(data.mailboxName);
if (error) return callback(error);
mailboxName = data.mailboxName;
values.mailboxName = data.mailboxName;
} else {
values.mailboxName = mailboxNameForLocation(location, app.manifest);
}
} else { // keep existing name or follow the new location
mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
values.mailboxName = app.mailboxName.endsWith('.app') ? mailboxNameForLocation(location, app.manifest) : app.mailboxName;
}
if ('alternateDomains' in data) {
@@ -752,6 +770,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));
@@ -773,34 +797,26 @@ function configure(appId, data, user, auditSource, callback) {
}
if ('enableBackup' in data) values.enableBackup = data.enableBackup;
if ('enableAutomaticUpdate' in data) values.enableAutomaticUpdate = data.enableAutomaticUpdate;
values.oldConfig = getAppConfig(app);
debug('Will configure app with id:%s values:%j', appId, values);
// make the mailbox name follow the apps new location, if the user did not set it explicitly
mailboxdb.updateName(app.mailboxName /* old */, values.oldConfig.domain, mailboxName, domain, function (error) {
if (mailboxName.endsWith('.app')) error = null; // ignore internal mailbox conflict errors since we want to show location conflict errors in the UI
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'This mailbox is already taken'));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_CONFIGURE, values, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE));
taskmanager.restartAppTask(appId);
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
// fetch fresh app object for eventlog
get(appId, function (error, result) {
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId: appId, app: result });
callback(null);
});
callback(null);
});
});
});
@@ -815,34 +831,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) {
@@ -851,6 +855,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'));
@@ -882,16 +905,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 ];
@@ -966,7 +992,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);
});
@@ -987,7 +1013,8 @@ function clone(appId, data, user, auditSource, callback) {
domain = data.domain.toLowerCase(),
portBindings = data.portBindings || null,
backupId = data.backupId,
ownerId = data.ownerId;
ownerId = data.ownerId,
mailboxName = data.mailboxName || '';
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof location, 'string');
@@ -1005,13 +1032,22 @@ function clone(appId, data, user, auditSource, callback) {
if (!backupInfo.manifest) callback(new AppsError(AppsError.EXTERNAL_ERROR, 'Could not get restore config'));
const manifest = backupInfo.manifest;
// re-validate because this new box version may not accept old configs
error = checkManifestConstraints(backupInfo.manifest);
error = checkManifestConstraints(manifest);
if (error) return callback(error);
error = validatePortBindings(portBindings, backupInfo.manifest);
error = validatePortBindings(portBindings, manifest);
if (error) return callback(error);
if (mailboxName) {
error = mail.validateName(mailboxName);
if (error) return callback(error);
} else {
mailboxName = mailboxNameForLocation(location, manifest);
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.EXTERNAL_ERROR, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -1020,7 +1056,7 @@ function clone(appId, data, user, auditSource, callback) {
error = domains.validateHostname(location, domainObject);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Bad location: ' + error.message));
var newAppId = uuid.v4(), manifest = backupInfo.manifest;
var newAppId = uuid.v4();
var data = {
installationState: appdb.ISTATE_PENDING_CLONE,
@@ -1029,7 +1065,7 @@ function clone(appId, data, user, auditSource, callback) {
xFrameOptions: app.xFrameOptions,
restoreConfig: { backupId: backupId, backupFormat: backupInfo.format },
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app',
mailboxName: mailboxName,
enableBackup: app.enableBackup,
robotsTxt: app.robotsTxt,
env: app.env
@@ -1220,6 +1256,7 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
assert.strictEqual(typeof callback, 'function');
function canAutoupdateApp(app, newManifest) {
if (!app.enableAutomaticUpdate) return new Error('Automatic update disabled');
if ((semver.major(app.manifest.version) !== 0) && (semver.major(app.manifest.version) !== semver.major(newManifest.version))) return new Error('Major version change'); // major changes are blocking
const newTcpPorts = newManifest.tcpPorts || { };
@@ -1347,6 +1384,7 @@ function downloadFile(appId, filePath, callback) {
assert.strictEqual(typeof filePath, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`downloadFile: ${filePath}`); // no need to escape filePath because we don't rely on bash
exec(appId, { cmd: [ 'stat', '--printf=%F-%s', filePath ], tty: true }, function (error, stream) {
if (error) return callback(error);
@@ -1412,15 +1450,26 @@ function uploadFile(appId, sourceFilePath, destFilePath, callback) {
assert.strictEqual(typeof destFilePath, 'string');
assert.strictEqual(typeof callback, 'function');
exec(appId, { cmd: [ 'bash', '-c', 'cat - > ' + destFilePath ], tty: false }, function (error, stream) {
if (error) return callback(error);
const done = once(function (error) {
safe.fs.unlinkSync(sourceFilePath); // remove file in /tmp
callback(error);
});
// the built-in bash printf understands "%q" but not /usr/bin/printf.
// ' gets replaced with '\'' . the first closes the quote and last one starts a new one
const escapedDestFilePath = safe.child_process.execSync(`printf %q '${destFilePath.replace(/'/g, '\'\\\'\'')}'`, { shell: '/bin/bash', encoding: 'utf8' });
debug(`uploadFile: ${sourceFilePath} -> ${escapedDestFilePath}`);
exec(appId, { cmd: [ 'bash', '-c', `cat - > ${escapedDestFilePath}` ], tty: false }, function (error, stream) {
if (error) return done(error);
var readFile = fs.createReadStream(sourceFilePath);
readFile.on('error', callback);
readFile.on('error', done);
stream.on('error', done);
stream.on('finish', done);
readFile.pipe(stream);
callback(null);
});
}

View File

@@ -14,6 +14,9 @@ exports = module.exports = {
getAccount: getAccount,
registerCloudron: registerCloudron,
getCloudron: getCloudron,
sendFeedback: sendFeedback,
AppstoreError: AppstoreError
@@ -251,7 +254,14 @@ function getBoxUpdate(callback) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
callback(null, updateInfo);
});
});
@@ -306,6 +316,47 @@ function getAccount(callback) {
});
}
function registerCloudron(adminDomain, userId, token, callback) {
assert.strictEqual(typeof adminDomain, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof token, 'string');
assert.strictEqual(typeof callback, 'function');
const url = `${config.apiServerOrigin()}/api/v1/users/${userId}/cloudrons`;
superagent.post(url).send({ domain: adminDomain }).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'invalid appstore token'));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'unable to register cloudron'));
const cloudronId = safe.query(result.body, 'cloudron.id');
if (!cloudronId) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
debug(`setAppstoreConfig: Cloudron registered with id ${cloudronId}`);
callback(null, cloudronId);
});
}
function getCloudron(appstoreConfig, callback) {
assert.strictEqual(typeof appstoreConfig, 'object');
assert.strictEqual(typeof callback, 'function');
const { userId, cloudronId, token } = appstoreConfig;
const url = config.apiServerOrigin() + '/api/v1/users/' + userId + '/cloudrons/' + cloudronId;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'invalid appstore token'));
if (result.statusCode === 403) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'wrong user'));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND, error.message));
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'unknown error'));
callback();
});
}
function sendFeedback(info, callback) {
assert.strictEqual(typeof info, 'object');
assert.strictEqual(typeof info.email, 'string');

View File

@@ -52,9 +52,12 @@ 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');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -133,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);
@@ -185,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));
@@ -210,7 +201,7 @@ function addCollectdProfile(app, callback) {
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId });
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(error);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], callback);
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, callback);
});
}
@@ -220,7 +211,7 @@ function removeCollectdProfile(app, callback) {
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], callback);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, callback);
});
}
@@ -239,7 +230,7 @@ function addLogrotateConfig(app, callback) {
var tmpFilePath = path.join(os.tmpdir(), app.id + '.logrotate');
fs.writeFile(tmpFilePath, logrotateConf, function (error) {
if (error) return callback(error);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], callback);
shell.sudo('addLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {}, callback);
});
});
}
@@ -248,7 +239,7 @@ function removeLogrotateConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], callback);
shell.sudo('removeLogrotateConfig', [ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {}, callback);
}
function verifyManifest(manifest, callback) {
@@ -467,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.
@@ -495,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) {
@@ -525,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) {
@@ -536,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),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
], next);
}
},
@@ -581,7 +585,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
// done!
function (callback) {
@@ -603,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' }),
@@ -611,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();
@@ -619,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' }),
@@ -634,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),
@@ -694,7 +705,7 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app)
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
], function (error) {
if (error) error.backupError = true;
next(error);
@@ -712,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();
@@ -798,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' }),

File diff suppressed because it is too large Load Diff

View File

@@ -4,29 +4,18 @@ exports = module.exports = {
verifySetupToken: verifySetupToken,
setupDone: setupDone,
changePlan: changePlan,
upgrade: upgrade,
sendHeartbeat: sendHeartbeat,
getBoxAndUserDetails: getBoxAndUserDetails,
setPtrRecord: setPtrRecord,
CaasError: CaasError
};
var assert = require('assert'),
backups = require('./backups.js'),
config = require('./config.js'),
debug = require('debug')('box:caas'),
locker = require('./locker.js'),
path = require('path'),
progress = require('./progress.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
util = require('util');
function CaasError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -53,20 +42,6 @@ CaasError.INVALID_TOKEN = 'Invalid Token';
CaasError.INTERNAL_ERROR = 'Internal Error';
CaasError.EXTERNAL_ERROR = 'External Error';
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function retire(reason, info, callback) {
assert(reason === 'migrate' || reason === 'upgrade');
info = info || { };
callback = callback || NOOP_CALLBACK;
var data = {
apiServerOrigin: config.apiServerOrigin(),
adminFqdn: config.adminFqdn()
};
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
}
function getCaasConfig(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -117,96 +92,6 @@ function setupDone(setupToken, callback) {
});
});
}
function doMigrate(options, caasConfig, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof caasConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var error = locker.lock(locker.OP_MIGRATE);
if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));
function unlock(error) {
debug('Failed to migrate', error);
locker.unlock(locker.OP_MIGRATE);
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
}
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
// initiate the migration in the background
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
if (error) return unlock(error);
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
superagent
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
.query({ token: caasConfig.token })
.send(options)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return unlock(error); // network error
if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
progress.set(progress.MIGRATE, 10, 'Migrating');
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
});
});
callback(null);
}
function changePlan(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
getCaasConfig(function (error, result) {
if (error) return callback(error);
doMigrate(options, result, callback);
});
}
// this function expects a lock
function upgrade(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
function upgradeError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
if (error) return upgradeError(error);
getCaasConfig(function (error, result) {
if (error) return upgradeError(error);
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
.query({ token: result.token })
.send({ version: boxUpdateInfo.version })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
progress.set(progress.UPDATE, 10, 'Updating base system');
// no need to unlock since this is the last thing we ever do on this box
callback();
retire('upgrade');
});
});
});
}
function sendHeartbeat() {
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
@@ -223,27 +108,6 @@ function sendHeartbeat() {
});
}
function getBoxAndUserDetails(callback) {
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(null, {});
getCaasConfig(function (error, caasConfig) {
if (error) return callback(error);
superagent
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
.query({ token: caasConfig.token })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, result.body);
});
});
}
function setPtrRecord(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');

View File

@@ -1,481 +0,0 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme1'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
parseLinks = require('parse-links'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme'
};
function Acme1Error(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(Acme1Error, Error);
Acme1Error.INTERNAL_ERROR = 'Internal Error';
Acme1Error.EXTERNAL_ERROR = 'External Error';
Acme1Error.ALREADY_EXISTS = 'Already Exists';
Acme1Error.NOT_COMPLETED = 'Not Completed';
Acme1Error.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme1(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.email = options.email;
}
Acme1.prototype.getNonce = function (callback) {
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = util.isBuffer(str) ? str : new Buffer(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
var that = this;
var header = {
alg: 'RS256',
jwk: {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
}
};
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
header: header,
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme1.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug('updateContact: %s %s', registrationUri, this.email);
// https://github.com/ietf-wg-acme/acme/issues/30
var payload = {
resource: 'reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
var that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
debug('updateContact: contact of user updated to %s', that.email);
callback();
});
};
Acme1.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-reg',
contact: [ 'mailto:' + this.email ],
agreement: LE_AGREEMENT
};
debug('registerUser: %s', this.email);
var that = this;
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerUser: registered user %s', that.email);
callback(null);
});
};
Acme1.prototype.registerDomain = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
resource: 'new-authz',
identifier: {
type: 'dns',
value: domain
}
};
debug('registerDomain: %s', domain);
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerDomain: registered %s', domain);
callback(null, result.body);
});
};
Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
var token = challenge.token;
assert(util.isBuffer(this.accountKeyPem));
var jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
var shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
var thumbprint = urlBase64Encode(shasum.digest('base64'));
var keyAuthorization = token + '.' + thumbprint;
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
if (error) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, error));
callback();
});
};
Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.uri);
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme1.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.uri);
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 202) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme1Error(Acme1Error.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var payload = {
resource: 'new-cert',
csr: b64(csrDer)
};
debug('signCertificate: sending new-cert request');
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
var certUrl = result.headers.location;
if (!certUrl) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
return callback(null, result.headers.location);
});
};
Acme1.prototype.createKeyAndCsr = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var csrFile = path.join(outdir, domain + '.csr');
var privateKeyFile = path.join(outdir, domain + '.key');
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
if (!key) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
// TODO: download the chain in a loop following 'up' header
Acme1.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
debug('downloadChain: linkHeader %s', linkHeader);
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
debug('downloadChain: downloading from %s', intermediateCertUrl);
superagent.get(intermediateCertUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
var that = this;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var certificateDer = result.text;
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
debug('downloadCertificate: cert der file for %s saved', domain);
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
if (!certificatePem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
callback();
});
});
};
Acme1.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
that.registerDomain(domain, function (error, result) {
if (error) return callback(error);
debug('acmeFlow: challenges: %j', result);
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'no http challenges'));
var challenge = httpChallenges[0];
async.waterfall([
that.prepareHttpChallenge.bind(that, challenge),
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, domain),
that.signCertificate.bind(that, domain),
that.downloadCertificate.bind(that, domain)
], callback);
});
});
};
Acme1.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: start acme flow for %s from %s', hostname, this.caOrigin);
this.acmeFlow(hostname, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.key'));
});
};
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme1(options || { });
acme.getCertificate(hostname, domain, callback);
}

View File

@@ -5,7 +5,6 @@ var assert = require('assert'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
execSync = require('safetydance').child_process.execSync,
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
@@ -88,7 +87,7 @@ function b64(str) {
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
@@ -227,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) {
@@ -291,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) {
@@ -351,14 +350,14 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = execSync('openssl genrsa 4096');
var key = safe.child_process.execSync('openssl genrsa 4096');
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping

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');
}

View File

@@ -8,39 +8,48 @@ exports = module.exports = {
getConfig: getConfig,
getDisks: getDisks,
getLogs: getLogs,
getStatus: getStatus,
reboot: reboot,
isRebootRequired: isRebootRequired,
onActivated: onActivated,
prepareDashboardDomain: prepareDashboardDomain,
setDashboardDomain: setDashboardDomain,
setDashboardAndMailDomain: setDashboardAndMailDomain,
renewCerts: renewCerts,
checkDiskSpace: checkDiskSpace,
runSystemChecks: runSystemChecks,
configureWebadmin: configureWebadmin,
getWebadminStatus: getWebadminStatus
// exposed for testing
_checkDiskSpace: checkDiskSpace
};
var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
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'),
mailer = require('./mailer.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
notifications = require('./notifications.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
progress = require('./progress.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
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');
@@ -48,16 +57,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');
@@ -82,8 +81,6 @@ CloudronError.INTERNAL_ERROR = 'Internal Error';
CloudronError.EXTERNAL_ERROR = 'External Error';
CloudronError.BAD_STATE = 'Bad state';
CloudronError.ALREADY_UPTODATE = 'No Update Available';
CloudronError.NOT_FOUND = 'Not found';
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -120,7 +117,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) {
@@ -171,7 +168,6 @@ function getConfig(callback) {
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
progress: progress.getAll(),
isDemo: config.isDemo(),
edition: config.edition(),
memory: os.totalmem(),
@@ -182,11 +178,42 @@ function getConfig(callback) {
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
}
function isRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
callback(null, fs.existsSync('/var/run/reboot-required'));
}
// called from cron.js
function runSystemChecks() {
async.parallel([
checkBackupConfiguration,
checkDiskSpace,
checkMailStatus,
checkRebootRequired
], function (error) {
debug('runSystemChecks: done', error);
});
}
function checkBackupConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Checking backup configuration');
backups.checkConfiguration(function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
});
}
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
assert.strictEqual(typeof callback, 'function');
debug('Checking disk space');
@@ -216,77 +243,76 @@ function checkDiskSpace(callback) {
debug('Disk space checked. ok: %s', !oos);
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
callback();
notifications.alert(notifications.ALERT_DISK_SPACE, 'Server is running out of disk space', oos ? 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');
mail.checkConfiguration(function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message, callback);
});
}
function checkRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking if reboot required');
isRebootRequired(function (error, rebootRequired) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish security updates, a [reboot](/#/system) is necessary.' : '', callback);
});
}
function getLogs(unit, options, callback) {
assert.strictEqual(typeof unit, 'string');
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);
var cp, transformStream;
if (unit === 'box') {
let args = [ '--no-pager', `--lines=${lines}` ];
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
if (follow) args.push('--follow');
args.push('--unit=box');
args.push('--unit=cloudron-updater');
cp = spawn('/bin/journalctl', args);
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
// need to handle box.log without subdir
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
else if (unit.startsWith('crash-')) args.push(path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log'));
else return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such unit'));
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var cp = spawn('/usr/bin/tail', args);
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
}) + '\n';
});
} else { // mail, mongodb, mysql, postgresql, backup
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
cp = spawn('/usr/bin/tail', args);
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
}
return JSON.stringify({
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
@@ -295,73 +321,73 @@ function getLogs(unit, options, callback) {
return callback(null, transformStream);
}
function configureWebadmin(callback) {
function prepareDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('configureWebadmin: adminDomain:%s status:%j', config.adminDomain(), gWebadminStatus);
debug(`prepareDashboardDomain: ${domain}`);
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();
});
});
});
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 getWebadminStatus() {
return gWebadminStatus;
}
function getStatus(callback) {
// call this only pre activation since it won't start mail server
function setDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
debug(`setDashboardDomain: ${domain}`);
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));
settings.getCloudronName(function (error, cloudronName) {
reverseProxy.writeAdminConfig(domain, function (error) {
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
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
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 });
callback(null);
});
});
});
}
// call this only post activation because it will restart mail server
function setDashboardAndMailDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`setDashboardAndMailDomain: ${domain}`);
setDashboardDomain(domain, auditSource, function (error) {
if (error) return callback(error);
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
callback(null);
});
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let task = tasks.startTask(tasks.TASK_RENEW_CERTS, [ options, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
}

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

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)
};

61
src/crashnotifier.js Normal file
View File

@@ -0,0 +1,61 @@
'use strict';
exports = module.exports = {
sendFailureLogs: sendFailureLogs
};
var assert = require('assert'),
eventlog = require('./eventlog.js'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
util = require('util');
const COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
const CRASH_LOG_TIMESTAMP_OFFSET = 1000 * 60 * 60; // 60 min
const CRASH_LOG_TIMESTAMP_FILE = '/tmp/crashlog.timestamp';
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
function collectLogs(unitName, callback) {
assert.strictEqual(typeof unitName, 'string');
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
if (!logs) return callback(safe.error);
callback(null, logs);
}
function sendFailureLogs(unitName, callback) {
assert.strictEqual(typeof unitName, 'string');
assert.strictEqual(typeof callback, 'function');
// check if we already sent a mail in the last CRASH_LOG_TIME_OFFSET window
const timestamp = safe.fs.readFileSync(CRASH_LOG_TIMESTAMP_FILE, 'utf8');
if (timestamp && (parseInt(timestamp) + CRASH_LOG_TIMESTAMP_OFFSET) > Date.now()) {
console.log('Crash log already sent within window');
return callback();
}
collectLogs(unitName, function (error, logs) {
if (error) {
console.error('Failed to collect logs.', error);
logs = util.format('Failed to collect logs.', error);
}
const crashId = `${new Date().toISOString()}`;
console.log(`Creating crash log for ${unitName} with id ${crashId}`);
if (!safe.fs.writeFileSync(path.join(paths.CRASH_LOG_DIR, `${crashId}.log`), logs)) console.log(`Failed to stash logs to ${crashId}.log:`, safe.error);
eventlog.add(eventlog.ACTION_PROCESS_CRASH, AUDIT_SOURCE, { processName: unitName, crashId: crashId }, function (error) {
if (error) console.log(`Error sending crashlog. Logs stashed at ${crashId}.log`);
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
callback();
});
});
}

View File

@@ -4,7 +4,9 @@ exports = module.exports = {
startPostActivationJobs: startPostActivationJobs,
startPreActivationJobs: startPreActivationJobs,
stopJobs: stopJobs
stopJobs: stopJobs,
handleSettingsChanged: handleSettingsChanged
};
var appHealthMonitor = require('./apphealthmonitor.js'),
@@ -22,7 +24,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 +37,7 @@ var gJobs = {
backup: null,
boxUpdateChecker: null,
caasHeartbeat: null,
checkDiskSpace: null,
systemChecks: null,
certificateRenew: null,
cleanupBackups: null,
cleanupEventlog: null,
@@ -48,7 +49,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
@@ -86,11 +87,6 @@ function startPostActivationJobs(callback) {
start: true
});
settings.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.on(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.on(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
@@ -103,6 +99,19 @@ function startPostActivationJobs(callback) {
});
}
function handleSettingsChanged(key, value) {
assert.strictEqual(typeof key, 'string');
// value is a variant
switch (key) {
case settings.TIME_ZONE_KEY: recreateJobs(value); break;
case settings.APP_AUTOUPDATE_PATTERN_KEY: appAutoupdatePatternChanged(value); break;
case settings.BOX_AUTOUPDATE_PATTERN_KEY: boxAutoupdatePatternChanged(value); break;
case settings.DYNAMIC_DNS_KEY: dynamicDnsChanged(value); break;
default: break;
}
}
function recreateJobs(tz) {
assert.strictEqual(typeof tz, 'string');
@@ -116,11 +125,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 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: cloudron.runSystemChecks,
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
@@ -130,7 +140,7 @@ function recreateJobs(tz) {
if (gJobs.boxUpdateCheckerJob) gJobs.boxUpdateCheckerJob.stop();
gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: updateChecker.checkBoxUpdates,
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -138,7 +148,7 @@ function recreateJobs(tz) {
if (gJobs.appUpdateChecker) gJobs.appUpdateChecker.stop();
gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: updateChecker.checkAppUpdates,
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -154,7 +164,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
});
@@ -186,7 +196,7 @@ function recreateJobs(tz) {
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: reverseProxy.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: cloudron.renewCerts.bind(null, {}, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -194,7 +204,7 @@ function recreateJobs(tz) {
if (gJobs.digestEmail) gJobs.digestEmail.stop();
gJobs.digestEmail = new CronJob({
cronTime: '00 00 00 * * 3', // every wednesday
onTick: digest.maybeSend,
onTick: digest.send,
start: true,
timeZone: tz
});
@@ -202,7 +212,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
});
@@ -282,11 +292,6 @@ function dynamicDnsChanged(enabled) {
function stopJobs(callback) {
assert.strictEqual(typeof callback, 'function');
settings.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.removeListener(settings.APP_AUTOUPDATE_PATTERN_KEY, appAutoupdatePatternChanged);
settings.removeListener(settings.BOX_AUTOUPDATE_PATTERN_KEY, boxAutoupdatePatternChanged);
settings.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
for (var job in gJobs) {
if (!gJobs[job]) continue;
gJobs[job].stop();

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
src/datalayout.js Normal file
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;

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'),
@@ -10,10 +9,10 @@ var appstore = require('./appstore.js'),
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
exports = module.exports = {
maybeSend: maybeSend
send: send
};
function maybeSend(callback) {
function send(callback) {
callback = callback || NOOP_CALLBACK;
settings.getEmailDigest(function (error, enabled) {
@@ -54,7 +53,7 @@ function maybeSend(callback) {
};
// always send digest for backup failure notification
debug('maybeSend: sending digest email', info);
debug('send: sending digest email', info);
mailer.sendDigest(info);
callback();

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');

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');

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');

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');

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');

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');

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

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
src/dns/namecheap.js Normal file
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);
}

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');

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, { });

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');

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);
});

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}`));

View File

@@ -1,9 +1,13 @@
'use strict';
exports = module.exports = {
DockerError: DockerError,
connection: connectionInstance(),
setRegistryConfig: setRegistryConfig,
ping: ping,
downloadImage: downloadImage,
createContainer: createContainer,
startContainer: startContainer,
@@ -18,24 +22,27 @@ exports = module.exports = {
getContainerIdByIp: getContainerIdByIp,
inspect: inspect,
inspectByName: inspect,
getEvents: getEvents,
memoryUsage: memoryUsage,
execContainer: execContainer,
createVolume: createVolume,
removeVolume: removeVolume,
clearVolume: clearVolume
};
function connectionInstance() {
// timeout is optional
function connectionInstance(timeout) {
var Docker = require('dockerode');
var docker;
if (process.env.BOX_ENV === 'test') {
// test code runs a docker proxy on this port
docker = new Docker({ host: 'http://localhost', port: 5687 });
docker = new Docker({ host: 'http://localhost', port: 5687, timeout: timeout });
// proxy code uses this to route to the real docker
docker.options = { socketPath: '/var/run/docker.sock' };
} else {
docker = new Docker({ socketPath: '/var/run/docker.sock' });
docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
}
return docker;
@@ -48,17 +55,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');
@@ -80,6 +85,7 @@ function DockerError(reason, errorOrMessage) {
}
util.inherits(DockerError, Error);
DockerError.INTERNAL_ERROR = 'Internal Error';
DockerError.NOT_FOUND = 'Not found';
DockerError.BAD_FIELD = 'Bad field';
function debugApp(app, args) {
@@ -104,12 +110,26 @@ function setRegistryConfig(auth, callback) {
});
}
function ping(callback) {
assert.strictEqual(typeof callback, 'function');
// do not let the request linger
var docker = connectionInstance(1000);
docker.ping(function (error, result) {
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
if (result !== 'OK') return callback(new DockerError(DockerError.INTERNAL_ERROR, 'Unable to ping the docker daemon'));
callback(null);
});
}
function pullImage(manifest, callback) {
var docker = exports.connection;
// Use docker CLI here to support downloading of private repos. for dockerode, we have to use
// https://github.com/apocas/dockerode#pull-from-private-repos
shell.exec('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], { }, function (error) {
shell.spawn('pullImage', '/usr/bin/docker', [ 'pull', manifest.dockerImage ], {}, function (error) {
if (error) {
debug(`pullImage: Error pulling image ${manifest.dockerImage} of ${manifest.id}: ${error.message}`);
return callback(new Error('Failed to pull image'));
@@ -161,8 +181,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
// TODO: these should all have the CLOUDRON_ prefix
var stdEnv = [
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
'WEBADMIN_ORIGIN=' + config.adminOrigin(),
'API_ORIGIN=' + config.adminOrigin(),
'APP_ORIGIN=https://' + domain,
@@ -204,9 +226,6 @@ function createSubcontainer(app, name, cmd, options, callback) {
// if required, we can make this a manifest and runtime argument later
if (!isAppContainer) memoryLimit *= 2;
// apparmor is disabled on few servers
var enableSecurityOpt = config.CLOUDRON && safe(function () { return child_process.spawnSync('aa-enabled').status === 0; }, false);
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(new Error('Error getting addon environment : ' + error));
@@ -257,7 +276,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
NetworkMode: 'cloudron',
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: enableSecurityOpt ? [ 'apparmor=docker-cloudron-app' ] : null // profile available only on cloudron
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
}
};
@@ -353,15 +372,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) {
@@ -445,7 +468,36 @@ function inspect(containerId, callback) {
var container = exports.connection.getContainer(containerId);
container.inspect(function (error, result) {
if (error) return callback(error);
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function getEvents(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
docker.getEvents(options, function (error, stream) {
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
callback(null, stream);
});
}
function memoryUsage(containerId, callback) {
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof callback, 'function');
var container = exports.connection.getContainer(containerId);
container.stats({ stream: false }, function (error, result) {
if (error && error.statusCode === 404) return callback(new DockerError(DockerError.NOT_FOUND));
if (error) return callback(new DockerError(DockerError.INTERNAL_ERROR, error));
callback(null, result);
});
}
@@ -481,16 +533,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',
@@ -505,7 +555,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) {
@@ -516,30 +567,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();
});
}

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();
});
}

View File

@@ -8,8 +8,7 @@ exports = module.exports = {
getAll: getAll,
update: update,
del: del,
_clear: clear
clear: clear
};
var assert = require('assert'),
@@ -17,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);
@@ -25,6 +24,8 @@ function postProcess(data) {
delete data.configJson;
delete data.tlsConfigJson;
data.locked = !!data.locked; // make it bool
return data;
}

View File

@@ -6,12 +6,10 @@ module.exports = exports = {
getAll: getAll,
update: update,
del: del,
isLocked: isLocked,
renewCerts: renewCerts,
clear: clear,
fqdn: fqdn,
setAdmin: setAdmin,
getName: getName,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
@@ -28,33 +26,29 @@ 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'),
caas = require('./caas.js'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
ReverseProxyError = reverseProxy.ReverseProxyError,
safe = require('safetydance'),
shell = require('./shell.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function DomainsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -98,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');
@@ -110,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));
@@ -232,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');
@@ -267,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));
@@ -290,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);
});
}
@@ -326,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();
});
});
});
@@ -369,40 +357,47 @@ function del(domain, auditSource, 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 clear(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.clear(function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
return callback(null);
});
}
// returns the 'name' that needs to be inserted into zone
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);
@@ -411,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);
@@ -431,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);
@@ -451,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');
@@ -462,42 +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);
});
}
function setAdmin(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('setAdmin domain:%s', domain);
get(domain, function (error, result) {
if (error) return callback(error);
var setPtrRecord = config.provider() === 'caas' ? caas.setPtrRecord : function (d, next) { next(); };
setPtrRecord(domain, function (error) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, 'Error setting PTR record:' + error.message));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
callback();
shell.sudo('restart', [ RESTART_CMD ], NOOP_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
@@ -518,15 +485,30 @@ function makeWildcard(hostname) {
return parts.join('.');
}
function renewCerts(domain, auditSource, callback) {
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');
// trigger renewal in the background
reverseProxy.renewCerts({ domain: domain }, auditSource, function (error) {
debug('renewCerts', error);
});
get(domain, function (error, domainObject) {
if (error) return callback(error);
callback();
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);
});
});
});
}

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();
});

View File

@@ -18,13 +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',
@@ -46,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');
@@ -87,12 +100,16 @@ 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));
callback(null, { id: id });
notifications.onEvent(id, action, source, data, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
callback(null, { id: id });
});
});
}

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);
});
}

View File

@@ -31,12 +31,10 @@ function startGraphite(existingInfra, callback) {
--dns-search=. \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \
-p 127.0.0.1:8417:8000 \
-v "${dataDir}/graphite:/var/lib/graphite" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startGraphite', cmd);
callback();
shell.exec('startGraphite', cmd, callback);
}

View File

@@ -34,7 +34,7 @@ function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE id = ? ORDER BY name', [ groupId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -47,9 +47,9 @@ function getWithMembers(groupId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' WHERE groups.id = ? ' +
' GROUP BY groups.id', [ groupId ], function (error, results) {
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' WHERE userGroups.id = ? ' +
' GROUP BY userGroups.id', [ groupId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -63,7 +63,7 @@ function getWithMembers(groupId, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM groups', function (error, results) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
@@ -72,8 +72,8 @@ function getAll(callback) {
function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
' GROUP BY groups.id', function (error, results) {
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -88,7 +88,7 @@ function add(id, name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO groups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -112,8 +112,8 @@ function update(id, data, callback) {
}
args.push(id);
database.query('UPDATE groups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('groups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('userGroups_name') !== -1) return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, 'name already exists'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -128,7 +128,7 @@ function del(id, callback) {
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -141,7 +141,7 @@ function del(id, callback) {
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM groups', function (error, result) {
database.query('SELECT COUNT(*) AS total FROM userGroups', function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
return callback(null, result[0].total);
@@ -152,7 +152,7 @@ function clear(callback) {
database.query('DELETE FROM groupMembers', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM groups', function (error) {
database.query('DELETE FROM userGroups', function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
@@ -266,7 +266,7 @@ function getGroups(userId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
' FROM groups INNER JOIN groupMembers ON groups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);

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.0@sha256:3c0fbb2a042ac471940ac3e9f6ffa900c8a294941fb7de509b2e3309b09fbffd' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.0@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.1.0@sha256:131db42dcb90111f679ab1f0f37c552f93f797d9b803b2346c7c202daf86ac36' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
}
};

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) {
@@ -271,7 +271,6 @@ function mailboxSearch(req, res, next) {
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`,
ownerType: mailbox.ownerType,
displayname: 'Max Mustermann',
givenName: 'Max',
username: 'mmustermann',
@@ -297,9 +296,6 @@ function mailboxSearch(req, res, next) {
var results = [];
// only send user mailboxes
result = result.filter(function (m) { return m.ownerType === mailboxdb.OWNER_TYPE_USER; });
// send mailbox objects
result.forEach(function (mailbox) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${domain},domain=${domain},ou=mailboxes,dc=cloudron`);
@@ -311,8 +307,7 @@ function mailboxSearch(req, res, next) {
objectcategory: 'mailbox',
cn: `${mailbox.name}@${domain}`,
uid: `${mailbox.name}@${domain}`,
mail: `${mailbox.name}@${domain}`,
ownerType: mailbox.ownerType
mail: `${mailbox.name}@${domain}`
}
};
@@ -449,14 +444,14 @@ 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()));
@@ -464,30 +459,61 @@ function authenticateMailbox(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) 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()));
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));
mail.getDomain(parts[1], function (error, domain) {
if (error && error.reason === MailError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
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));
if (mailbox.ownerType === mailboxdb.OWNER_TYPE_APP) {
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
var name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
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));
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
if (error) return next(new ldap.OperationsError(error.message));
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
});
}
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
return res.end();
});
} else if (mailbox.ownerType === mailboxdb.OWNER_TYPE_USER) {
if (!domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
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()));
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()));
const addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
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 (addonId === 'recvmail' && !domain.enabled) return next(new ldap.NoSuchObjectError(req.dn.toString()));
let name;
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
else return next(new ldap.OperationsError('Invalid DN'));
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, name, req.credentials || '', function (error, appId) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
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()));
@@ -497,9 +523,7 @@ function authenticateMailbox(req, res, next) {
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
} else {
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
}
});
});
});
}
@@ -523,13 +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=mailboxes,dc=cloudron', authenticateMailbox);
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
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);

View File

@@ -18,7 +18,6 @@ Locker.prototype.OP_BOX_UPDATE = 'box_update';
Locker.prototype.OP_PLATFORM_START = 'platform_start';
Locker.prototype.OP_FULL_BACKUP = 'full_backup';
Locker.prototype.OP_APPTASK = 'apptask';
Locker.prototype.OP_MIGRATE = 'migrate';
Locker.prototype.lock = function (operation) {
assert.strictEqual(typeof operation, 'string');

View File

@@ -1,76 +0,0 @@
'use strict';
exports = module.exports = {
sendFailureLogs: sendFailureLogs
};
var assert = require('assert'),
mailer = require('./mailer.js'),
safe = require('safetydance'),
path = require('path'),
util = require('util');
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
function collectLogs(unitName, callback) {
assert.strictEqual(typeof unitName, 'string');
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
logs = logs + '\n\n=====================================\n\n';
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;
}
// 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) {
if (error) {
console.error('Failed to collect logs.', error);
newLogs = util.format('Failed to collect logs.', error);
}
console.log('Sending failure logs for', processName);
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);
}
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);
}
// 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);
});
});
}

View File

@@ -2,14 +2,19 @@
exports = module.exports = {
getStatus: getStatus,
checkConfiguration: checkConfiguration,
getDomains: getDomains,
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
clearDomains: clearDomains,
removePrivateFields: removePrivateFields,
setDnsRecords: setDnsRecords,
onMailFqdnChanged: onMailFqdnChanged,
validateName: validateName,
@@ -19,14 +24,16 @@ exports = module.exports = {
setMailEnabled: setMailEnabled,
startMail: restartMail,
restartMail: restartMail,
handleCertChanged: handleCertChanged,
sendTestMail: sendTestMail,
getMailboxes: getMailboxes,
listMailboxes: listMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailbox: updateMailbox,
updateMailboxOwner: updateMailboxOwner,
removeMailbox: removeMailbox,
listAliases: listAliases,
@@ -47,18 +54,19 @@ 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'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
infra = require('./infra_version.js'),
mailboxdb = require('./mailboxdb.js'),
maildb = require('./maildb.js'),
mailer = require('./mailer.js'),
net = require('net'),
nodemailer = require('nodemailer'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
@@ -109,9 +117,6 @@ function validateName(name) {
// also need to consider valid LDAP characters here (e.g '+' is reserved)
if (/[^a-zA-Z0-9.-]/.test(name)) return new MailError(MailError.BAD_FIELD, 'mailbox name can only contain alphanumerals and dot');
// app emails are sent using the .app suffix
if (name.indexOf('.app') !== -1) return new MailError(MailError.BAD_FIELD, 'mailbox name pattern is reserved for apps');
return null;
}
@@ -143,13 +148,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);
});
@@ -225,13 +230,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
};
@@ -257,13 +266,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
};
@@ -271,7 +284,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(' ');
}
@@ -312,12 +325,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
};
@@ -441,9 +457,9 @@ function getStatus(domain, callback) {
// ensure we always have a valid toplevel properties for the api
var results = {
dns: {},
rbl: {},
relay: {}
dns: {}, // { mx: { expected, value }, dmarc: { expected, value }, dkim: { expected, value }, spf: { expected, value }, ptr: { expected, value } }
rbl: {}, // { status, ip, servers: [{name,site,dns}]} optional. only for cloudron-smtp
relay: {} // { status, value } always checked
};
function recordResult(what, func) {
@@ -458,20 +474,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))
);
@@ -485,7 +506,54 @@ function getStatus(domain, callback) {
});
}
function createMailConfig(callback) {
function checkConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
let messages = {};
domains.getAll(function (error, allDomains) {
if (error) return callback(error);
async.eachSeries(allDomains, function (domainObject, iteratorCallback) {
getStatus(domainObject.domain, function (error, result) {
if (error) return iteratorCallback(error);
let message = [];
Object.keys(result.dns).forEach((type) => {
const record = result.dns[type];
if (!record.status) message.push(`${type.toUpperCase()} DNS record did not match. Expected: \`${record.expected}\`. Actual: \`${record.value}\``);
});
if (!result.relay.status) message.push(`Relay error: ${result.relay.value}`);
if (result.rbl && result.rbl.status === false) { // rbl field contents is optional
const servers = result.rbl.servers.map((bs) => `[${bs.name}](${bs.site})`); // in markdown
message.push(`This server's IP \`${result.rbl.ip}\` is blacklisted in the following servers - ${servers.join(', ')}`);
}
if (message.length) messages[domainObject.domain] = message;
iteratorCallback(null);
});
}, function (error) {
if (error) return callback(error);
// create bulleted list for each domain
let markdownMessage = '';
Object.keys(messages).forEach((domain) => {
markdownMessage += `**${domain}**\n`;
markdownMessage += messages[domain].map((m) => `* ${m}\n`).join('');
markdownMessage += '\n\n';
});
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
callback(null, markdownMessage); // empty message means all status checks succeeded
});
});
}
function createMailConfig(mailFqdn, callback) {
assert.strictEqual(typeof mailFqdn, 'string');
assert.strictEqual(typeof callback, 'function');
debug('createMailConfig: generating mail config');
@@ -494,9 +562,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
@@ -505,7 +571,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));
}
@@ -545,19 +611,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), 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
@@ -567,38 +635,49 @@ function restartMail(callback) {
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new Error('Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new Error('Could not create key file:' + safe.error.message));
shell.execSync('startMail', 'docker rm -f mail || true');
createMailConfig(function (error, allowInbound) {
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
createMailConfig(mailFqdn, function (error, allowInbound) {
if (error) return callback(error);
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
shell.execSync('startMail', cmd);
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
--net-alias mail \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--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} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
callback();
shell.exec('startMail', cmd, callback);
});
});
});
}
function restartMail(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
debug(`restartMail: restarting mail container with ${config.mailFqdn()} ${config.adminDomain()}`);
configureMail(config.mailFqdn(), config.adminDomain(), callback);
}
function restartMailIfActivated(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -613,6 +692,13 @@ function restartMailIfActivated(callback) {
});
}
function handleCertChanged(callback) {
assert.strictEqual(typeof callback, 'function');
debug('handleCertChanged: will restart if activated');
restartMailIfActivated(callback);
}
function getDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -628,7 +714,7 @@ function getDomain(domain, callback) {
function getDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.getAll(function (error, results) {
maildb.list(function (error, results) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
return callback(null, results);
@@ -636,8 +722,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 +739,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 +806,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 +830,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 +858,32 @@ function setDnsRecords(domain, callback) {
});
}
function setDnsRecords(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
upsertDnsRecords(domain, config.mailFqdn(), callback);
}
function onMailFqdnChanged(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = config.mailFqdn(),
mailDomain = config.adminDomain();
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 +894,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
@@ -805,6 +919,26 @@ function removeDomain(domain, callback) {
});
}
function clearDomains(callback) {
assert.strictEqual(typeof callback, 'function');
maildb.clear(function (error) {
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
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');
@@ -840,16 +974,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);
});
});
});
}
@@ -886,7 +1030,7 @@ function sendTestMail(domain, to, callback) {
});
}
function getMailboxes(domain, callback) {
function listMailboxes(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -933,7 +1077,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
var error = validateName(name);
if (error) return callback(error);
mailboxdb.addMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
mailboxdb.addMailbox(name, domain, userId, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new MailError(MailError.ALREADY_EXISTS, `mailbox ${name} already exists`));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));
@@ -943,7 +1087,7 @@ function addMailbox(name, domain, userId, auditSource, callback) {
});
}
function updateMailbox(name, domain, userId, callback) {
function updateMailboxOwner(name, domain, userId, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof userId, 'string');
@@ -951,10 +1095,7 @@ function updateMailbox(name, domain, userId, callback) {
name = name.toLowerCase();
var error = validateName(name);
if (error) return callback(error);
mailboxdb.updateMailbox(name, domain, userId, mailboxdb.OWNER_TYPE_USER, function (error) {
mailboxdb.updateMailboxOwner(name, domain, userId, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new MailError(MailError.NOT_FOUND, 'no such mailbox'));
if (error) return callback(new MailError(MailError.INTERNAL_ERROR, error));

View File

@@ -7,8 +7,8 @@ The application '<%= title %>' installed at <%= appFqdn %> is not responding.
This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app in the app configuration dialog
* Restore the app to the latest backup
* Restart the app by opening the app's web terminal - https://cloudron.io/documentation/apps/#web-terminal
* Restore the app to the latest backup - https://cloudron.io/documentation/backups/#restoring-an-app
* Contact us via support@cloudron.io or https://forum.cloudron.io

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 { %>
<% } %>

View File

@@ -2,7 +2,10 @@
Dear <%= cloudronName %> Admin,
creating a backup has failed.
Cloudron failed to create a complete backup. Please see https://cloudron.io/documentation/troubleshooting/#backups
for troubleshooting.
Logs for this failure are available at <%= logUrl %>
-------------------------------------

View File

@@ -1,56 +0,0 @@
<%if (format === 'text') { %>
Dear <%= cloudronName %> Admin,
Version <%= newBoxVersion %> is now available!
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
* <%- changelog[i] %>
<% } %>
<% if (!hasSubscription) { -%>
*Keep your Cloudron automatically up-to-date and secure by upgrading to a paid plan at* <%= webadminUrl %>/#/settings
<% } -%>
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<div style="width: 650px; text-align: left;">
<p>
Version <b><%= newBoxVersion %></b> is now available!
</p>
<h5>Changelog:</h5>
<ul>
<% for (var i = 0; i < changelogHTML.length; i++) { %>
<li><%- changelogHTML[i] %></li>
<% } %>
</ul>
<br/>
<% if (!hasSubscription) { %>
<p>Keep your Cloudron automatically up-to-date and secure by upgrading to a <a href="<%= webadminUrl %>/#/settings">paid plan</a>.</p>
<% } %>
<br/>
</div>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.
</div>
</center>
<% } %>

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