Compare commits

..

1433 Commits

Author SHA1 Message Date
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
Johannes Zellner e53aaddc9c Add 3.3.3 changes 2018-11-13 12:54:40 +01:00
Johannes Zellner d3ebb99131 Fix apptask concurrency
This broke due to async behavior introduced with 1dc649b7a2
2018-11-13 12:54:40 +01:00
Girish Ramakrishnan 82037b70e4 retry apply of platform config
it seems that scaling down addons can fail at times. this can happen
if the kernel is busy swapping things etc. so, we do not block the
platform startup for this operation.

Nov 13 05:20:00 localhost dockerd[28831]: time="2018-11-13T05:20:00.365056059Z" level=error msg="Handler for POST /v1.37/containers/mongodb/update returned error: Cannot update container 6532d4a923ce9f10303f2e2aa7f03c35383864f44f3db6abd5c58da3c1a7702f: docker-runc did not terminate sucessfully: failed to write 419430400 to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/6532d4a923ce9f10303f2e2aa7f03c35383864f44f3db6abd5c58da3c1a7702f/memory.memsw.limit_in_bytes: device or resource busy\n: unknown"
Nov 13 05:20:00 localhost node[5486]: box:shell updatemongodb (stderr): Error response from daemon: Cannot update container 6532d4a923ce9f10303f2e2aa7f03c35383864f44f3db6abd5c58da3c1a7702f: docker-runc did not terminate sucessfully: failed to write 419430400 to memory.memsw.limit_in_bytes: write /sys/fs/cgroup/memory/docker/6532d4a923ce9f10303f2e2aa7f03c35383864f44f3db6abd5c58da3c1a7702f/memory.memsw.limit_in_bytes: device or resource busy
2018-11-12 21:58:57 -08:00
Girish Ramakrishnan 323dfb1853 More typos 2018-11-12 10:27:56 -08:00
Girish Ramakrishnan 59b6d2ef66 mail: clear timeout for dns route 2018-11-12 10:25:15 -08:00
Girish Ramakrishnan 7b99167d30 typo 2018-11-12 09:32:02 -08:00
Girish Ramakrishnan c829f190df 3.3.2 changes 2018-11-11 22:18:46 -08:00
Girish Ramakrishnan f306f334c0 Update mongodb for invalid index fix 2018-11-11 22:15:51 -08:00
Girish Ramakrishnan 3fb8243264 Add route to trigger app import 2018-11-11 21:58:20 -08:00
Girish Ramakrishnan b8861b9d78 Use debug instead 2018-11-11 21:57:45 -08:00
Girish Ramakrishnan 61d98ca4ca give addons more memory when starting up / importing
we scale it back after the import is done
2018-11-11 21:45:02 -08:00
Girish Ramakrishnan a3a22604ba Mark import errors as app errors 2018-11-11 10:36:11 -08:00
Girish Ramakrishnan 1766c3f6d6 lint 2018-11-11 10:12:51 -08:00
Girish Ramakrishnan aae259479f move configure webadmin and status out of setup logic 2018-11-11 09:56:01 -08:00
Girish Ramakrishnan b640a053e3 Always generate webadmin config on startup 2018-11-11 08:19:24 -08:00
Girish Ramakrishnan a3cc17705d Do not remove default.conf and admin.conf when re-configuring apps 2018-11-10 22:02:42 -08:00
Girish Ramakrishnan 78ac1d2a12 Add isCloudronManaged label to containers managed by cloudron 2018-11-10 19:00:03 -08:00
Girish Ramakrishnan 6ec36c9605 Do not crash if platform.start fails
With this change, the box code always starts up even if nginx fails,
docker fails etc.
2018-11-10 18:34:37 -08:00
Girish Ramakrishnan bd2b03876b Add users.isActivated
it's easier to see where we do activation checks
2018-11-10 18:09:00 -08:00
Girish Ramakrishnan 0c5cb9c98f Remove settings.initialize/uninitialize
this simplifies the startup sequence
2018-11-10 14:55:23 -08:00
Girish Ramakrishnan eddcc6fe27 Fix another test 2018-11-10 01:35:49 -08:00
Girish Ramakrishnan 953b55439c eventlog: add domain events 2018-11-10 01:25:34 -08:00
Girish Ramakrishnan e526890e3e Add a separate disabled event 2018-11-10 00:35:56 -08:00
Girish Ramakrishnan 71e1e2468b fix test 2018-11-10 00:22:38 -08:00
Girish Ramakrishnan fcb7c3e82a fix casing 2018-11-10 00:18:56 -08:00
Girish Ramakrishnan 0d65635bc4 eventlog: add email enabled/disabled events 2018-11-10 00:17:38 -08:00
Girish Ramakrishnan 4aa76a859f eventlog: mail list add,remove events 2018-11-10 00:17:25 -08:00
Girish Ramakrishnan 003789265c eventlog: mailbox add,remove events 2018-11-10 00:06:02 -08:00
Girish Ramakrishnan 1bb3b5e51b addons: refactor code into dumpPath 2018-11-09 12:02:38 -08:00
Girish Ramakrishnan 032ac3ed97 cloudron-provision args
zone -> domain
appstore configs -> license
+backup_config
cloudflare* -> dns_config
2018-11-08 21:44:56 -08:00
Girish Ramakrishnan edc2c25bda Add libpython
On ubuntu 18.04, on hetzner and vultr, collectd does not start without
this.
2018-11-08 16:11:01 -08:00
Girish Ramakrishnan 2fa44879e9 cloudron-support: Add more services 2018-11-08 16:05:37 -08:00
Girish Ramakrishnan 7e27413b29 18.04: Add sudo
This was hit on scaleway
2018-11-08 14:47:49 -08:00
Johannes Zellner 77508d180e Add changes for 3.3.1 2018-11-08 21:51:07 +01:00
Johannes Zellner 111f5a7c99 Update postgres addon to 2.0.2 2018-11-08 20:51:14 +01:00
Johannes Zellner d2182559e8 change the ssh user detection to rely on SUDO_USER 2018-11-08 19:39:25 +01:00
Johannes Zellner 9fe91cf9cb Log if we receive SIGINT or SIGTERM 2018-11-08 14:35:22 +01:00
Girish Ramakrishnan de000648dc do not chown redis during updates 2018-11-07 15:21:12 -08:00
Girish Ramakrishnan 0958a57c45 Fix path to conf file 2018-11-07 10:37:00 -08:00
Johannes Zellner 9aae0d9d4c Add initial version of the cloudron-support tool 2018-11-07 17:37:16 +01:00
Girish Ramakrishnan ccfd385beb Fix waitForDns 2018-11-05 23:00:55 -08:00
Girish Ramakrishnan ee6cca5cdf Make cert an object 2018-11-05 22:39:06 -08:00
Girish Ramakrishnan 0093e840c6 Fix various typos 2018-11-05 21:26:53 -08:00
Girish Ramakrishnan 7c6e5ac32b newline replace certs for json databag 2018-11-05 21:23:22 -08:00
Girish Ramakrishnan 15039bf293 Fix validation of fallback certs 2018-11-05 21:13:34 -08:00
Girish Ramakrishnan 89b6cd9d1f cloudron-provision: set tls cert and key 2018-11-05 20:59:29 -08:00
Girish Ramakrishnan 60992405d5 generate cert with correct CN 2018-11-05 20:48:28 -08:00
Girish Ramakrishnan d96b1cc864 generate fallback cert correctly for hyphenated domains 2018-11-05 19:53:50 -08:00
Girish Ramakrishnan 5165cd8f40 Use debug 2018-11-05 17:36:23 -08:00
Girish Ramakrishnan 9f8b47daa9 domains.fqdn only takes 2 args 2018-11-05 17:35:50 -08:00
Girish Ramakrishnan 9372afad9a Fix debug 2018-11-05 17:12:04 -08:00
Girish Ramakrishnan eef6056174 bump timeout of setup route 2018-11-05 16:55:46 -08:00
Girish Ramakrishnan a1dfc758c6 Fix debug 2018-11-02 17:10:16 -07:00
Girish Ramakrishnan 8caf5cc741 typo 2018-11-02 14:50:45 -07:00
Girish Ramakrishnan 7739f8f174 Set container hostname to something constant
currently, it defaults to the container name. apps might use this hostname
for communication with child containers. but this breaks if app gets updated
and gets a new hostname
2018-11-02 14:47:00 -07:00
Girish Ramakrishnan 0618431be7 print the cloudronId on registration 2018-11-01 23:10:26 -07:00
Girish Ramakrishnan 609c4388f0 cloudron-provision: take appstore user id and token 2018-11-01 23:08:26 -07:00
Girish Ramakrishnan 28243956db Add cert and appstore info to provision script 2018-11-01 22:38:21 -07:00
Girish Ramakrishnan ff3a4f65dd autoprovision: register cloudron when provided appstore config 2018-11-01 22:28:51 -07:00
Girish Ramakrishnan 44da148fd1 remove collectd directory again 2018-11-01 20:33:40 -07:00
Girish Ramakrishnan 0b37479838 Add provision script 2018-11-01 20:17:15 -07:00
Girish Ramakrishnan c09aa2a498 Make LE work with hyphenated domains 2018-11-01 19:08:05 -07:00
Girish Ramakrishnan 00d032616f More changes 2018-11-01 13:55:27 -07:00
Johannes Zellner 041285b187 Add dynamic dns settings routes 2018-10-31 16:02:51 +01:00
Johannes Zellner fa9aa50fdf support --output again in createReleaseTarball script for hotfix to work
This avoids having to calculate the output filename with revisions and
exact format in the cli tool.
2018-10-31 08:54:24 +01:00
Girish Ramakrishnan e0b1ebba92 verifyOperator -> isUnmanaged 2018-10-30 21:17:34 -07:00
Girish Ramakrishnan 581bbafa06 registry_config route does not require operator check 2018-10-30 20:54:35 -07:00
Girish Ramakrishnan ce93518c0a allow hyphenated subdomains on all editions 2018-10-30 20:35:35 -07:00
Girish Ramakrishnan 0ba0b009c7 make the domain locking code more clear 2018-10-30 20:35:32 -07:00
Girish Ramakrishnan eed8f109bc operator check is now directly based on edition type 2018-10-30 20:26:22 -07:00
Girish Ramakrishnan 63946509b3 Get autoconf data as separate object
This was done because restore API already takes a backupConfig.
So, it's best to split up the primary parameters and the autoconf
2018-10-30 19:27:14 -07:00
Girish Ramakrishnan 668ff99450 Add support for restricted certs as part of dns setup 2018-10-30 18:15:26 -07:00
Girish Ramakrishnan 03984a811f dnsSetup -> setup
It now takes a dnsConfig object. It will end up taking other config
objects for provisioning.
2018-10-30 14:01:02 -07:00
Girish Ramakrishnan 7c733ae150 Fix error handling 2018-10-29 20:19:48 -07:00
Girish Ramakrishnan 9fe02d3818 lint 2018-10-29 20:19:42 -07:00
Girish Ramakrishnan f10b80d90d Make apps test great again 2018-10-29 12:49:41 -07:00
Girish Ramakrishnan caf1d18250 Add flag for tests to check if platform is ready 2018-10-29 12:46:02 -07:00
Girish Ramakrishnan c700635656 certs: make renew switch to fallback certs 2018-10-28 19:03:57 -07:00
Girish Ramakrishnan d6d2ee7d19 Add version to release filename
This is required because we have to patch the VERSION file inside
the tarball for 'rerelease'. Which then requires release tarballs
to have a unique name.

The version is at the end because there is code  in release script
which relies on the box sha at the front
2018-10-28 16:16:51 -07:00
Girish Ramakrishnan f5a5da6731 3.0 changes 2018-10-28 14:43:20 -07:00
Girish Ramakrishnan 8f2ce5f4f8 cloudron-setup: Fixup script to support pre-3.2 versions
This is required for e2e and restore of old cloudrons
2018-10-28 14:21:51 -07:00
Girish Ramakrishnan 62619d21c0 Handle 409 in container exec 2018-10-27 14:15:52 -07:00
Girish Ramakrishnan bf7707b70b cleanup old base image as well 2018-10-27 13:10:02 -07:00
Girish Ramakrishnan 698f3c833b cloudflare: Fix crash
{ success: false,
  errors:
   [ { code: 9103, message: 'Unknown X-Auth-Key or X-Auth-Email' } ],
  messages: [],
  result: null }
/home/yellowtent/box/src/dns/cloudflare.js:36
        } else if (error.error_chain[0] && error.error_chain[0].message) {
                                    ^
TypeError: Cannot read property '0' of undefined
    at translateRequestError (/home/yellowtent/box/src/dns/cloudflare.js:36:37)
    at /home/yellowtent/box/src/dns/cloudflare.js:57:83
2018-10-27 12:51:24 -07:00
Girish Ramakrishnan 5996a107ed Return a bogus version to fix the tests 2018-10-27 12:26:39 -07:00
Girish Ramakrishnan 0307dc5145 Carefully prune addon images
With the docker image prune we ended up removing the redis image
which was ununsed...

This also prevents us from removing images of some app using the
docker addon
2018-10-27 12:01:22 -07:00
Girish Ramakrishnan f9aa09f717 typo 2018-10-27 11:28:30 -07:00
Girish Ramakrishnan 2688a57d46 Move version out of config.js
We now store this in a file called VERSION inside the tarball
itself.
2018-10-27 11:13:36 -07:00
Girish Ramakrishnan 7ad069fd94 Make config.setVersion hidden 2018-10-27 10:44:21 -07:00
Girish Ramakrishnan 06d043dac4 Make baseImage an array of objects 2018-10-26 16:58:18 -07:00
Girish Ramakrishnan 94be6a9e3c Pull images in installer.sh
This way if the network is not working, then the update will just
fail and use the old version

This also tags the addon images with the semver properly
2018-10-26 16:35:18 -07:00
Girish Ramakrishnan 97567b7d2a Do not re-create cloudron.conf
All this argument passing going around is just needless

* boxdata/version file is not used
* dashboard config.json is also unused
2018-10-26 15:29:14 -07:00
Girish Ramakrishnan 95d691154d scripts/cloudron-setup: --data-dir is obsolete 2018-10-26 10:59:06 -07:00
Girish Ramakrishnan 9ac9b49522 Move ntp setup to base image init script 2018-10-26 10:57:19 -07:00
Girish Ramakrishnan 6a887c2bba SSH port configuration will be done in appstore 2018-10-26 10:13:35 -07:00
Girish Ramakrishnan 0250508a89 Add comment for installer.sh and start.sh 2018-10-26 10:13:35 -07:00
Girish Ramakrishnan f97973626c start.sh: curl is not used
we will move network stuff to installer.sh anyways
2018-10-26 09:55:13 -07:00
Girish Ramakrishnan 5cdf9d1c6f Move host configuration to setup script since Cloudron does not rely on it
Also hostname cannot be set here since admin_fqdn is not available on
initial setup
2018-10-25 18:49:05 -07:00
Girish Ramakrishnan 009e888686 remove dead migration of nginx certs 2018-10-25 18:43:24 -07:00
Girish Ramakrishnan e3478c9d13 Use docker cli to cleanup old images 2018-10-25 16:33:12 -07:00
Girish Ramakrishnan 2e3ddba7e5 cloudron-setup: Log arguments later 2018-10-25 11:31:39 -07:00
Girish Ramakrishnan 81ac44b7da Fix failing test 2018-10-25 00:01:32 -07:00
Girish Ramakrishnan ffe50ff977 detect change in provider type and renew accordingly 2018-10-24 20:40:05 -07:00
Girish Ramakrishnan 73faba3c28 certs: Fix the config filename of admin domain 2018-10-24 20:32:03 -07:00
Girish Ramakrishnan c1db52927e Add a debug 2018-10-24 20:32:03 -07:00
Girish Ramakrishnan e7120bd086 Move renewal logic to domain model code 2018-10-24 20:32:03 -07:00
Girish Ramakrishnan 91ad94f978 No more prereleases 2018-10-24 15:28:41 -07:00
Girish Ramakrishnan ee517da4f4 Add route to renew certs of a domain 2018-10-24 13:09:41 -07:00
Girish Ramakrishnan 0d04213199 Ensure app can be uninstalled even if image format is invalid 2018-10-24 13:09:41 -07:00
Johannes Zellner 114f6c596d Use new mongodb addon with fixed return paths 2018-10-24 10:45:30 +02:00
Johannes Zellner 5dadd083be Use new mysql addon with fixed db import 2018-10-23 15:59:40 +02:00
Johannes Zellner 28d61a4d70 Use postgres addon with fixed restore 2018-10-23 13:05:39 +02:00
Girish Ramakrishnan a49969f2be Move apphealthmonitor into a cron job
This makes sure that it only runs post activation

See also a9c1af50f7
2018-10-22 20:08:49 -07:00
Girish Ramakrishnan 65eaf0792e Remove any appdata dir symlinks correctly 2018-10-18 19:58:37 -07:00
Girish Ramakrishnan 6153f45fd9 Remove legacy redis directory 2018-10-18 19:30:10 -07:00
Girish Ramakrishnan d5ffb8b118 Fix appstore casing
The API on cloudron.io wants appstore and not appStore
2018-10-18 14:21:09 -07:00
Girish Ramakrishnan bc283f1485 Validate env vars 2018-10-18 13:40:26 -07:00
Girish Ramakrishnan 2d427a86f0 Send appstoreId as part of purchase/unpurchase 2018-10-18 11:01:32 -07:00
Girish Ramakrishnan 2a6edd53b6 Update postgresql (clone/restore fix) 2018-10-17 18:49:35 -07:00
Girish Ramakrishnan cf8bb3da9e Update mysql (prefix backup and restore) 2018-10-17 18:40:48 -07:00
Girish Ramakrishnan 7c1325cb34 Update mail addon (gnutls-bin) 2018-10-17 14:18:54 -07:00
Girish Ramakrishnan f4ad912cf3 redis: do not import if not upgrading
There will be no backup to import from
2018-10-17 10:55:11 -07:00
Girish Ramakrishnan 78936a35c5 redis: Check the new dump location first
we do not cleanup the old dumps (yet). this then means that we will
constantly be importing the older dump.
2018-10-17 10:33:41 -07:00
Johannes Zellner 4ab2f8c153 Fix typo 2018-10-17 18:20:39 +02:00
Girish Ramakrishnan 858f03e02d Update redis addon container
Fixes #223
2018-10-16 15:40:40 -07:00
Girish Ramakrishnan 045cfeeb0d Move the addon startup logic to addons.js
Moved the graphite logic to new graphs.js

The settings code now does change notification itself. Over time,
it makes sense to just having settings code do this for everything
and not have this change listener logic. This lets us:
* Maybe the settings can only return based on final handler result
* All dependant modules otherwise have to "init"ed to listen on startup
* Easier to test those handlers without having to actually change the
  setting (since they will now be in "exports" naturally)

Also, maybe someday with this abstraction we can allow apps to have their
own isolated databases etc
2018-10-16 14:40:29 -07:00
Girish Ramakrishnan bbc121399e Stop containers by name before killing them outright 2018-10-16 13:24:11 -07:00
Girish Ramakrishnan 03d513a3b1 Remove infra upgrade logic
We now upgrade using the dumps of the last backups which are in the
filesystem itself instead of downloading the entire app from the
backup all over.
2018-10-16 11:06:01 -07:00
Girish Ramakrishnan 539447409e Fix mongodb addon digest 2018-10-16 10:06:04 -07:00
Johannes Zellner 4525c6f39e Revert base image change 2018-10-16 15:10:11 +02:00
Girish Ramakrishnan a6618c5813 Update mongodb (healthcheck) 2018-10-16 00:23:18 -07:00
Girish Ramakrishnan 5c1a0c1305 Check the status field for healthcheck 2018-10-16 00:21:48 -07:00
Girish Ramakrishnan 62c9fc90f9 Fix crash when oldConfig is null 2018-10-15 16:29:30 -07:00
Girish Ramakrishnan 2c60614d4b remove special rm script for redis 2018-10-15 16:14:16 -07:00
Girish Ramakrishnan 816fa94555 Upgrade the db addons using dumps when major version changes 2018-10-15 16:14:13 -07:00
Girish Ramakrishnan bbdafc6a2f pass existing infra information to start funcs 2018-10-15 14:09:13 -07:00
Girish Ramakrishnan 5333db5239 Add method to import database from dumps 2018-10-15 10:15:47 -07:00
Girish Ramakrishnan 6254fe196a Install resolvconf
When we disable systemd-resolved, the system goes into a state
where no DNS queries work anymore. Even if unbound is installed
right after. Installing 'resolvconf' fixes this.

There is competition even for this tiny package! (openresolv)
2018-10-15 08:58:37 -07:00
Girish Ramakrishnan 79d3713a4b All apps are being updated already to 1.0.0 2018-10-14 16:31:49 -07:00
Girish Ramakrishnan f1da537c80 wait for addons to start using heathcheck routes 2018-10-14 13:26:01 -07:00
Girish Ramakrishnan b19fc23cb2 Fix the tests 2018-10-12 17:05:31 -07:00
Girish Ramakrishnan 5366524dc0 Add route to set/unset the registry config 2018-10-12 17:04:04 -07:00
Girish Ramakrishnan f86d4f0755 Update manifestformat 2018-10-12 16:13:28 -07:00
Girish Ramakrishnan 3ad495528f whitespace 2018-10-11 16:18:38 -07:00
Girish Ramakrishnan 5bfb253869 Custom env vars for apps
Add a table and the install/configure routes. Initially, I thought
we can just keep the env vars in docker container but that doesn't
work since we create the container only later in apptask. And if the
container gets deleted we lose this information.
2018-10-11 16:17:18 -07:00
Girish Ramakrishnan 630fbb373c healthCheckPath is optional for non-appstore apps 2018-10-11 13:20:31 -07:00
Girish Ramakrishnan 8d09ec5ca6 Disable systemd-resolved on 18.04 2018-10-08 20:09:45 -07:00
Girish Ramakrishnan 23af20ddc9 Update graphite addon 2018-10-08 14:18:13 -07:00
Girish Ramakrishnan 86441bfeb6 Update mail container with latest base image 2018-10-08 10:08:05 -07:00
Johannes Zellner be51daabf0 Use restified addons based on new base image as v2.0.0 2018-10-08 13:41:39 +02:00
Girish Ramakrishnan 86e8db435a Add new 18.04 base image 2018-10-05 14:11:31 -07:00
Girish Ramakrishnan d8401f9ef9 Add 18.04 to changelog 2018-10-05 10:47:17 -07:00
Girish Ramakrishnan 64f98aca5a 18.04 support 2018-10-05 10:28:02 -07:00
Johannes Zellner f660947594 Do not silently retry on dns upsert in apptask 2018-10-05 17:07:34 +02:00
Girish Ramakrishnan 576e22e1a0 Update hash of mail container 2018-10-05 08:00:27 -07:00
Johannes Zellner e004a00073 Add some 3.3.0 changes 2018-10-05 16:24:32 +02:00
Johannes Zellner c0fdac5b34 The additional fetch wont hurt but makes it more readable 2018-10-05 16:21:06 +02:00
Johannes Zellner a2a035235e Use postgres addon 2.0.2 2018-10-05 16:07:22 +02:00
Girish Ramakrishnan 85ac2bbe52 Update 3.2.2 changelog
(cherry picked from commit e67fc64e65)
2018-10-04 21:21:04 -07:00
Girish Ramakrishnan c26e9bbef7 Fix debugs to match function name 2018-10-04 10:29:40 -07:00
Girish Ramakrishnan 3e85029ea1 Add 3.2.1 changes (updated dashboard)
(cherry picked from commit 0189539b27)
2018-09-30 15:03:43 -07:00
Girish Ramakrishnan 8dd3c55ecf Use async unlink 2018-09-28 17:05:53 -07:00
Girish Ramakrishnan 1ee902a541 typoe 2018-09-28 17:01:56 -07:00
Girish Ramakrishnan 55cbe46c7c godaddy: empty the text record for del 2018-09-28 14:35:57 -07:00
Girish Ramakrishnan 5a8a4e7907 acme2: Display any errors when cleaning up challenge 2018-09-28 14:33:08 -07:00
Girish Ramakrishnan 3b5be641f0 acme2: fix challenge subdomain calculation in cleanup 2018-09-28 13:24:34 -07:00
Girish Ramakrishnan a34fe120fb TXT values must be quoted 2018-09-27 20:17:39 -07:00
Girish Ramakrishnan b9918cb6fb Fix changelog ordering 2018-09-26 22:34:14 -07:00
Girish Ramakrishnan 21a86175b4 Use the local branch in hotfix
(cherry picked from commit ffb02a3ba8)
2018-09-26 22:30:23 -07:00
Girish Ramakrishnan 84150f53e7 Revert "Do not require dashboard remote branch with the same name for hotfix"
This reverts commit b2469de9b0.

This affects the release scripts and always creates dashboard tarball from master
2018-09-26 22:03:38 -07:00
Girish Ramakrishnan 42c1f8bb04 start most cron jobs only after activation
Importing the db might take some time. If a cron runs in the middle,
it crashes.

TypeError: Cannot read property 'domain' of undefined
    at Object.fqdn (/home/yellowtent/box/src/domains.js:126:111)
    at /home/yellowtent/box/src/apps.js:460:36
    at /home/yellowtent/box/node_modules/async/dist/async.js:3110:16
    at replenish (/home/yellowtent/box/node_modules/async/dist/async.js:1011:17)
    at /home/yellowtent/box/node_modules/async/dist/async.js:1016:9
    at eachLimit$1 (/home/yellowtent/box/node_modules/async/dist/async.js:3196:24)
    at Object.<anonymous> (/home/yellowtent/box/node_modules/async/dist/async.js:1046:16)
    at /home/yellowtent/box/src/apps.js:458:19
    at /home/yellowtent/box/src/appdb.js:232:13
    at Query.args.(anonymous function) [as _callback] (/home/yellowtent/box/src/database.js
2018-09-26 14:56:45 -07:00
Girish Ramakrishnan a504759b95 more debugs and comments 2018-09-26 12:39:33 -07:00
Girish Ramakrishnan af763eadd4 Fix perms issue when restoring
Fixes #536
2018-09-26 11:59:19 -07:00
Girish Ramakrishnan 818735e2c8 Fix import of older redis backups 2018-09-26 09:48:18 -07:00
Girish Ramakrishnan aefa8ed0d6 Add .eslintrc
Looks like only this supports async/await nicely
2018-09-25 18:31:15 -07:00
Girish Ramakrishnan 78d3aafd7a Fix case where mailbox already exists 2018-09-25 17:04:17 -07:00
Girish Ramakrishnan 1fed7335cf Update the db addons 2018-09-25 14:24:32 -07:00
Girish Ramakrishnan 34626abdcf reset mailboxname to .app when empty
fixes #587
2018-09-25 11:26:38 -07:00
Girish Ramakrishnan 477f8a3ca1 mysql: Add message to error string 2018-09-24 16:45:08 -07:00
Girish Ramakrishnan bd7bd2adae 3.1.4 changes 2018-09-24 09:29:58 -07:00
Girish Ramakrishnan d47fd34f66 Use alternateDomain fqdn for ensuring certificate
this makes it work for hyphenated domains as well
2018-09-22 16:26:28 -07:00
Girish Ramakrishnan f6ceee7f50 list domains only once 2018-09-22 16:19:12 -07:00
Girish Ramakrishnan a08f05fb44 Attach fqdn to all the alternateDomains 2018-09-22 15:11:01 -07:00
Girish Ramakrishnan 6214ba7b31 allow hyphenated subdomains in caas 2018-09-22 12:37:38 -07:00
Girish Ramakrishnan e69004548b waitForDnsRecord: use subdomain as argument
this allows to hyphenate the subdomain correctly in all places

the original issue was that altDomain in caas was not working
because waitForDnsRecord was not hyphenating.
2018-09-22 11:26:33 -07:00
Girish Ramakrishnan c8216d84ac register alt domains in install route 2018-09-22 10:09:46 -07:00
Girish Ramakrishnan 5cfc3b22fa Add redis healthcheck 2018-09-20 12:46:08 -07:00
Girish Ramakrishnan a755aecfc5 typo 2018-09-19 17:25:40 -07:00
Girish Ramakrishnan 64e34e13be display error messages 2018-09-19 15:55:42 -07:00
Girish Ramakrishnan e455ea987a postgresql: fix various issues during backup/restore/clear 2018-09-19 15:35:47 -07:00
Girish Ramakrishnan bf87d3fc8b Update redis sha256 2018-09-19 15:11:55 -07:00
Girish Ramakrishnan cfdb939bff redis: various fixes 2018-09-19 14:29:27 -07:00
Girish Ramakrishnan 16fab63442 Update mysql for quoting fix 2018-09-19 12:26:18 -07:00
Johannes Zellner fe14bcf155 Use updated mysql addon 2018-09-19 12:48:44 +02:00
Girish Ramakrishnan 9732b899b0 redis: backup returns 200 now 2018-09-18 14:11:42 -07:00
Girish Ramakrishnan 23b9854c57 Use REST API instead of volumes for redis 2018-09-18 13:09:24 -07:00
Girish Ramakrishnan 0b3f65c70e postgres: restore need not call setup anymore 2018-09-18 12:29:40 -07:00
Girish Ramakrishnan 83d56f79c6 lint 2018-09-18 12:22:19 -07:00
Johannes Zellner 8632939c6e Use restified mysql addon 2018-09-18 20:25:07 +02:00
Johannes Zellner 93cdba8137 Use post instead of delete for postgres clear 2018-09-18 20:23:19 +02:00
Johannes Zellner 2686a1b9e3 Fixup copy and paste errors and linter warnings 2018-09-18 20:20:27 +02:00
Johannes Zellner eec1fe1272 Start mysql addon with env vars instead of vars.sh file 2018-09-18 20:18:39 +02:00
Johannes Zellner f7f26fdf78 Fix logging in getAddonDetails() 2018-09-18 20:18:39 +02:00
Johannes Zellner d980d44833 Talk to the mysql addon via rest apis 2018-09-18 20:18:39 +02:00
Girish Ramakrishnan af4b2b075e redis: fix clear return code 2018-09-17 20:44:13 -07:00
Girish Ramakrishnan 3fcf6aa339 Add a timeout to wait for redis to start 2018-09-17 20:40:07 -07:00
Girish Ramakrishnan 1b205ac107 postgresql: fix clear route to have username and remove extra } 2018-09-17 19:53:12 -07:00
Girish Ramakrishnan bcee0aa2ad Update postgresql (reset root password fix) 2018-09-17 19:22:27 -07:00
Girish Ramakrishnan e2bf52b69d postgresql: Add missing db prefix in routes 2018-09-17 18:11:38 -07:00
Girish Ramakrishnan 3c6dffbbc7 Remove old docker images correctly
Old images had no sha256, so it prints "<none>". These images
have to be accessed by tag which uses the ":tag" format whereas
the sha images use the "@sha" format
2018-09-17 16:39:31 -07:00
Girish Ramakrishnan 691b876d61 Update postgresql image (re-setup fix) 2018-09-17 16:25:17 -07:00
Girish Ramakrishnan ed14115ff1 Fix new account return value
https://tools.ietf.org/html/draft-ietf-acme-acme-07#section-7.3
2018-09-17 15:30:16 -07:00
Girish Ramakrishnan 6d9c6ffba3 acme2: register new account returns 201 2018-09-17 15:19:19 -07:00
Johannes Zellner b8dd01d502 Bump postgres addon image again with latest fixes 2018-09-17 15:56:02 +02:00
Johannes Zellner 7e16b96abe Use updated postgres addon 2018-09-17 12:54:05 +02:00
Johannes Zellner 705d0ba7f9 Use restified postgresql addon 2018-09-16 22:56:26 +02:00
Girish Ramakrishnan 306c80dd93 Fix addons.js with the new env var names 2018-09-15 20:38:18 -07:00
Girish Ramakrishnan c8ed1d950b Update mongodb addon (restore does not clear anymore) 2018-09-15 20:29:04 -07:00
Girish Ramakrishnan 705bf3db98 Use only CLOUDRON_* prefixed vars (we reserve these) 2018-09-15 20:15:58 -07:00
Girish Ramakrishnan e2b388f721 Update redis addon that has clear route 2018-09-15 20:14:16 -07:00
Girish Ramakrishnan 3f34734933 withFileTypes is only available on node 10 2018-09-15 18:47:34 -07:00
Girish Ramakrishnan 391ee00db8 Fix cleanup of old images 2018-09-15 18:41:06 -07:00
Girish Ramakrishnan 64a7b80395 Fix issue deleting app dir during restore
During restore, only the superfluous addons are teardown. Rest of
the addons are kept as-is. This is done to preserve the addon
configuration across restores (thus preserving db passwords).

We then rely on the restoreApp logic to call restore in each addon.
Each restore currently setup, clear and them imports the backup.

With the volume changes, we have moved volume create/delete to the
localstorage addon. Currently, the localstorage addon has no concept of restore
because the backup download extracts it directly into the volume.

Because of the above, during the restore code path, we don't teardown
the localstorage addon and thus files are left behind in appid/data/. This
prevents deleteAppDir() from removing the appid/ directory.

The fix is to add a new 'clear' command to addons. Before restore, we
clear the addons.
2018-09-15 17:35:47 -07:00
Girish Ramakrishnan 46a00c839b rename create/deleteVolume to avoid confusion 2018-09-15 15:30:26 -07:00
Girish Ramakrishnan 9f6621434f Use sha256 to accidentally prevent overwritten tags 2018-09-14 20:20:46 -07:00
Johannes Zellner 3963eb687f Use the sudo script for app volume 2018-09-14 14:37:20 +02:00
Johannes Zellner 56cd97147a Unify the code to get addon container ip and token 2018-09-14 13:26:56 +02:00
Johannes Zellner 614b3ed5d1 Remove unused variable 2018-09-14 13:19:47 +02:00
Johannes Zellner 9f622c5e65 Do not store the cloudron redis service token in the addon env blob 2018-09-14 13:19:33 +02:00
Girish Ramakrishnan 4f6a467181 bump minor version to make apps reconfigure themselves
this is because the container configuration has changed
(we create volume)
2018-09-13 15:47:08 -07:00
Girish Ramakrishnan a46e208c63 Use docker volumes instead of bind mount for app data 2018-09-13 15:35:41 -07:00
Johannes Zellner a0fd60408b Use https with selfsigned certs for addon service calls 2018-09-13 21:37:55 +02:00
Johannes Zellner ffbbba938a Replace superagent with request for talking to addons 2018-09-13 12:54:33 +02:00
Johannes Zellner a222b3ed58 Use new restified mongodb and redis addons 2018-09-13 12:54:33 +02:00
Girish Ramakrishnan 6ba574432a calculate subdomain correctly for non-wildcard domains 2018-09-12 15:55:20 -07:00
Girish Ramakrishnan 96075c7c20 Fix double callback 2018-09-12 14:43:15 -07:00
Girish Ramakrishnan 64665542bc select app's cert based on domain's wildcard flag
this also removes the confusing type field in the bundle. we instead
check the current nginx config to see what cert is in use.
2018-09-12 14:22:54 -07:00
Girish Ramakrishnan 54d2a4f17b rework args to ensureCertificate 2018-09-12 12:50:04 -07:00
Girish Ramakrishnan 1d829c4af2 Allow wildcard only with programmable DNS backend 2018-09-12 12:36:13 -07:00
Girish Ramakrishnan dc4dc05628 Fixup changes file 2018-09-12 10:00:16 -07:00
Johannes Zellner b2469de9b0 Do not require dashboard remote branch with the same name for hotfix 2018-09-12 12:56:24 +02:00
Johannes Zellner 812d3576a9 Use mongodb rest api for backup and restore 2018-09-12 12:56:24 +02:00
Johannes Zellner 6012eb7898 docker inspect and the rest api have a different output format 2018-09-12 12:56:24 +02:00
Johannes Zellner a591b5910e Restify mongodb addon 2018-09-12 12:56:24 +02:00
Johannes Zellner 1e084e98d1 Change mongodb addon from env vars file to passing directly 2018-09-12 12:56:24 +02:00
Johannes Zellner b4224a7f8d Call the rest route on redis backup
The addons are an array of key/value
2018-09-12 12:56:24 +02:00
Johannes Zellner ab8a010b94 Redis does not use the vars.sh file anymore 2018-09-12 12:56:24 +02:00
Johannes Zellner 4c164c17cf Support internal only addon configs with redis persistent token 2018-09-12 12:56:24 +02:00
Johannes Zellner 650f181a07 Provision redis addon with new set of required env vars 2018-09-12 12:56:24 +02:00
Girish Ramakrishnan 97ab521038 make ensureCertificate check any wildcard cert 2018-09-11 23:47:23 -07:00
Girish Ramakrishnan c138c4bb5f acme2: implement wildcard certs 2018-09-11 23:15:50 -07:00
Girish Ramakrishnan 1067ff882a Move type validation to routes logic 2018-09-11 22:17:40 -07:00
Girish Ramakrishnan b6ad6e121b refactor to validateTlsConfig 2018-09-11 21:53:18 -07:00
Girish Ramakrishnan a756345138 consolidate hyphenatedSubdomains handling 2018-09-11 21:41:50 -07:00
Girish Ramakrishnan 35f69cfea9 acme2: wait for dns 2018-09-11 19:41:41 -07:00
Girish Ramakrishnan 3f0bc6165b Enhance waitForDns to support TXT records 2018-09-11 19:41:38 -07:00
Girish Ramakrishnan d0dde04695 acme2: dns authorization 2018-09-10 21:46:53 -07:00
Girish Ramakrishnan 2f38a4018c pass domain arg to getCertificate API 2018-09-10 20:48:47 -07:00
Girish Ramakrishnan 2c76716bc7 rename func 2018-09-10 20:35:48 -07:00
Girish Ramakrishnan f38b87c660 lint 2018-09-10 20:30:38 -07:00
Girish Ramakrishnan 9bac2acc37 Fix callback use 2018-09-10 17:39:13 -07:00
Girish Ramakrishnan 68536b6d7d acme2 implementation 2018-09-10 16:26:24 -07:00
Girish Ramakrishnan 1d0a52404a Fix tests 2018-09-10 13:45:05 -07:00
Girish Ramakrishnan 017460b497 acme -> acme1 2018-09-10 10:57:48 -07:00
Girish Ramakrishnan 8efd496579 storage: add access denied function (unused) 2018-09-10 10:41:08 -07:00
Girish Ramakrishnan 229fe0f66f Add 3.1.4 changes 2018-09-10 09:28:08 -07:00
Girish Ramakrishnan efa36ab161 cloudflare: Add the chain message 2018-09-07 11:46:10 -07:00
Girish Ramakrishnan 191c85d01f cloudflare: priority is now an integer 2018-09-07 11:44:31 -07:00
Girish Ramakrishnan 88330ab415 Return 424 for external errors 2018-09-07 11:27:19 -07:00
Girish Ramakrishnan 76f5b22c07 dns: implement wildcard dns validation 2018-09-06 20:45:10 -07:00
Girish Ramakrishnan 0639ca1594 Make wildcard a separate provider
this is required because the config object is not returned for
locked domains and the UI display for the provider field is then
wrong.
2018-09-06 20:09:08 -07:00
Girish Ramakrishnan e620a26c04 typo 2018-09-06 20:05:08 -07:00
Girish Ramakrishnan 867c3595b2 more changes 2018-09-06 13:18:26 -07:00
Girish Ramakrishnan df66d77a68 cloudflare: Fix crash when there is an external error updating dns records 2018-09-06 12:26:11 -07:00
Girish Ramakrishnan 5e919b90f5 Better fix for grub 2018-09-06 11:56:50 -07:00
Girish Ramakrishnan 428269f503 3.1.3 changes 2018-09-06 00:41:45 -07:00
Girish Ramakrishnan b03e26a510 Fix typo 2018-09-06 00:41:37 -07:00
Girish Ramakrishnan 1e15b63a4a prevent deletion of mail domain as well 2018-09-06 00:16:12 -07:00
Girish Ramakrishnan 8d5e70f6aa lock the admin domain based on the edition 2018-09-06 00:15:45 -07:00
Girish Ramakrishnan 91a1bc7a01 move verifyOperator to users routes 2018-09-06 00:10:09 -07:00
Girish Ramakrishnan 0e3f9c9569 Move verifyAppOwnership to app route 2018-09-06 00:09:42 -07:00
Girish Ramakrishnan 2ad0a57fc1 Typo 2018-09-05 23:59:40 -07:00
Girish Ramakrishnan def3521ee1 Do not allow admin domain to be deleted 2018-09-05 17:12:02 -07:00
Girish Ramakrishnan 3d004b3dcc Disable various server/operator routes based on edition
The initial idea was to put an owner flag but this means that the
owner will be visible inside apps.
2018-09-05 15:31:58 -07:00
Girish Ramakrishnan 0439bd8869 move addSpacesSuffix to model logic 2018-09-04 16:37:08 -07:00
Girish Ramakrishnan 10b4043358 Add alternateDomains to app install route 2018-09-04 16:27:35 -07:00
Girish Ramakrishnan ac3b0f0082 Add spaces suffix for alternate domains 2018-09-04 14:12:50 -07:00
Girish Ramakrishnan d49a1dea7a Fix usage of domains.fqdn 2018-09-04 11:35:01 -07:00
Girish Ramakrishnan ec9c96da6f Fix comments 2018-09-04 10:48:54 -07:00
Johannes Zellner 2de630e491 Put the app owner also into ldap groups
Fixes #585
2018-09-03 17:14:11 +02:00
Johannes Zellner 3af358b9bc List app owner as admins in ldap search 2018-09-03 16:08:05 +02:00
Johannes Zellner b61478edc9 Attach req.app for further use in ldap routes 2018-09-03 15:38:50 +02:00
Johannes Zellner b23afdd32d Fix tests to match the adjusted purchase logic 2018-09-01 11:53:05 +02:00
Girish Ramakrishnan 43055da614 Add route to let admin set user password 2018-08-31 14:35:01 -07:00
Girish Ramakrishnan 2c3f1ab720 Fix the error messages 2018-08-31 14:06:06 -07:00
Johannes Zellner 35a31922a5 Always run all tests 2018-08-31 22:25:06 +02:00
Girish Ramakrishnan bf432dc26f Revert "Disable memory accounting setup"
This reverts commit ad22df6f71.

This actually serves the purpose of enabling swap.

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

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

This reverts commit b3aa59de19.
2018-08-13 20:39:23 +02:00
Johannes Zellner a1b983de23 Initial code for docker addon proxy 2018-08-13 20:38:39 +02:00
Johannes Zellner b3aa59de19 Expose the host /app/data folder when localstorage and docker addons are used 2018-08-13 19:40:41 +02:00
Girish Ramakrishnan 796ced999f Add some 3.1 changes 2018-08-13 09:18:44 -07:00
Girish Ramakrishnan 353b5e07bf Save correct type of port in db
Part of #504
2018-08-13 08:33:17 -07:00
Girish Ramakrishnan c29eef4c14 Set the udp ports in docker configuration
Part of #504
2018-08-12 22:47:59 -07:00
Girish Ramakrishnan 8bc7dc9724 Pass the manifest to validatePortBindings 2018-08-12 22:37:36 -07:00
Girish Ramakrishnan 60984d18dd Add type field to port bindings table
Part of #504
2018-08-12 22:32:36 -07:00
Girish Ramakrishnan df1dc80fc1 Change the internal portBindings representation
Part of #504
2018-08-12 22:32:31 -07:00
Girish Ramakrishnan 8e2f0cdf73 Update cloudron-manifestformat (udpPorts) 2018-08-12 19:00:28 -07:00
Ian Fijolek bf1e19f8e6 Add more detailed checking of DMARC
Fixes #570
2018-08-12 13:47:24 -07:00
Girish Ramakrishnan 9a7214ea07 Update manifestformat for docker addon 2018-08-10 12:31:46 -07:00
Johannes Zellner 4499f08357 Revert "Remove _docker addon"
This reverts commit d6f49eb54f.
2018-08-09 11:54:46 +02:00
Girish Ramakrishnan 8671c4c015 3.0.1 changes 2018-08-06 19:45:46 -07:00
Girish Ramakrishnan 2612cc18fa Expire existing webadmin token so that the UI gets a new token
This is because we added a new appstore scope
2018-08-06 19:40:46 -07:00
Girish Ramakrishnan 36a21acae8 skip verification for prereleases because we remove it from release.json 2018-08-06 16:23:23 -07:00
Girish Ramakrishnan 1ed4710c68 Better error message 2018-08-05 23:38:49 -07:00
Girish Ramakrishnan 75b6688734 Clear timeout when setting backup config 2018-08-05 22:29:27 -07:00
Girish Ramakrishnan f7a7e4e95a Fix text in account setup screen 2018-08-05 21:58:07 -07:00
Girish Ramakrishnan a8ba0b91f7 Allow subdomain in the location field
This allows one to easily add "dev.staging@domain.com" etc without having to create
yet another domain. This plays well with the concept that we have a
mail domain for every domain. So we get mails from @domain.com working for
these subdomain installations.
2018-08-04 09:37:18 -07:00
Girish Ramakrishnan 95540e8cbc Do ownership check in exec route 2018-08-03 22:19:42 -07:00
Girish Ramakrishnan 9ebd22d6f7 add api note 2018-08-03 22:19:36 -07:00
Girish Ramakrishnan 1cf5807fb9 Send spaces config in config route
This is here because normal users cannot access settings route
2018-08-03 18:26:16 -07:00
Girish Ramakrishnan cc7824681b Return ownerId in apps response 2018-08-03 18:15:22 -07:00
Girish Ramakrishnan a0a523ae71 spaces: verify app ownership in app management routes 2018-08-03 17:35:58 -07:00
Girish Ramakrishnan fba70d888b Remove tokenScopes
decided that the UI won't use this any more and will just the admin flag
2018-08-03 10:07:23 -07:00
Girish Ramakrishnan ce9fc7b3f7 Better error message 2018-08-03 09:46:22 -07:00
Girish Ramakrishnan 3d4df8e26c Add test for spaces config 2018-08-03 09:46:16 -07:00
Girish Ramakrishnan d3f9647cd5 Add settings for Spaces 2018-08-03 09:40:57 -07:00
Girish Ramakrishnan 2a49569805 Send admin flag in the profile 2018-08-03 09:34:25 -07:00
Girish Ramakrishnan 47c8700d42 make scopesForUser async 2018-08-03 09:34:19 -07:00
Girish Ramakrishnan d302dbc634 Fix tests 2018-08-02 14:59:50 -07:00
Girish Ramakrishnan eab3cd6b2b ownerId for domains is not implemented yet 2018-08-01 18:53:49 -07:00
Girish Ramakrishnan 92151b1e42 Add note on npm rebuild 2018-08-01 18:37:40 -07:00
Girish Ramakrishnan 621d00a5c6 GPG verify releases
Part of #189
2018-08-01 17:16:42 -07:00
Girish Ramakrishnan 5bd7cd6749 Check if syslog binary exists first
The command fails in a subshell and strange things happen

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

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

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

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

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

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

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

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

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

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

also, fixes #514
2018-02-08 11:37:58 -08:00
Girish Ramakrishnan 209f37312b createReleaseTarball must use the local branch for master branch 2018-02-08 08:58:20 -08:00
Johannes Zellner 5bd218b3b6 Fix intrinsicFqdn removal breakage 2018-02-08 15:23:38 +01:00
Johannes Zellner d57b772ada We can use js multiline strings 2018-02-08 15:19:00 +01:00
Johannes Zellner b6384d5025 Remove intrinsicFqdn 2018-02-08 15:07:49 +01:00
Johannes Zellner fa65576688 Remove unused require 2018-02-08 15:04:13 +01:00
Johannes Zellner 3572b4eb91 Do not crash if certs cannot be found. Error object does not exist 2018-02-08 10:27:30 +01:00
Johannes Zellner e710a210fd Fixup the unit tests 2018-02-08 09:00:31 +01:00
Johannes Zellner 265db7d0f7 Fix typo in appdb 2018-02-08 09:00:31 +01:00
Johannes Zellner b1939e73f4 Remove all occurances of altDomain in the code
Tests are pending
2018-02-08 09:00:31 +01:00
Johannes Zellner 28f5f62414 Add altDomain migration script 2018-02-08 09:00:31 +01:00
Girish Ramakrishnan ff577a8ed5 stop and disable postfix for good measure 2018-02-07 09:08:04 -08:00
Johannes Zellner 63d06d7024 Use fresh settings key for app autoupdate pattern 2018-02-07 16:51:53 +01:00
Johannes Zellner 4d4b77d6fb Add 1.10.2 changes 2018-02-07 16:27:54 +01:00
Johannes Zellner 3b4ff18881 Keep the invite email for users, which have not yet setup a username 2018-02-07 16:27:49 +01:00
Girish Ramakrishnan d65cb93158 Remove obsolete action 2018-02-06 23:14:02 -08:00
Girish Ramakrishnan e00f98884c setup SPF record of non-primary domain correctly 2018-02-06 23:11:47 -08:00
Girish Ramakrishnan 21016cc2e0 createReleaseTarball: Make sure we pick the current branch on webadmin 2018-02-06 16:20:29 -08:00
Girish Ramakrishnan d12803bb9d Add 1.10.1 changes 2018-02-06 16:11:06 -08:00
Girish Ramakrishnan 039a31318a Generate per-domain enable_outbound relay settings 2018-02-06 14:43:14 -08:00
Johannes Zellner 3eb11ee20a Fixup updatechecker tests 2018-02-06 19:25:03 +01:00
Johannes Zellner 11d740682e Split box and app autoupdate pattern settings 2018-02-06 19:25:03 +01:00
Johannes Zellner 09b33e7ef9 Disable autoupdates by default 2018-02-06 19:25:03 +01:00
Johannes Zellner 19fafca9df Drop users email unique constraint for the migration timeframe 2018-02-06 12:14:11 +01:00
Girish Ramakrishnan da29c69be4 generate per-domain mail configuration 2018-02-05 15:13:35 -08:00
Johannes Zellner c4531e32d5 Fix all app route tests 2018-02-05 22:17:16 +01:00
Johannes Zellner 8f74cacfd0 Remove unused require 2018-02-05 20:45:53 +01:00
Girish Ramakrishnan 9ba830ab21 "installing" is easier to understand 2018-02-05 11:13:51 -08:00
Girish Ramakrishnan ad152bacdd Do not allow dns setup and restore to run in parallel
In the e2e, we did not check the webadminStatus after a dnsSetup
and immediately rushed into restore. This ended up mangling the
cert/key files of the admin domain.
2018-02-05 09:35:16 -08:00
Johannes Zellner 89673fa7f0 Make more of the app route tests work 2018-02-05 17:28:30 +01:00
Johannes Zellner c8613e646b Show more descriptive error message if minBoxVersion blocks update 2018-02-05 15:20:42 +01:00
273 changed files with 27425 additions and 16336 deletions
+29
View File
@@ -0,0 +1,29 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 2017
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-console": "off"
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
# following files are skipped when exporting using git archive
test export-ignore
docs export-ignore
.jshintrc export-ignore
.gitlab export-ignore
.gitattributes export-ignore
.gitignore export-ignore
-1
View File
@@ -6,4 +6,3 @@ installer/src/certs/server.key
# vim swap files
*.swp
+6
View File
@@ -0,0 +1,6 @@
Please do not use this issue tracker for support requests and bug reports.
This issue tracker is used by the Cloudron development team to track actual
bugs in the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
+7
View File
@@ -0,0 +1,7 @@
Please do not use this issue tracker for support requests and feature reports.
This issue tracker is used by the Cloudron development team to track issues in
the code.
Please use the forum at https://forum.cloudron.io to report bugs. For
confidential issues, please email us at support@cloudron.io.
-8
View File
@@ -1,8 +0,0 @@
{
"node": true,
"browser": true,
"unused": true,
"globalstrict": true,
"predef": [ "angular", "$" ],
"esnext": true
}
+327
View File
@@ -1205,3 +1205,330 @@
* Configure Exoscale SOS to use new SOS NG endpoint
* Fix S3 storage backend CopySource encoding rules
[1.11.0]
* Update Haraka to 2.8.17 to fix various crashes
* Report dependency error for clone if backup or domain was not found
* Enable auto-updates for major versions
[2.0.0]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
[2.0.1]
* Multi-domain support
* Update Haraka to 2.8.18
* Split box and app autoupdate pattern settings
* Stop and disable any pre-installed postfix server
* Migrate altDomain as a manual DNS provider
* Use node's native dns resolve instead of dig
* DNS records can now be a A record or a CNAME record
* Fix generation of fallback certificates to include naked domain
* Merge multi-string DKIM records
* scheduler: do not start cron jobs all at once
* scheduler: give cron jobs a grace period of 30 minutes to complete
* Rework the eventlog view
* App clone now clones the robotsTxt and backup settings
[2.1.0]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
[2.1.1]
* Make S3 backend work reliably with slow internet connections
* Update docker to 18.03.0-ce
* Finalize the Email and Mailbox API
* Move mailbox settings from users to email view
* mail: fix issue where hosts with valid SPF for a Cloudron domain are unable to send mail to Cloudron
* mail: fix crash when bounce emails have a null sender
* Add CSP header for dashboard
* Add support for installing private docker images
[2.2.0]
* Add 2FA support for the admin dashboard
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
[2.2.1]
* Add 2FA support for the admin dashboard
* Add Gandi & GoDaddy DNS providers
* Fix zone detection logic on Route53 accounts with more than 100 zones
* Warn using when disabling email
* Cleanup scope management in REST API
* Enhance user creation API to take a password
* Relax restriction on mailbox names now that it is decoupled from user management
* Fix issue where mail container incorrectly advertised CRAM-MD5 support
[2.3.0]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
[2.3.1]
* Add Name.com DNS provider
* Fix issue where account setup page was crashing
* Add advanced DNS configuration UI
* Preserve addon/database configuration across app updates and restores
* ManageSieve port now offers STARTTLS
* Allow mailbox name to be set for apps
* Rework the Email server UI
* Add the ability to manually trigger a backup of an application
* Enable/disable mail from validation within UI
* Allow setting app visibility for non-SSO apps
* Add Clone UI
[2.3.2]
* Fix issue where multi-db apps were not provisioned correctly
* Improve setup, restore views to have field labels
[2.4.0]
* Use custom logging backend to have more control over log rotation
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[2.4.1]
* Use custom logging backend to have more control over log rotation
* Mail logs and box logs UI
* Make user explicitly confirm that fs backup dir is on external storage
* Update node to 8.11.2
* Update docker to 18.03.1
* Fix docker exec terminal resize issue
* Make the mailbox name follow the apps new location, if the user did not set it explicitly
* Add backups view
[3.0.0]
* Support alternate app domains with redirects
* Allow hyphen in mailbox names
* Fix issue where the UI timesout when relay server is not reachable
* Add support for personal spaces
* Add UI to edit users in the groups dialog
* Add UI to set groups when creating a user
* Open logs and terminal in a new tab instead of a window
* Add button to view backup logs
* Add Mailjet mail relay support
* Encryption support for incremental backups
* Display restore errors in the UI
* Update Haraka to 2.8.19
* GPG verify releases
* Allow subdomains in location field
[3.0.1]
* Support alternate app domains with redirects
* Allow hyphen in mailbox names
* Fix issue where the UI timesout when relay server is not reachable
* Add support for personal spaces
* Add UI to edit users in the groups dialog
* Add UI to set groups when creating a user
* Open logs and terminal in a new tab instead of a window
* Add button to view backup logs
* Add Mailjet mail relay support
* Encryption support for incremental backups
* Display restore errors in the UI
* Update Haraka to 2.8.19
* GPG verify releases
* Allow subdomains in location field
[3.0.2]
* Fix issue where normal users are shown apps they don't have access to
* Re-configure email apps when email is enabled/disabled
[3.1.0]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
[3.1.1]
* Fix caas domain migration
[3.1.2]
* Add UDP support
* Clicking invite button does not send an invite immediately
* Implement docker addon
* Automatically login after password reset and account setup
* Make backup interval configurable
* Fix alternate domain certificate renewal
* API token can now have a name
[3.1.3]
* Prevent dashboard domain from being deleted
* Add alternateDomains to app install route
* cloudflare: Fix crash when access denied
[3.1.4]
* Fix issue where support tab was redirecting
[3.2.0]
* Add DO Spaces SFO2 region
* Wildcard DNS now validates the config
* Add ACMEv2 support
* Add Wildcard Let's Encrypt provider
[3.2.1]
* Add acme2 support. This provides DNS based validation removing inbound port 80 requirement
* Add support for wildcard certificates
* Allow mailbox name to be reset to the buit-in '.app' name
* Fix permission issue when restoring a Cloudron
* Fix a crash when restoring Cloudron
* Allow alternate domains to be set in app installation REST API
* Add SFO2 region for DigitalOcean Spaces
* Show the title in port bindings instead of the long description
[3.2.2]
* Update Haraka to 2.8.20
* (mail) Fix issue where LDAP connections where not cleaned up
[3.3.0]
* 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
[3.3.1]
* 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
[3.3.2]
* 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.3.3]
* 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.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
+1 -1
View File
@@ -630,7 +630,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
box
Copyright (C) 2016,2017 Cloudron UG
Copyright (C) 2016-2019 Cloudron UG
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
+6 -1
View File
@@ -48,6 +48,11 @@ apps up-to-date and secure.
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.html)
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
## Documentation
* [Documentation](https://cloudron.io/documentation/)
@@ -59,6 +64,6 @@ the containers in the Cloudron.
## Community
* [Chat](https://chat.cloudron.io/)
* [Forum](https://forum.cloudron.io/)
* [Support](mailto:support@cloudron.io)
+1
View File
@@ -0,0 +1 @@
# release version. do not edit manually
+35 -6
View File
@@ -14,8 +14,11 @@ function die {
export DEBIAN_FRONTEND=noninteractive
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
apt-get -o Dpkg::Options::="--force-confdef" dist-upgrade -y
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
apt-mark unhold grub* >/dev/null
echo "==> Installing required packages"
@@ -23,20 +26,25 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password password pas
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \
acl \
awscli \
build-essential \
cron \
curl \
dmsetup \
$gpg_package \
iptables \
libpython2.7 \
logrotate \
mysql-server-5.7 \
nginx-full \
openssh-server \
pwgen \
rcconf \
resolvconf \
sudo \
swaks \
unattended-upgrades \
unbound \
@@ -61,7 +69,7 @@ echo "==> Installing Docker"
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/docker.deb
rm /tmp/docker.deb
@@ -72,8 +80,9 @@ if [[ "${storage_driver}" != "overlay2" ]]; then
exit 1
fi
# do not upgrade grub because it might prompt user and break this script
echo "==> Enable memory accounting"
apt-get -y install grub2
apt-get -y --no-upgrade install grub2-common
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
update-grub
@@ -83,11 +92,12 @@ if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
exit 1
fi
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
docker pull "${image}"
docker pull "${image%@sha256:*}" # this will tag the image for readability
done
echo "==> Install collectd"
@@ -97,6 +107,11 @@ if ! apt-get install -y collectd collectd-utils; then
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
fi
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
@@ -105,3 +120,17 @@ systemctl disable bind9 || true
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
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
+21 -8
View File
@@ -2,17 +2,21 @@
'use strict';
// 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)));
};
});
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
let async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
dockerProxy = require('./src/dockerproxy.js'),
server = require('./src/server.js');
console.log();
@@ -25,6 +29,9 @@ console.log(' Version: ', config.version());
console.log(' Admin Origin: ', config.adminOrigin());
console.log(' Appstore API server origin: ', config.apiServerOrigin());
console.log(' Appstore Web server origin: ', config.webServerOrigin());
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
console.log(' LDAP Server Port: ', config.get('ldapPort'));
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
console.log();
console.log('==========================================');
console.log();
@@ -32,7 +39,7 @@ console.log();
async.series([
server.start,
ldap.start,
appHealthMonitor.start,
dockerProxy.start
], function (error) {
if (error) {
console.error('Error starting server', error);
@@ -44,13 +51,19 @@ async.series([
var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
console.log('Received SIGINT. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
console.log('Received SIGTERM. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
+6 -5
View File
@@ -6,17 +6,18 @@ var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
// This is triggered by systemd with the crashed unit name as argument
function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
var unitName = process.argv[2];
console.log('Started crash notifier for', unitName);
// mailer needs the db
// eventlog api needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
sendFailureLogs(processName, { unit: processName });
sendFailureLogs(unitName);
});
}
@@ -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);
});
@@ -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) {
@@ -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);
@@ -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) {
@@ -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'),
+1 -1
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);
@@ -0,0 +1,24 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
// migrate the 'daily' update pattern
var appUpdatePattern = results[0].value;
if (appUpdatePattern === '00 00 1,3,5,23 * * *') appUpdatePattern = '00 30 1,3,5,23 * * *';
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', ['autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['app_autoupdate_pattern', appUpdatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,121 @@
'use strict';
var async = require('async'),
crypto = require('crypto'),
fs = require('fs'),
os = require('os'),
path = require('path'),
safe = require('safetydance'),
tldjs = require('tldjs');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, callback) {
if (!app.altDomain) {
console.log('App %s does not use altDomain, skip', app.id);
return callback();
}
const domain = tldjs.getDomain(app.altDomain);
const subdomain = tldjs.getSubdomain(app.altDomain);
const mailboxName = (subdomain ? subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
console.log('App %s is on domain %s and subdomain %s with mailbox', app.id, domain, subdomain, mailboxName);
async.series([
// Add domain if not exists
function (callback) {
const query = 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, domain, 'manual', JSON.stringify({}), JSON.stringify({ provider: 'letsencrypt-prod' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s', domain);
// ensure we have a fallback cert for the newly added domain. This is the same as in reverseproxy.js
// WARNING this will only work on the cloudron itself not during local testing!
const certFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.cert`;
const keyFilePath = `/home/yellowtent/boxdata/certs/${domain}.host.key`;
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) { // generate it
let opensslConf = safe.fs.readFileSync('/etc/ssl/openssl.cnf', 'utf8');
let opensslConfWithSan = `${opensslConf}\n[SAN]\nsubjectAltName=DNS:${domain}\n`;
let configFile = path.join(os.tmpdir(), 'openssl-' + crypto.randomBytes(4).readUInt32LE(0) + '.conf');
let certCommand = `openssl req -x509 -newkey rsa:2048 -keyout ${keyFilePath} -out ${certFilePath} -days 3650 -subj /CN=*.${domain} -extensions SAN -config ${configFile} -nodes`;
safe.fs.writeFileSync(configFile, opensslConfWithSan, 'utf8');
if (!safe.child_process.execSync(certCommand)) return callback(safe.error.message);
safe.fs.unlinkSync(configFile);
}
callback();
});
},
// Add domain to mail table if not exists
function (callback) {
const query = 'INSERT INTO mail (domain, enabled, mailFromValidation, catchAllJson, relayJson) VALUES (?, ?, ?, ?, ?)';
const args = [ domain, 0, 1, '[]', JSON.stringify({ provider: 'cloudron-smtp' }) ];
db.runSql(query, args, function (error) {
if (error && error.code !== 'ER_DUP_ENTRY') return callback(error);
console.log('Added domain %s to mail table', domain);
callback();
});
},
// Remove old mailbox record if any
function (callback) {
const query = 'DELETE FROM mailboxes WHERE ownerId=?';
const args = [ app.id ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Cleaned up mailbox record for app %s', app.id);
callback();
});
},
// Add new mailbox record
function (callback) {
const query = 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)';
const args = [ mailboxName, domain, app.id, 'app' /* mailboxdb.TYPE_APP */ ];
db.runSql(query, args, function (error) {
if (error) return callback(error);
console.log('Added mailbox record for app %s', app.id);
callback();
});
},
// Update app record
function (callback) {
const query = 'UPDATE apps SET location=?, domain=?, altDomain=? WHERE id=?';
const args = [ subdomain, domain, '', app.id ];
db.runSql(query, args, function (error) {
if (error) return error;
console.log('Updated app %s with new domain', app.id);
callback();
});
}
], callback);
}, function (error) {
if (error) return callback(error);
// finally drop the altDomain db field
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', [], callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', [], callback);
};
@@ -0,0 +1,19 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_domain_constraint FOREIGN KEY(domain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_domain_constraint', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,51 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
var users = { }, groupMembers = { };
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN membersJson TEXT'),
function getUsers(done) {
db.all('SELECT * from users', [ ], function (error, results) {
if (error) return done(error);
results.forEach(function (result) { users[result.id] = result; });
done();
});
},
function getGroups(done) {
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM 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) {
var userIds = result.userIds ? result.userIds.split(',') : [];
var members = userIds.map(function (id) { return users[id].username; });
groupMembers[result.id] = members;
});
done();
});
},
function removeGroupIdAndSetMembers(done) {
async.eachSeries(Object.keys(groupMembers), function (gid, iteratorDone) {
console.log(`Migrating group id ${gid} to ${JSON.stringify(groupMembers[gid])}`);
db.runSql('UPDATE mailboxes SET membersJson = ?, ownerId = ? WHERE ownerId = ?', [ JSON.stringify(groupMembers[gid]), 'admin', gid ], iteratorDone);
}, done);
},
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,34 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN type VARCHAR(16)'),
function addMailboxType(done) {
db.all('SELECT * from mailboxes', [ ], function (error, results) {
if (error) return done(error);
async.eachSeries(results, function (mailbox, iteratorCallback) {
let type = 'mailbox';
if (mailbox.aliasTarget) {
type = 'alias';
} else if (mailbox.membersJson) {
type = 'list';
}
db.runSql('UPDATE mailboxes SET type = ? WHERE name = ? AND domain = ?', [ type, mailbox.name, mailbox.domain ], iteratorCallback);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY type VARCHAR(16) NOT NULL'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mailboxes DROP COLUMN membersJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "", ADD COLUMN twoFactorAuthenticationEnabled BOOLEAN DEFAULT false', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP twoFactorAuthenticationSecret, DROP twoFactorAuthenticationEnabled', function (error) {
if (error) console.error(error);
callback(error);
});
};
+21
View File
@@ -0,0 +1,21 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE clients SET scope=? WHERE id=? OR id=? OR id=?', ['*', 'cid-webadmin', 'cid-sdk', 'cid-cli'], function (error) {
if (error) console.error(error);
db.runSql('UPDATE tokens SET scope=? WHERE scope LIKE ?', ['*', '%*%'], function (error) { // remove the roleSdk
if (error) console.error(error);
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
if (error) console.error(error);
callback(error);
});
});
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN ownerId VARCHAR(128)'),
function (next) {
db.all('SELECT id FROM users ORDER BY createdAt LIMIT 1', [ ], function (error, results) {
if (error || results.length === 0) return next(error);
var ownerId = results[0].id;
db.runSql('UPDATE apps SET ownerId=?', [ ownerId ], next);
});
},
db.runSql.bind(db, 'ALTER TABLE apps MODIFY ownerId VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_owner_constraint FOREIGN KEY(ownerId) REFERENCES users(id)'),
db.runSql.bind(db, 'COMMIT'),
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN ownerId', function (error) {
if (error) console.error(error);
callback(error);
});
};
+15
View File
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN ts ', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,25 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS subdomains(' +
'appId VARCHAR(128) NOT NULL,' +
'domain VARCHAR(128) NOT NULL,' +
'subdomain VARCHAR(128) NOT NULL,' +
'type VARCHAR(128) NOT NULL,' +
'dnsRecordId VARCHAR(512),' +
'FOREIGN KEY(domain) REFERENCES domains(domain),' +
'FOREIGN KEY(appId) REFERENCES apps(id),' +
'UNIQUE (subdomain, domain)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE subdomains', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from apps', [ ], function (error, results) {
if (error) return callback(error);
var queries = [
db.runSql.bind(db, 'START TRANSACTION;')
];
results.forEach(function (app) {
queries.push(db.runSql.bind(db, 'INSERT INTO subdomains (appId, domain, subdomain, type, dnsRecordId) VALUES (?, ?, ?, ?, ?)', [ app.id, app.domain, app.location, 'primary', app.dnsRecordId ]));
});
queries.push(db.runSql.bind(db, 'COMMIT'));
async.series(queries, callback);
});
};
exports.down = function(db, callback) {
db.runSql('DELETE FROM subdomains', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,41 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP INDEX location_domain_unique_index, DROP FOREIGN KEY apps_domain_constraint, DROP COLUMN domain, DROP COLUMN location, DROP COLUMN dnsRecordId', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.all('SELECT * from subdomains WHERE type = ?', [ 'primary' ], function (error, results) {
if (error) return callback(error);
var cmd = 'ALTER TABLE apps'
+ ' ADD COLUMN location VARCHAR(128),'
+ ' ADD COLUMN domain VARCHAR(128),'
+ ' ADD COLUMN dnsRecordId VARCHAR(512)';
db.runSql(cmd, function (error) {
if (error) return callback(error);
var queries = [ db.runSql.bind(db, 'START TRANSACTION;') ];
results.forEach(function (d) {
queries.push(db.runSql.bind(db, 'UPDATE apps SET domain = ?, location = ?, dnsRecordId = ? WHERE id = ?', [ d.domain, d.subdomain, d.appId, d.dnsRecordId ]));
});
queries.push(db.runSql.bind(db, 'COMMIT'));
async.series(queries, function (error) {
if (error) return callback(error);
var cmd = 'ALTER TABLE apps'
+ ' ADD CONSTRAINT apps_domain_constraint FOREIGN KEY(domain) REFERENCES domains(domain),'
+ ' ADD UNIQUE location_domain_unique_index (location, domain)';
db.runSql(cmd, callback);
});
});
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE subdomains DROP COLUMN dnsRecordId', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE subdomains ADD COLUMN dnsRecordId VARCHAR(512)', function (error) {
if (error) return callback(error);
callback();
});
};
@@ -0,0 +1,34 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN admin BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
db.all('SELECT userId FROM groupMembers WHERE groupId=?', [ 'admin' ], function (error, results) {
if (error) return callback(error);
if (results.length === 0) return callback();
async.eachSeries(results, function (result, iteratorDone) {
db.runSql('UPDATE users SET admin=1 WHERE id=?', [ result.userId ], iteratorDone);
}, function (error) {
if (error) return callback(error);
async.series([
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
], callback);
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN admin', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,13 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1525636734905, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,18 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings ADD COLUMN type VARCHAR(8) NOT NULL DEFAULT "tcp"'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP INDEX hostPort'), // this drops the unique constraint
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort, type)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP PRIMARY KEY, ADD PRIMARY KEY(hostPort)'),
db.runSql.bind(db, 'ALTER TABLE appPortBindings DROP COLUMN type')
], callback);
};
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
backupConfig.intervalSecs = 24 * 60 * 60;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,23 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
// first check precondtion of domain entry in settings
db.all('SELECT * FROM domains', [ ], function (error, domains) {
if (error) return callback(error);
let caasDomains = domains.filter(function (d) { return d.provider === 'caas'; });
async.eachSeries(caasDomains, function (domain, iteratorCallback) {
let config = JSON.parse(domain.configJson);
config.hyphenatedSubdomains = true;
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,12 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tokens ADD COLUMN name VARCHAR(64) DEFAULT ""', [], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tokens DROP COLUMN name', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,21 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * from domains WHERE provider=?', [ 'manual' ], function (error, results) {
if (error) return callback(error);
async.eachSeries(results, function (result, iteratorDone) {
var config = JSON.parse(result.configJson || '{}');
if (!config.wildcard) return iteratorDone();
delete config.wildcard;
db.runSql('UPDATE domains SET provider=?, configJson=? WHERE domain=?', [ 'wildcard', JSON.stringify(config), result.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,21 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE IF NOT EXISTS appEnvVars(' +
'appId VARCHAR(128) NOT NULL,' +
'name TEXT NOT NULL,' +
'value TEXT NOT NULL,' +
'FOREIGN KEY(appId) REFERENCES apps(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 appEnvVars', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -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);
});
};
@@ -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();
};
@@ -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();
};
@@ -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();
};
@@ -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();
};
@@ -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);
});
};
@@ -0,0 +1,27 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE notifications(' +
'id int NOT NULL AUTO_INCREMENT,' +
'userId VARCHAR(128) NOT NULL,' +
'eventId VARCHAR(128) NOT NULL,' +
'title VARCHAR(512) NOT NULL,' +
'message TEXT,' +
'action VARCHAR(512) NOT NULL,' +
'acknowledged BOOLEAN DEFAULT false,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'FOREIGN KEY(eventId) REFERENCES eventlog(id),' +
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE notifications', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE result resultJson TEXT', [], function (error) {
if (error) console.error(error);
db.runSql('DELETE FROM tasks', callback); // empty tasks table since we have bad results format
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE resultJson result TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN dataDir VARCHAR(256) UNIQUE', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN dataDir', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE domains ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE domains DROP COLUMN locked', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,22 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
// WARNING in the future always give constraints proper names to not rely on automatic ones
db.runSql.bind(db, 'ALTER TABLE notifications DROP FOREIGN KEY notifications_ibfk_1'),
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128) NOT NULL'),
db.runSql.bind(db, 'ALTER TABLE notifications ADD FOREIGN KEY(eventId) REFERENCES eventlog(id)'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
@@ -0,0 +1,23 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM domains', function (error, domains) {
if (error) return callback(error);
async.eachSeries(domains, function (domain, iteratorCallback) {
if (domain.provider !== 'namecheap') return iteratorCallback();
let config = JSON.parse(domain.configJson);
config.token = config.apiKey;
delete config.apiKey;
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'ALTER TABLE apps ADD COLUMN healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN healthTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
+82 -30
View File
@@ -21,13 +21,15 @@ CREATE TABLE IF NOT EXISTS users(
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
modifiedAt VARCHAR(512) NOT NULL,
admin INTEGER NOT NULL,
displayName VARCHAR(512) DEFAULT '',
fallbackEmail VARCHAR(512) DEFAULT ""
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
admin BOOLEAN DEFAULT false,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groups(
CREATE TABLE IF NOT EXISTS userGroups(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE,
PRIMARY KEY(id));
@@ -35,10 +37,11 @@ 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(
name VARCHAR(64) DEFAULT "", // description
accessToken VARCHAR(128) NOT NULL UNIQUE,
identifier VARCHAR(128) NOT NULL,
clientId VARCHAR(128),
@@ -48,7 +51,7 @@ CREATE TABLE IF NOT EXISTS tokens(
CREATE TABLE IF NOT EXISTS clients(
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
appId VARCHAR(128) NOT NULL,
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
type VARCHAR(16) NOT NULL,
clientSecret VARCHAR(512) NOT NULL,
redirectURI VARCHAR(512) NOT NULL,
@@ -62,33 +65,38 @@ 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,
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
updatedAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
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,
altDomain VARCHAR(256),
xFrameOptions VARCHAR(512),
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
debugModeJson TEXT, // options for development mode
robotsTxt TEXT,
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this 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
oldConfigJson TEXT, // used to pass old config for apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config for apptask (update)
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
updateConfigJson TEXT, // used to pass new config to apptask (update)
FOREIGN KEY(domain) REFERENCES domains(domain),
ownerId VARCHAR(128),
FOREIGN KEY(ownerId) REFERENCES users(id),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
hostPort INTEGER NOT NULL UNIQUE,
type VARCHAR(8) NOT NULL DEFAULT "tcp",
environmentVariable VARCHAR(128) NOT NULL,
appId VARCHAR(128) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id),
@@ -113,9 +121,15 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
value VARCHAR(512) NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS appEnvVars(
appId VARCHAR(128) NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
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 */
@@ -130,25 +144,10 @@ CREATE TABLE IF NOT EXISTS eventlog(
action VARCHAR(128) NOT NULL,
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data TEXT, /* free flowing json based on action */
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id));
/* Future fields:
* accessRestriction - to determine who can access it. So this has foreign keys
* quota - per mailbox quota
*/
CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
aliasTarget VARCHAR(128), /* the target name type is an alias */
creationTime TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES domains(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS domains(
domain VARCHAR(128) NOT NULL UNIQUE, /* if this needs to be larger, InnoDB has a limit of 767 bytes for PRIMARY KEY values! */
zoneName VARCHAR(128) NOT NULL, /* this mostly contains the domain itself again */
@@ -174,4 +173,57 @@ CREATE TABLE IF NOT EXISTS mail(
CHARACTER SET utf8 COLLATE utf8_bin;
/* 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, /* user id */
aliasTarget VARCHAR(128), /* the target name type is an alias */
membersJson TEXT, /* members of a group */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS subdomains(
appId VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL,
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, domain));
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),
title VARCHAR(512) NOT NULL,
message TEXT,
action VARCHAR(512) NOT NULL,
acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);
CHARACTER SET utf8 COLLATE utf8_bin;
+6529 -6024
View File
File diff suppressed because it is too large Load Diff
+41 -51
View File
@@ -14,38 +14,39 @@
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^0.7.0",
"@google-cloud/storage": "^1.2.1",
"@google-cloud/dns": "^0.7.2",
"@google-cloud/storage": "^1.7.0",
"@sindresorhus/df": "^2.1.0",
"async": "^2.6.0",
"aws-sdk": "^2.151.0",
"body-parser": "^1.18.2",
"cloudron-manifestformat": "^2.11.0",
"async": "^2.6.1",
"aws-sdk": "^2.253.1",
"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-session": "^1.3.2",
"cron": "^1.3.0",
"cron": "^1.5.1",
"csurf": "^1.6.6",
"db-migrate": "^0.10.0-beta.24",
"db-migrate": "^0.11.1",
"db-migrate-mysql": "^1.1.10",
"debug": "^3.1.0",
"dockerode": "^2.5.3",
"ejs": "^2.5.7",
"ejs-cli": "^2.0.0",
"express": "^4.16.2",
"dockerode": "^2.5.5",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"express": "^4.16.3",
"express-session": "^1.15.6",
"hat": "0.0.3",
"json": "^9.0.3",
"ldapjs": "^1.0.0",
"ldapjs": "^1.0.2",
"lodash.chunk": "^4.2.0",
"mime": "^2.0.3",
"moment-timezone": "^0.5.14",
"mime": "^2.3.1",
"moment-timezone": "^0.5.17",
"morgan": "^1.9.0",
"multiparty": "^4.1.2",
"multiparty": "^4.1.4",
"mysql": "^2.15.0",
"nodemailer": "^4.4.0",
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
"nodemailer": "^4.6.5",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.3.2",
@@ -55,57 +56,46 @@
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"password-generator": "^2.2.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"recursive-readdir": "^2.2.1",
"request": "^2.83.0",
"s3-block-read-stream": "^0.2.0",
"qrcode": "^1.2.0",
"readdirp": "^2.1.0",
"request": "^2.87.0",
"rimraf": "^2.6.2",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^0.7.1",
"semver": "^5.4.1",
"showdown": "^1.8.2",
"semver": "^5.5.0",
"showdown": "^1.8.6",
"speakeasy": "^2.0.0",
"split": "^1.0.0",
"superagent": "^3.8.1",
"supererror": "^0.7.1",
"tar-fs": "^1.16.0",
"tar-stream": "^1.5.5",
"tldjs": "^2.2.0",
"underscore": "^1.7.0",
"uuid": "^3.1.0",
"superagent": "^3.8.3",
"supererror": "^0.7.2",
"tar-fs": "^1.16.2",
"tar-stream": "^1.6.1",
"tldjs": "^2.3.1",
"underscore": "^1.9.1",
"uuid": "^3.2.1",
"valid-url": "^1.0.9",
"validator": "^9.1.1",
"ws": "^3.3.1"
"validator": "^10.3.0",
"ws": "^5.2.0"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",
"expect.js": "*",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^4.0.0",
"gulp-concat": "^2.4.3",
"gulp-cssnano": "^2.1.0",
"gulp-ejs": "^3.1.0",
"gulp-sass": "^3.1.0",
"gulp-serve": "^1.0.0",
"gulp-sourcemaps": "^2.6.1",
"gulp-uglify": "^3.0.0",
"hock": "^1.3.2",
"istanbul": "*",
"js2xmlparser": "^3.0.0",
"mocha": "*",
"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",
"readdirp": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz",
"yargs": "^10.0.3"
"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/[^a]*",
"test_all": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
"test": "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",
"webadmin": "node_modules/.bin/gulp"
"dashboard": "node_modules/.bin/gulp"
}
}
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
set -eu -o pipefail
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400 --http1.1"
ip=""
dns_config=""
tls_cert_file=""
tls_key_file=""
license_file=""
backup_config=""
args=$(getopt -o "" -l "ip:,backup-config:,license:,dns-config:,tls-cert:,tls-key:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--ip) ip="$2"; shift 2;;
--dns-config) dns_config="$2"; shift 2;;
--tls-cert) tls_cert_file="$2"; shift 2;;
--tls-key) tls_key_file="$2"; shift 2;;
--license) license_file="$2"; shift 2;;
--backup-config) backup_config="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# validate arguments in the absence of data
if [[ -z "${ip}" ]]; then
echo "--ip is required"
exit 1
fi
if [[ -z "${dns_config}" ]]; then
echo "--dns-config is required"
exit 1
fi
if [[ ! -f "${license_file}" ]]; then
echo "--license must be a valid license file"
exit 1
fi
function get_status() {
key="$1"
if status=$($curl -q -f -k "https://${ip}/api/v1/cloudron/status" 2>/dev/null); then
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
echo "${currentValue}"
return 0
fi
return 1
}
function wait_for_status() {
key="$1"
expectedValue="$2"
echo "wait_for_status: $key to be $expectedValue"
while true; do
if currentValue=$(get_status "${key}"); then
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
if [[ "${currentValue}" == $expectedValue ]]; then
break
fi
fi
sleep 3
done
}
echo "=> Waiting for cloudron to be ready"
wait_for_status "version" '*'
domain=$(echo "${dns_config}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["domain"])')
echo "Provisioning Cloudron ${domain}"
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
else
fallback_cert=None
fi
tls_config='{ "provider": "fallback" }'
dns_config=$(echo "${dns_config}" | python3 -c "import json,sys;obj=json.load(sys.stdin);obj.update(tlsConfig=${tls_config});obj.update(fallbackCertficate=${fallback_cert});print(json.dumps(obj))")
license=$(cat "${license_file}")
if [[ -z "${backup_config:-}" ]]; then
backup_config='{ "provider": "filesystem", "backupFolder": "/var/backups", "format": "tgz" }'
fi
setupData=$(printf '{ "dnsConfig": %s, "autoconf": { "appstoreConfig": %s, "backupConfig": %s } }' "${dns_config}" "${license}" "${backup_config}")
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
echo "Failed to setup with ${setupData} ${setupResult}"
exit 1
fi
wait_for_status "webadminStatus" '*"tls": true*'
echo "Cloudron is ready at https://my-${domain}"
+98 -44
View File
@@ -2,19 +2,8 @@
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $(lsb_release -rs) != "16.04" ]]; then
echo "Cloudron requires Ubuntu 16.04" > /dev/stderr
exit 1
fi
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
@@ -26,6 +15,10 @@ readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
readonly RED='\033[31m'
readonly GREEN='\033[32m'
readonly DONE='\033[m'
# verify the system has minimum requirements met
if [[ "${rootfs_type}" != "ext4" ]]; then
echo "Error: Cloudron requires '/' to be ext4" # see #364
@@ -42,67 +35,92 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
exit 1
fi
if systemctl -q is-active box; then
echo "Error: Cloudron is already installed. To reinstall, start afresh"
exit 1
fi
initBaseImage="true"
# provisioning data
provider=""
edition=""
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
baseDataDir=""
args=$(getopt -o "" -l "help,skip-baseimage-init,data-dir:,provider:,version:,env:,prerelease,skip-reboot" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--edition) edition="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
prerelease="true"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--prerelease) prerelease="true"; shift;;
--data-dir) baseDataDir=$(realpath "$2"); shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# Only --help works as non-root
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
# Only --help works with mismatched ubuntu
ubuntu_version=$(lsb_release -rs)
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
exit 1
fi
# Can only write after we have confirmed script has root access
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
if [[ -z "${provider}" ]]; then
echo "--provider is required (azure, cloudscale, digitalocean, ec2, exoscale, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
echo "--provider is required (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, 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" && \
"${provider}" != "digitalocean" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "netcup" && \
"${provider}" != "netcup-image" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, 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
@@ -119,24 +137,36 @@ echo ""
echo " Follow setup logs in a second terminal with:"
echo " $ tail -f ${LOG_FILE}"
echo ""
echo " Join us at https://chat.cloudron.io for any questions."
echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> 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"
echo "Could not update package repositories. See ${LOG_FILE}"
exit 1
fi
if ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl)"
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
exit 1
fi
fi
echo "=> Checking version"
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?prerelease=${prerelease}&boxVersion=${requestedVersion}"); then
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
echo "Failed to get release information"
exit 1
fi
@@ -152,18 +182,6 @@ if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=
exit 1
fi
# Build data
# from 1.9, we use autoprovision.json
data=$(cat <<EOF
{
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
echo "=> Downloading version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
@@ -181,13 +199,44 @@ if [[ "${initBaseImage}" == "true" ]]; then
echo ""
fi
# NOTE: this install script only supports 3.x and above
echo "=> Installing version ${version} (this takes some time) ..."
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
if [[ "${version}" =~ 3\.[0-2]+\.[0-9]+ ]]; then
readonly DATA_FILE="/root/cloudron-install-data.json"
data=$(cat <<EOF
{
"provider": "${provider}",
"edition": "${edition}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"version": "${version}"
}
EOF
)
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
else
mkdir -p /etc/cloudron
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
{
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"provider": "${provider}",
"edition": "${edition}"
}
CONF_END
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
fi
rm "${DATA_FILE}"
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
@@ -198,10 +247,15 @@ while true; do
sleep 10
done
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
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\nRebooting this server now to let bootloader changes take effect.\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
+115
View File
@@ -0,0 +1,115 @@
#!/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
echo "This script should be run as root." > /dev/stderr
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
ssh_user="root"
ssh_folder="/root/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
else
ssh_user="$SUDO_USER"
ssh_folder="/home/$SUDO_USER/.ssh/"
authorized_key_file="${ssh_folder}/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo -e $LINE"cloudron.conf"$LINE >> $OUT
cat /etc/cloudron/cloudron.conf &>> $OUT
echo -e $LINE"Docker container"$LINE >> $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
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
echo "Done"
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
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 email the following link to support@cloudron.io"
echo ""
echo "${PASTEBIN}/${paste_key}"
+33 -42
View File
@@ -7,21 +7,28 @@ set -eu
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "output:" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "output:,version:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
bundle_file=""
version=""
while true; do
case "$1" in
--output) bundle_file="$2"; shift 2;;
--version) version="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
if [[ -z "${version}" ]]; then
echo "--version is required"
exit 1
fi
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
@@ -29,65 +36,49 @@ if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
exit 1
fi
if ! $(cd "${SOURCE_DIR}/../webadmin" && git diff --exit-code >/dev/null); then
echo "You have local changes in webadmin, stash or commit them to proceed"
if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
echo "You have local changes in dashboard, stash or commit them to proceed"
exit 1
fi
if [[ "$(node --version)" != "v8.9.3" ]]; then
echo "This script requires node 8.9.3"
if [[ "$(node --version)" != "v8.11.2" ]]; then
echo "This script requires node 8.11.2"
exit 1
fi
box_version=$(cd "${SOURCE_DIR}" && git rev-parse "HEAD")
branch=$(git rev-parse --abbrev-ref HEAD)
webadmin_version=$(cd "${SOURCE_DIR}/../webadmin" && git fetch && git rev-parse "origin/${branch}")
dashboard_version=$(cd "${SOURCE_DIR}/../dashboard" && git fetch && git rev-parse "${branch}")
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${webadmin_version:0:10}.tar.gz"
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${box_version:0:10}-${dashboard_version:0:10}-${version}.tar.gz"
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
echo "Checking out code box version [${box_version}] and webadmin version [${webadmin_version}] into ${bundle_dir}"
echo "==> Checking out code box version [${box_version}] and dashboard version [${dashboard_version}] into ${bundle_dir}"
(cd "${SOURCE_DIR}" && git archive --format=tar ${box_version} | (cd "${bundle_dir}" && tar xf -))
(cd "${SOURCE_DIR}/../webadmin" && git archive --format=tar ${webadmin_version} | (cd "${bundle_dir}" && tar xf -))
(cp "${SOURCE_DIR}/../webadmin/LICENSE" "${bundle_dir}")
(cd "${SOURCE_DIR}/../dashboard" && git archive --format=tar ${dashboard_version} | (mkdir -p "${bundle_dir}/dashboard.build" && cd "${bundle_dir}/dashboard.build" && tar xf -))
(cp "${SOURCE_DIR}/../dashboard/LICENSE" "${bundle_dir}")
echo "${version}" > "${bundle_dir}/VERSION"
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.all" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing dev modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
else
echo "Installing modules with dev dependencies"
(cd "${bundle_dir}" && npm install)
echo "==> Installing modules for dashboard asset generation"
(cd "${bundle_dir}/dashboard.build" && npm install --production)
echo "Caching dev dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-all"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-all/"
cp "${bundle_dir}/package-lock.json" "${TMPDIR}/boxtarball.cache/package-lock.json.all"
fi
echo "==> Building dashboard assets"
(cd "${bundle_dir}/dashboard.build" && ./node_modules/.bin/gulp --revision ${dashboard_version})
echo "Building webadmin assets"
(cd "${bundle_dir}" && ./node_modules/.bin/gulp)
echo "==> Move built dashboard assets into destination"
mkdir -p "${bundle_dir}/dashboard"
mv "${bundle_dir}/dashboard.build/dist" "${bundle_dir}/dashboard/"
echo "Remove intermediate files required at build-time only"
rm -rf "${bundle_dir}/node_modules/"
rm -rf "${bundle_dir}/webadmin/src"
rm -rf "${bundle_dir}/gulpfile.js"
echo "==> Cleanup dashboard build artifacts"
rm -rf "${bundle_dir}/dashboard.build"
if diff "${TMPDIR}/boxtarball.cache/package-lock.json.prod" "${bundle_dir}/package-lock.json" >/dev/null 2>&1; then
echo "Reusing prod modules from cache"
cp -r "${TMPDIR}/boxtarball.cache/node_modules-prod/." "${bundle_dir}/node_modules"
else
echo "Installing modules for production"
(cd "${bundle_dir}" && npm install --production --no-optional)
echo "==> Installing toplevel node modules"
(cd "${bundle_dir}" && npm install --production --no-optional)
echo "Caching prod dependencies"
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-prod"
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-prod/"
cp "${bundle_dir}/package-lock.json" "${TMPDIR}/boxtarball.cache/package-lock.json.prod"
fi
echo "Create final tarball"
echo "==> Create final tarball"
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
echo "Cleaning up ${bundle_dir}"
echo "==> Cleaning up ${bundle_dir}"
rm -rf "${bundle_dir}"
echo "Tarball saved at ${bundle_file}"
echo "==> Tarball saved at ${bundle_file}"
+48 -36
View File
@@ -1,5 +1,9 @@
#!/bin/bash
# This script is run before the box code is switched. This means that we can
# put network related/curl downloads here. If the script fails, the old code
# will continue to run
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
@@ -10,36 +14,19 @@ fi
readonly USER=yellowtent
readonly BOX_SRC_DIR=/home/${USER}/box
readonly BASE_DATA_DIR=/home/${USER}
readonly CLOUDRON_CONF=/home/yellowtent/configs/cloudron.conf
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
readonly is_update=$([[ -f "${CLOUDRON_CONF}" ]] && echo "yes" || echo "no")
arg_data=""
arg_data_dir=""
args=$(getopt -o "" -l "data:,data-file:,data-dir:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--data) arg_data="$2"; shift 2;;
--data-file) arg_data=$(cat $2); shift 2;;
--data-dir) arg_data_dir="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.09.0~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
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) != "d33f6eb134f0ab0876148bd96de95ea47d583d7f2cddfdc6757979453f9bd9bf" ]]; then
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
echo "==> installer: docker binary download is corrupt"
exit 5
fi
@@ -54,6 +41,12 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
sleep 1
done
# the latest docker might need newer packages
while ! apt update -y; do
echo "==> installer: Failed to update packages. Retry"
sleep 1
done
while ! apt install -y /tmp/docker.deb; do
echo "==> installer: Failed to install docker. Retry"
sleep 1
@@ -63,14 +56,15 @@ if [[ $(docker version --format {{.Client.Version}}) != "17.09.0-ce" ]]; then
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v8.9.3" ]]; then
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
if [[ "$(node --version)" != "v8.11.2" ]]; then
mkdir -p /usr/local/node-8.11.2
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
rm -rf /usr/local/node-6.11.5
fi
# this is here (and not in updater.js) because rebuild requires the above node
for try in `seq 1 10`; do
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
@@ -87,6 +81,33 @@ if [[ ${try} -eq 10 ]]; then
exit 4
fi
echo "==> installer: downloading new addon images"
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
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"
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
CLOUDRON_SYSLOG_VERSION="1.0.3"
while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLOUDRON_SYSLOG_VERSION} ]]; do
rm -rf "${CLOUDRON_SYSLOG_DIR}"
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@${CLOUDRON_SYSLOG_VERSION}; then break; fi
echo "===> installer: Failed to install cloudron-syslog, trying again"
sleep 5
done
if ! id "${USER}" 2>/dev/null; then
useradd "${USER}" -m
fi
@@ -96,15 +117,6 @@ if [[ "${is_update}" == "yes" ]]; then
${BOX_SRC_DIR}/setup/stop.sh
fi
# setup links to data directory
if [[ -n "${arg_data_dir}" ]]; then
echo "==> installer: setting up links to data directory"
mkdir "${arg_data_dir}/appsdata"
ln -s "${arg_data_dir}/appsdata" "${BASE_DATA_DIR}/appsdata"
mkdir "${arg_data_dir}/platformdata"
ln -s "${arg_data_dir}/platformdata" "${BASE_DATA_DIR}/platformdata"
fi
# ensure we are not inside the source directory, which we will remove now
cd /root
@@ -114,4 +126,4 @@ mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
echo "==> installer: calling box setup script"
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
"${BOX_SRC_DIR}/setup/start.sh"
-71
View File
@@ -1,71 +0,0 @@
#!/bin/bash
source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
json="${source_dir}/../node_modules/.bin/json"
arg_api_server_origin=""
arg_fqdn="" # remove after 1.10
arg_admin_domain=""
arg_admin_location=""
arg_admin_fqdn=""
arg_retire_reason=""
arg_retire_info=""
arg_version=""
arg_web_server_origin=""
arg_provider=""
arg_is_demo="false"
args=$(getopt -o "" -l "data:,retire-reason:,retire-info:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--retire-reason)
arg_retire_reason="$2"
shift 2
;;
--retire-info)
arg_retire_info="$2"
shift 2
;;
--data)
# these params must be valid in all cases
arg_fqdn=$(echo "$2" | $json fqdn)
arg_admin_fqdn=$(echo "$2" | $json adminFqdn)
arg_admin_location=$(echo "$2" | $json adminLocation)
[[ "${arg_admin_location}" == "" ]] && arg_admin_location="my"
arg_admin_domain=$(echo "$2" | $json adminDomain)
[[ "${arg_admin_domain}" == "" ]] && arg_admin_domain="${arg_fqdn}"
# only update/restore have this valid (but not migrate)
arg_api_server_origin=$(echo "$2" | $json apiServerOrigin)
[[ "${arg_api_server_origin}" == "" ]] && arg_api_server_origin="https://api.cloudron.io"
arg_web_server_origin=$(echo "$2" | $json webServerOrigin)
[[ "${arg_web_server_origin}" == "" ]] && arg_web_server_origin="https://cloudron.io"
# TODO check if and where this is used
arg_version=$(echo "$2" | $json version)
# read possibly empty parameters here
arg_is_demo=$(echo "$2" | $json isDemo)
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_provider=$(echo "$2" | $json provider)
[[ "${arg_provider}" == "" ]] && arg_provider="generic"
shift 2
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
echo "Parsed arguments:"
echo "api server: ${arg_api_server_origin}"
echo "admin fqdn: ${arg_admin_fqdn}"
echo "fqdn: ${arg_fqdn}"
echo "version: ${arg_version}"
echo "web server: ${arg_web_server_origin}"
echo "provider: ${arg_provider}"
+27 -62
View File
@@ -2,6 +2,9 @@
set -eu -o pipefail
# This script is run after the box code is switched. This means that this script
# should pretty much always succeed. No network logic/download code here.
echo "==> Cloudron Start"
readonly USER="yellowtent"
@@ -10,19 +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 curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
readonly ubuntu_version=$(lsb_release -rs)
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
timedatectl set-ntp 1
timedatectl set-timezone UTC
hostnamectl set-hostname "${arg_admin_fqdn}"
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
@@ -48,19 +44,6 @@ if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /e
fi
docker network create --subnet=172.18.0.0/16 cloudron || true
# caas has ssh on port 202 and we disable password login
if [[ "${arg_provider}" == "caas" ]]; then
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
sed -e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
-e 's/^#\?Port .*/Port 202/g' \
-i /etc/ssh/sshd_config
# required so we can connect to this machine since port 22 is blocked by iptables by now
systemctl reload sshd
fi
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
@@ -71,11 +54,14 @@ mkdir -p "${PLATFORM_DATA_DIR}/graphite"
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/redis"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
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" "${PLATFORM_DATA_DIR}/logs/updater" "${PLATFORM_DATA_DIR}/logs/tasks"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
mkdir -p "${BOX_DATA_DIR}/certs"
@@ -104,22 +90,22 @@ 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}"
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
systemctl enable cloudron.target
systemctl enable cloudron-firewall
@@ -132,6 +118,9 @@ systemctl enable --now cron
# ensure unbound runs
systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
echo "==> Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
@@ -146,6 +135,8 @@ echo "==> Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
cp "${script_dir}/start/box-logrotate" "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/"
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/box-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
echo "==> Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
@@ -163,14 +154,8 @@ if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.servi
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# remove this migration after 1.10
[[ -f /etc/nginx/cert/host.cert ]] && cp /etc/nginx/cert/host.cert "/etc/nginx/cert/${arg_admin_domain}.host.cert"
[[ -f /etc/nginx/cert/host.key ]] && cp /etc/nginx/cert/host.key "/etc/nginx/cert/${arg_admin_domain}.host.key"
systemctl start nginx
# bookkeep the version as part of data
echo "{ \"version\": \"${arg_version}\", \"apiServerOrigin\": \"${arg_api_server_origin}\" }" > "${BOX_DATA_DIR}/version"
# restart mysql to make sure it has latest config
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
# wait for all running mysql jobs
@@ -194,32 +179,7 @@ mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
echo "==> Creating cloudron.conf"
cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
{
"version": "${arg_version}",
"apiServerOrigin": "${arg_api_server_origin}",
"webServerOrigin": "${arg_web_server_origin}",
"adminDomain": "${arg_admin_domain}",
"adminFqdn": "${arg_admin_fqdn}",
"adminLocation": "${arg_admin_location}",
"provider": "${arg_provider}",
"isDemo": ${arg_is_demo}
}
CONF_END
echo "==> Creating config.json for webadmin"
cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
{
"webServerOrigin": "${arg_web_server_origin}"
}
CONF_END
(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)"
@@ -230,10 +190,15 @@ else
fi
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/logrotate.d" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup"
# 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}"
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
# do not chown the boxdata/mail directory; dovecot gets upset
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
+10
View File
@@ -0,0 +1,10 @@
# logrotate config for app logs
/home/yellowtent/platformdata/logs/*/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
size 10M
# we never compress so we can simply tail the files
nocompress
copytruncate
}
+9
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
}
+26 -12
View File
@@ -1,15 +1,29 @@
#!/bin/sh
# motd hook to remind admins about updates
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
#!/bin/bash
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
printf "\t\t\tWELCOME TO CLOUDRON\n"
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://<IP> on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
else
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
printf "Please do not run apt upgrade manually as it will update packages that\n"
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
fi
fi
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
printf "**********************************************************************\n"
+20 -8
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/createappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.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/rmappdir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmappdir.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
+6 -2
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
@@ -0,0 +1,14 @@
[Unit]
Description=Cloudron Syslog
After=network.target
[Service]
ExecStart=/usr/local/cloudron-syslog/bin/cloudron-syslog --port 2514 --logdir /home/yellowtent/platformdata/logs
WorkingDirectory=/usr/local/cloudron-syslog
Environment="NODE_ENV=production"
Restart=always
User=yellowtent
Group=yellowtent
[Install]
WantedBy=multi-user.target
+143
View File
@@ -0,0 +1,143 @@
'use strict';
exports = module.exports = {
SCOPE_APPS_READ: 'apps:read',
SCOPE_APPS_MANAGE: 'apps:manage',
SCOPE_CLIENTS: 'clients',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_DOMAINS_READ: 'domains:read',
SCOPE_DOMAINS_MANAGE: 'domains:manage',
SCOPE_MAIL: 'mail',
SCOPE_PROFILE: 'profile',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS_READ: 'users:read',
SCOPE_USERS_MANAGE: 'users:manage',
SCOPE_APPSTORE: 'appstore',
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
SCOPE_ANY: '*',
validateScopeString: validateScopeString,
hasScopes: hasScopes,
canonicalScopeString: canonicalScopeString,
intersectScopes: intersectScopes,
validateToken: validateToken,
scopesForUser: scopesForUser
};
var assert = require('assert'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:accesscontrol'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
_ = require('underscore');
// returns scopes that does not have wildcards and is sorted
function canonicalScopeString(scope) {
if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(',');
return scope.split(',').sort().join(',');
}
function intersectScopes(allowedScopes, wantedScopes) {
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
let wantedScopesMap = new Map();
let results = [];
// make a map of scope -> [ subscopes ]
for (let w of wantedScopes) {
let parts = w.split(':');
let subscopes = wantedScopesMap.get(parts[0]) || new Set();
subscopes.add(parts[1] || '*');
wantedScopesMap.set(parts[0], subscopes);
}
for (let a of allowedScopes) {
let parts = a.split(':');
let as = parts[1] || '*';
let subscopes = wantedScopesMap.get(parts[0]);
if (!subscopes) continue;
if (subscopes.has('*') || subscopes.has(as)) {
results.push(a);
} else if (as === '*') {
results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; }));
}
}
return results;
}
function validateScopeString(scope) {
assert.strictEqual(typeof scope, 'string');
if (scope === '') return new Error('Empty scope not allowed');
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
// us not write a migration script every time we add a new scope
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; });
if (!allValid) return new Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
return null;
}
// tests if all requiredScopes are attached to the request
function hasScopes(authorizedScopes, requiredScopes) {
assert(Array.isArray(authorizedScopes), 'Expecting array');
assert(Array.isArray(requiredScopes), 'Expecting array');
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
for (var i = 0; i < requiredScopes.length; ++i) {
const scopeParts = requiredScopes[i].split(':');
// this allows apps:write if the token has a higher apps scope
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
debug('scope: missing scope "%s".', requiredScopes[i]);
return new Error('Missing required scope "' + requiredScopes[i] + '"');
}
}
return null;
}
function scopesForUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
if (user.admin) return callback(null, exports.VALID_SCOPES);
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
}
function validateToken(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error); // this triggers 'internal error' in passport
users.get(token.identifier, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error);
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
callback(null, user, info);
});
});
});
}
+1183 -180
View File
File diff suppressed because it is too large Load Diff
@@ -66,8 +66,9 @@ server {
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# ciphers according to https://weakdh.org/sysadmin.html
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -89,6 +90,11 @@ server {
add_header Referrer-Policy "no-referrer-when-downgrade";
proxy_hide_header Referrer-Policy;
<% if ( endpoint === 'admin' ) { -%>
# CSP headers for the admin/dashboard resources
add_header Content-Security-Policy "default-src 'none'; connect-src wss: https: 'self' *.cloudron.io; script-src https: 'self' 'unsafe-inline' 'unsafe-eval'; img-src * data:; style-src https: 'unsafe-inline'; object-src 'none'; font-src https: 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';";
<% } -%>
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
@@ -106,7 +112,7 @@ server {
proxy_set_header Connection $connection_upgrade;
# only serve up the status page if we get proxy gateway errors
root <%= sourceDir %>/webadmin/dist;
root <%= sourceDir %>/dashboard/dist;
error_page 502 503 504 /appstatus.html;
location /appstatus.html {
internal;
@@ -154,17 +160,22 @@ server {
}
# graphite paths (uncomment block below and visit /graphite/index.html)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
# proxy_pass http://127.0.0.1:8000;
# proxy_pass http://127.0.0.1:8417;
# client_max_body_size 1m;
# }
location / {
root <%= sourceDir %>/webadmin/dist;
root <%= sourceDir %>/dashboard/dist;
index index.html index.htm;
}
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'redirect' ) { %>
# redirect everything to the app. this is temporary because there is no way
# to clear a permanent redirect on the browser
return 302 https://<%= redirectTo %>$request_uri;
<% } %>
}
}
+213 -47
View File
@@ -18,12 +18,16 @@ exports = module.exports = {
getAddonConfigByName: getAddonConfigByName,
unsetAddonConfig: unsetAddonConfig,
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
setHealth: setHealth,
setInstallationCommand: setInstallationCommand,
setRunCommand: setRunCommand,
getAppStoreIds: getAppStoreIds,
setOwner: setOwner,
transferOwnership: transferOwnership,
// installation codes (keep in sync in UI)
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
ISTATE_PENDING_CLONE: 'pending_clone', // clone
@@ -39,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',
@@ -47,6 +51,10 @@ exports = module.exports = {
HEALTH_ERROR: 'error',
HEALTH_DEAD: 'dead',
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
_clear: clear
};
@@ -54,17 +62,19 @@ var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.domain', 'apps.dnsRecordId',
'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.altDomain', 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime' ].join(',');
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
@@ -89,14 +99,16 @@ function postProcess(result) {
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
result.portBindings = { };
var hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
var environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
delete result.hostPorts;
delete result.environmentVariables;
delete result.portTypes;
for (var i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
for (let i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
}
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
@@ -109,10 +121,28 @@ 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);
delete result.debugModeJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
delete d.type;
});
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
delete result.envNames;
delete result.envValues;
result.env = {};
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) {
@@ -120,14 +150,25 @@ function get(id, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE apps.id = ? GROUP BY apps.id', [ id ], function (error, result) {
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
@@ -136,14 +177,24 @@ function getByHttpPort(httpPort, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE httpPort = ? GROUP BY apps.id', [ httpPort ], function (error, result) {
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
@@ -152,14 +203,24 @@ function getByContainerId(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, result[0]);
result[0].alternateDomains = alternateDomains;
postProcess(result[0]);
callback(null, result[0]);
});
});
}
@@ -167,24 +228,41 @@ function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' GROUP BY apps.id ORDER BY apps.id', function (error, results) {
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
+ ' FROM apps'
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, results);
alternateDomains.forEach(function (d) {
var domain = results.find(function (a) { return d.appId === a.id; });
if (!domain) return;
domain.alternateDomains = domain.alternateDomains || [];
domain.alternateDomains.push(d);
});
results.forEach(postProcess);
callback(null, results);
});
});
}
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
@@ -196,31 +274,48 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
var accessRestriction = data.accessRestriction || null;
var accessRestrictionJson = JSON.stringify(accessRestriction);
var memoryLimit = data.memoryLimit || 0;
var altDomain = data.altDomain || null;
var xFrameOptions = data.xFrameOptions || '';
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
var sso = 'sso' in data ? data.sso : null;
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var env = data.env || {};
const mailboxName = data.mailboxName || null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, domain, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, restoreConfigJson, sso, debugModeJson ]
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, 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({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
});
Object.keys(portBindings).forEach(function (env) {
queries.push({
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, appId) VALUES (?, ?, ?)',
args: [ env, portBindings[env], id ]
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
});
});
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
Object.keys(env).forEach(function (name) {
queries.push({
query: 'INSERT INTO mailboxes (name, domain, ownerId, ownerType) VALUES (?, ?, ?, ?)',
args: [ data.mailboxName, domain, id, mailboxdb.TYPE_APP ]
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, env[name] ]
});
});
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
});
});
}
@@ -253,18 +348,19 @@ function getPortBindings(id, callback) {
var portBindings = { };
for (var i = 0; i < results.length; i++) {
portBindings[results[i].environmentVariable] = results[i].hostPort;
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
}
callback(null, portBindings);
});
}
function delPortBinding(hostPort, callback) {
function delPortBinding(hostPort, type, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=?', [ hostPort ], function (error, result) {
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
@@ -277,14 +373,15 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
var queries = [
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM subdomains WHERE appId = ?', 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 ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[3].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
@@ -294,8 +391,10 @@ function clear(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
database.query.bind(null, 'DELETE FROM subdomains'),
database.query.bind(null, 'DELETE FROM appPortBindings'),
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
database.query.bind(null, 'DELETE FROM appEnvVars'),
database.query.bind(null, 'DELETE FROM apps')
], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -314,6 +413,8 @@ function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
assert(!('env' in app) || typeof app.env === 'object');
var queries = [ ];
@@ -322,8 +423,30 @@ function updateWithConstraints(id, app, constraints, callback) {
// replace entries by app id
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
Object.keys(portBindings).forEach(function (env) {
var values = [ portBindings[env], env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, environmentVariable, appId) VALUES(?, ?, ?)', args: values });
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
});
}
if ('env' in app) {
queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] });
Object.keys(app.env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, app.env[name] ]
});
});
}
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) {
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
});
}
@@ -332,7 +455,7 @@ function updateWithConstraints(id, app, constraints, callback) {
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings') {
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
fields.push(p + ' = ?');
values.push(app[p]);
}
@@ -353,12 +476,13 @@ function updateWithConstraints(id, app, constraints, callback) {
}
// not sure if health should influence runState
function setHealth(appId, health, callback) {
function setHealth(appId, health, healthTime, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
assert(util.isDate(healthTime));
assert.strictEqual(typeof callback, 'function');
var values = { health: health };
var values = { health, healthTime };
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
@@ -491,6 +615,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');
@@ -504,3 +642,31 @@ function getAddonConfigByName(appId, addonId, name, callback) {
callback(null, results[0].value);
});
}
function setOwner(appId, ownerId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
callback(null);
});
}
function transferOwnership(oldOwnerId, newOwnerId, callback) {
assert.strictEqual(typeof oldOwnerId, 'string');
assert.strictEqual(typeof newOwnerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
+95 -101
View File
@@ -5,32 +5,28 @@ var appdb = require('./appdb.js'),
assert = require('assert'),
async = require('async'),
DatabaseError = require('./databaseerror.js'),
config = require('./config.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
eventlog = require('./eventlog.js'),
superagent = require('superagent'),
util = require('util');
exports = module.exports = {
start: start,
stop: stop
run: run
};
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
var gHealthInfo = { }; // { time, emailSent }
var gRunTimeout = null;
var gDockerEventStream = null;
const 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 OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
const AUDIT_SOURCE = { userId: null, username: 'healthmonitor' };
function debugApp(app) {
assert(!app || typeof app === 'object');
assert(typeof app === 'object');
var prefix = app ? app.intrinsicFqdn : '(no app)';
var manifestAppId = app ? app.manifest.id : '';
var id = app ? app.id : '';
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
debug(app.fqdn + ' ' + app.manifest.id + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
}
function setHealth(app, health, callback) {
@@ -38,27 +34,29 @@ function setHealth(app, health, callback) {
assert.strictEqual(typeof health, 'string');
assert.strictEqual(typeof callback, 'function');
var now = new Date();
if (!(app.id in gHealthInfo)) { // add new apps to list
gHealthInfo[app.id] = { time: now, emailSent: false };
}
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
if (health === appdb.HEALTH_HEALTHY) {
gHealthInfo[app.id].time = now;
} else if (Math.abs(now - gHealthInfo[app.id].time) > UNHEALTHY_THRESHOLD) {
if (gHealthInfo[app.id].emailSent) return callback(null);
healthTime = now;
if (curHealth && curHealth !== appdb.HEALTH_HEALTHY) { // app starts out with null health
debugApp(app, 'app switched from %s to healthy', curHealth);
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, AUDIT_SOURCE, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (curHealth === appdb.HEALTH_HEALTHY) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
if (!app.debugMode) mailer.appDied(app); // do not send mails for dev apps
gHealthInfo[app.id].emailSent = true;
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, AUDIT_SOURCE, { app: app });
}
} else {
debugApp(app, 'waiting for sometime to update the app health');
debugApp(app, 'waiting for %s seconds to update the app health', (UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000);
return callback(null);
}
appdb.setHealth(app.id, health, function (error) {
appdb.setHealth(app.id, health, healthTime, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
if (error) return callback(error);
@@ -71,6 +69,9 @@ function setHealth(app, health, callback) {
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
@@ -90,6 +91,9 @@ function checkAppHealth(app, callback) {
return setHealth(app, appdb.HEALTH_DEAD, callback);
}
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
// poll through docker network instead of nginx to bypass any potential oauth proxy
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
superagent
@@ -112,7 +116,60 @@ function checkAppHealth(app, callback) {
});
}
function processApps(callback) {
/*
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(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
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) {
if (error) return callback(error);
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
var containerId = ev.id;
appdb.getByContainerId(containerId, function (error, app) { // this can error for addons
var program = error || !app.id ? containerId : `app-${app.id}`;
var now = Date.now();
const notifyUser = (!app || !app.debugMode) && (now - gLastOomMailTime > OOM_MAIL_LIMIT);
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now, ev);
// do not send mails for dev apps
if (notifyUser) {
// app can be null for addon containers
eventlog.add(eventlog.ACTION_APP_OOM, AUDIT_SOURCE, { ev: ev, containerId: containerId, app: app || null });
gLastOomMailTime = now;
}
});
});
stream.on('error', function (error) {
debug('Error reading docker events', error);
callback();
});
stream.on('end', callback);
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
});
}
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, result) {
if (error) return callback(error);
@@ -130,79 +187,16 @@ function processApps(callback) {
});
}
function run() {
processApps(function (error) {
if (error) console.error(error);
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
});
}
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents() {
// note that for some reason, the callback is called only on the first event
debug('Listening for docker events');
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
var lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return console.error(error);
gDockerEventStream = stream;
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';
debug('OOM Context: %s', context);
// 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;
}
});
});
stream.on('error', function (error) {
console.error('Error reading docker events', error);
gDockerEventStream = null; // TODO: reconnect?
});
stream.on('end', function () {
console.error('Docker event stream ended');
gDockerEventStream = null; // TODO: reconnect?
});
});
}
function start(callback) {
function run(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
debug('Starting apphealthmonitor');
async.series([
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs)
], function (error) {
if (error) debug(error);
processDockerEvents();
run();
callback();
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
clearTimeout(gRunTimeout);
if (gDockerEventStream) gDockerEventStream.end();
callback();
callback();
});
}
+481 -278
View File
File diff suppressed because it is too large Load Diff
+114 -70
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
unpurchase: unpurchase,
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
@@ -18,9 +19,12 @@ exports = module.exports = {
AppstoreError: AppstoreError
};
var assert = require('assert'),
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
os = require('os'),
@@ -86,18 +90,21 @@ function getSubscription(callback) {
});
}
function purchase(appId, appstoreId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof callback, 'function');
function isFreePlan(subscription) {
return !subscription || subscription.plan.id === 'free';
}
if (appstoreId === '') return callback(null);
// See app.js install it will create a db record first but remove it again if appstore purchase fails
function purchase(appId, data, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object');
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof callback, 'function');
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
var data = { appstoreId: appstoreId };
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
@@ -111,13 +118,12 @@ function purchase(appId, appstoreId, callback) {
});
}
function unpurchase(appId, appstoreId, callback) {
function unpurchase(appId, data, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof appstoreId, 'string');
assert.strictEqual(typeof data, 'object');
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof callback, 'function');
if (appstoreId === '') return callback(null);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
@@ -127,9 +133,9 @@ function unpurchase(appId, appstoreId, callback) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
superagent.del(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
@@ -140,62 +146,87 @@ function unpurchase(appId, appstoreId, callback) {
});
}
function sendAliveStatus(data, callback) {
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var allSettings, allDomains, mailDomains, loginEvents;
mail.getAll(function (error, mailDomains) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
loginEvents = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !result[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: mailDomains.length
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
var data = {
version: config.version(),
adminFqdn: config.adminFqdn(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
});
callback(null);
});
});
});
@@ -210,17 +241,17 @@ function getBoxUpdate(callback) {
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/boxupdate';
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
if (result.statusCode === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
var updateInfo = result.body;
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
}
// { version, changelog, upgrade, sourceTarballUrl}
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
callback(null, updateInfo);
});
});
@@ -242,9 +273,12 @@ function getAppUpdate(app, callback) {
const updateInfo = result.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(app.manifest.version, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, app.manifest.version, updateInfo);
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
@@ -281,16 +315,26 @@ function sendFeedback(info, callback) {
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
}
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (result) info.app = result;
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
callback(null);
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
callback(null);
});
});
});
}
+220 -92
View File
@@ -10,22 +10,16 @@ exports = module.exports = {
_reserveHttpPort: reserveHttpPort,
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createVolume: createVolume,
_deleteVolume: deleteVolume,
_createAppDir: createAppDir,
_deleteAppDir: deleteAppDir,
_verifyManifest: verifyManifest,
_registerSubdomain: registerSubdomain,
_unregisterSubdomain: unregisterSubdomain,
_waitForDnsPropagation: waitForDnsPropagation,
_waitForAltDomainDnsPropagation: waitForAltDomainDnsPropagation
_waitForDnsPropagation: waitForDnsPropagation
};
require('supererror')({ splatchError: true });
// remove timestamp from debug() based output
require('debug').formatArgs = function formatArgs(args) {
args[0] = this.namespace + ' ' + args[0];
};
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
@@ -38,29 +32,31 @@ var addons = require('./addons.js'),
debug = require('debug')('box:apptask'),
docker = require('./docker.js'),
domains = require('./domains.js'),
DomainError = domains.DomainError,
DomainsError = domains.DomainsError,
ejs = require('ejs'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
mkdirp = require('mkdirp'),
net = require('net'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
reverseProxy = require('./reverseproxy.js'),
rimraf = require('rimraf'),
safe = require('safetydance'),
shell = require('./shell.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
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'),
RMAPPDIR_CMD = path.join(__dirname, 'scripts/rmappdir.sh'),
CREATEAPPDIR_CMD = path.join(__dirname, 'scripts/createappdir.sh');
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');
@@ -71,8 +67,7 @@ function initialize(callback) {
function debugApp(app) {
assert.strictEqual(typeof app, 'object');
var prefix = app ? (app.intrinsicFqdn || '(bare)') : '(no app)';
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
// updates the app object and the database
@@ -141,32 +136,62 @@ function createContainer(app, 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);
});
}
function createVolume(app, callback) {
function createAppDir(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('createVolume', [ CREATEAPPDIR_CMD, app.id ], callback);
mkdirp(path.join(paths.APPS_DATA_DIR, app.id), callback);
}
function deleteVolume(app, options, callback) {
function deleteAppDir(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('deleteVolume', [ RMAPPDIR_CMD, app.id, !!options.removeDirectory ], callback);
const appDataDir = path.join(paths.APPS_DATA_DIR, app.id);
// resolve any symlinked data dir
const stat = safe.fs.lstatSync(appDataDir);
if (!stat) return callback(null);
const resolvedAppDataDir = stat.isSymbolicLink() ? safe.fs.readlinkSync(appDataDir) : appDataDir;
if (safe.fs.existsSync(resolvedAppDataDir)) {
const entries = safe.fs.readdirSync(resolvedAppDataDir);
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));
});
}
// if this fails, it's probably because the localstorage/redis addons have not cleaned up properly
if (options.removeDirectory) {
if (stat.isSymbolicLink()) {
if (!safe.fs.unlinkSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
} else {
if (!safe.fs.rmdirSync(appDataDir)) return callback(safe.error.code === 'ENOENT' ? null : safe.error);
}
}
callback(null);
}
function addCollectdProfile(app, callback) {
@@ -176,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);
});
}
@@ -186,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);
});
}
@@ -205,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);
});
});
}
@@ -214,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) {
@@ -266,26 +291,27 @@ function registerSubdomain(app, overwrite, callback) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.intrinsicFqdn, overwrite);
debugApp(app, 'Registering subdomain location [%s] overwrite: %s', app.fqdn, overwrite);
// get the current record before updating it
domains.getDNSRecords(app.location, app.domain, 'A', function (error, values) {
domains.getDnsRecords(app.location, app.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDNSRecords(app.location, app.domain, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
retryCallback(null, error || changeId);
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
// dnsRecordId tracks whether we created this DNS record so that we can unregister later
updateApp(app, { dnsRecordId: result }, callback);
callback(null);
});
});
}
@@ -296,31 +322,97 @@ function unregisterSubdomain(app, location, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!app.dnsRecordId) {
debugApp(app, 'Skip unregister of record not created by cloudron');
return callback(null);
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s', app.intrinsicFqdn);
debugApp(app, 'Unregistering subdomain: %s', app.fqdn);
domains.removeDNSRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainError.STILL_BUSY || error.reason === DomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
domains.removeDnsRecords(location, domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null); // domain can be not found if oldConfig.domain or restoreConfig.domain was removed
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
updateApp(app, { dnsRecordId: null }, callback);
callback(null);
});
});
}
function registerAlternateDomains(app, overwrite, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (domain, callback) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering alternate subdomain [%s] overwrite: %s', (domain.subdomain ? (domain.subdomain + '.') : '') + domain.domain, overwrite);
// get the current record before updating it
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
if (error) return retryCallback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) {
debug('Upsert error. Will retry.', error.message);
return retryCallback(error); // try again
}
retryCallback(null, error);
});
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
});
}, callback);
});
}
function unregisterAlternateDomains(app, all, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof all, 'boolean');
assert.strictEqual(typeof callback, 'function');
let obsoleteDomains = [];
if (all) {
obsoleteDomains = app.alternateDomains;
} else if (app.oldConfig) { // oldConfig can be null during an infra update
obsoleteDomains = app.oldConfig.alternateDomains.filter(function (o) {
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
});
}
if (obsoleteDomains.length === 0) return callback();
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(obsoleteDomains, function (domain, callback) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && error.reason === DomainsError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === DomainsError.STILL_BUSY || error.reason === DomainsError.EXTERNAL_ERROR)) return retryCallback(error); // try again
retryCallback(null, error);
});
}, function (error, result) {
if (error || result instanceof Error) return callback(error || result);
callback();
});
}, callback);
});
}
function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -331,6 +423,18 @@ function removeIcon(app, callback) {
});
}
function cleanupLogs(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
// note that redis container logs are cleaned up by the addon
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
callback(null);
});
}
function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -343,25 +447,28 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
domains.waitForDNSRecord(app.intrinsicFqdn, app.domain, ip, 'A', { interval: 5000, times: 120 }, callback);
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
if (error) return callback(error);
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, iteratorCallback);
}, callback);
});
});
}
function waitForAltDomainDnsPropagation(app, callback) {
if (!app.altDomain) return callback(null);
function migrateDataDir(app, sourceDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof sourceDir, 'string');
assert.strictEqual(typeof callback, 'function');
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
let resolvedSourceDir = apps.getDataDir(app, sourceDir);
let resolvedTargetDir = apps.getDataDir(app, app.dataDir);
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), ip, 'A', { interval: 10000, times: 60 }, callback);
});
} else {
domains.waitForDNSRecord(app.altDomain, tld.getDomain(app.altDomain), app.intrinsicFqdn + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
debug(`migrateDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, callback);
}
// Ordering is based on the following rationale:
@@ -392,10 +499,14 @@ function install(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
// oldConfig can be null during upgrades
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : app.manifest.addons),
deleteVolume.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
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 appdata dir
// for restore case
function deleteImageIfChanged(done) {
@@ -412,11 +523,14 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, isRestoring /* overwrite */),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
createVolume.bind(null, app),
updateApp.bind(null, app, { installationProgress: '50, Creating app data directory' }),
createAppDir.bind(null, app),
function restoreFromBackup(next) {
if (!restoreConfig) {
@@ -426,8 +540,10 @@ function install(app, callback) {
], next);
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig),
updateApp.bind(null, app, { installationProgress: '65, Download backup and restoring addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
], next);
}
},
@@ -446,9 +562,6 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !restoreConfig
updateApp.bind(null, app, { installationProgress: '95, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
@@ -472,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) {
@@ -494,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.intrinsicFqdn !== app.intrinsicFqdn);
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' }),
@@ -502,7 +616,8 @@ function configure(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
unregisterAlternateDomains.bind(null, app, false /* all */),
function (next) {
if (!locationChanged) return next();
@@ -514,19 +629,29 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app, !locationChanged /* overwrite */), // if location changed, do not overwrite to detect conflicts
updateApp.bind(null, app, { installationProgress: '35, Registering alternate domains'}),
registerAlternateDomains.bind(null, app, true /* overwrite */), // figure out when to overwrite
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
createVolume.bind(null, app),
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),
@@ -541,9 +666,6 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
@@ -583,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);
@@ -601,7 +723,7 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteContainers.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
@@ -613,14 +735,14 @@ function update(app, callback) {
// free unused ports
function (next) {
// make sure we always have objects
var currentPorts = app.portBindings || {};
var newPorts = app.updateConfig.manifest.tcpPorts || {};
const currentPorts = app.portBindings || {};
const newTcpPorts = app.updateConfig.manifest.tcpPorts || {};
const newUdpPorts = app.updateConfig.manifest.udpPorts || {};
async.each(Object.keys(currentPorts), function (portName, callback) {
if (newPorts[portName]) return callback(); // port still in use
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(); // port still in use
appdb.delPortBinding(currentPorts[portName], function (error) {
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) console.error('Portbinding does not exist in database.');
else if (error) return next(error);
@@ -687,26 +809,30 @@ 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' }),
deleteVolume.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '40, Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '60, Unregistering subdomain' }),
updateApp.bind(null, app, { installationProgress: '60, Unregistering domains' }),
unregisterAlternateDomains.bind(null, app, true /* all */),
unregisterSubdomain.bind(null, app, app.location, app.domain),
updateApp.bind(null, app, { installationProgress: '80, Cleanup icon' }),
updateApp.bind(null, app, { installationProgress: '70, Cleanup icon' }),
removeIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Unconfiguring reverse proxy' }),
updateApp.bind(null, app, { installationProgress: '80, Unconfiguring reverse proxy' }),
unconfigureReverseProxy.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Cleanup logs' }),
cleanupLogs.bind(null, app),
updateApp.bind(null, app, { installationProgress: '95, Remove app from database' }),
appdb.del.bind(null, app.id)
], function seriesDone(error) {
@@ -795,6 +921,8 @@ function startTask(appId, callback) {
if (require.main === module) {
assert.strictEqual(process.argv.length, 3, 'Pass the appid as argument');
// add a separator for the log file
debug('------------------------------------------------------------');
debug('Apptask for %s', process.argv[2]);
process.on('SIGTERM', function () {
-125
View File
@@ -1,125 +0,0 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
accessTokenAuth: accessTokenAuth
};
var assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
clients = require('./clients'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
ClientsError = clients.ClientsError,
DatabaseError = require('./databaseerror'),
debug = require('debug')('box:auth'),
LocalStrategy = require('passport-local').Strategy,
crypto = require('crypto'),
passport = require('passport'),
tokendb = require('./tokendb'),
user = require('./user'),
UserError = user.UserError,
_ = require('underscore');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
passport.deserializeUser(function(userId, callback) {
user.get(userId, function (error, result) {
if (error) return callback(error);
var md5 = crypto.createHash('md5').update(result.email).digest('hex');
result.gravatar = 'https://www.gravatar.com/avatar/' + md5 + '.jpg?s=24&d=mm';
callback(null, result);
});
});
passport.use(new LocalStrategy(function (username, password, callback) {
if (username.indexOf('@') === -1) {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
});
} else {
user.verifyWithEmail(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, _.pick(result, 'id', 'username', 'email', 'admin'));
});
}
}));
passport.use(new BasicStrategy(function (username, password, callback) {
if (username.indexOf('cid-') === 0) {
debug('BasicStrategy: detected client id %s instead of username:password', username);
// username is actually client id here
// password is client secret
clients.get(username, function (error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret != password) return callback(null, false);
return callback(null, client);
});
} else {
user.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
}
}));
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function(error, client) {
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
if (error) { return callback(error); }
if (client.clientSecret != clientSecret) { return callback(null, false); }
return callback(null, client);
});
}));
passport.use(new BearerStrategy(accessTokenAuth));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
function accessTokenAuth(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.get(accessToken, function (error, token) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// scopes here can define what capabilities that token carries
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
});
});
}
+483 -241
View File
File diff suppressed because it is too large Load Diff
+1 -137
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');
-479
View File
@@ -1,479 +0,0 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme'),
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 AcmeError(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(AcmeError, Error);
AcmeError.INTERNAL_ERROR = 'Internal Error';
AcmeError.EXTERNAL_ERROR = 'External Error';
AcmeError.ALREADY_EXISTS = 'Already Exists';
AcmeError.NOT_COMPLETED = 'Not Completed';
AcmeError.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme(options) {
assert.strictEqual(typeof options, 'object');
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
this.accountKeyPem = null; // Buffer
this.email = options.email;
}
Acme.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');
}
Acme.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);
});
});
};
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
debug('updateContact: contact of user updated to %s', that.email);
callback();
});
};
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerUser: registered user %s', that.email);
callback(null);
});
};
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('registerDomain: registered %s', domain);
callback(null, result.body);
});
};
Acme.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 AcmeError(AcmeError.INTERNAL_ERROR, error));
callback();
});
};
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 202) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new AcmeError(AcmeError.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
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
var certUrl = result.headers.location;
if (!certUrl) return callback(new AcmeError(AcmeError.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);
});
};
Acme.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 AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
// TODO: download the chain in a loop following 'up' header
Acme.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
debug('downloadChain: linkHeader %s', linkHeader);
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
var chainDer = result.text;
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
callback(null, chainPem);
});
};
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
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 AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
that.downloadChain(result.header['link'], function (error, chainPem) {
if (error) return callback(error);
var certificateFile = path.join(outdir, domain + '.cert');
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
callback();
});
});
};
Acme.prototype.acmeFlow = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
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 AcmeError(AcmeError.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);
});
});
};
Acme.prototype.getCertificate = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
this.acmeFlow(domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
});
};
function getCertificate(domain, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme(options || { });
acme.getCertificate(domain, callback);
}
+636
View File
@@ -0,0 +1,636 @@
'use strict';
var assert = require('assert'),
async = require('async'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
_ = require('underscore');
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme',
_getChallengeSubdomain: getChallengeSubdomain
};
function Acme2Error(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(Acme2Error, Error);
Acme2Error.INTERNAL_ERROR = 'Internal Error';
Acme2Error.EXTERNAL_ERROR = 'External Error';
Acme2Error.ALREADY_EXISTS = 'Already Exists';
Acme2Error.NOT_COMPLETED = 'Not Completed';
Acme2Error.FORBIDDEN = 'Forbidden';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = null; // Buffer
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
this.directory = {};
this.performHttpAuthorization = !!options.performHttpAuthorization;
this.wildcard = !!options.wildcard;
}
Acme2.prototype.getNonce = function (callback) {
superagent.get(this.directory.newNonce).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 204) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
});
};
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = util.isBuffer(str) ? str : new Buffer(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = 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;
return Buffer.from(match[1], 'hex');
}
Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
const that = this;
let header = {
url: url,
alg: 'RS256'
};
// keyId is null when registering account
if (this.keyId) {
header.kid = this.keyId;
} else {
header.jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
}
var payload64 = b64(payload);
this.getNonce(function (error, nonce) {
if (error) return callback(error);
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
protected: protected64,
payload: payload64,
signature: signature64
};
superagent.post(url).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
if (error && !error.response) return callback(error); // network errors
callback(null, res);
});
});
};
Acme2.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
// https://github.com/ietf-wg-acme/acme/issues/30
const payload = {
contact: [ 'mailto:' + this.email ]
};
const that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
debug(`updateContact: contact of user updated to ${that.email}`);
callback();
});
};
Acme2.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
var that = this;
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
// 200 if already exists. 201 for new accounts
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
debug(`registerUser: user registered keyid: ${result.headers.location}`);
that.keyId = result.headers.location;
that.updateContact(result.headers.location, callback);
});
};
Acme2.prototype.newOrder = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
identifiers: [{
type: 'dns',
value: domain
}]
};
debug('newOrder: %s', domain);
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
callback(null, order, orderUrl);
});
};
Acme2.prototype.waitForOrder = function (orderUrl, callback) {
assert.strictEqual(typeof orderUrl, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`waitForOrder: ${orderUrl}`);
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitForOrder: getting status');
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForOrder: network error getting uri %s', orderUrl);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 200) {
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
});
}, callback);
};
Acme2.prototype.getKeyAuthorization = function (token) {
assert(util.isBuffer(this.accountKeyPem));
let jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
let shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
let thumbprint = urlBase64Encode(shasum.digest('base64'));
return token + '.' + thumbprint;
};
Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.url);
const keyAuthorization = this.getKeyAuthorization(challenge.token);
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
callback();
});
};
Acme2.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) {
debug('waitForChallenge: network error getting uri %s', challenge.url);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
}
if (result.statusCode !== 200) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof finalizationUrl, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
const payload = {
csr: b64(csrDer)
};
debug('signCertificate: sending sign request');
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
return callback(null);
});
};
Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
var csrFile = path.join(outdir, `${certName}.csr`);
var privateKeyFile = path.join(outdir, `${certName}.key`);
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = 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 = 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
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
superagent.get(certUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
const fullChainPem = result.text;
const certName = hostname.replace('*.', '_.');
var certificateFile = path.join(outdir, `${certName}.cert`);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
callback();
});
};
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
callback(null, challenge);
});
};
Acme2.prototype.cleanupHttpChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.unlink(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), callback);
};
function getChallengeSubdomain(hostname, domain) {
let challengeSubdomain;
if (hostname === domain) {
challengeSubdomain = '_acme-challenge';
} else if (hostname.includes('*')) { // wildcard
let subdomain = hostname.slice(0, -domain.length - 1);
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
} else {
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
}
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
return challengeSubdomain;
}
Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
let challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
callback(null, challenge);
});
});
};
Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
callback(null);
});
};
Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof callback, 'function');
const that = this;
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
const authorization = response.body;
if (that.performHttpAuthorization) {
that.prepareHttpChallenge(hostname, domain, authorization, callback);
} else {
that.prepareDnsChallenge(hostname, domain, authorization, callback);
}
});
};
Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
if (this.performHttpAuthorization) {
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
} else {
this.cleanupDnsChallenge(hostname, domain, challenge, callback);
}
};
Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
that.newOrder(hostname, function (error, order, orderUrl) {
if (error) return callback(error);
async.eachSeries(order.authorizations, function (authorizationUrl, iteratorCallback) {
debug(`acmeFlow: authorizing ${authorizationUrl}`);
that.prepareChallenge(hostname, domain, authorizationUrl, function (error, challenge) {
if (error) return iteratorCallback(error);
async.waterfall([
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, hostname),
that.signCertificate.bind(that, hostname, order.finalize),
that.waitForOrder.bind(that, orderUrl),
that.downloadCertificate.bind(that, hostname)
], function (error) {
that.cleanupChallenge(hostname, domain, challenge, function (cleanupError) {
if (cleanupError) debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
iteratorCallback(error);
});
});
});
}, callback);
});
});
};
Acme2.prototype.getDirectory = function (callback) {
const that = this;
superagent.get(this.caDirectory).timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) return callback(error);
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching directory : ' + response.statusCode));
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') return callback(new Error(`Invalid response body : ${response.body}`));
that.directory = response.body;
callback(null);
});
};
Acme2.prototype.getCertificate = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getCertificate: start acme flow for ${hostname} from ${this.caDirectory}`);
if (hostname !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
hostname = domains.makeWildcard(hostname);
debug(`getCertificate: will get wildcard cert for ${hostname}`);
}
const that = this;
this.getDirectory(function (error) {
if (error) return callback(error);
that.acmeFlow(hostname, domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
callback(null, path.join(outdir, `${certName}.cert`), path.join(outdir, `${certName}.key`));
});
});
};
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var acme = new Acme2(options || { });
acme.getCertificate(hostname, domain, callback);
}
+4 -3
View File
@@ -10,12 +10,13 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', vhost);
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
+4 -3
View File
@@ -10,12 +10,13 @@ exports = module.exports = {
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(vhost, options, callback) {
assert.strictEqual(typeof vhost, 'string');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', vhost);
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
+2 -1
View File
@@ -12,7 +12,8 @@ exports = module.exports = {
var assert = require('assert');
function getCertificate(domain, options, callback) {
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
+3 -3
View File
@@ -182,8 +182,8 @@ function clear(callback) {
function addDefaultClients(callback) {
async.series([
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', 'cloudron,profile,users,apps,settings'),
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*,roleSdk'),
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*,roleSdk')
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', '*'),
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*'),
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*')
], callback);
}
+68 -60
View File
@@ -8,26 +8,16 @@ exports = module.exports = {
del: del,
getAll: getAll,
getByAppIdAndType: getByAppIdAndType,
getClientTokensByUserId: getClientTokensByUserId,
delClientTokensByUserId: delClientTokensByUserId,
getTokensByUserId: getTokensByUserId,
delTokensByUserId: delTokensByUserId,
delByAppIdAndType: delByAppIdAndType,
addClientTokenByUserId: addClientTokenByUserId,
addTokenByUserId: addTokenByUserId,
delToken: delToken,
issueDeveloperToken: issueDeveloperToken,
addDefaultClients: addDefaultClients,
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
SCOPE_APPS: 'apps',
SCOPE_DEVELOPER: 'developer', // obsolete
SCOPE_PROFILE: 'profile',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_SETTINGS: 'settings',
SCOPE_USERS: 'users',
// roles are handled just like the above scopes, they are parallel to scopes
// scopes enclose API groups, roles specify the usage role
SCOPE_ROLE_SDK: 'roleSdk',
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
@@ -39,10 +29,15 @@ var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clientdb = require('./clientdb.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:clients'),
hat = require('hat'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
accesscontrol = require('./accesscontrol.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
UsersError = users.UsersError,
util = require('util'),
uuid = require('uuid');
@@ -73,7 +68,7 @@ ClientsError.NOT_FOUND = 'Not found';
ClientsError.INTERNAL_ERROR = 'Internal Error';
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
function validateName(name) {
function validateClientName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
@@ -84,24 +79,10 @@ function validateName(name) {
return null;
}
function validateScope(scope) {
assert.strictEqual(typeof scope, 'string');
function validateTokenName(name) {
assert.strictEqual(typeof name, 'string');
var VALID_SCOPES = [
exports.SCOPE_APPS,
exports.SCOPE_DEVELOPER,
exports.SCOPE_PROFILE,
exports.SCOPE_CLOUDRON,
exports.SCOPE_SETTINGS,
exports.SCOPE_USERS,
'*', // includes all scopes, but not roles
exports.SCOPE_ROLE_SDK
];
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
return null;
}
@@ -113,14 +94,10 @@ function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
// allow whitespace
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
var error = accesscontrol.validateScopeString(scope);
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
var error = validateScope(scope);
if (error) return callback(error);
// appId is also client name
error = validateName(appId);
error = validateClientName(appId);
if (error) return callback(error);
var id = 'cid-' + uuid.v4();
@@ -191,7 +168,7 @@ function getAll(callback) {
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
record.domain = result.altDomain || result.intrinsicFqdn;
record.domain = result.fqdn;
tmp.push(record);
@@ -216,7 +193,7 @@ function getByAppIdAndType(appId, type, callback) {
});
}
function getClientTokensByUserId(clientId, userId, callback) {
function getTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -235,7 +212,7 @@ function getClientTokensByUserId(clientId, userId, callback) {
});
}
function delClientTokensByUserId(clientId, userId, callback) {
function delTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -275,31 +252,65 @@ function delByAppIdAndType(appId, type, callback) {
});
}
function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const name = options.name || '';
let error = validateTokenName(name);
if (error) return callback(error);
get(clientId, function (error, result) {
if (error) return callback(error);
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
users.get(userId, function (error, user) {
if (error && error.reason === UsersError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such user'));
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
identifier: userId,
clientId: result.id,
scope: result.id,
expires: expiresAt
accesscontrol.scopesForUser(user, function (error, userScopes) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
var scope = accesscontrol.canonicalScopeString(result.scope);
var authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
var token = tokendb.generateToken();
tokendb.add(token, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
callback(null, {
accessToken: token,
tokenScopes: authorizedScopes,
identifier: userId,
clientId: result.id,
expires: expiresAt
});
});
});
});
});
}
// this issues a cid-cli token that does not require a password in various routes
function issueDeveloperToken(userObject, auditSource, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
callback(null, result);
});
}
function delToken(clientId, tokenId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof tokenId, 'string');
@@ -324,13 +335,10 @@ function addDefaultClients(origin, callback) {
debug('Adding default clients');
// The domain might have changed, therefor we have to update the record
// !!! This needs to be in sync with the webadmin, specifically login_callback.js
const ADMIN_SCOPES = 'cloudron,developer,profile,users,apps,settings';
// id, appId, type, clientSecret, redirectURI, scope
async.series([
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, ADMIN_SCOPES),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*,roleSdk'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*, roleSdk')
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, '*'),
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*'),
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
], callback);
}
+194 -202
View File
@@ -9,43 +9,49 @@ exports = module.exports = {
getDisks: getDisks,
getLogs: getLogs,
updateToLatest: updateToLatest,
reboot: reboot,
isRebootRequired: isRebootRequired,
onActivated: onActivated,
checkDiskSpace: checkDiskSpace
prepareDashboardDomain: prepareDashboardDomain,
setDashboardDomain: setDashboardDomain,
renewCerts: renewCerts,
runSystemChecks: runSystemChecks,
// exposed for testing
_checkDiskSpace: checkDiskSpace
};
var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
caas = require('./caas.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'),
eventlog = require('./eventlog.js'),
locker = require('./locker.js'),
mailer = require('./mailer.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'),
updateChecker = require('./updatechecker.js'),
user = require('./user.js'),
util = require('util'),
_ = require('underscore');
tasks = require('./tasks.js'),
users = require('./users.js'),
util = require('util');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
UPDATE_CMD = path.join(__dirname, 'scripts/update.sh');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -73,41 +79,50 @@ 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');
async.series([
settings.initialize,
reverseProxy.configureDefaultServer,
cron.initialize, // required for caas heartbeat before activation
onActivated
], callback);
cron.startPreActivationJobs(callback);
runStartupTasks();
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
cron.uninitialize,
platform.stop,
settings.uninitialize
cron.stopJobs,
platform.stop
], callback);
}
function onActivated(callback) {
callback = callback || NOOP_CALLBACK;
assert.strictEqual(typeof callback, 'function');
// Starting the platform after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
user.count(function (error, count) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
if (!count) return callback(); // not activated
async.series([
platform.start,
cron.startPostActivationJobs
], callback);
}
platform.start(callback);
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
// configure nginx to be reachable by IP
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
// check activation state and start the platform
users.isActivated(function (error, activated) {
if (error) return debug(error);
if (!activated) return debug('initialize: not activated yet'); // not activated
onActivated(NOOP_CALLBACK);
});
}
@@ -140,172 +155,66 @@ function getDisks(callback) {
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
// result to not depend on the appstore
const BOX_AND_USER_TEMPLATE = {
box: {
region: null,
size: null,
plan: 'Custom Plan'
},
user: {
billing: false,
currency: ''
}
};
settings.getAll(function (error, allSettings) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
caas.getBoxAndUserDetails(function (error, result) {
if (error) debug('Failed to fetch cloudron details.', error.reason, error.message);
result = _.extend(BOX_AND_USER_TEMPLATE, result || {});
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
adminLocation: config.adminLocation(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
update: updateChecker.getUpdateInfo(),
progress: progress.getAll(),
isDemo: config.isDemo(),
region: result.box.region,
size: result.box.size,
billing: !!result.user.billing,
plan: result.box.plan,
currency: result.user.currency,
memory: os.totalmem(),
provider: config.provider(),
cloudronName: cloudronName
});
// be picky about what we send out here since this is sent for 'normal' users as well
callback(null, {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
adminFqdn: config.adminFqdn(),
mailFqdn: config.mailFqdn(),
version: config.version(),
isDemo: config.isDemo(),
edition: config.edition(),
memory: os.totalmem(),
provider: config.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
});
});
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], callback);
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
}
function update(boxUpdateInfo, auditSource, callback) {
assert.strictEqual(typeof boxUpdateInfo, 'object');
assert.strictEqual(typeof auditSource, 'object');
function isRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
if (!boxUpdateInfo) return callback(null);
var error = locker.lock(locker.OP_BOX_UPDATE);
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
// ensure tools can 'wait' on progress
progress.set(progress.UPDATE, 0, 'Starting');
// initiate the update/upgrade but do not wait for it
if (boxUpdateInfo.upgrade) {
debug('Starting upgrade');
caas.upgrade(boxUpdateInfo, function (error) {
if (error) {
debug('Upgrade failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
} else {
debug('Starting update');
doUpdate(boxUpdateInfo, function (error) {
if (error) {
debug('Update failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
}
callback(null);
// 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'));
}
function updateToLatest(auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
// called from cron.js
function runSystemChecks() {
async.parallel([
checkBackupConfiguration,
checkDiskSpace,
checkMailStatus
], function () {
debug('runSystemChecks: done');
});
}
function checkBackupConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
if (!boxUpdateInfo.sourceTarballUrl) return callback(new CloudronError(CloudronError.BAD_STATE, 'No automatic update available'));
debug('Checking backup configuration');
// check if this is just a version number change
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
doShortCircuitUpdate(boxUpdateInfo, function (error) {
if (error) debug('Short-circuit update failed', error);
});
settings.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
return callback(null);
}
if (boxUpdateInfo.upgrade && config.provider() !== 'caas') return callback(new CloudronError(CloudronError.SELF_UPGRADE_NOT_SUPPORTED));
update(boxUpdateInfo, auditSource, callback);
}
function doShortCircuitUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
config.setVersion(boxUpdateInfo.version);
progress.clear(progress.UPDATE);
updateChecker.resetUpdateInfo();
callback();
}
function doUpdate(boxUpdateInfo, callback) {
assert(boxUpdateInfo && typeof boxUpdateInfo === 'object');
function updateError(e) {
progress.set(progress.UPDATE, -1, e.message);
callback(e);
}
progress.set(progress.UPDATE, 5, 'Backing up for update');
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
if (error) return updateError(error);
// NOTE: this data is opaque and will be passed through the installer.sh
var data= {
provider: config.provider(),
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin(),
adminDomain: config.adminDomain(),
adminFqdn: config.adminFqdn(),
adminLocation: config.adminLocation(),
isDemo: config.isDemo(),
appstore: {
apiServerOrigin: config.apiServerOrigin()
},
caas: {
apiServerOrigin: config.apiServerOrigin(),
webServerOrigin: config.webServerOrigin()
},
version: boxUpdateInfo.version
};
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
shell.sudo('update', [ UPDATE_CMD, boxUpdateInfo.sourceTarballUrl, JSON.stringify(data) ], function (error) {
if (error) return updateError(error);
// Do not add any code here. The installer script will stop the box code any instant
});
if (backupConfig.provider === 'noop') {
notifications.backupConfigWarning('Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://cloudron.io/documentation/backups/#storage-providers for more information.');
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
notifications.backupConfigWarning('Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://cloudron.io/documentation/backups/#storage-providers for storing backups in an external location.');
}
});
}
function checkDiskSpace(callback) {
callback = callback || NOOP_CALLBACK;
assert.strictEqual(typeof callback, 'function');
debug('Checking disk space');
@@ -335,53 +244,81 @@ function checkDiskSpace(callback) {
debug('Disk space checked. ok: %s', !oos);
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
if (oos) notifications.diskSpaceWarning(JSON.stringify(entries, null, 4));
callback();
}).catch(function (error) {
debug('df error %s', error.message);
mailer.outOfDiskSpace(error.message);
return callback();
if (error) console.error(error);
callback();
});
});
}
function getLogs(options, callback) {
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking mail status');
domains.getAll(function (error, allDomains) {
if (error) return callback(error);
async.filterSeries(allDomains, function (domainObject, iteratorCallback) {
mail.getStatus(config.adminDomain(), function (error, result) {
if (error) return iteratorCallback(null, true);
let mailError = Object.keys(result.dns).some((record) => !result.dns[record].status);
if (result.rbl && !result.rbl.status) mailError = true;
if (result.relay && !result.relay.status) mailError = true;
iteratorCallback(null, mailError);
});
}, function (error, erroredDomainObjects) {
if (error || erroredDomainObjects.length === 0) return callback(error);
const erroredDomains = erroredDomainObjects.map((d) => d.domain);
debug(`checkMailStatus: ${erroredDomains.join(',')} failed status checks`);
if (erroredDomains.length) notifications.mailStatusWarning(`Email status check of the following domain(s) failed - ${erroredDomains.join(',')}. See the Status tab in the [Email view](/#/email/) for more information.`);
callback();
});
});
}
function getLogs(unit, options, callback) {
assert.strictEqual(typeof unit, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
var units = options.units || [],
lines = options.lines || 100,
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;
follow = options.follow;
assert(Array.isArray(units));
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
debug('Getting logs for %s as %s', unit, format);
debug('Getting logs for %j', units);
var args = [ '--no-pager', '--lines=' + lines ];
units.forEach(function (u) {
if (u === 'box') args.push('--unit=box');
else if (u === 'mail') args.push('CONTAINER_NAME=mail');
});
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
let args = [ '--lines=' + lines ];
if (follow) args.push('--follow');
var cp = spawn('/bin/journalctl', args);
// need to handle box.log without subdir
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
else args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
var cp = spawn('/usr/bin/tail', args);
var transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var obj = safe.JSON.parse(line);
if (!obj) return undefined;
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
message: obj.MESSAGE,
source: obj.SYSLOG_IDENTIFIER || ''
realtimeTimestamp: timestamp * 1000,
message: line.slice(data[0].length+1),
source: unit
}) + '\n';
});
@@ -391,3 +328,58 @@ function getLogs(options, callback) {
return callback(null, transformStream);
}
function prepareDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`prepareDashboardDomain: ${domain}`);
let task = tasks.startTask(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ]);
task.on('error', (error) => callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => callback(null, taskId));
}
function setDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`setDashboardDomain: ${domain}`);
domains.get(domain, function (error, 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));
reverseProxy.writeAdminConfig(domain, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
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 });
mail.setMailFqdn(fqdn, domain, NOOP_CALLBACK);
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));
}
+30 -19
View File
@@ -22,8 +22,8 @@ exports = module.exports = {
setAdminFqdn: setAdminFqdn,
setAdminLocation: setAdminLocation,
version: version,
setVersion: setVersion,
database: database,
edition: edition,
// these values are derived
adminOrigin: adminOrigin,
@@ -36,8 +36,12 @@ exports = module.exports = {
hasIPv6: hasIPv6,
dkimSelector: dkimSelector,
isManaged: isManaged,
isDemo: isDemo,
// feature flags based on editions (these have a separate license from standard edition)
isSpacesEnabled: isSpacesEnabled,
// for testing resets to defaults
_reset: _reset
};
@@ -52,31 +56,28 @@ var assert = require('assert'),
// assert on unknown environment can't proceed
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
var homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
var data = { };
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
if (exports.CLOUDRON) return homeDir;
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
}
var cloudronConfigFileName = path.join(baseDir(), 'configs/cloudron.conf');
// only tests can run without a config file on disk, they use the defaults with runtime overrides
if (exports.CLOUDRON) assert(fs.existsSync(cloudronConfigFileName), 'No cloudron.conf found, cannot proceed');
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
function saveSync() {
// only save values we want to have in the cloudron.conf, see start.sh
var conf = {
version: data.version,
apiServerOrigin: data.apiServerOrigin,
webServerOrigin: data.webServerOrigin,
adminDomain: data.adminDomain,
adminFqdn: data.adminFqdn,
adminLocation: data.adminLocation,
provider: data.provider,
isDemo: data.isDemo
isDemo: data.isDemo,
edition: data.edition
};
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
@@ -96,13 +97,14 @@ function initConfig() {
data.adminDomain = '';
data.adminLocation = 'my';
data.port = 3000;
data.version = null;
data.apiServerOrigin = null;
data.webServerOrigin = null;
data.provider = 'generic';
data.smtpPort = 2525; // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.dockerProxyPort = 3003;
data.edition = '';
// keep in sync with start.sh
data.database = {
@@ -115,11 +117,11 @@ function initConfig() {
// overrides for local testings
if (exports.TEST) {
data.version = '1.1.1-test';
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.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
@@ -204,11 +206,8 @@ function sysadminOrigin() {
}
function version() {
return get('version');
}
function setVersion(version) {
set('version', version);
if (exports.TEST) return '3.0.0-test';
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
}
function database() {
@@ -219,13 +218,22 @@ function isDemo() {
return get('isDemo') === true;
}
function isSpacesEnabled() {
return get('edition') === 'education';
}
function provider() {
return get('provider');
}
function isManaged() {
return edition() === 'hostingprovider';
}
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
@@ -235,3 +243,6 @@ function dkimSelector() {
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
}
function edition() {
return get('edition');
}
+2 -7
View File
@@ -17,14 +17,9 @@ exports = module.exports = {
'admins', 'users' // ldap code uses 'users' pseudo group
],
ADMIN_NAME: 'Settings',
ADMIN_LOCATION: 'my',
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
ADMIN_GROUP_NAME: 'admin',
ADMIN_GROUP_ID: 'admin',
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
+82 -43
View File
@@ -1,11 +1,14 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize
startPostActivationJobs: startPostActivationJobs,
startPreActivationJobs: startPreActivationJobs,
stopJobs: stopJobs
};
var apps = require('./apps.js'),
var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
backups = require('./backups.js'),
@@ -19,31 +22,32 @@ var apps = require('./apps.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
reverseProxy = require('./reverseproxy.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
updater = require('./updater.js'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
autoUpdater: null,
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
backup: null,
boxUpdateChecker: null,
caasHeartbeat: null,
checkDiskSpace: null,
systemChecks: null,
certificateRenew: null,
cleanupBackups: null,
cleanupEventlog: null,
cleanupTokens: null,
digestEmail: null,
dockerVolumeCleaner: null,
dynamicDNS: null,
schedulerSync: null
dynamicDns: null,
schedulerSync: null,
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
@@ -54,9 +58,7 @@ var AUDIT_SOURCE = { userId: null, username: 'cron' };
// Months: 0-11
// Day of Week: 0-6
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
function startPreActivationJobs(callback) {
if (config.provider() === 'caas') {
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
@@ -70,6 +72,12 @@ function initialize(callback) {
});
}
callback();
}
function startPostActivationJobs(callback) {
assert.strictEqual(typeof callback, 'function');
var randomHourMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
@@ -77,15 +85,17 @@ function initialize(callback) {
start: true
});
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.on(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
settings.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);
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
@@ -99,17 +109,18 @@ function recreateJobs(tz) {
if (gJobs.backup) gJobs.backup.stop();
gJobs.backup = new CronJob({
cronTime: '00 00 */6 * * *', // every 6 hours. backups.ensureBackup() will only trigger a backup once per day
cronTime: '00 00 */6 * * *', // check every 6 hours
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gJobs.checkDiskSpace) gJobs.checkDiskSpace.stop();
gJobs.checkDiskSpace = new CronJob({
cronTime: '00 30 */4 * * *', // every 4 hours
onTick: cloudron.checkDiskSpace,
if (gJobs.systemChecks) gJobs.systemChecks.stop();
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every hour
onTick: cloudron.runSystemChecks,
start: true,
runOnInit: true, // run system check immediately
timeZone: tz
});
@@ -143,7 +154,7 @@ function recreateJobs(tz) {
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
gJobs.cleanupBackups = new CronJob({
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
onTick: backups.cleanup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
onTick: backups.startCleanupTask.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
});
@@ -175,7 +186,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
});
@@ -187,34 +198,61 @@ function recreateJobs(tz) {
start: true,
timeZone: tz
});
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
start: true,
timeZone: tz
});
}
function autoupdatePatternChanged(pattern) {
function boxAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Auto update pattern changed to %s', pattern);
debug('Box auto update pattern changed to %s', pattern);
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.autoUpdater = new CronJob({
gJobs.boxAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
if (semver.major(updateInfo.box.version) === semver.major(config.version())) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('Block automatic update for major version');
}
} else if (updateInfo.apps) {
debug('Starting autoupdate to %j', updateInfo.box);
updater.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No box auto updates available');
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
}
function appAutoupdatePatternChanged(pattern) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
debug('Apps auto update pattern changed to %s', pattern);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.appAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('No auto updates available');
debug('No app auto updates available');
}
},
start: true,
@@ -229,24 +267,25 @@ function dynamicDnsChanged(enabled) {
debug('Dynamic DNS setting changed to %s', enabled);
if (enabled) {
gJobs.dynamicDNS = new CronJob({
gJobs.dynamicDns = new CronJob({
cronTime: '00 */10 * * * *',
onTick: dyndns.sync,
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
});
} else {
if (gJobs.dynamicDNS) gJobs.dynamicDNS.stop();
gJobs.dynamicDNS = null;
if (gJobs.dynamicDns) gJobs.dynamicDns.stop();
gJobs.dynamicDns = null;
}
}
function uninitialize(callback) {
function stopJobs(callback) {
assert.strictEqual(typeof callback, 'function');
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
settings.events.removeListener(settings.DYNAMIC_DNS_KEY, dynamicDnsChanged);
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;
+6 -23
View File
@@ -6,10 +6,6 @@ exports = module.exports = {
query: query,
transaction: transaction,
beginTransaction: beginTransaction,
rollback: rollback,
commit: commit,
importFromFile: importFromFile,
exportToFile: exportToFile,
@@ -27,21 +23,13 @@ var assert = require('assert'),
var gConnectionPool = null,
gDefaultConnection = null;
function initialize(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {
connectionLimit: 5
};
}
assert.strictEqual(typeof options.connectionLimit, 'number');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool !== null) return callback(null);
gConnectionPool = mysql.createPool({
connectionLimit: options.connectionLimit,
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
host: config.database().hostname,
user: config.database().username,
password: config.database().password,
@@ -97,14 +85,13 @@ 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);
async.series([
child_process.exec.bind(null, cmd),
require('./clientdb.js')._addDefaultClients,
require('./groupdb.js')._addDefaultGroups
require('./clientdb.js')._addDefaultClients
], callback);
}
@@ -190,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'),
@@ -204,9 +189,7 @@ function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysqldump -u root ${password} --single-transaction --routines \
--triggers ${config.database().name} > "${file}"`;
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
child_process.exec(cmd, callback);
}
+55
View File
@@ -0,0 +1,55 @@
'use strict';
let assert = require('assert'),
path = require('path');
class DataLayout {
constructor(localRoot, dirMap) {
assert.strictEqual(typeof localRoot, 'string');
assert(Array.isArray(dirMap), 'Expecting layout to be an array');
this._localRoot = localRoot;
this._dirMap = dirMap;
this._remoteRegexps = dirMap.map((l) => new RegExp('^\\./' + l.remoteDir + '/?'));
this._localRegexps = dirMap.map((l) => new RegExp('^' + l.localDir + '/?'));
}
toLocalPath(remoteName) {
assert.strictEqual(typeof remoteName, 'string');
for (let i = 0; i < this._remoteRegexps.length; i++) {
if (!remoteName.match(this._remoteRegexps[i])) continue;
return remoteName.replace(this._remoteRegexps[i], this._dirMap[i].localDir + '/'); // make paths absolute
}
return remoteName.replace(new RegExp('^\\.'), this._localRoot);
}
toRemotePath(localName) {
assert.strictEqual(typeof localName, 'string');
for (let i = 0; i < this._localRegexps.length; i++) {
if (!localName.match(this._localRegexps[i])) continue;
return localName.replace(this._localRegexps[i], './' + this._dirMap[i].remoteDir + '/'); // make paths relative
}
return localName.replace(new RegExp('^' + this._localRoot + '/?'), './');
}
localRoot() {
return this._localRoot;
}
getBasename() { // used to generate cache file names
return path.basename(this._localRoot);
}
toString() {
return JSON.stringify({ localRoot: this._localRoot, layout: this._dirMap });
}
localPaths() {
return [ this._localRoot ].concat(this._dirMap.map((l) => l.localDir));
}
directoryMap() {
return this._dirMap;
}
static fromString(str) {
const obj = JSON.parse(str);
return new DataLayout(obj.localRoot, obj.layout);
}
}
exports = module.exports = DataLayout;
-56
View File
@@ -1,56 +0,0 @@
/* jslint node: true */
'use strict';
exports = module.exports = {
DeveloperError: DeveloperError,
issueDeveloperToken: issueDeveloperToken
};
var assert = require('assert'),
clients = require('./clients.js'),
constants = require('./constants.js'),
eventlog = require('./eventlog.js'),
tokendb = require('./tokendb.js'),
util = require('util');
function DeveloperError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(DeveloperError, Error);
DeveloperError.INTERNAL_ERROR = 'Internal Error';
DeveloperError.EXTERNAL_ERROR = 'External Error';
function issueDeveloperToken(user, auditSource, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var token = tokendb.generateToken();
var expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
});
}
-46
View File
@@ -1,46 +0,0 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:dig');
function resolve(domain, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// dig @server cloudron.io TXT +short
var args = [ ];
if (options.server) args.push('@' + options.server);
if (type === 'PTR') {
args.push('-x', domain);
} else {
args.push(domain, type);
}
args.push('+short');
child_process.execFile('/usr/bin/dig', args, { encoding: 'utf8', killSignal: 'SIGKILL', timeout: options.timeout || 0 }, function (error, stdout, stderr) {
if (error && error.killed) error.code = 'ETIMEDOUT';
if (error || stderr) debug('resolve error (%j): %j %s %s', args, error, stdout, stderr);
if (error) return callback(error);
debug('resolve (%j): %s', args, stdout);
if (!stdout) return callback(); // timeout or no result
var lines = stdout.trim().split('\n');
if (type === 'MX') {
lines = lines.map(function (line) {
var parts = line.split(' ');
return { priority: parts[0], exchange: parts[1] };
});
}
return callback(null, lines);
});
}
+24 -33
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'),
@@ -28,44 +27,36 @@ function maybeSend(callback) {
var pendingAppUpdates = updateInfo.apps || {};
pendingAppUpdates = Object.keys(pendingAppUpdates).map(function (key) { return pendingAppUpdates[key]; });
appstore.getSubscription(function (error, result) {
if (error) debug('Error getting subscription:', error);
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
var hasSubscription = result && result.plan.id !== 'free' && result.plan.id !== 'undecided';
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
eventlog.getByCreationTime(new Date(new Date() - 7*86400000), function (error, events) {
if (error) return callback(error);
if (error) return callback(error);
var appUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_APP_UPDATE; }).map(function (e) { return e.data; });
var boxUpdates = events.filter(function (e) { return e.action === eventlog.ACTION_UPDATE; }).map(function (e) { return e.data; });
var certRenewals = events.filter(function (e) { return e.action === eventlog.ACTION_CERTIFICATE_RENEWAL; }).map(function (e) { return e.data; });
var usersAdded = events.filter(function (e) { return e.action === eventlog.ACTION_USER_ADD; }).map(function (e) { return e.data; });
var usersRemoved = events.filter(function (e) { return e.action === eventlog.ACTION_USER_REMOVE; }).map(function (e) { return e.data; });
var finishedBackups = events.filter(function (e) { return e.action === eventlog.ACTION_BACKUP_FINISH && !e.errorMessage; }).map(function (e) { return e.data; });
var info = {
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
if (error) return callback(error);
finishedAppUpdates: appUpdates,
finishedBoxUpdates: boxUpdates,
var info = {
hasSubscription: hasSubscription,
certRenewals: certRenewals,
finishedBackups: finishedBackups, // only the successful backups
usersAdded: usersAdded,
usersRemoved: usersRemoved // unused because we don't have username to work with
};
pendingAppUpdates: pendingAppUpdates,
pendingBoxUpdate: updateInfo.box || null,
// always send digest for backup failure notification
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
finishedAppUpdates: appUpdates,
finishedBoxUpdates: boxUpdates,
certRenewals: certRenewals,
finishedBackups: finishedBackups, // only the successful backups
usersAdded: usersAdded,
usersRemoved: usersRemoved // unused because we don't have username to work with
};
// always send digest for backup failure notification
debug('maybeSend: sending digest email', info);
mailer.sendDigest(info);
callback();
});
callback();
});
});
}
+91 -54
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'),
DomainError = require('../domains.js').DomainError,
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,
@@ -45,58 +63,47 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new DomainError(DomainError.STILL_BUSY));
if (result.statusCode !== 201) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new DomainsError(DomainsError.STILL_BUSY));
if (result.statusCode !== 201) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, result.body.changeId);
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');
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)
.query({ token: dnsConfig.token, type: type })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode !== 200) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null, result.body.values);
});
}
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,32 +111,62 @@ 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)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new DomainError(DomainError.STILL_BUSY));
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND));
if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 400) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new DomainsError(DomainsError.STILL_BUSY));
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null);
});
}
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
};
return callback(null, credentials);
const location = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
}
+133 -87
View File
@@ -1,43 +1,51 @@
'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'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
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');
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = error.message;
if (error.code === 6003) {
if (error.error_chain[0] && error.error_chain[0].code === 6103) message = 'Invalid API Key';
else message = 'Invalid credentials';
}
return callback(new DomainError(DomainError.ACCESS_DENIED, message));
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
return callback(new DomainsError(DomainsError.ACCESS_DENIED, message));
}
callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
}
function getZoneByName(dnsConfig, zoneName, callback) {
@@ -52,22 +60,20 @@ function getZoneByName(dnsConfig, zoneName, callback) {
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (!result.body.result.length) return callback(new DomainError(DomainError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
if (!result.body.result.length) return callback(new DomainsError(DomainsError.NOT_FOUND, util.format('%s %j', result.statusCode, result.body)));
callback(null, result.body.result[0]);
});
}
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)
@@ -83,36 +89,34 @@ 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') {
priority = value.split(' ')[0];
priority = parseInt(value.split(' ')[0], 10);
value = value.split(' ')[1];
}
@@ -121,57 +125,62 @@ 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);
});
}
}, function (error) {
if (error) return callback(error);
callback(null, 'unused');
});
}, 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; });
@@ -182,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);
@@ -206,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);
@@ -226,15 +238,49 @@ 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');
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainError(DomainError.BAD_FIELD, 'email must be a non-empty string'));
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,
@@ -243,26 +289,26 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
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'));
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);
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
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');
+89 -60
View File
@@ -1,21 +1,25 @@
'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'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('dns'),
DomainError = require('../domains.js').DomainError,
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');
@@ -39,13 +52,13 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(new DomainError(DomainError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(new DomainsError(DomainsError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
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
@@ -101,10 +115,10 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 201) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode !== 201) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(safe.query(result.body, 'domain_record.id'));
@@ -119,34 +133,31 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
// increment, as we have consumed the record
++i;
if (error && !error.response) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new DomainError(DomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 200) return iteratorCallback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
if (error && !error.response) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode === 422) return iteratorCallback(new DomainsError(DomainsError.BAD_FIELD, result.body.message));
if (result.statusCode !== 200) return iteratorCallback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(safe.query(result.body, 'domain_record.id'));
return iteratorCallback(null);
});
}
}, function (error) {
if (error) return callback(error);
callback(null, '' + recordIds[0]); // DO ids are integers
});
}, 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
@@ -158,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);
@@ -185,10 +197,10 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new DomainError(DomainError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (error && !error.response) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, util.format('Network error %s', error.message)));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainError(DomainError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new DomainError(DomainError.EXTERNAL_ERROR, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new DomainsError(DomainsError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
@@ -197,36 +209,53 @@ 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
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolveNs(zoneName, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainError(DomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainError(DomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
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.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) {
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
return callback(new DomainError(DomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
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');

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