Compare commits

..

173 Commits

Author SHA1 Message Date
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
114 changed files with 4678 additions and 2018 deletions
+17
View File
@@ -1505,3 +1505,20 @@
* 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
+9 -1
View File
@@ -27,13 +27,15 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \
acl \
awscli \
build-essential \
cron \
curl \
dmsetup \
$gpg_package \
iptables \
libpython2.7 \
logrotate \
@@ -126,3 +128,9 @@ systemctl disable postfix || true
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
# ubuntu's default config for unbound does not work if ipv6 is disabled. this config is overwritten in start.sh
# we need unbound to work as this is required for installer.sh to do any DNS requests
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl restart unbound
+6 -5
View File
@@ -6,17 +6,18 @@ var database = require('./src/database.js');
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
// This is triggered by systemd with the crashed unit name as argument
function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
var processName = process.argv[2];
console.log('Started crash notifier for', processName);
var unitName = process.argv[2];
console.log('Started crash notifier for', unitName);
// mailer needs the db
// eventlog api needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
sendFailureLogs(processName, { unit: processName });
sendFailureLogs(unitName);
});
}
@@ -0,0 +1,27 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE notifications(' +
'id int NOT NULL AUTO_INCREMENT,' +
'userId VARCHAR(128) NOT NULL,' +
'eventId VARCHAR(128) NOT NULL,' +
'title VARCHAR(512) NOT NULL,' +
'message TEXT,' +
'action VARCHAR(512) NOT NULL,' +
'acknowledged BOOLEAN DEFAULT false,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'FOREIGN KEY(eventId) REFERENCES eventlog(id),' +
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE notifications', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE result resultJson TEXT', [], function (error) {
if (error) console.error(error);
db.runSql('DELETE FROM tasks', callback); // empty tasks table since we have bad results format
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tasks CHANGE resultJson result TEXT', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN dataDir VARCHAR(256) UNIQUE', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN dataDir', function (error) {
if (error) console.error(error);
callback(error);
});
};
+14 -1
View File
@@ -212,5 +212,18 @@ CREATE TABLE IF NOT EXISTS tasks(
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id));
CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE IF NOT EXISTS notifications(
id int NOT NULL AUTO_INCREMENT,
userId VARCHAR(128) NOT NULL,
eventId VARCHAR(128) 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;
+226
View File
@@ -829,6 +829,35 @@
}
}
},
"@sinonjs/commons": {
"version": "1.3.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/commons/-/commons-1.3.0.tgz",
"integrity": "sha1-UKJ1QBa28wqZTO2m2aCow2rdqEk=",
"dev": true,
"requires": {
"type-detect": "4.0.8"
}
},
"@sinonjs/formatio": {
"version": "3.1.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/formatio/-/formatio-3.1.0.tgz",
"integrity": "sha1-asnR6xghmE2ExJlnJuRdFkbYzOU=",
"dev": true,
"requires": {
"@sinonjs/samsam": "^2 || ^3"
}
},
"@sinonjs/samsam": {
"version": "3.0.2",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/@sinonjs/samsam/-/samsam-3.0.2.tgz",
"integrity": "sha1-ME+zO9VYWgst+KTIAfy0f6hNjkM=",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.0.2",
"array-from": "^2.1.1",
"lodash.get": "^4.4.2"
}
},
"JSONStream": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.2.tgz",
@@ -997,6 +1026,12 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"array-from": {
"version": "2.1.1",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/array-from/-/array-from-2.1.1.tgz",
"integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=",
"dev": true
},
"array-uniq": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
@@ -1208,6 +1243,11 @@
"tweetnacl": "^0.14.3"
}
},
"bindings": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.1.tgz",
"integrity": "sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew=="
},
"bl": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
@@ -2460,6 +2500,12 @@
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
},
"diff": {
"version": "3.5.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/diff/-/diff-3.5.0.tgz",
"integrity": "sha1-gAwN0eCov7yVg1wgKtIg/jF+WhI=",
"dev": true
},
"dijkstrajs": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.1.tgz",
@@ -4293,6 +4339,12 @@
}
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
},
"has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -4510,6 +4562,21 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isemail": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
"requires": {
"punycode": "2.x.x"
},
"dependencies": {
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}
}
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4602,6 +4669,23 @@
"resolved": "https://registry.npmjs.org/java-packagename-regex/-/java-packagename-regex-1.0.0.tgz",
"integrity": "sha1-lR9he9WhlCIO0GcLm4KowOxcYiQ="
},
"joi": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/joi/-/joi-13.7.0.tgz",
"integrity": "sha512-xuY5VkHfeOYK3Hdi91ulocfuFopwgbSORmIwzcwHKESQhC7w1kD5jaVSPnqDxS2I8t3RZ9omCKAxNwXN5zG1/Q==",
"requires": {
"hoek": "5.x.x",
"isemail": "3.x.x",
"topo": "3.x.x"
},
"dependencies": {
"hoek": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz",
"integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w=="
}
}
},
"js-base64": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz",
@@ -4719,6 +4803,12 @@
}
}
},
"just-extend": {
"version": "4.0.2",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/just-extend/-/just-extend-4.0.2.tgz",
"integrity": "sha1-8/R/ffyg+YnFVBCn68iFSwcQivw=",
"dev": true
},
"jwa": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.5.tgz",
@@ -4887,6 +4977,12 @@
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8="
},
"lodash.get": {
"version": "4.4.2",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=",
"dev": true
},
"lodash.groupby": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz",
@@ -4913,6 +5009,12 @@
"resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz",
"integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY="
},
"lolex": {
"version": "3.0.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lolex/-/lolex-3.0.0.tgz",
"integrity": "sha1-8E7hqKoT9g8avXsOj0IT7HLsGT4=",
"dev": true
},
"longest": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
@@ -5532,6 +5634,14 @@
}
}
},
"namecheap": {
"version": "github:joshuakarjala/node-namecheap#464a9528b7ded3ee2520c2688bc98cbffb08e603",
"from": "github:joshuakarjala/node-namecheap#464a952",
"requires": {
"request": "*",
"xml2json": "*"
}
},
"nan": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz",
@@ -5549,6 +5659,42 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
"integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
},
"nise": {
"version": "1.4.8",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/nise/-/nise-1.4.8.tgz",
"integrity": "sha1-zpHDHobPmyxMrEnX/Nf1Z3m/1rA=",
"dev": true,
"requires": {
"@sinonjs/formatio": "^3.1.0",
"just-extend": "^4.0.2",
"lolex": "^2.3.2",
"path-to-regexp": "^1.7.0",
"text-encoding": "^0.6.4"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
"dev": true
},
"lolex": {
"version": "2.7.5",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/lolex/-/lolex-2.7.5.tgz",
"integrity": "sha1-ETAB1Wv8fgLVbjYpHMXEE9GqBzM=",
"dev": true
},
"path-to-regexp": {
"version": "1.7.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"dev": true,
"requires": {
"isarray": "0.0.1"
}
}
}
},
"nock": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/nock/-/nock-9.3.2.tgz",
@@ -5583,6 +5729,22 @@
}
}
},
"node-expat": {
"version": "2.3.17",
"resolved": "https://registry.npmjs.org/node-expat/-/node-expat-2.3.17.tgz",
"integrity": "sha512-mNTxY/GMiZGayqdKZXyf6lJR7OM1JqyL0EISjE4XF7Ov7+X4zJjmlnfxCi6Gml90IEOyiYBcyJg9MHDsDp6YHw==",
"requires": {
"bindings": "^1.2.1",
"nan": "^2.10.0"
},
"dependencies": {
"nan": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
"integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw=="
}
}
},
"node-forge": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.1.tgz",
@@ -7997,6 +8159,32 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"sinon": {
"version": "7.2.2",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/sinon/-/sinon-7.2.2.tgz",
"integrity": "sha1-OI7KvUL6k8WSv8cdNacIlNWgygc=",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.2.0",
"@sinonjs/formatio": "^3.1.0",
"@sinonjs/samsam": "^3.0.2",
"diff": "^3.5.0",
"lolex": "^3.0.0",
"nise": "^1.4.7",
"supports-color": "^5.5.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha1-4uaaRKyHcveKHsCzW2id9lMO/I8=",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"sntp": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
@@ -8614,6 +8802,12 @@
}
}
},
"text-encoding": {
"version": "0.6.4",
"resolved": "https://eis.jfrog.io/eis/api/npm/npm/text-encoding/-/text-encoding-0.6.4.tgz",
"integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=",
"dev": true
},
"through2": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
@@ -8636,6 +8830,21 @@
"resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz",
"integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg=="
},
"topo": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/topo/-/topo-3.0.3.tgz",
"integrity": "sha512-IgpPtvD4kjrJ7CRA3ov2FhWQADwv+Tdqbsf1ZnPUSAtCJ9e1Z44MmoSGDXGk4IppoZA7jd/QRkNddlLJWlUZsQ==",
"requires": {
"hoek": "6.x.x"
},
"dependencies": {
"hoek": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.2.tgz",
"integrity": "sha512-6qhh/wahGYZHFSFw12tBbJw5fsAhhwrrG/y3Cs0YMTv2WzMnL0oLPnQJjv1QJvEfylRSOFuP+xCu+tdx0tD16Q=="
}
}
},
"tough-cookie": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
@@ -8952,6 +9161,23 @@
}
}
},
"xml2json": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/xml2json/-/xml2json-0.11.2.tgz",
"integrity": "sha512-ZJpHpPOL0T5lOvAHMnWm59iQOPqNtam5t2TMUllWZ1k5Wm8L5YyvQnkeaVnRKCvDwY5EumqXWyOjjMdQVz272A==",
"requires": {
"hoek": "^4.2.1",
"joi": "^13.1.2",
"node-expat": "^2.3.15"
},
"dependencies": {
"hoek": {
"version": "4.2.1",
"resolved": "http://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz",
"integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA=="
}
}
},
"xtend": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+4 -4
View File
@@ -45,6 +45,7 @@
"morgan": "^1.9.0",
"multiparty": "^4.1.4",
"mysql": "^2.15.0",
"namecheap": "github:joshuakarjala/node-namecheap#464a952",
"nodemailer": "^4.6.5",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
@@ -87,12 +88,11 @@
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^9.0.14",
"node-sass": "^4.6.1",
"recursive-readdir": "^2.2.2"
"recursive-readdir": "^2.2.2",
"sinon": "^7.2.2"
},
"scripts": {
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test",
"test": "src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --no-timeouts --exit -R spec ./src/test ./src/routes/test/[^a]*js",
"postmerge": "/bin/true",
"precommit": "/bin/true",
"prepush": "npm test",
+9 -2
View File
@@ -101,6 +101,7 @@ elif [[ \
"${provider}" != "azure" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
@@ -117,7 +118,7 @@ elif [[ \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
echo "--provider must be one of: azure, cloudscale.ch, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, netcup, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -138,6 +139,12 @@ echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Installing software-properties-common"
if ! apt-get install -y software-properties-common &>> "${LOG_FILE}"; then
echo "Could not install software-properties-common (for add-apt-repository below). See ${LOG_FILE}"
exit 1
fi
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
@@ -243,7 +250,7 @@ echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate t
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
read -p "This server has to rebooted to apply all the settings. Reboot now ? [Y/n] " yn
read -p "The server has to be rebooted to apply all the settings. Reboot now ? [Y/n] " yn
yn=${yn:-y}
case $yn in
[Yy]* ) systemctl reboot;;
+9 -17
View File
@@ -13,10 +13,12 @@ readonly BOX_SRC_DIR="${HOME_DIR}/box"
readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
readonly CONFIG_DIR="${HOME_DIR}/configs"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
@@ -88,25 +90,19 @@ systemctl daemon-reload
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
echo "==> Creating config directory"
mkdir -p "${CONFIG_DIR}"
# remove old cloudron.conf. Can be removed after 3.4
rm -f "${CONFIG_DIR}/cloudron.conf"
$json -f /etc/cloudron/cloudron.conf -I -e "delete this.version" # remove the version field
chown -R "${USER}" /etc/cloudron
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
# If IP6 is not enabled, dns queries seem to fail on some hosts
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: yes\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# If IP6 is not enabled, dns queries seem to fail on some hosts. -s returns false if file missing or 0 size
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: ${ip6}\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30\n\tcache-max-ttl: 300\n\t#logfile: /var/log/unbound.log\n\t#verbosity: 10" > /etc/unbound/unbound.conf.d/cloudron-network.conf
# update the root anchor after a out-of-disk-space situation (see #269)
unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
@@ -183,11 +179,7 @@ mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
echo "==> Migrating data"
sudo -u "${USER}" -H bash <<EOF
set -eu
cd "${BOX_SRC_DIR}"
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
EOF
(cd "${BOX_SRC_DIR}" && BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up)
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
@@ -198,8 +190,8 @@ else
fi
echo "==> Changing ownership"
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
chown -R "${USER}" /etc/cloudron
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
+10 -4
View File
@@ -1,8 +1,14 @@
# sudo logging breaks journalctl output with very long urls (systemd bug)
Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
Defaults!/home/yellowtent/box/src/scripts/clearvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/clearvolume.sh
Defaults!/home/yellowtent/box/src/scripts/mvvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mvvolume.sh
Defaults!/home/yellowtent/box/src/scripts/mkdirvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mkdirvolume.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
@@ -25,8 +31,8 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh
Defaults!/home/yellowtent/box/src/scripts/remotesupport.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remotesupport.sh
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
+4 -1
View File
@@ -17,9 +17,12 @@ ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.lo
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
OOMScoreAdjust=-999
User=yellowtent
Group=yellowtent
MemoryLimit=200M
; OOM killer is invoked in this unit beyond this. The start script replaces this with MemoryLimit for Ubuntu 16
MemoryMax=400M
TimeoutStopSec=5s
StartLimitInterval=1
StartLimitBurst=60
+23 -12
View File
@@ -33,6 +33,7 @@ exports = module.exports = {
var accesscontrol = require('./accesscontrol.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
@@ -109,7 +110,7 @@ var KNOWN_ADDONS = {
clear: NOOP,
},
localstorage: {
setup: setupLocalStorage, // docker creates the directory for us
setup: setupLocalStorage,
teardown: teardownLocalStorage,
backup: NOOP, // no backup because it's already inside app data
restore: NOOP,
@@ -369,24 +370,25 @@ function getServiceLogs(serviceName, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
if (!KNOWN_SERVICES[serviceName]) return callback(new AddonsError(AddonsError.NOT_FOUND));
debug(`Getting logs for ${serviceName}`);
var lines = options.lines || 100,
var lines = options.lines,
format = options.format || 'json',
follow = !!options.follow;
follow = options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var cmd;
var args = [ '--lines=' + lines ];
let cmd, args = [];
// docker and unbound use journald
if (serviceName === 'docker' || serviceName === 'unbound') {
cmd = 'journalctl';
args.push('--lines=' + (lines === -1 ? 'all' : lines));
args.push(`--unit=${serviceName}`);
args.push('--no-pager');
args.push('--output=short-iso');
@@ -395,6 +397,7 @@ function getServiceLogs(serviceName, options, callback) {
} else {
cmd = '/usr/bin/tail';
args.push('--lines=' + (lines === -1 ? '+1' : lines));
if (follow) args.push('--follow', '--retry', '--quiet'); // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args.push(path.join(paths.LOG_DIR, serviceName, 'app.log'));
}
@@ -726,8 +729,13 @@ function setupLocalStorage(app, options, callback) {
debugApp(app, 'setupLocalStorage');
// if you change the name, you have to change getMountsSync
docker.createVolume(app, `${app.id}-localstorage`, 'data', callback);
const volumeDataDir = apps.getDataDir(app, app.dataDir);
// reomve any existing volume in case it's bound with an old dataDir
async.series([
docker.removeVolume.bind(null, app, `${app.id}-localstorage`),
docker.createVolume.bind(null, app, `${app.id}-localstorage`, volumeDataDir)
], callback);
}
function clearLocalStorage(app, options, callback) {
@@ -737,7 +745,7 @@ function clearLocalStorage(app, options, callback) {
debugApp(app, 'clearLocalStorage');
docker.clearVolume(app, `${app.id}-localstorage`, 'data', callback);
docker.clearVolume(app, `${app.id}-localstorage`, { removeDirectory: false }, callback);
}
function teardownLocalStorage(app, options, callback) {
@@ -747,7 +755,10 @@ function teardownLocalStorage(app, options, callback) {
debugApp(app, 'teardownLocalStorage');
docker.removeVolume(app, `${app.id}-localstorage`, 'data', callback);
async.series([
docker.clearVolume.bind(null, app, `${app.id}-localstorage`, { removeDirectory: true }),
docker.removeVolume.bind(null, app, `${app.id}-localstorage`)
], callback);
}
function setupOauth(app, options, callback) {
+5 -1
View File
@@ -69,7 +69,8 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate', 'apps.ts' ].join(',');
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts' ].join(',');
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -139,6 +140,9 @@ function postProcess(result) {
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
// in the db, we store dataDir as unique/nullable
result.dataDir = result.dataDir || '';
}
function get(id, callback) {
+16 -8
View File
@@ -7,7 +7,7 @@ var appdb = require('./appdb.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js').connection,
mailer = require('./mailer.js'),
eventlog = require('./eventlog.js'),
superagent = require('superagent'),
util = require('util');
@@ -46,7 +46,9 @@ function setHealth(app, health, callback) {
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
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, { app: app }, {});
gHealthInfo[app.id].emailSent = true;
} else {
debugApp(app, 'waiting for sometime to update the app health');
@@ -132,19 +134,25 @@ function processDockerEvents(intervalSecs, callback) {
stream.setEncoding('utf8');
stream.on('data', function (data) {
var ev = JSON.parse(data);
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
var context = JSON.stringify(ev);
var 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();
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
const notifyUser = (!app || !app.debugMode) && (now - gLastOomMailTime > OOM_MAIL_LIMIT);
debug('OOM Context: %s. notifyUser: %s. lastOomTime: %s (now: %s)', context, notifyUser, gLastOomMailTime, now);
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now, ev);
// do not send mails for dev apps
if (notifyUser) {
mailer.oomEvent(program, context); // app can be null if it's an addon crash
var auditSource = {
containerId: containerId,
app: app || null // app can be null if it's an addon crash
};
eventlog.add(eventlog.ACTION_APP_OOM, auditSource, ev);
gLastOomMailTime = now;
}
});
+78 -34
View File
@@ -38,6 +38,7 @@ exports = module.exports = {
configureInstalledApps: configureInstalledApps,
getAppConfig: getAppConfig,
getDataDir: getDataDir,
downloadFile: downloadFile,
uploadFile: uploadFile,
@@ -301,26 +302,50 @@ function validateEnv(env) {
return null;
}
function validateDataDir(dataDir) {
if (dataDir === '') return null; // revert back to default dataDir
if (path.resolve(dataDir) !== dataDir) return new AppsError(AppsError.BAD_FIELD, 'dataDir must be an absolute path');
// nfs shares will have the directory mounted already
let stat = safe.fs.lstatSync(dataDir);
if (stat) {
if (!stat.isDirectory()) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not a directory`);
let entries = safe.fs.readdirSync(dataDir);
if (!entries) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} could not be listed`);
if (entries.length !== 0) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not empty`);
}
// backup logic relies on paths not overlapping (because it recurses)
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be inside apps data`);
// if we made it this far, it cannot start with any of these realistically
const fhs = [ '/bin', '/boot', '/etc', '/lib', '/lib32', '/lib64', '/proc', '/run', '/sbin', '/tmp', '/usr' ];
if (fhs.some((p) => dataDir.startsWith(p))) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be placed inside this location`);
return null;
}
function getDuplicateErrorDetails(location, portBindings, error) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert.strictEqual(error.reason, DatabaseError.ALREADY_EXISTS);
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key/);
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key '(.*)'/);
if (!match) {
debug('Unexpected SQL error message.', error);
return new AppsError(AppsError.INTERNAL_ERROR);
return new AppsError(AppsError.INTERNAL_ERROR, error);
}
// check if the location conflicts
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS);
if (match[1] === location) return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
// check if any of the port bindings conflict
for (let portName in portBindings) {
if (portBindings[portName] === parseInt(match[1])) return new AppsError(AppsError.PORT_CONFLICT, match[1]);
}
return new AppsError(AppsError.ALREADY_EXISTS);
return new AppsError(AppsError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
}
// app configs that is useful for 'archival' into the app backup config.json
@@ -329,6 +354,7 @@ function getAppConfig(app) {
manifest: app.manifest,
location: app.location,
domain: app.domain,
fqdn: app.fqdn,
accessRestriction: app.accessRestriction,
portBindings: app.portBindings,
memoryLimit: app.memoryLimit,
@@ -336,17 +362,22 @@ function getAppConfig(app) {
robotsTxt: app.robotsTxt,
sso: app.sso,
alternateDomains: app.alternateDomains || [],
env: app.env
env: app.env,
dataDir: app.dataDir
};
}
function getDataDir(app, dataDir) {
return dataDir || path.join(paths.APPS_DATA_DIR, app.id, 'data');
}
function removeInternalFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health',
'location', 'domain', 'fqdn', 'mailboxName',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions',
'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts',
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate');
'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir');
}
function removeRestrictedFields(app) {
@@ -746,6 +777,12 @@ function configure(appId, data, user, auditSource, callback) {
if (error) return callback(error);
}
if ('dataDir' in data && data.dataDir !== app.dataDir) {
error = validateDataDir(data.dataDir);
if (error) return callback(error);
values.dataDir = data.dataDir;
}
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message));
@@ -801,34 +838,22 @@ function update(appId, data, auditSource, callback) {
debug('Will update app with id:%s', appId);
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
get(appId, function (error, app) {
if (error) return callback(error);
var updateConfig = { };
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
updateConfig.manifest = manifest;
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
}
}
get(appId, function (error, app) {
downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return callback(error);
var updateConfig = { };
error = manifestFormat.parse(manifest);
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest error:' + error.message));
error = checkManifestConstraints(manifest);
if (error) return callback(error);
updateConfig.manifest = manifest;
// prevent user from installing a app with different manifest id over an existing app
// this allows cloudron install -f --app <appid> for an app installed from the appStore
if (app.manifest.id !== updateConfig.manifest.id) {
@@ -837,6 +862,22 @@ function update(appId, data, auditSource, callback) {
updateConfig.appStoreId = '';
}
if (app.appStoreId !== '' && semver.lte(updateConfig.manifest.version, app.manifest.version)) {
if (!data.force) return callback(new AppsError(AppsError.BAD_FIELD, 'Downgrades are not permitted for apps installed from AppStore. force to override'));
}
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new AppsError(AppsError.BAD_FIELD, 'icon is not base64'));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), new Buffer(data.icon, 'base64'))) {
return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.png'));
}
}
// do not update apps in debug mode
if (app.debugMode && !data.force) return callback(new AppsError(AppsError.BAD_STATE, 'debug mode enabled. force to override'));
@@ -868,16 +909,19 @@ function getLogs(appId, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
debug('Getting logs for %s', appId);
get(appId, function (error, app) {
if (error) return callback(error);
var lines = options.lines || 100,
var lines = options.lines === -1 ? '+1' : options.lines,
format = options.format || 'json',
follow = !!options.follow;
follow = options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
var args = [ '--lines=' + lines ];
@@ -952,7 +996,7 @@ function restore(appId, data, auditSource, callback) {
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { appId: appId, app: app });
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId: backupInfo.id, fromManifest: app.manifest, toManifest: backupInfo.manifest });
callback(null);
});
+39 -30
View File
@@ -52,6 +52,7 @@ var addons = require('./addons.js'),
var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'),
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
@@ -135,27 +136,14 @@ function createContainer(app, callback) {
});
}
// Only delete the main container of the app, not destroy any docker addon created ones
function deleteMainContainer(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting main app container');
docker.deleteContainer(app.containerId, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
});
}
function deleteContainers(app, callback) {
function deleteContainers(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'deleting app containers (app, scheduler)');
docker.deleteContainers(app.id, function (error) {
docker.deleteContainers(app.id, options, function (error) {
if (error) return callback(new Error('Error deleting container: ' + error));
updateApp(app, { containerId: null }, callback);
@@ -187,6 +175,7 @@ function deleteAppDir(app, options, callback) {
if (!entries) return callback(`Error listing ${resolvedAppDataDir}: ${safe.error.message}`);
// remove only files. directories inside app dir are currently volumes managed by the addons
// we cannot delete those dirs anyway because of perms
entries.forEach(function (entry) {
let stat = safe.fs.statSync(path.join(resolvedAppDataDir, entry));
if (stat && !stat.isDirectory()) safe.fs.unlinkSync(path.join(resolvedAppDataDir, entry));
@@ -469,6 +458,19 @@ function waitForDnsPropagation(app, callback) {
});
}
function migrateDataDir(app, sourceDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof sourceDir, 'string');
assert.strictEqual(typeof callback, 'function');
let resolvedSourceDir = apps.getDataDir(app, sourceDir);
let resolvedTargetDir = apps.getDataDir(app, app.dataDir);
debug(`migrateDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, callback);
}
// Ordering is based on the following rationale:
// - configure nginx, icon, oauth
// - register subdomain.
@@ -497,14 +499,14 @@ function install(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function teardownAddons(next) {
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
var addonsToRemove = !isRestoring ? app.manifest.addons : _.omit(app.oldConfig.manifest.addons, Object.keys(app.manifest.addons));
addons.teardownAddons(app, addonsToRemove, next);
},
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked volume
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked appdata dir
// for restore case
function deleteImageIfChanged(done) {
@@ -527,7 +529,7 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '50, Creating volume' }),
updateApp.bind(null, app, { installationProgress: '50, Creating app data directory' }),
createAppDir.bind(null, app),
function restoreFromBackup(next) {
@@ -538,10 +540,10 @@ function install(app, callback) {
], next);
} else {
async.series([
updateApp.bind(null, app, { installationProgress: '60, Download backup and restoring addons' }),
updateApp.bind(null, app, { installationProgress: '65, Download backup and restoring addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
addons.clearAddons.bind(null, app, app.manifest.addons),
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => updateApp(app, { installationProgress: `65, Restore - ${progress.message}` }, NOOP_CALLBACK))
], next);
}
},
@@ -583,7 +585,7 @@ function backup(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '10, Backing up' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK)),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `30, ${progress.message}` }, NOOP_CALLBACK)),
// done!
function (callback) {
@@ -605,7 +607,8 @@ function configure(app, callback) {
assert.strictEqual(typeof callback, 'function');
// oldConfig can be null during an infra update
var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
const locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn);
const dataDirChanged = app.oldConfig && (app.oldConfig.dataDir !== app.dataDir);
async.series([
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
@@ -613,7 +616,7 @@ function configure(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
unregisterAlternateDomains.bind(null, app, false /* all */),
function (next) {
if (!locationChanged) return next();
@@ -621,7 +624,6 @@ function configure(app, callback) {
unregisterSubdomain(app, app.oldConfig.location, app.oldConfig.domain, next);
},
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
@@ -636,13 +638,20 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
updateApp.bind(null, app, { installationProgress: '45, Ensuring app data directory' }),
createAppDir.bind(null, app),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
addons.setupAddons.bind(null, app, app.manifest.addons),
// migrate dataDir
function (next) {
if (!dataDirChanged) return next();
migrateDataDir(app, app.oldConfig.dataDir, next);
},
updateApp.bind(null, app, { installationProgress: '60, Creating container' }),
createContainer.bind(null, app),
@@ -696,7 +705,7 @@ function update(app, callback) {
async.series([
updateApp.bind(null, app, { installationProgress: '15, Backing up app' }),
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: progress.message }, NOOP_CALLBACK))
backups.backupApp.bind(null, app, (progress) => updateApp(app, { installationProgress: `15, Backup - ${progress.message}` }, NOOP_CALLBACK))
], function (error) {
if (error) error.backupError = true;
next(error);
@@ -714,7 +723,7 @@ function update(app, callback) {
removeCollectdProfile.bind(null, app),
removeLogrotateConfig.bind(null, app),
stopApp.bind(null, app),
deleteMainContainer.bind(null, app),
deleteContainers.bind(null, app, { managedOnly: true }),
function deleteImageIfChanged(done) {
if (app.manifest.dockerImage === app.updateConfig.manifest.dockerImage) return done();
@@ -800,12 +809,12 @@ function uninstall(app, callback) {
stopApp.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Deleting container' }),
deleteContainers.bind(null, app),
deleteContainers.bind(null, app, {}),
updateApp.bind(null, app, { installationProgress: '30, Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons),
updateApp.bind(null, app, { installationProgress: '40, Deleting volume' }),
updateApp.bind(null, app, { installationProgress: '40, Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
updateApp.bind(null, app, { installationProgress: '50, Deleting image' }),
+214 -122
View File
@@ -22,13 +22,12 @@ exports = module.exports = {
upload: upload,
startCleanupTask: startCleanupTask,
cleanup: cleanup,
cleanupCacheFilesSync: cleanupCacheFilesSync,
// for testing
_getBackupFilePath: getBackupFilePath,
_createTarPackStream: createTarPackStream,
_tarExtract: tarExtract,
_restoreFsMetadata: restoreFsMetadata,
_saveFsMetadata: saveFsMetadata
};
@@ -44,6 +43,7 @@ var addons = require('./addons.js'),
crypto = require('crypto'),
database = require('./database.js'),
DatabaseError = require('./databaseerror.js'),
DataLayout = require('./datalayout.js'),
debug = require('debug')('box:backups'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
@@ -64,7 +64,6 @@ var addons = require('./addons.js'),
util = require('util'),
zlib = require('zlib');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
function debugApp(app) {
@@ -228,7 +227,7 @@ function createReadStream(sourceFile, key) {
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
stream.on('error', function (error) {
debug('createReadStream: tar stream error.', error);
debug('createReadStream: read stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
@@ -262,15 +261,16 @@ function createWriteStream(destFile, key) {
}
}
function createTarPackStream(sourceDir, key) {
assert.strictEqual(typeof sourceDir, 'string');
function tarPack(dataLayout, key, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
var pack = tar.pack('/', {
dereference: false, // pack the symlink and not what it points to
entries: [ sourceDir ],
entries: dataLayout.localPaths(),
map: function(header) {
header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative
header.name = dataLayout.toRemotePath(header.name);
return header;
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
@@ -280,35 +280,40 @@ function createTarPackStream(sourceDir, key) {
var ps = progressStream({ time: 10000 }); // emit 'pgoress' every 10 seconds
pack.on('error', function (error) {
debug('createTarPackStream: tar stream error.', error);
debug('tarPack: tar stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
gzip.on('error', function (error) {
debug('createTarPackStream: gzip stream error.', error);
debug('tarPack: gzip stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('createTarPackStream: encrypt stream error.', error);
debug('tarPack: encrypt stream error.', error);
ps.emit('error', new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
return pack.pipe(gzip).pipe(encrypt).pipe(ps);
pack.pipe(gzip).pipe(encrypt).pipe(ps);
} else {
return pack.pipe(gzip).pipe(ps);
pack.pipe(gzip).pipe(ps);
}
callback(null, ps);
}
function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
function sync(backupConfig, backupId, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
syncer.sync(dataDir, function processTask(task, iteratorCallback) {
// the number here has to take into account the s3.upload partSize (which is 10MB). So 20=200MB
const concurrency = backupConfig.syncConcurrency || (backupConfig.provider === 's3' ? 20 : 10);
syncer.sync(dataLayout, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
// the empty task.path is special to signify the directory
const destPath = task.path && backupConfig.key ? encryptFilePath(task.path, backupConfig.key) : task.path;
@@ -329,16 +334,18 @@ function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
progressCallback({ message: `${task.operation} ${task.path} try ${retryCount}` });
if (task.operation === 'add') {
progressCallback({ message: `Adding ${task.path}` + (retryCount > 1 ? ` (Try ${retryCount})` : '') });
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
var stream = createReadStream(path.join(dataDir, task.path), backupConfig.key || null);
var stream = createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig.key || null);
stream.on('error', function (error) {
debug(`read stream error for ${task.path}: ${error.message}`);
retryCallback();
}); // ignore error if file disappears
stream.on('progress', function(progress) {
progressCallback({ message: `Uploading ${task.path}: ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}` });
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: `Uploading ${task.path}` }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading ${task.path}: ${transferred}M@${speed}Mbps` }); // 0M@0Mbps looks wrong
});
api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
@@ -346,42 +353,52 @@ function sync(backupConfig, backupId, dataDir, progressCallback, callback) {
});
}
}, iteratorCallback);
}, backupConfig.syncConcurrency || 10 /* concurrency */, function (error) {
}, concurrency, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
callback();
});
}
function saveFsMetadata(appDataDir, callback) {
assert.strictEqual(typeof appDataDir, 'string');
// this is not part of 'snapshotting' because we need root access to traverse
function saveFsMetadata(dataLayout, metadataFile, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof metadataFile, 'string');
assert.strictEqual(typeof callback, 'function');
var emptyDirs = safe.child_process.execSync('find . -type d -empty', { cwd: `${appDataDir}`, encoding: 'utf8' });
if (emptyDirs === null) return callback(safe.error);
var execFiles = safe.child_process.execSync('find . -type f -executable', { cwd: `${appDataDir}`, encoding: 'utf8' });
if (execFiles === null) return callback(safe.error);
var metadata = {
emptyDirs: emptyDirs.length === 0 ? [ ] : emptyDirs.trim().split('\n'),
execFiles: execFiles.length === 0 ? [ ] : execFiles.trim().split('\n')
// contains paths prefixed with './'
let metadata = {
emptyDirs: [],
execFiles: []
};
if (!safe.fs.writeFileSync(`${appDataDir}/fsmetadata.json`, JSON.stringify(metadata, null, 4))) return callback(safe.error);
for (let lp of dataLayout.localPaths()) {
var emptyDirs = safe.child_process.execSync(`find ${lp} -type d -empty\n`, { encoding: 'utf8' });
if (emptyDirs === null) return callback(safe.error);
if (emptyDirs.length) metadata.emptyDirs = metadata.emptyDirs.concat(emptyDirs.trim().split('\n').map((ed) => dataLayout.toRemotePath(ed)));
var execFiles = safe.child_process.execSync(`find ${lp} -type f -executable\n`, { encoding: 'utf8' });
if (execFiles === null) return callback(safe.error);
if (execFiles.length) metadata.execFiles = metadata.execFiles.concat(execFiles.trim().split('\n').map((ef) => dataLayout.toRemotePath(ef)));
}
if (!safe.fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 4))) return callback(safe.error);
callback();
}
// this function is called via backupupload (since it needs root to traverse app's directory)
function upload(backupId, format, dataDir, progressCallback, callback) {
function upload(backupId, format, dataLayoutString, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert.strictEqual(typeof dataLayoutString, 'string');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`upload: id ${backupId} format ${format} dataDir ${dataDir}`);
debug(`upload: id ${backupId} format ${format} dataLayout ${dataLayoutString}`);
const dataLayout = DataLayout.fromString(dataLayoutString);
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
@@ -390,87 +407,99 @@ function upload(backupId, format, dataDir, progressCallback, callback) {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
var tarStream = createTarPackStream(dataDir, backupConfig.key || null);
tarStream.on('progress', function(progress) {
progressCallback({ message: `Uploading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BackupsError
tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) {
if (error) return retryCallback(error);
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
tarStream.on('progress', function(progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Uploading ${transferred}M@${speed}Mbps` });
});
tarStream.on('error', retryCallback); // already returns BackupsError
api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback);
});
}, callback);
} else {
async.series([
saveFsMetadata.bind(null, dataDir),
sync.bind(null, backupConfig, backupId, dataDir, progressCallback)
saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`),
sync.bind(null, backupConfig, backupId, dataLayout, progressCallback)
], callback);
}
});
}
function tarExtract(inStream, destination, key, callback) {
function tarExtract(inStream, dataLayout, key, callback) {
assert.strictEqual(typeof inStream, 'object');
assert.strictEqual(typeof destination, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
callback = once(callback);
var gunzip = zlib.createGunzip({});
var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
var extract = tar.extract(destination);
var extract = tar.extract('/', {
map: function (header) {
header.name = dataLayout.toLocalPath(header.name);
return header;
}
});
const emitError = once((error) => ps.emit('error', error));
inStream.on('error', function (error) {
debug('tarExtract: input stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
gunzip.on('error', function (error) {
debug('tarExtract: gunzip stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
extract.on('error', function (error) {
debug('tarExtract: extract stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
extract.on('finish', function () {
debug('tarExtract: done.');
callback(null);
// we use a separate event because ps is a through2 stream which emits 'finish' event indicating end of inStream and not extract
ps.emit('done');
});
if (key !== null) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('tarExtract: decrypt stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
emitError(new BackupsError(BackupsError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
});
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
} else {
inStream.pipe(ps).pipe(gunzip).pipe(extract);
}
return ps;
callback(null, ps);
}
function restoreFsMetadata(appDataDir, callback) {
assert.strictEqual(typeof appDataDir, 'string');
function restoreFsMetadata(dataLayout, metadataFile, callback) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof metadataFile, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`Recreating empty directories in ${appDataDir}`);
debug(`Recreating empty directories in ${dataLayout.toString()}`);
var metadataJson = safe.fs.readFileSync(path.join(appDataDir, 'fsmetadata.json'), 'utf8');
var metadataJson = safe.fs.readFileSync(metadataFile, 'utf8');
if (metadataJson === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error loading fsmetadata.txt:' + safe.error.message));
var metadata = safe.JSON.parse(metadataJson);
if (metadata === null) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error parsing fsmetadata.txt:' + safe.error.message));
async.eachSeries(metadata.emptyDirs, function createPath(emptyDir, iteratorDone) {
mkdirp(path.join(appDataDir, emptyDir), iteratorDone);
mkdirp(dataLayout.toLocalPath(emptyDir), iteratorDone);
}, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to create path: ${error.message}`));
async.eachSeries(metadata.execFiles, function createPath(execFile, iteratorDone) {
fs.chmod(path.join(appDataDir, execFile), parseInt('0755', 8), iteratorDone);
fs.chmod(dataLayout.toLocalPath(execFile), parseInt('0755', 8), iteratorDone);
}, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `unable to chmod: ${error.message}`));
@@ -479,14 +508,14 @@ function restoreFsMetadata(appDataDir, callback) {
});
}
function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, callback) {
function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
assert.strictEqual(typeof destDir, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`downloadDir: ${backupFilePath} to ${destDir}`);
debug(`downloadDir: ${backupFilePath} to ${dataLayout.toString()}`);
function downloadFile(entry, callback) {
let relativePath = path.relative(backupFilePath, entry.fullPath);
@@ -494,55 +523,79 @@ function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, ca
relativePath = decryptFilePath(relativePath, backupConfig.key);
if (!relativePath) return callback(new BackupsError(BackupsError.BAD_STATE, 'Unable to decrypt file'));
}
const destFilePath = path.join(destDir, relativePath);
const destFilePath = dataLayout.toLocalPath('./' + relativePath);
mkdirp(path.dirname(destFilePath), function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
if (error) return callback(error);
sourceStream.on('error', callback);
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
let destStream = createWriteStream(destFilePath, backupConfig.key || null);
destStream.on('error', callback);
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
let closeAndRetry = once((error) => {
if (error) progressCallback({ message: `Download ${entry.fullPath} errored: ${error.message}` });
else progressCallback({ message: `Download ${entry.fullPath} finished` });
destStream.destroy();
retryCallback(error);
});
sourceStream.pipe(destStream, { end: true }).on('finish', callback);
});
api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
if (error) return closeAndRetry(error);
sourceStream.on('error', closeAndRetry);
destStream.on('error', closeAndRetry);
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
sourceStream.pipe(destStream, { end: true }).on('finish', closeAndRetry);
});
}, callback);
});
}
api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) {
async.each(entries, downloadFile, done);
// https://www.digitalocean.com/community/questions/rate-limiting-on-spaces?answer=40441
const concurrency = backupConfig.downloadConcurrency || (backupConfig.provider === 's3' ? 30 : 10);
async.eachLimit(entries, concurrency, downloadFile, done);
}, callback);
}
function download(backupConfig, backupId, format, dataDir, progressCallback, callback) {
function download(backupConfig, backupId, format, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`download - Downloading ${backupId} of format ${format} to ${dataDir}`);
debug(`download - Downloading ${backupId} of format ${format} to ${dataLayout.toString()}`);
const backupFilePath = getBackupFilePath(backupConfig, backupId, format);
if (format === 'tgz') {
api(backupConfig.provider).download(backupConfig, getBackupFilePath(backupConfig, backupId, format), function (error, sourceStream) {
if (error) return callback(error);
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
api(backupConfig.provider).download(backupConfig, backupFilePath, function (error, sourceStream) {
if (error) return retryCallback(error);
let ps = tarExtract(sourceStream, dataDir, backupConfig.key || null, callback);
ps.on('progress', function (progress) {
progressCallback({ message: `Downloading ${Math.round(progress.transferred/1024/1024)}M@${Math.round(progress.speed/1024/1024)}Mbps` });
tarExtract(sourceStream, dataLayout, backupConfig.key || null, function (error, ps) {
if (error) return retryCallback(error);
ps.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Downloading' }); // 0M@0Mbps looks wrong
progressCallback({ message: `Downloading ${transferred}M@${speed}Mbps` });
});
ps.on('error', retryCallback);
ps.on('done', retryCallback);
});
});
});
}, callback);
} else {
downloadDir(backupConfig, getBackupFilePath(backupConfig, backupId, format), dataDir, progressCallback, function (error) {
downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
restoreFsMetadata(dataDir, callback);
restoreFsMetadata(dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`, callback);
});
}
}
@@ -553,12 +606,14 @@ function restore(backupConfig, backupId, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
const dataLayout = new DataLayout(paths.BOX_DATA_DIR, []);
download(backupConfig, backupId, backupConfig.format, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
debug('restore: download completed, importing database');
database.importFromFile(`${paths.BOX_DATA_DIR}/box.mysqldump`, function (error) {
database.importFromFile(`${dataLayout.localRoot()}/box.mysqldump`, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('restore: database imported');
@@ -575,7 +630,9 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
if (!appDataDir) return callback(safe.error);
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
var startTime = new Date();
@@ -583,7 +640,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.series([
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir, progressCallback),
download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback),
addons.restoreAddons.bind(null, app, addonsToRestore)
], function (error) {
debug('restoreApp: time: %s', (new Date() - startTime)/1000);
@@ -593,16 +650,16 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb
});
}
function runBackupUpload(backupId, format, dataDir, progressCallback, callback) {
function runBackupUpload(backupId, format, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataDir, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
let result = '';
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataDir ], { preserveEnv: true, ipc: true }, function (error) {
shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataLayout.toString() ], { preserveEnv: true, ipc: true }, function (error) {
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Backuptask crashed'));
} else if (error && error.code === 50) { // exited with error
@@ -662,7 +719,11 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
snapshotBox(progressCallback, function (error) {
if (error) return callback(error);
runBackupUpload('snapshot/box', backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) {
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
if (!boxDataDir) return callback(safe.error);
const dataLayout = new DataLayout(boxDataDir, []);
runBackupUpload('snapshot/box', backupConfig.format, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000);
@@ -771,7 +832,7 @@ function snapshotApp(app, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
progressCallback({ message: `Snapshotting app ${app.id}` });
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(apps.getAppConfig(app)))) {
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error creating config.json: ' + safe.error.message));
@@ -834,9 +895,13 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
snapshotApp(app, progressCallback, function (error) {
if (error) return callback(error);
var backupId = util.format('snapshot/app_%s', app.id);
var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
runBackupUpload(backupId, backupConfig.format, appDataDir, progressCallback, function (error) {
const backupId = util.format('snapshot/app_%s', app.id);
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
if (!appDataDir) return callback(safe.error);
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
runBackupUpload(backupId, backupConfig.format, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000);
@@ -924,9 +989,9 @@ function backupBoxAndApps(progressCallback, callback) {
function startBackupTask(auditSource, callback) {
let error = locker.lock(locker.OP_FULL_BACKUP);
if (error) return callback(error);
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, `Cannot backup now: ${error.message}`));
let task = tasks.startTask(tasks.TASK_BACKUP, [], auditSource);
let task = tasks.startTask(tasks.TASK_BACKUP, []);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
@@ -1006,22 +1071,24 @@ function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
assert.strictEqual(typeof callback, 'function');
const now = new Date();
let removedAppBackups = [];
// we clean app backups of any state because the ones to keep are determined by the box cleanup code
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, appBackups) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.eachSeries(appBackups, function iterator(backup, iteratorDone) {
if (referencedAppBackups.indexOf(backup.id) !== -1) return iteratorDone();
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
async.eachSeries(appBackups, function iterator(appBackup, iteratorDone) {
if (referencedAppBackups.indexOf(appBackup.id) !== -1) return iteratorDone();
if ((now - appBackup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
debug('cleanupAppBackups: removing %s', backup.id);
debug('cleanupAppBackups: removing %s', appBackup.id);
cleanupBackup(backupConfig, backup, iteratorDone);
removedAppBackups.push(appBackup.id);
cleanupBackup(backupConfig, appBackup, iteratorDone);
}, function () {
debug('cleanupAppBackups: done');
callback();
callback(null, removedAppBackups);
});
});
}
@@ -1032,12 +1099,12 @@ function cleanupBoxBackups(backupConfig, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
const now = new Date();
var referencedAppBackups = [];
let referencedAppBackups = [], removedBoxBackups = [];
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, boxBackups) {
if (error) return callback(error);
if (boxBackups.length === 0) return callback(null, []);
if (boxBackups.length === 0) return callback(null, { removedBoxBackups, referencedAppBackups });
// search for the first valid backup
var i;
@@ -1054,21 +1121,22 @@ function cleanupBoxBackups(backupConfig, auditSource, callback) {
debug('cleanupBoxBackups: no box backup to preserve');
}
async.eachSeries(boxBackups, function iterator(backup, nextBackup) {
async.eachSeries(boxBackups, function iterator(boxBackup, iteratorNext) {
// TODO: errored backups should probably be cleaned up before retention time, but we will
// have to be careful not to remove any backup currently being created
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) {
referencedAppBackups = referencedAppBackups.concat(backup.dependsOn);
return nextBackup();
if ((now - boxBackup.creationTime) < (backupConfig.retentionSecs * 1000)) {
referencedAppBackups = referencedAppBackups.concat(boxBackup.dependsOn);
return iteratorNext();
}
debug('cleanupBoxBackups: removing %s', backup.id);
debug('cleanupBoxBackups: removing %s', boxBackup.id);
cleanupBackup(backupConfig, backup, nextBackup);
removedBoxBackups.push(boxBackup.id);
cleanupBackup(backupConfig, boxBackup, iteratorNext);
}, function () {
debug('cleanupBoxBackups: done');
return callback(null, referencedAppBackups);
callback(null, { removedBoxBackups, referencedAppBackups });
});
});
}
@@ -1122,29 +1190,53 @@ function cleanupSnapshots(backupConfig, callback) {
});
}
function cleanup(auditSource, callback) {
function cleanup(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
callback = callback || NOOP_CALLBACK;
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
if (backupConfig.retentionSecs < 0) {
debug('cleanup: keeping all backups');
return callback();
return callback(null, {});
}
cleanupBoxBackups(backupConfig, auditSource, function (error, referencedAppBackups) {
progressCallback({ percent: 10, message: 'Cleaning box backups' });
cleanupBoxBackups(backupConfig, auditSource, function (error, result) {
if (error) return callback(error);
cleanupAppBackups(backupConfig, referencedAppBackups, function (error) {
progressCallback({ percent: 40, message: 'Cleaning app backups' });
cleanupAppBackups(backupConfig, result.referencedAppBackups, function (error, removedAppBackups) {
if (error) return callback(error);
cleanupSnapshots(backupConfig, callback);
progressCallback({ percent: 90, message: 'Cleaning snapshots' });
cleanupSnapshots(backupConfig, function (error) {
if (error) return callback(error);
callback(null, { removedBoxBackups: result.removedBoxBackups, removedAppBackups: removedAppBackups });
});
});
});
});
}
function startCleanupTask(auditSource, callback) {
let task = tasks.startTask(tasks.TASK_CLEAN_BACKUPS, [ auditSource ]);
task.on('error', (error) => callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)));
task.on('start', (taskId) => {
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_START, auditSource, { taskId });
callback(null, taskId);
});
task.on('finish', (error, result) => { // result is { removedBoxBackups, removedAppBackups }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
});
});
}
+2 -2
View File
@@ -226,7 +226,7 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
debug(`waitForOrder: ${orderUrl}`);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitForOrder: getting status');
superagent.get(orderUrl).timeout(30 * 1000).end(function (error, result) {
@@ -290,7 +290,7 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
debug('waitingForChallenge: %j', challenge);
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
superagent.get(challenge.url).timeout(30 * 1000).end(function (error, result) {
+29 -99
View File
@@ -8,26 +8,24 @@ exports = module.exports = {
getConfig: getConfig,
getDisks: getDisks,
getLogs: getLogs,
getStatus: getStatus,
reboot: reboot,
isRebootRequired: isRebootRequired,
onActivated: onActivated,
prepareDashboardDomain: prepareDashboardDomain,
setDashboardDomain: setDashboardDomain,
renewCerts: renewCerts,
checkDiskSpace: checkDiskSpace,
configureWebadmin: configureWebadmin,
getWebadminStatus: getWebadminStatus
checkDiskSpace: checkDiskSpace
};
var assert = require('assert'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
@@ -44,7 +42,6 @@ var assert = require('assert'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
util = require('util');
@@ -53,16 +50,6 @@ var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
let gWebadminStatus = {
dns: false,
tls: false,
configuring: false,
restore: {
active: false,
error: null
}
};
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -123,7 +110,7 @@ function runStartupTasks() {
reverseProxy.configureDefaultServer(NOOP_CALLBACK);
// always generate webadmin config since we have no versioning mechanism for the ejs
configureWebadmin(NOOP_CALLBACK);
if (config.adminDomain()) reverseProxy.writeAdminConfig(config.adminDomain(), NOOP_CALLBACK);
// check activation state and start the platform
users.isActivated(function (error, activated) {
@@ -241,15 +228,13 @@ function getLogs(unit, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
var lines = options.lines || 100,
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
var lines = options.lines === -1 ? '+1' : options.lines,
format = options.format || 'json',
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
follow = options.follow;
debug('Getting logs for %s as %s', unit, format);
@@ -283,75 +268,16 @@ function getLogs(unit, options, callback) {
return callback(null, transformStream);
}
function configureWebadmin(callback) {
function prepareDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('configureWebadmin: adminDomain:%s status:%j', config.adminDomain(), gWebadminStatus);
debug(`prepareDashboardDomain: ${domain}`);
if (process.env.BOX_ENV === 'test' || !config.adminDomain() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function configureReverseProxy(error) {
debug('configureReverseProxy: error %j', error || null);
reverseProxy.configureAdmin({ userId: null, username: 'setup' }, function (error) {
debug('configureWebadmin: done error: %j', error || {});
gWebadminStatus.configuring = false;
if (error) return callback(error);
gWebadminStatus.tls = true;
callback();
});
}
// update the DNS. configure nginx regardless of whether it succeeded so that
// box is accessible even if dns creds are invalid
sysinfo.getPublicIp(function (error, ip) {
if (error) return configureReverseProxy(error);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
debug('addWebadminDnsRecord: updated records with error:', error);
if (error) return configureReverseProxy(error);
domains.waitForDnsRecord(config.adminLocation(), config.adminDomain(), 'A', ip, { interval: 30000, times: 50000 }, function (error) {
if (error) return configureReverseProxy(error);
gWebadminStatus.dns = true;
configureReverseProxy();
});
});
});
}
function getWebadminStatus() {
return gWebadminStatus;
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null, {
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: activated,
edition: config.edition(),
webadminStatus: gWebadminStatus // only valid when !activated
});
});
});
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, callback) {
@@ -360,20 +286,24 @@ function setDashboardDomain(domain, callback) {
debug(`setDashboardDomain: ${domain}`);
domains.get(domain, function (error, result) {
domains.get(domain, function (error, domainObject) {
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new CloudronError(CloudronError.BAD_FIELD, 'No such domain'));
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
config.setAdminDomain(result.domain);
config.setAdminLocation('my');
config.setAdminFqdn('my' + (result.config.hyphenatedSubdomains ? '-' : '.') + result.domain);
clients.addDefaultClients(config.adminOrigin(), function (error) {
reverseProxy.writeAdminConfig(domain, function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null);
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
configureWebadmin(NOOP_CALLBACK); // ## trigger as task
config.setAdminDomain(domain);
config.setAdminLocation(constants.ADMIN_LOCATION);
config.setAdminFqdn(fqdn);
clients.addDefaultClients(config.adminOrigin(), function (error) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
callback(null);
});
});
});
}
+5 -3
View File
@@ -119,8 +119,9 @@ function initConfig() {
if (exports.TEST) {
data.port = 5454;
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
data.database.password = '';
data.database.name = 'boxtest';
// see setupTest script how the mysql-server is run
data.database.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
// overwrite defaults with saved config
@@ -231,7 +232,8 @@ function isManaged() {
function hasIPv6() {
const IPV6_PROC_FILE = '/proc/net/if_inet6';
return fs.existsSync(IPV6_PROC_FILE);
// on contabo, /proc/net/if_inet6 is an empty file. so just exists is not enough
return fs.existsSync(IPV6_PROC_FILE) && fs.readFileSync(IPV6_PROC_FILE, 'utf8').trim().length !== 0;
}
// it has to change with the adminLocation so that multiple cloudrons
+1 -2
View File
@@ -17,9 +17,8 @@ exports = module.exports = {
'admins', 'users' // ldap code uses 'users' pseudo group
],
ADMIN_NAME: 'Settings',
ADMIN_LOCATION: 'my',
NGINX_ADMIN_CONFIG_FILE_NAME: 'admin.conf',
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
+1 -1
View File
@@ -153,7 +153,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
});
+3 -7
View File
@@ -85,7 +85,7 @@ function reconnect(callback) {
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
var cmd = util.format('mysql --host=%s --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host=%s --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
config.database().hostname, config.database().username, config.database().password, config.database().name,
config.database().hostname, config.database().username, config.database().password, config.database().name);
@@ -177,9 +177,7 @@ function importFromFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysql -u ${config.database().username} ${password} ${config.database().name} < ${file}`;
var cmd = `/usr/bin/mysql -h "${config.database().hostname}" -u ${config.database().username} -p${config.database().password} ${config.database().name} < ${file}`;
async.series([
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
@@ -191,9 +189,7 @@ function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var password = config.database().password ? '-p' + config.database().password : '--skip-password';
var cmd = `/usr/bin/mysqldump -u root ${password} --single-transaction --routines \
--triggers ${config.database().name} > "${file}"`;
var cmd = `/usr/bin/mysqldump -h "${config.database().hostname}" -u root -p${config.database().password} --single-transaction --routines --triggers ${config.database().name} > "${file}"`;
child_process.exec(cmd, callback);
}
+55
View File
@@ -0,0 +1,55 @@
'use strict';
let assert = require('assert'),
path = require('path');
class DataLayout {
constructor(localRoot, dirMap) {
assert.strictEqual(typeof localRoot, 'string');
assert(Array.isArray(dirMap), 'Expecting layout to be an array');
this._localRoot = localRoot;
this._dirMap = dirMap;
this._remoteRegexps = dirMap.map((l) => new RegExp('^\\./' + l.remoteDir + '/?'));
this._localRegexps = dirMap.map((l) => new RegExp('^' + l.localDir + '/?'));
}
toLocalPath(remoteName) {
assert.strictEqual(typeof remoteName, 'string');
for (let i = 0; i < this._remoteRegexps.length; i++) {
if (!remoteName.match(this._remoteRegexps[i])) continue;
return remoteName.replace(this._remoteRegexps[i], this._dirMap[i].localDir + '/'); // make paths absolute
}
return remoteName.replace(new RegExp('^\\.'), this._localRoot);
}
toRemotePath(localName) {
assert.strictEqual(typeof localName, 'string');
for (let i = 0; i < this._localRegexps.length; i++) {
if (!localName.match(this._localRegexps[i])) continue;
return localName.replace(this._localRegexps[i], './' + this._dirMap[i].remoteDir + '/'); // make paths relative
}
return localName.replace(new RegExp('^' + this._localRoot + '/?'), './');
}
localRoot() {
return this._localRoot;
}
getBasename() { // used to generate cache file names
return path.basename(this._localRoot);
}
toString() {
return JSON.stringify({ localRoot: this._localRoot, layout: this._dirMap });
}
localPaths() {
return [ this._localRoot ].concat(this._dirMap.map((l) => l.localDir));
}
directoryMap() {
return this._dirMap;
}
static fromString(str) {
const obj = JSON.parse(str);
return new DataLayout(obj.localRoot, obj.layout);
}
}
exports = module.exports = DataLayout;
+49 -44
View File
@@ -4,35 +4,38 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
config = require('../config.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
function getFqdn(subdomain, domain) {
assert.strictEqual(typeof subdomain, 'string');
function getFqdn(location, domain) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
return (subdomain === '') ? domain : subdomain + '-' + domain;
return (location === '') ? domain : location + '-' + domain;
}
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
const dnsConfig = domainObject.config;
debug('add: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values);
let fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('add: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
@@ -54,16 +57,16 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + dnsConfig.fqdn : getFqdn(subdomain, dnsConfig.fqdn);
const dnsConfig = domainObject.config;
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', dnsConfig.fqdn, subdomain, type, fqdn);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
superagent
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
@@ -77,26 +80,15 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
add(dnsConfig, zoneName, subdomain, type, values, callback);
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('del: %s for zone %s of type %s with values %j', subdomain, dnsConfig.fqdn, type, values);
const dnsConfig = domainObject.config;
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
@@ -104,7 +96,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
};
superagent
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(subdomain, dnsConfig.fqdn))
.del(config.apiServerOrigin() + '/api/v1/domains/' + getFqdn(location, domainObject.domain))
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
@@ -119,29 +111,42 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token,
fqdn: domain,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
};
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+106 -62
View File
@@ -4,7 +4,7 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
@@ -12,9 +12,11 @@ var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
// we are using latest v4 stable API https://api.cloudflare.com/#getting-started-endpoints
@@ -53,16 +55,14 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, callback) {
// gets records filtered by zone, type and fqdn
function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneId, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
@@ -78,32 +78,30 @@ function getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, cal
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
getZoneByName(dnsConfig, zoneName, function(error, result){
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
var zoneId = result.id;
let zoneId = result.id;
getDnsRecordsByZoneId(dnsConfig, zoneId, zoneName, subdomain, type, function (error, result) {
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
if (error) return callback(error);
var dnsRecords = result;
let i = 0; // // used to track available records to update instead of create
// used to track available records to update instead of create
var i = 0;
async.eachSeries(values, function (value, callback) {
async.eachSeries(values, function (value, iteratorCallback) {
var priority = null;
if (type === 'MX') {
@@ -116,35 +114,41 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
name: fqdn,
content: value,
priority: priority,
proxied: false,
ttl: 120 // 1 means "automatic" (meaning 300ms) and 120 is the lowest supported
};
if (i >= dnsRecords.length) {
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records')
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
if (i >= dnsRecords.length) { // create a new record
debug(`upsert: Adding new record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: false`);
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
callback(null);
iteratorCallback(null);
});
} else {
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
} else { // replace existing record
data.proxied = dnsRecords[i].proxied; // preserve proxied parameter
debug(`upsert: Updating existing record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: ${data.proxied}`);
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id)
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
++i; // increment, as we have consumed the record
if (error && !error.response) return callback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
if (error && !error.response) return iteratorCallback(error);
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
callback(null);
iteratorCallback(null);
});
}
}, callback);
@@ -152,17 +156,20 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getZoneByName(dnsConfig, zoneName, function(error, result){
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDnsRecords(dnsConfig, zone.id, fqdn, type, function (error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.content; });
@@ -173,18 +180,21 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
getZoneByName(dnsConfig, zoneName, function(error, result){
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
getDnsRecordsByZoneId(dnsConfig, result.id, zoneName, subdomain, type, function(error, result) {
getDnsRecords(dnsConfig, zone.id, fqdn, type, function(error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -197,8 +207,8 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
async.eachSeries(tmp, function (record, callback) {
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
.set('X-Auth-Key',dnsConfig.token)
.set('X-Auth-Email',dnsConfig.email)
.set('X-Auth-Key', dnsConfig.token)
.set('X-Auth-Email', dnsConfig.email)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(error);
@@ -217,16 +227,50 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('wait: %s for zone %s of type %s', fqdn, zoneName, type);
getZoneByName(dnsConfig, zoneName, function(error, result) {
if (error) return callback(error);
let zoneId = result.id;
getDnsRecords(dnsConfig, zoneId, fqdn, type, function (error, dnsRecords) {
if (error) return callback(error);
if (dnsRecords.length === 0) return callback(new DomainsError(DomainsError.NOT_FOUND, 'Domain not found'));
if (!dnsRecords[0].proxied) return waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
debug('wait: skipping wait of proxied domain');
callback(null); // maybe we can check for dns to be cloudflare IPs? https://api.cloudflare.com/#cloudflare-ips-cloudflare-ip-details
});
});
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'email must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token,
email: dnsConfig.email
@@ -238,22 +282,22 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
getZoneByName(dnsConfig, zoneName, function(error, result) {
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
if (!_.isEqual(result.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, result.name_servers);
if (!_.isEqual(zone.name_servers.sort(), nameservers.sort())) {
debug('verifyDnsConfig: %j and %j do not match', nameservers, zone.name_servers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Cloudflare'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+54 -34
View File
@@ -4,7 +4,7 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
@@ -12,10 +12,12 @@ var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
var DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com';
@@ -23,10 +25,10 @@ function formatError(response) {
return util.format('DigitalOcean DNS error [%s] %j', response.statusCode, response.body);
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
function getInternal(dnsConfig, zoneName, name, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -45,7 +47,7 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
if (result.statusCode !== 200) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(result)));
matchingRecords = matchingRecords.concat(result.body.domain_records.filter(function (record) {
return (record.type === type && record.name === subdomain);
return (record.type === type && record.name === name);
}));
nextPage = (result.body.links && result.body.links.pages) ? result.body.links.pages.next : null;
@@ -61,19 +63,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 +92,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
var data = {
type: type,
name: subdomain,
name: name,
data: value,
priority: priority,
ttl: 1
@@ -133,16 +136,17 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
// We only return the value string
@@ -154,17 +158,18 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback(null);
@@ -193,15 +198,30 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token
};
@@ -217,14 +237,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+52 -32
View File
@@ -4,16 +4,18 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
@@ -21,24 +23,25 @@ function formatError(response) {
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
'rrset_ttl': 300, // this is the minimum allowed
'rrset_values': values // for mx records, value is already of the '<priority> <server>' format
};
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
superagent.put(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.send(data)
@@ -52,18 +55,19 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
superagent.get(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -78,19 +82,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${subdomain}/${type}`)
superagent.del(`${GANDI_API}/domains/${zoneName}/records/${name}/${type}`)
.set('X-Api-Key', dnsConfig.token)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -105,19 +110,34 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a non-empty string'));
var credentials = {
token: dnsConfig.token
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -129,14 +149,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Gandi'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+55 -32
View File
@@ -4,16 +4,18 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
GCDNS = require('@google-cloud/dns'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
function getDnsCredentials(dnsConfig) {
@@ -55,22 +57,23 @@ function getZoneByName(dnsConfig, zoneName, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function (error, oldRecords) {
zone.getRecords({ type: type, name: fqdn + '.' }, function (error, oldRecords) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) {
debug('upsert->zone.getRecords', error);
@@ -78,12 +81,12 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
}
var newRecord = zone.record(type, {
name: domain,
name: fqdn + '.',
data: values,
ttl: 1
});
zone.createChange({ delete: oldRecords, add: newRecord }, function(error, change) {
zone.createChange({ delete: oldRecords, add: newRecord }, function(error /*, change */) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error && error.code === 412) return callback(new DomainsError(DomainsError.STILL_BUSY, error.message));
if (error) {
@@ -97,18 +100,21 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
name: (subdomain ? subdomain + '.' : '') + zoneName + '.',
name: fqdn + '.',
type: type
};
@@ -122,20 +128,21 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
var domain = (subdomain ? subdomain + '.' : '') + zoneName + '.';
zone.getRecords({ type: type, name: domain }, function(error, oldRecords) {
zone.getRecords({ type: type, name: fqdn + '.' }, function(error, oldRecords) {
if (error && error.code === 403) return callback(new DomainsError(DomainsError.ACCESS_DENIED, error.message));
if (error) {
debug('del->zone.getRecords', error);
@@ -156,19 +163,35 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (typeof dnsConfig.projectId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'projectId must be a string'));
if (!dnsConfig.credentials || typeof dnsConfig.credentials !== 'object') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials must be an object'));
if (typeof dnsConfig.credentials.client_email !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.client_email must be a string'));
if (typeof dnsConfig.credentials.private_key !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'credentials.private_key must be a string'));
var credentials = getDnsCredentials(dnsConfig);
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -184,14 +207,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Google Cloud DNS'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+53 -33
View File
@@ -4,16 +4,18 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
// const GODADDY_API_OTE = 'https://api.ote-godaddy.com/v1/domains';
const GODADDY_API = 'https://api.godaddy.com/v1/domains';
@@ -27,17 +29,18 @@ function formatError(response) {
return util.format(`GoDaddy DNS error [${response.statusCode}] ${response.body.message}`);
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var records = [ ];
values.forEach(function (value) {
@@ -53,7 +56,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
records.push(record);
});
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.send(records)
@@ -68,18 +71,19 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type}`);
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
superagent.get(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.timeout(30 * 1000)
.end(function (error, result) {
@@ -98,22 +102,23 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`get: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`get: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
if (type !== 'A' && type !== 'TXT') return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, new Error('Record deletion is not supported by GoDaddy API')));
// check if the record exists at all so that we don't insert the "Dead" record for no reason
get(dnsConfig, zoneName, subdomain, type, function (error, values) {
get(domainObject, location, type, function (error, values) {
if (error) return callback(error);
if (values.length === 0) return callback();
@@ -123,7 +128,7 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
data: type === 'A' ? GODADDY_INVALID_IP : GODADDY_INVALID_TXT
}];
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${subdomain}`)
superagent.put(`${GODADDY_API}/${zoneName}/records/${type}/${name}`)
.set('Authorization', `sso-key ${dnsConfig.apiKey}:${dnsConfig.apiSecret}`)
.send(records)
.timeout(30 * 1000)
@@ -140,16 +145,31 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
if (!dnsConfig.apiSecret || typeof dnsConfig.apiSecret !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiSecret must be a non-empty string'));
const ip = '127.0.0.1';
var credentials = {
apiKey: dnsConfig.apiKey,
apiSecret: dnsConfig.apiSecret
@@ -166,14 +186,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to GoDaddy'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+23 -19
View File
@@ -10,18 +10,16 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
DomainsError = require('../domains.js').DomainsError,
util = require('util');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -31,10 +29,9 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
callback(new Error('not implemented'));
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -43,10 +40,9 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
callback(new Error('not implemented'));
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -56,11 +52,19 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
callback(new Error('not implemented'));
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
callback();
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
// Result: dnsConfig object
+31 -20
View File
@@ -4,43 +4,42 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -48,13 +47,25 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const zoneName = domainObject.zoneName;
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
+280
View File
@@ -0,0 +1,280 @@
'use strict';
exports = module.exports = {
upsert: upsert,
get: get,
del: del,
verifyDnsConfig: verifyDnsConfig,
wait: wait
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
Namecheap = require('namecheap'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js');
var namecheap;
function formatError(response) {
return util.format('NameCheap DNS error [%s] %j', response.code, response.message);
}
// Only send required fields - https://www.namecheap.com/support/api/methods/domains-dns/set-hosts.aspx
function mapHosts(hosts) {
return hosts.map(function (host) {
let tmp = {};
tmp.TTL = '300';
tmp.RecordType = host.RecordType || host.Type;
tmp.HostName = host.HostName || host.Name;
tmp.Address = host.Address;
if (tmp.RecordType === 'MX') {
tmp.EmailType = 'MX';
if (host.MXPref) tmp.MXPref = host.MXPref;
}
return tmp;
});
}
function init(dnsConfig, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (namecheap) return callback();
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
// Note that for all NameCheap calls to go through properly, the public IP returned by the getPublicIp method below must be whitelisted on NameCheap's API dashboard
namecheap = new Namecheap(dnsConfig.username, dnsConfig.apiKey, ip);
namecheap.setUsername(dnsConfig.username);
callback();
});
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
init(dnsConfig, function (error) {
if (error) return callback(error);
namecheap.domains.dns.getHosts(zoneName, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
debug('entire getInternal response: %j', result);
return callback(null, result['DomainDNSGetHostsResult']['host']);
});
});
}
function setInternal(zoneName, hosts, callback) {
let mappedHosts = mapHosts(hosts);
namecheap.domains.dns.setHosts(zoneName, mappedHosts, function (error, result) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, formatError(error)));
return callback(null, result);
});
}
function upsert(domainObject, subdomain, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// Array to keep track of records that need to be inserted
let toInsert = [];
for (var i = 0; i < values.length; i++) {
let curValue = values[i];
let wasUpdate = false;
for (var j = 0; j < result.length; j++) {
let curHost = result[j];
if (curHost.Type === type && curHost.Name === subdomain) {
// Updating an already existing host
wasUpdate = true;
if (type === 'MX') {
curHost.MXPref = curValue.split(' ')[0];
curHost.Address = curValue.split(' ')[1];
} else {
curHost.Address = curValue;
}
}
}
// We don't have this host at all yet, let's push to toInsert array
if (!wasUpdate) {
let newRecord = {
RecordType: type,
HostName: subdomain,
Address: curValue
};
// Special case for MX records
if (type === 'MX') {
newRecord.MXPref = curValue.split(' ')[0];
newRecord.Address = curValue.split(' ')[1];
}
toInsert.push(newRecord);
}
}
let toUpsert = result.concat(toInsert);
setInternal(zoneName, toUpsert, callback);
});
}
function get(domainObject, subdomain, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
// We need to filter hosts to ones with this subdomain and type
let actualHosts = result.filter((host) => host.Type === type && host.Name === subdomain);
// We only return the value string
var tmp = actualHosts.map(function (record) { return record.Address; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function del(domainObject, subdomain, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
let removed = false;
for (var i = 0; i < values.length; i++) {
let curValue = values[i];
for (var j = 0; j < result.length; j++) {
let curHost = result[i];
if (curHost.Type === type && curHost.Name === subdomain && curHost.Address === curValue) {
removed = true;
result.splice(i, 1); // Remove element from result array
}
}
}
// Only set hosts if we actually removed a host
if (removed) return setInternal(zoneName, result, callback);
callback();
});
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
const ip = '127.0.0.1';
if (!dnsConfig.username || typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a non-empty string'));
if (!dnsConfig.apiKey || typeof dnsConfig.apiKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'apiKey must be a non-empty string'));
var credentials = {
username: dnsConfig.username,
apKey: dnsConfig.apiKey
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (nameservers.some(function (n) { return n.toLowerCase().indexOf('.registrar-servers.com') === -1; })) {
debug('verifyDnsConfig: %j does not contains NC NS', nameservers);
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to NameCheap'));
}
const testSubdomain = 'cloudrontestdns';
upsert(domainObject, testSubdomain, 'A', [ip], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
del(domainObject, testSubdomain, 'A', [ip], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, dnsConfig);
});
});
});
}
function wait(domainObject, subdomain, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(subdomain, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
+70 -49
View File
@@ -4,16 +4,19 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
safe = require('safetydance'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
superagent = require('superagent');
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
const NAMECOM_API = 'https://api.name.com/v4';
@@ -21,18 +24,18 @@ function formatError(response) {
return `Name.com DNS error [${response.statusCode}] ${response.text}`;
}
function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`add: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`add: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: subdomain,
host: name,
type: type,
ttl: 300 // 300 is the lowest
};
@@ -57,19 +60,19 @@ function addRecord(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, callback) {
function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof recordId, 'number');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug(`update:${recordId} on ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`update:${recordId} on ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
var data = {
host: subdomain,
host: name,
type: type,
ttl: 300 // 300 is the lowest
};
@@ -94,16 +97,14 @@ function updateRecord(dnsConfig, zoneName, recordId, subdomain, type, values, ca
});
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
function getInternal(dnsConfig, zoneName, name, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
debug(`getInternal: ${subdomain} in zone ${zoneName} of type ${type}`);
debug(`getInternal: ${name} in zone ${zoneName} of type ${type}`);
superagent.get(`${NAMECOM_API}/domains/${zoneName}/records`)
.auth(dnsConfig.username, dnsConfig.token)
@@ -123,7 +124,7 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
});
var results = result.body.records.filter(function (r) {
return (r.host === subdomain && r.type === type);
return (r.host === name && r.type === type);
});
debug('getInternal: %j', results);
@@ -132,35 +133,39 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`upsert: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return addRecord(dnsConfig, zoneName, subdomain, type, values, callback);
if (result.length === 0) return addRecord(dnsConfig, zoneName, name, type, values, callback);
return updateRecord(dnsConfig, zoneName, result[0].id, subdomain, type, values, callback);
return updateRecord(dnsConfig, zoneName, result[0].id, name, type, values, callback);
});
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
var tmp = result.map(function (record) { return record.answer; });
@@ -171,19 +176,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
subdomain = subdomain || '@';
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
debug(`del: ${subdomain} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
@@ -201,13 +207,26 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (typeof dnsConfig.username !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'username must be a string'));
if (typeof dnsConfig.token !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'token must be a string'));
@@ -216,6 +235,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
token: dnsConfig.token
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -227,14 +248,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Name.com'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(dnsConfig, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+16 -22
View File
@@ -4,7 +4,7 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: waitForDns,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
@@ -12,33 +12,30 @@ var assert = require('assert'),
debug = require('debug')('box:dns/noop'),
util = require('util');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -46,9 +43,9 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function waitForDns(domain, zoneName, type, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
@@ -57,11 +54,8 @@ function waitForDns(domain, zoneName, type, value, options, callback) {
callback();
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(null, { });
+51 -41
View File
@@ -4,19 +4,18 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
verifyDnsConfig: verifyDnsConfig,
// not part of "dns" interface
getHostedZone: getHostedZone
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
AWS = require('aws-sdk'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
function getDnsCredentials(dnsConfig) {
@@ -82,20 +81,22 @@ function getHostedZone(dnsConfig, zoneName, callback) {
});
}
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; }); // for mx records, value is already of the '<priority> <server>' format
var params = {
@@ -126,31 +127,23 @@ function add(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
add(dnsConfig, zoneName, subdomain, type, values, callback);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var params = {
HostedZoneId: zone.Id,
MaxItems: '1',
StartRecordName: (subdomain ? subdomain + '.' : '') + zoneName + '.',
StartRecordName: fqdn + '.',
StartRecordType: type
};
@@ -169,18 +162,20 @@ function get(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
var fqdn = subdomain === '' ? zoneName : subdomain + '.' + zoneName;
var records = values.map(function (v) { return { Value: v }; });
var resourceRecordSet = {
@@ -226,13 +221,26 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
});
}
function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.accessKeyId || typeof dnsConfig.accessKeyId !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'accessKeyId must be a non-empty string'));
if (!dnsConfig.secretAccessKey || typeof dnsConfig.secretAccessKey !== 'string') return callback(new DomainsError(DomainsError.BAD_FIELD, 'secretAccessKey must be a non-empty string'));
@@ -244,6 +252,8 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
listHostedZonesByName: true, // new/updated creds require this perm
};
const ip = '127.0.0.1';
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
@@ -258,14 +268,14 @@ function verifyDnsConfig(dnsConfig, fqdn, zoneName, ip, callback) {
return callback(new DomainsError(DomainsError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
}
const testSubdomain = 'cloudrontestdns';
const location = 'cloudrontestdns';
upsert(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error, changeId) {
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added with change id %s', changeId);
debug('verifyDnsConfig: Test A record added');
del(credentials, zoneName, testSubdomain, 'A', [ ip ], function (error) {
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+13 -13
View File
@@ -30,8 +30,8 @@ function resolveIp(hostname, options, callback) {
});
}
function isChangeSynced(domain, type, value, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
function isChangeSynced(hostname, type, value, nameserver, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof nameserver, 'string');
@@ -46,16 +46,16 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
async.every(nsIps, function (nsIp, iteratorCallback) {
const resolveOptions = { server: nsIp, timeout: 5000 };
const resolver = type === 'A' ? resolveIp.bind(null, domain) : dns.resolve.bind(null, domain, 'TXT');
const resolver = type === 'A' ? resolveIp.bind(null, hostname) : dns.resolve.bind(null, hostname, 'TXT');
resolver(resolveOptions, function (error, answer) {
if (error && error.code === 'TIMEOUT') {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${domain} (${type})`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) timed out when resolving ${hostname} (${type})`);
return iteratorCallback(null, true); // should be ok if dns server is down
}
if (error) {
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${domain} (${type}): ${error}`);
debug(`isChangeSynced: NS ${nameserver} (${nsIp}) errored when resolve ${hostname} (${type}): ${error}`);
return iteratorCallback(null, false);
}
@@ -66,7 +66,7 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
match = answer.some(function (a) { return value === a.join(''); });
}
debug(`isChangeSynced: ${domain} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
debug(`isChangeSynced: ${hostname} (${type}) was resolved to ${answer} at NS ${nameserver} (${nsIp}). Expecting ${value}. Match ${match}`);
iteratorCallback(null, match);
});
@@ -76,26 +76,26 @@ function isChangeSynced(domain, type, value, nameserver, callback) {
}
// check if IP change has propagated to every nameserver
function waitForDns(domain, zoneName, type, value, options, callback) {
assert.strictEqual(typeof domain, 'string');
function waitForDns(hostname, zoneName, type, value, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
debug('waitForDns: domain %s to be %s in zone %s.', domain, value, zoneName);
debug('waitForDns: hostname %s to be %s in zone %s.', hostname, value, zoneName);
var attempt = 0;
async.retry(options, function (retryCallback) {
++attempt;
debug(`waitForDns (try ${attempt}): ${domain} to be ${value} in zone ${zoneName}`);
debug(`waitForDns (try ${attempt}): ${hostname} to be ${value} in zone ${zoneName}`);
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new DomainsError(DomainsError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, type, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
async.every(nameservers, isChangeSynced.bind(null, hostname, type, value), function (error, synced) {
debug('waitForDns: %s %s ns: %j', hostname, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new DomainsError(DomainsError.EXTERNAL_ERROR, 'ETRYAGAIN'));
});
@@ -103,7 +103,7 @@ function waitForDns(domain, zoneName, type, value, options, callback) {
}, function retryDone(error) {
if (error) return callback(error);
debug(`waitForDns: ${domain} has propagated`);
debug(`waitForDns: ${hostname} has propagated`);
callback(null);
});
+34 -22
View File
@@ -4,44 +4,43 @@ exports = module.exports = {
upsert: upsert,
get: get,
del: del,
waitForDns: require('./waitfordns.js'),
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
DomainsError = require('../domains.js').DomainsError,
sysinfo = require('../sysinfo.js'),
util = require('util');
util = require('util'),
waitForDns = require('./waitfordns.js');
function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
return callback(null);
}
function get(dnsConfig, zoneName, subdomain, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
callback(null, [ ]); // returning ip confuses apptask into thinking the entry already exists
}
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof subdomain, 'string');
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
@@ -49,20 +48,33 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
return callback();
}
function verifyDnsConfig(dnsConfig, domain, zoneName, ip, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof ip, 'string');
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const zoneName = domainObject.zoneName;
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
const separator = dnsConfig.hyphenatedSubdomains ? '-' : '.';
const fqdn = `cloudrontest${separator}${domain}`;
const location = 'cloudrontestdns';
const fqdn = domains.fqdn(location, domainObject);
dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) {
if (error && error.code === 'ENOTFOUND') return callback(new DomainsError(DomainsError.BAD_FIELD, `Unable to resolve ${fqdn}`));
if (error || !result) return callback(new DomainsError(DomainsError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`));
+27 -21
View File
@@ -54,17 +54,15 @@ var addons = require('./addons.js'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:docker.js'),
mkdirp = require('mkdirp'),
once = require('once'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
shell = require('./shell.js'),
spawn = child_process.spawn,
util = require('util'),
_ = require('underscore');
const RMVOLUME_CMD = path.join(__dirname, 'scripts/rmvolume.sh');
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
function DockerError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -373,15 +371,19 @@ function deleteContainer(containerId, callback) {
});
}
function deleteContainers(appId, callback) {
function deleteContainers(appId, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
var docker = exports.connection;
debug('deleting containers of %s', appId);
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
if (error) return callback(error);
async.eachSeries(containers, function (container, iteratorDone) {
@@ -517,16 +519,14 @@ function execContainer(containerId, cmd, options, callback) {
if (options.stdin) options.stdin.pipe(cp.stdin).on('error', callback);
}
function createVolume(app, name, subdir, callback) {
function createVolume(app, name, volumeDataDir, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
const volumeDataDir = path.join(paths.APPS_DATA_DIR, app.id, subdir);
const volumeOptions = {
Name: name,
Driver: 'local',
@@ -541,7 +541,8 @@ function createVolume(app, name, subdir, callback) {
},
};
mkdirp(volumeDataDir, function (error) {
// requires sudo because the path can be outside appsdata
shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) {
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
docker.createVolume(volumeOptions, function (error) {
@@ -552,30 +553,35 @@ function createVolume(app, name, subdir, callback) {
});
}
function clearVolume(app, name, subdir, callback) {
function clearVolume(app, name, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
let docker = exports.connection;
let volume = docker.getVolume(name);
volume.inspect(function (error, v) {
if (error && error.statusCode === 404) return callback();
if (error) return callback(error);
const volumeDataDir = v.Options.device;
shell.sudo('clearVolume', [ CLEARVOLUME_CMD, options.removeDirectory ? 'rmdir' : 'clear', volumeDataDir ], {}, callback);
});
}
function removeVolume(app, name, subdir, callback) {
// this only removes the volume and not the data
function removeVolume(app, name, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof subdir, 'string');
assert.strictEqual(typeof callback, 'function');
let docker = exports.connection;
let volume = docker.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) {
debug(`removeVolume: Error removing volume of ${app.id} ${error}`);
callback(error);
}
if (error && error.statusCode !== 404) return callback(new Error(`removeVolume: Error removing volume of ${app.id} ${error.message}`));
shell.sudo('removeVolume', [ RMVOLUME_CMD, app.id, subdir ], {}, callback);
callback();
});
}
+2 -2
View File
@@ -70,7 +70,7 @@ function attachDockerRequest(req, res, next) {
function containersCreate(req, res, next) {
safe.set(req.body, 'HostConfig.NetworkMode', 'cloudron'); // overwrite the network the container lives in
safe.set(req.body, 'NetworkingConfig', {}); // drop any custom network configs
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id })); // overwrite the app id to track containers of an app
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
@@ -122,7 +122,7 @@ function start(callback) {
if (config.TEST) {
proxyServer.use(function (req, res, next) {
console.log('Proxying: ' + req.method, req.url);
debug('proxying: ' + req.method, req.url);
next();
});
}
+80 -63
View File
@@ -10,6 +10,7 @@ module.exports = exports = {
isLocked: isLocked,
fqdn: fqdn,
getName: getName,
getDnsRecords: getDnsRecords,
upsertDnsRecords: upsertDnsRecords,
@@ -26,13 +27,13 @@ module.exports = exports = {
parentDomain: parentDomain,
DomainsError: DomainsError,
prepareDashboardDomain: prepareDashboardDomain,
// exported for testing
_getName: getName
DomainsError: DomainsError
};
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
@@ -90,6 +91,7 @@ function api(provider) {
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
case 'wildcard': return require('./dns/wildcard.js');
@@ -102,18 +104,18 @@ function parentDomain(domain) {
return domain.replace(/^\S+?\./, ''); // +? means non-greedy
}
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, ip, callback) {
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var backend = api(provider);
if (!backend) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Invalid provider'));
api(provider).verifyDnsConfig(dnsConfig, domain, zoneName, ip, function (error, result) {
const domainObject = { config: dnsConfig, domain: domain, zoneName: zoneName };
api(provider).verifyDnsConfig(domainObject, function (error, result) {
if (error && error.reason === DomainsError.ACCESS_DENIED) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Incorrect configuration. Access denied'));
if (error && error.reason === DomainsError.NOT_FOUND) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Zone not found'));
if (error && error.reason === DomainsError.EXTERNAL_ERROR) return callback(new DomainsError(DomainsError.BAD_FIELD, 'Configuration error: ' + error.message));
@@ -224,23 +226,19 @@ function add(domain, data, auditSource, callback) {
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new DomainsError(DomainsError.ALREADY_EXISTS));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
callback();
});
callback();
});
});
});
@@ -318,25 +316,21 @@ function update(domain, data, auditSource, callback) {
error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
verifyDnsConfig(config, domain, zoneName, provider, ip, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
domaindb.update(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DomainsError(DomainsError.NOT_FOUND));
if (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
if (!fallbackCertificate) return callback();
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
callback();
});
callback();
});
});
});
@@ -372,39 +366,36 @@ function clear(callback) {
}
// returns the 'name' that needs to be inserted into zone
function getName(domain, subdomain, type) {
// hack for supporting special caas domains. if we want to remove this, we have to fix the appstore domain API first
if (domain.provider === 'caas') return subdomain;
function getName(domain, location, type) {
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
if (subdomain === '') return part;
if (location === '') return part;
if (!domain.config.hyphenatedSubdomains) return part ? `${subdomain}.${part}` : subdomain;
if (!domain.config.hyphenatedSubdomains) return part ? `${location}.${part}` : location;
// hyphenatedSubdomains
if (type !== 'TXT') return `${subdomain}-${part}`;
if (type !== 'TXT') return `${location}-${part}`;
if (subdomain.startsWith('_acme-challenge.')) {
return `${subdomain}-${part}`;
} else if (subdomain === '_acme-challenge') {
if (location.startsWith('_acme-challenge.')) {
return `${location}-${part}`;
} else if (location === '_acme-challenge') {
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
return up ? `${subdomain}.${up}` : subdomain;
return up ? `${location}.${up}` : location;
} else {
return `${subdomain}.${part}`;
return `${location}.${part}`;
}
}
function getDnsRecords(subdomain, domain, type, callback) {
assert.strictEqual(typeof subdomain, 'string');
function getDnsRecords(location, domain, type, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, result) {
get(domain, function (error, domainObject) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).get(result.config, result.zoneName, getName(result, subdomain, type), type, function (error, values) {
api(domainObject.provider).get(domainObject, location, type, function (error, values) {
if (error) return callback(error);
callback(null, values);
@@ -413,19 +404,19 @@ function getDnsRecords(subdomain, domain, type, callback) {
}
// note: for TXT records the values must be quoted
function upsertDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
function upsertDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsertDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values);
get(domain, function (error, result) {
get(domain, function (error, domainObject) {
if (error) return callback(new DomainsError(DomainsError.INTERNAL_ERROR, error));
api(result.provider).upsert(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
api(domainObject.provider).upsert(domainObject, location, type, values, function (error) {
if (error) return callback(error);
callback(null);
@@ -433,19 +424,19 @@ function upsertDnsRecords(subdomain, domain, type, values, callback) {
});
}
function removeDnsRecords(subdomain, domain, type, values, callback) {
assert.strictEqual(typeof subdomain, 'string');
function removeDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('removeDNSRecord: %s on %s type %s values', subdomain, domain, type, values);
debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values);
get(domain, function (error, result) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
api(result.provider).del(result.config, result.zoneName, getName(result, subdomain, type), type, values, function (error) {
api(domainObject.provider).del(domainObject, location, type, values, function (error) {
if (error && error.reason !== DomainsError.NOT_FOUND) return callback(error);
callback(null);
@@ -453,8 +444,8 @@ function removeDnsRecords(subdomain, domain, type, values, callback) {
});
}
function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
assert.strictEqual(typeof subdomain, 'string');
function waitForDnsRecord(location, domain, type, value, options, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
@@ -464,9 +455,7 @@ function waitForDnsRecord(subdomain, domain, type, value, options, callback) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
const hostname = fqdn(subdomain, domainObject);
api(domainObject.provider).waitForDns(hostname, domainObject.zoneName, type, value, options, callback);
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
});
}
@@ -494,3 +483,31 @@ function makeWildcard(hostname) {
parts[0] = '*';
return parts.join('.');
}
function prepareDashboardDomain(domain, auditSource, progressCallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
if (error) return callback(error);
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new DomainsError(DomainsError.EXTERNAL_ERROR, error.message));
async.series([
(done) => { progressCallback({ percent: 10, message: 'Updating DNS' }); done(); },
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
(done) => { progressCallback({ percent: 40, message: 'Waiting for DNS' }); done(); },
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ percent: 70, message: 'Getting certificate' }); done(); },
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
callback(null);
});
});
});
}
+16 -3
View File
@@ -10,6 +10,9 @@ var appdb = require('./appdb.js'),
config = require('./config.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
sysinfo = require('./sysinfo.js');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -21,12 +24,18 @@ function sync(callback) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
debug('refreshDNS: current ip %s', ip);
let info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ip: null };
if (info.ip === ip) {
debug(`refreshDNS: no change in IP ${ip}`);
return callback();
}
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
domains.upsertDnsRecords(config.adminLocation(), config.adminDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: done for admin location');
debug('refreshDNS: updated admin location');
apps.getAll(function (error, result) {
if (error) return callback(error);
@@ -39,7 +48,11 @@ function sync(callback) {
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for apps');
debug('refreshDNS: updated apps');
eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, { userId: null, username: 'cron' }, { fromIp: info.ip, toIp: ip });
info.ip = ip;
safe.fs.writeFileSync(paths.DYNDNS_INFO_FILE, JSON.stringify(info), 'utf8');
callback();
});
+29 -1
View File
@@ -18,10 +18,14 @@ exports = module.exports = {
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_APP_LOGIN: 'app.login',
ACTION_APP_OOM: 'app.oom',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_TASK_CRASH: 'app.task.crash',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_BACKUP_CLEANUP: 'backup.cleanup',
ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start',
ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish',
ACTION_CERTIFICATE_NEW: 'certificate.new',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',
@@ -47,12 +51,17 @@ exports = module.exports = {
ACTION_USER_REMOVE: 'user.remove',
ACTION_USER_UPDATE: 'user.update',
ACTION_USER_TRANSFER: 'user.transfer',
ACTION_DYNDNS_UPDATE: 'dyndns.update',
ACTION_PROCESS_CRASH: 'system.crash'
};
var assert = require('assert'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
notifications = require('./notifications.js'),
util = require('util'),
uuid = require('uuid');
@@ -93,6 +102,25 @@ function add(action, source, data, callback) {
eventlogdb.add(id, action, source, data, function (error) {
if (error) return callback(new EventLogError(EventLogError.INTERNAL_ERROR, error));
// decide if we want to add notifications as well
if (action === exports.ACTION_USER_ADD) {
notifications.userAdded(source.userId, id, data.user);
} if (action === exports.ACTION_USER_REMOVE) {
notifications.userRemoved(source.userId, id, data.user);
} if (action === exports.ACTION_USER_UPDATE && data.adminStatusChanged) {
notifications.adminChanged(source.userId, id, data.user);
} if (action === exports.ACTION_APP_OOM) {
notifications.oomEvent(id, source.app ? source.app.id : source.containerId, { app: source.app, details: data });
} if (action === exports.ACTION_APP_DOWN) {
notifications.appDied(id, source.app);
} if (action === exports.ACTION_APP_TASK_CRASH) {
notifications.apptaskCrash(id, source.appId, data.crashLogFile);
} if (action === exports.ACTION_PROCESS_CRASH) {
notifications.processCrash(id, source.processName, data.crashLogFile);
} else {
// no notification
}
callback(null, { id: id });
});
}
+13 -4
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
};
var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mysql = require('mysql'),
@@ -125,11 +126,19 @@ function delByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
var query = 'DELETE FROM eventlog WHERE creationTime < ?';
database.query(query, [ creationTime ], function (error) {
// since notifications reference eventlog items, we have to clean them up as well
database.query('SELECT * FROM eventlog WHERE creationTime < ?', [ creationTime ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(error);
async.eachSeries(result, function (item, callback) {
database.query('DELETE FROM notifications WHERE eventId=?', [ item.id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
database.query('DELETE FROM eventlog WHERE id=?', [ item.id ], function (error) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback();
});
});
}, callback);
});
}
+1 -1
View File
@@ -19,7 +19,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.0.2@sha256:95e006390ddce7db637e1672eb6f3c257d3c2652747424f529b1dee3cbe6728c' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.0.0@sha256:8a88dd334b62b578530a014ca1a2425a54cb9df1e475f5d3a36806e5cfa22121' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.0.1@sha256:deee3739011670d45abd8997a8a0b8d3c4cd577a93f235417614dea58338e0f9' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.1.0@sha256:131db42dcb90111f679ab1f0f37c552f93f797d9b803b2346c7c202daf86ac36' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.0.2@sha256:454f035d60b768153d4f31210380271b5ba1c09367c9d95c7fa37f9e39d2f59c' }
}
};
+1 -1
View File
@@ -47,7 +47,7 @@ function getUsersWithAccessToApp(req, callback) {
assert.strictEqual(typeof req.app, 'object');
assert.strictEqual(typeof callback, 'function');
users.list(function (error, result) {
users.getAll(function (error, result) {
if (error) return callback(new ldap.OperationsError(error.toString()));
async.filter(result, apps.hasAccessTo.bind(null, req.app), function (error, allowedUsers) {
+11 -30
View File
@@ -5,7 +5,7 @@ exports = module.exports = {
};
var assert = require('assert'),
mailer = require('./mailer.js'),
eventlog = require('./eventlog.js'),
safe = require('safetydance'),
path = require('path'),
util = require('util');
@@ -15,7 +15,6 @@ var COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
var CRASH_LOG_TIMESTAMP_OFFSET = 1000 * 60 * 60; // 60 min
var CRASH_LOG_TIMESTAMP_FILE = '/tmp/crashlog.timestamp';
var CRASH_LOG_STASH_FILE = '/tmp/crashlog';
var CRASH_LOG_FILE_LIMIT = 2 * 1024 * 1024; // 2mb
function collectLogs(unitName, callback) {
assert.strictEqual(typeof unitName, 'string');
@@ -29,50 +28,32 @@ function collectLogs(unitName, callback) {
callback(null, logs);
}
function stashLogs(logs) {
var stat = safe.fs.statSync(CRASH_LOG_STASH_FILE);
if (stat && (stat.size > CRASH_LOG_FILE_LIMIT)) {
console.error('Dropping logs since crash file has become too big');
return;
}
function sendFailureLogs(unitName) {
assert.strictEqual(typeof unitName, 'string');
// append here
safe.fs.writeFileSync(CRASH_LOG_STASH_FILE, logs, { flag: 'a' });
}
function sendFailureLogs(processName, options) {
assert.strictEqual(typeof processName, 'string');
assert.strictEqual(typeof options, 'object');
collectLogs(options.unit || processName, function (error, newLogs) {
collectLogs(unitName, function (error, logs) {
if (error) {
console.error('Failed to collect logs.', error);
newLogs = util.format('Failed to collect logs.', error);
logs = util.format('Failed to collect logs.', error);
}
console.log('Sending failure logs for', processName);
console.log('Sending failure logs for', unitName);
if (!safe.fs.writeFileSync(CRASH_LOG_STASH_FILE, logs)) console.log(`Failed to stash logs to ${CRASH_LOG_STASH_FILE}`);
var timestamp = safe.fs.readFileSync(CRASH_LOG_TIMESTAMP_FILE, 'utf8');
// check if we already sent a mail in the last CRASH_LOG_TIME_OFFSET window
if (timestamp && (parseInt(timestamp) + CRASH_LOG_TIMESTAMP_OFFSET) > Date.now()) {
console.log('Crash log already sent within window. Stashing logs.');
return stashLogs(newLogs);
return;
}
var stashedLogs = safe.fs.readFileSync(CRASH_LOG_STASH_FILE, 'utf8');
var compiledLogs = stashedLogs ? (stashedLogs + newLogs) : newLogs;
var mailSubject = processName + (stashedLogs ? ' and others' : '');
mailer.unexpectedExit(mailSubject, compiledLogs, function (error) {
if (error) {
console.log('Error sending crashlog. Stashing logs.');
return stashLogs(newLogs);
}
eventlog.add(eventlog.ACTION_PROCESS_CRASH, { processName: unitName }, { crashLogFile: CRASH_LOG_STASH_FILE }, function (error) {
if (error) console.log(`Error sending crashlog. Logs stashed at ${CRASH_LOG_STASH_FILE}`);
// write the new timestamp file and delete stash file
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
safe.fs.unlinkSync(CRASH_LOG_STASH_FILE);
});
});
}
+7 -7
View File
@@ -141,13 +141,13 @@ function checkOutboundPort25(callback) {
});
client.on('timeout', function () {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' timed out';
relay.value = `Connect to ${smtpServer} timed out. Please check if port 25 is blocked`;
client.destroy();
callback(new Error('Timeout'), relay);
});
client.on('error', function (error) {
relay.status = false;
relay.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
relay.value = `Connect to ${smtpServer} failed: ${error.message}. Please check if port 25 is blocked`;
client.destroy();
callback(error, relay);
});
@@ -493,8 +493,7 @@ function createMailConfig(callback) {
users.getOwner(function (error, owner) {
const mailFqdn = config.mailFqdn();
const defaultDomain = config.adminDomain();
const alertsFrom = `no-reply@${defaultDomain}`;
const alertsFrom = `no-reply@${config.adminDomain()}`;
const alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(','); // owner may not exist yet
@@ -503,7 +502,7 @@ function createMailConfig(callback) {
const mailInDomains = mailDomains.filter(function (d) { return d.enabled; }).map(function (d) { return d.domain; }).join(',');
if (!safe.fs.writeFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_default_domain=${defaultDomain}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
`mail_in_domains=${mailInDomains}\nmail_out_domains=${mailOutDomains}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}\n\n`, 'utf8')) {
return callback(new Error('Could not create mail var file:' + safe.error.message));
}
@@ -553,10 +552,10 @@ function restartMail(callback) {
const tag = infra.images.mail.tag;
const memoryLimit = 4 * 256;
const cloudronToken = hat(8 * 128);
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
// admin and mail share the same certificate
reverseProxy.getCertificate({ fqdn: config.adminFqdn(), domain: config.adminDomain() }, function (error, bundle) {
reverseProxy.getCertificate(config.adminFqdn(), config.adminDomain(), function (error, bundle) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
@@ -586,6 +585,7 @@ function restartMail(callback) {
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_MAIL_TOKEN="${cloudronToken}" \
-e CLOUDRON_RELAY_TOKEN="${relayToken}" \
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
+1 -1
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
Unfortunately <%= program %> exited unexpectedly!
<%= subject %>
Please see some excerpt of the logs below:
+42 -31
View File
@@ -40,8 +40,7 @@ var assert = require('assert'),
showdown = require('showdown'),
smtpTransport = require('nodemailer-smtp-transport'),
users = require('./users.js'),
util = require('util'),
_ = require('underscore');
util = require('util');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -122,9 +121,21 @@ function sendMails(queue, callback) {
var mailServerIp = safe.query(data, 'NetworkSettings.Networks.cloudron.IPAddress');
if (!mailServerIp) return callback('Error querying mail server IP');
// extract the relay token for auth
const env = safe.query(data, 'Config.Env', null);
if (!env) return callback(new Error('Error getting mail env'));
const tmp = env.find(function (e) { return e.indexOf('CLOUDRON_RELAY_TOKEN') === 0; });
if (!tmp) return callback(new Error('Error getting CLOUDRON_RELAY_TOKEN env var'));
const relayToken = tmp.slice('CLOUDRON_RELAY_TOKEN'.length + 1); // +1 for the = sign
if (!relayToken) return callback(new Error('Error parsing CLOUDRON_RELAY_TOKEN'));
var transport = nodemailer.createTransport(smtpTransport({
host: mailServerIp,
port: config.get('smtpPort')
port: config.get('smtpPort'),
auth: {
user: `no-reply@${config.adminDomain()}`,
pass: relayToken
}
}));
debug('Processing mail queue of size %d (through %s:2525)', queue.length, mailServerIp);
@@ -170,18 +181,17 @@ function render(templateFile, params) {
return content;
}
function mailUserEventToAdmins(user, event) {
function mailUserEvent(mailTo, user, event) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof event, 'string');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
var mailOptions = {
from: mailConfig.notificationFrom,
to: adminEmails.join(', '),
to: mailTo,
subject: util.format('[%s] %s %s', mailConfig.cloudronName, user.username || user.fallbackEmail || user.email, event),
text: render('user_event.ejs', { user: user, event: event, format: 'text' }),
};
@@ -226,16 +236,15 @@ function sendInvite(user, invitor) {
});
}
function userAdded(user) {
function userAdded(mailTo, user) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
debug('Sending mail for userAdded');
debug(`userAdded: Sending mail for added users ${user.fallbackEmail} to ${mailTo}`);
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var adminEmails = _.difference(mailConfig.adminEmails, [ user.email ]);
var templateData = {
user: user,
cloudronName: mailConfig.cloudronName,
@@ -250,7 +259,7 @@ function userAdded(user) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: adminEmails.join(', '),
to: mailTo,
subject: util.format('[%s] User %s added', mailConfig.cloudronName, user.fallbackEmail),
text: render('user_added.ejs', templateDataText),
html: render('user_added.ejs', templateDataHTML)
@@ -260,21 +269,23 @@ function userAdded(user) {
});
}
function userRemoved(user) {
function userRemoved(mailTo, user) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
debug('Sending mail for userRemoved.', user.id, user.email);
debug('Sending mail for userRemoved.', user.id, user.username, user.email);
mailUserEventToAdmins(user, 'was removed');
mailUserEvent(mailTo, user, 'was removed');
}
function adminChanged(user, admin) {
function adminChanged(mailTo, user, isAdmin) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof admin, 'boolean');
assert.strictEqual(typeof isAdmin, 'boolean');
debug('Sending mail for adminChanged');
mailUserEventToAdmins(user, admin ? 'is now an admin' : 'is no more an admin');
mailUserEvent(mailTo, user, isAdmin ? 'is now an admin' : 'is no more an admin');
}
function passwordReset(user) {
@@ -310,7 +321,8 @@ function passwordReset(user) {
});
}
function appDied(app) {
function appDied(mailTo, app) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof app, 'object');
debug('Sending mail for app %s @ %s died', app.id, app.fqdn);
@@ -320,7 +332,7 @@ function appDied(app) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
to: mailTo,
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
};
@@ -488,7 +500,8 @@ function certificateRenewalError(domain, message) {
});
}
function oomEvent(program, context) {
function oomEvent(mailTo, program, context) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'string');
@@ -497,7 +510,7 @@ function oomEvent(program, context) {
var mailOptions = {
from: mailConfig.notificationFrom,
to: config.provider() === 'caas' ? 'support@cloudron.io' : mailConfig.adminEmails.join(', '),
to: mailTo,
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('oom_event.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
};
@@ -508,24 +521,22 @@ function oomEvent(program, context) {
// this function bypasses the queue intentionally. it is also expected to work without the mailer module initialized
// NOTE: crashnotifier should ideally be able to send mail when there is no db, however we need the 'from' address domain from the db
function unexpectedExit(program, context, callback) {
assert.strictEqual(typeof program, 'string');
function unexpectedExit(mailTo, subject, context) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof subject, 'string');
assert.strictEqual(typeof context, 'string');
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
var mailOptions = {
from: mailConfig.notificationFrom,
to: 'support@cloudron.io',
subject: util.format('[%s] %s exited unexpectedly', mailConfig.cloudronName, program),
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, program: program, context: context, format: 'text' })
to: mailTo,
subject: `[${mailConfig.cloudronName}] ${subject}`,
text: render('unexpected_exit.ejs', { cloudronName: mailConfig.cloudronName, subject: subject, context: context, format: 'text' })
};
sendMails([ mailOptions ], callback);
sendMails([ mailOptions ]);
});
}
+108
View File
@@ -0,0 +1,108 @@
'use strict';
exports = module.exports = {
get: get,
add: add,
update: update,
del: del,
listByUserIdPaged: listByUserIdPaged
};
let assert = require('assert'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
const NOTIFICATION_FIELDS = [ 'id', 'userId', 'eventId', 'title', 'message', 'action', 'creationTime', 'acknowledged' ];
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.id = String(result.id);
// convert to boolean
result.acknowledged = !!result.acknowledged;
}
function add(notification, callback) {
assert.strictEqual(typeof notification, 'object');
assert.strictEqual(typeof callback, 'function');
const query = 'INSERT INTO notifications (userId, eventId, title, message, action) VALUES (?, ?, ?, ?, ?)';
const args = [ notification.userId, notification.eventId, notification.title, notification.message, notification.action ];
database.query(query, args, function (error, result) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such eventlog entry'));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null, String(result.insertId));
});
}
function update(id, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
let args = [ ];
let fields = [ ];
for (let k in data) {
fields.push(k + ' = ?');
args.push(data[k]);
}
args.push(id);
database.query('UPDATE notifications SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
return callback(null);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
postProcess(result[0]);
callback(null, result[0]);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM notifications WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
}
function listByUserIdPaged(userId, page, perPage, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var data = [ userId ];
var query = 'SELECT ' + NOTIFICATION_FIELDS + ' FROM notifications WHERE userId=?';
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
+245
View File
@@ -0,0 +1,245 @@
'use strict';
exports = module.exports = {
NotificationsError: NotificationsError,
add: add,
get: get,
ack: ack,
getAllPaged: getAllPaged,
// specialized notifications
userAdded: userAdded,
userRemoved: userRemoved,
adminChanged: adminChanged,
oomEvent: oomEvent,
appDied: appDied,
processCrash: processCrash,
apptaskCrash: apptaskCrash
};
var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:notifications'),
mailer = require('./mailer.js'),
notificationdb = require('./notificationdb.js'),
safe = require('safetydance'),
users = require('./users.js'),
util = require('util');
function NotificationsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(NotificationsError, Error);
NotificationsError.INTERNAL_ERROR = 'Internal Error';
NotificationsError.NOT_FOUND = 'Not Found';
function add(userId, eventId, title, message, action, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof title, 'string');
assert.strictEqual(typeof message, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof callback, 'function');
debug('add: ', userId, title, action);
notificationdb.add({
userId: userId,
eventId: eventId,
title: title,
message: message,
action: action
}, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND, error.message));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
callback(null, { id: result });
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
notificationdb.get(id, function (error, result) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
callback(null, result);
});
}
function ack(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
notificationdb.update(id, { acknowledged: true }, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new NotificationsError(NotificationsError.NOT_FOUND));
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
callback(null);
});
}
// if acknowledged === null we return all, otherwise yes or no based on acknowledged as a boolean
function getAllPaged(userId, acknowledged, page, perPage, callback) {
assert.strictEqual(typeof userId, 'string');
assert(acknowledged === null || typeof acknowledged === 'boolean');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
notificationdb.listByUserIdPaged(userId, page, perPage, function (error, result) {
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
if (acknowledged === null) return callback(null, result);
callback(null, result.filter(function (r) { return r.acknowledged === acknowledged; }));
});
}
// Calls iterator with (admin, callback)
function actionForAllAdmins(skippingUserIds, iterator, callback) {
assert(Array.isArray(skippingUserIds));
assert.strictEqual(typeof iterator, 'function');
assert.strictEqual(typeof callback, 'function');
users.getAllAdmins(function (error, result) {
if (error) return callback(new NotificationsError(NotificationsError.INTERNAL_ERROR, error));
// filter out users we want to skip (like the user who did the action or the user the action was performed on)
result = result.filter(function (r) { return skippingUserIds.indexOf(r.id) === -1; });
async.each(result, iterator, callback);
});
}
function userAdded(performedBy, eventId, user) {
assert.strictEqual(typeof performedBy, 'string');
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof user, 'object');
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
mailer.userAdded(admin.email, user);
add(admin.id, eventId, 'User added', `User ${user.fallbackEmail} was added`, '/#/users', callback);
}, function (error) {
if (error) console.error(error);
});
}
function userRemoved(performedBy, eventId, user) {
assert.strictEqual(typeof performedBy, 'string');
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof user, 'object');
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
mailer.userRemoved(admin.email, user);
add(admin.id, eventId, 'User removed', `User ${user.username || user.email || user.fallbackEmail} was removed`, '/#/users', callback);
}, function (error) {
if (error) console.error(error);
});
}
function adminChanged(performedBy, eventId, user) {
assert.strictEqual(typeof performedBy, 'string');
assert.strictEqual(typeof user, 'object');
actionForAllAdmins([ performedBy, user.id ], function (admin, callback) {
mailer.adminChanged(admin.email, user, user.admin);
add(admin.id, eventId, 'Admin status change', `User ${user.username || user.email || user.fallbackEmail} ${user.admin ? 'is now an admin' : 'is no more an admin'}`, '/#/users', callback);
}, function (error) {
if (error) console.error(error);
});
}
function oomEvent(eventId, program, context) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof program, 'string');
assert.strictEqual(typeof context, 'object');
// also send us a notification mail
if (config.provider() === 'caas') mailer.oomEvent('support@cloudron.io', program, JSON.stringify(context, null, 4));
actionForAllAdmins([], function (admin, callback) {
mailer.oomEvent(admin.email, program, JSON.stringify(context, null, 4));
var message;
if (context.app) message = `The application ${context.app.manifest.title} with id ${context.app.id} ran out of memory.`;
else message = `The container with id ${context.details.id} ran out of memory`;
add(admin.id, eventId, 'Process died out-of-memory', message, context.app ? '/#/apps' : '', callback);
}, function (error) {
if (error) console.error(error);
});
}
function appDied(eventId, app) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof app, 'object');
// also send us a notification mail
if (config.provider() === 'caas') mailer.appDied('support@cloudron.io', app);
actionForAllAdmins([], function (admin, callback) {
mailer.appDied(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, '/#/apps', callback);
}, function (error) {
if (error) console.error(error);
});
}
function processCrash(eventId, processName, crashLogFile) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof processName, 'string');
assert.strictEqual(typeof crashLogFile, 'string');
var subject = `${processName} exited unexpectedly`;
var crashLogs = safe.fs.readFileSync(crashLogFile, 'utf8') || `No logs found at ${crashLogFile}`;
// also send us a notification mail
if (config.provider() === 'caas') mailer.unexpectedExit('support@cloudron.io', subject, crashLogs);
actionForAllAdmins([], function (admin, callback) {
mailer.unexpectedExit(admin.email, subject, crashLogs);
add(admin.id, eventId, subject, 'Detailed logs have been sent to your email address.', '/#/system', callback);
}, function (error) {
if (error) console.error(error);
});
}
function apptaskCrash(eventId, appId, crashLogFile) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof crashLogFile, 'string');
var subject = `Apptask for ${appId} crashed`;
var crashLogs = safe.fs.readFileSync(crashLogFile, 'utf8') || `No logs found at ${crashLogFile}`;
// also send us a notification mail
if (config.provider() === 'caas') mailer.unexpectedExit('support@cloudron.io', subject, crashLogs);
actionForAllAdmins([], function (admin, callback) {
mailer.unexpectedExit(admin.email, subject, crashLogs);
add(admin.id, eventId, subject, 'Detailed logs have been sent to your email address.', '/#/system', callback);
}, function (error) {
if (error) console.error(error);
});
}
+1 -1
View File
@@ -8,7 +8,6 @@ exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(config.baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(config.baseDir(), 'boxdata'),
@@ -23,6 +22,7 @@ exports = module.exports = {
BACKUP_INFO_DIR: path.join(config.baseDir(), 'platformdata/backup'),
UPDATE_DIR: path.join(config.baseDir(), 'platformdata/update'),
SNAPSHOT_INFO_FILE: path.join(config.baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(config.baseDir(), 'platformdata/dyndns-info.json'),
// this is not part of appdata because an icon may be set before install
APP_ICONS_DIR: path.join(config.baseDir(), 'boxdata/appicons'),
+96 -39
View File
@@ -4,6 +4,7 @@ exports = module.exports = {
setup: setup,
restore: restore,
activate: activate,
getStatus: getStatus,
ProvisionError: ProvisionError
};
@@ -21,21 +22,32 @@ var assert = require('assert'),
DomainsError = domains.DomainsError,
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
path = require('path'),
safe = require('safetydance'),
semver = require('semver'),
settingsdb = require('./settingsdb.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
superagent = require('superagent'),
users = require('./users.js'),
UsersError = users.UsersError,
tld = require('tldjs'),
util = require('util');
util = require('util'),
_ = require('underscore');
var RESTART_CMD = path.join(__dirname, 'scripts/restart.sh');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
// we cannot use tasks since the tasks table gets overwritten when db is imported
let gProvisionStatus = {
setup: {
active: false,
message: '',
errorMessage: null
},
restore: {
active: false,
message: '',
errorMessage: null
}
};
function ProvisionError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -63,6 +75,12 @@ ProvisionError.INTERNAL_ERROR = 'Internal Error';
ProvisionError.EXTERNAL_ERROR = 'External Error';
ProvisionError.ALREADY_PROVISIONED = 'Already Provisioned';
function setProgress(task, message, callback) {
debug(`setProgress: ${task} - ${message}`);
gProvisionStatus[task].message = message;
callback();
}
function autoprovision(autoconf, callback) {
assert.strictEqual(typeof autoconf, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -102,7 +120,7 @@ function unprovision(callback) {
config.setAdminDomain('');
config.setAdminFqdn('');
config.setAdminLocation('my');
config.setAdminLocation(constants.ADMIN_LOCATION);
// TODO: also cancel any existing configureWebadmin task
async.series([
@@ -117,23 +135,27 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
gProvisionStatus.setup = { active: true, errorMessage: '', message: 'Adding domain' };
function done(error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
callback(error);
}
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_SETUP));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_SETUP));
unprovision(function (error) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
let webadminStatus = cloudron.getWebadminStatus();
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
const adminFqdn = 'my' + (dnsConfig.config.hyphenatedSubdomains ? '-' : '.') + domain;
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName} using admin fqdn ${adminFqdn}`);
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName}`);
let data = {
zoneName: zoneName,
@@ -144,16 +166,24 @@ function setup(dnsConfig, autoconf, auditSource, callback) {
};
domains.add(domain, data, auditSource, function (error) {
if (error && error.reason === DomainsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error && error.reason === DomainsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === DomainsError.ALREADY_EXISTS) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(); // now that args are validated run the task in the background
async.series([
mail.addDomain.bind(null, domain),
cloudron.setDashboardDomain.bind(null, domain), // triggers task to setup my. dns/cert/reverseproxy
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain), // this sets up the config.fqdn()
mail.addDomain.bind(null, domain), // this relies on config.mailFqdn()
setProgress.bind(null, 'setup', 'Applying auto-configuration'),
autoprovision.bind(null, autoconf),
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], callback);
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
});
});
});
});
@@ -229,40 +259,67 @@ function restore(backupConfig, backupId, version, autoconf, auditSource, callbac
if (!semver.valid(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'version is not a valid semver'));
if (semver.major(config.version()) !== semver.major(version) || semver.minor(config.version()) !== semver.minor(version)) return callback(new ProvisionError(ProvisionError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
let webadminStatus = cloudron.getWebadminStatus();
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already setting up or restoring'));
if (webadminStatus.configuring || webadminStatus.restore.active) return callback(new ProvisionError(ProvisionError.BAD_STATE, 'Already restoring or configuring'));
gProvisionStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config' };
function done(error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
callback(error);
}
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return callback(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (activated) return done(new ProvisionError(ProvisionError.ALREADY_PROVISIONED, 'Already activated'));
backups.testConfig(backupConfig, function (error) {
if (error && error.reason === BackupsError.BAD_FIELD) return callback(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
if (error && error.reason === BackupsError.BAD_FIELD) return done(new ProvisionError(ProvisionError.BAD_FIELD, error.message));
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return done(new ProvisionError(ProvisionError.EXTERNAL_ERROR, error.message));
if (error) return done(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
webadminStatus.restore.active = true;
webadminStatus.restore.error = null;
callback(null); // do no block
callback(); // now that the fields are validated, continue task in the background
async.series([
backups.restore.bind(null, backupConfig, backupId, (progress) => debug(`restore: ${progress}`)),
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
setProgress.bind(null, 'restore', 'Applying auto-configuration'),
autoprovision.bind(null, autoconf),
// currently, our suggested restore flow is after a dnsSetup. The dnSetup creates DKIM keys and updates the DNS
// for this reason, we have to re-setup DNS after a restore so it has DKIm from the backup
// Once we have a 100% IP based restore, we can skip this
mail.setDnsRecords.bind(null, config.adminDomain()),
shell.sudo.bind(null, 'restart', [ RESTART_CMD ], {})
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
debug('restore:', error);
if (error) webadminStatus.restore.error = error.message;
webadminStatus.restore.active = false;
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
if (!error) cloudron.onActivated(NOOP_CALLBACK);
});
});
});
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
users.isActivated(function (error, activated) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
settings.getCloudronName(function (error, cloudronName) {
if (error) return callback(new ProvisionError(ProvisionError.INTERNAL_ERROR, error));
callback(null, _.extend({
version: config.version(),
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.adminDomain() ? config.adminFqdn() : null,
activated: activated,
edition: config.edition()
}, gProvisionStatus));
});
});
}
+51 -21
View File
@@ -12,16 +12,19 @@ exports = module.exports = {
validateCertificate: validateCertificate,
getCertificate: getCertificate,
ensureCertificate: ensureCertificate,
renewAll: renewAll,
renewCerts: renewCerts,
// the 'configure' functions always ensure a certificate
configureDefaultServer: configureDefaultServer,
configureAdmin: configureAdmin,
configureApp: configureApp,
unconfigureApp: unconfigureApp,
writeAdminConfig: writeAdminConfig,
reload: reload,
removeAppConfigs: removeAppConfigs,
@@ -54,7 +57,7 @@ var acme2 = require('./cert/acme2.js'),
users = require('./users.js'),
util = require('util');
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/../setup/start/nginx/appconfig.ejs', { encoding: 'utf8' }),
var NGINX_APPCONFIG_EJS = fs.readFileSync(__dirname + '/appconfig.ejs', { encoding: 'utf8' }),
RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh');
function ReverseProxyError(reason, errorOrMessage) {
@@ -313,17 +316,18 @@ function getCertificateByHostname(hostname, domainObject, callback) {
callback(null);
}
function getCertificate(app, callback) {
assert.strictEqual(typeof app, 'object');
function getCertificate(fqdn, domain, callback) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.get(app.domain, function (error, domainObject) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
getCertificateByHostname(app.fqdn, domainObject, function (error, result) {
getCertificateByHostname(fqdn, domainObject, function (error, result) {
if (error || result) return callback(error, result);
return getFallbackCertificate(app.domain, callback);
return getFallbackCertificate(domain, callback);
});
});
}
@@ -373,7 +377,7 @@ function ensureCertificate(vhost, domain, auditSource, callback) {
});
}
function writeAdminConfig(bundle, configFileName, vhost, callback) {
function writeAdminNginxConfig(bundle, configFileName, vhost, callback) {
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof configFileName, 'string');
assert.strictEqual(typeof vhost, 'string');
@@ -395,21 +399,47 @@ function writeAdminConfig(bundle, configFileName, vhost, callback) {
if (!safe.fs.writeFileSync(nginxConfigFilename, nginxConf)) return callback(safe.error);
if (vhost) safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, 'admin.conf')); // remove legacy admin.conf. remove after 3.5
reload(callback);
}
function configureAdmin(auditSource, callback) {
function configureAdmin(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
ensureCertificate(config.adminFqdn(), config.adminDomain(), auditSource, function (error, bundle) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
writeAdminConfig(bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
ensureCertificate(adminFqdn, domainObject.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
});
});
}
function writeAppConfig(app, bundle, callback) {
function writeAdminConfig(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
getCertificate(adminFqdn, domainObject.domain, function (error, bundle) {
if (error) return callback(error);
writeAdminNginxConfig(bundle, `${adminFqdn}.conf`, adminFqdn, callback);
});
});
}
function writeAppNginxConfig(app, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof bundle, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -442,7 +472,7 @@ function writeAppConfig(app, bundle, callback) {
reload(callback);
}
function writeAppRedirectConfig(app, fqdn, bundle, callback) {
function writeAppRedirectNginxConfig(app, fqdn, bundle, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof bundle, 'object');
@@ -481,14 +511,14 @@ function configureApp(app, auditSource, callback) {
ensureCertificate(app.fqdn, app.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAppConfig(app, bundle, function (error) {
writeAppNginxConfig(app, bundle, function (error) {
if (error) return callback(error);
async.eachSeries(app.alternateDomains, function (alternateDomain, callback) {
ensureCertificate(alternateDomain.fqdn, alternateDomain.domain, auditSource, function (error, bundle) {
if (error) return callback(error);
writeAppRedirectConfig(app, alternateDomain.fqdn, bundle, callback);
writeAppRedirectNginxConfig(app, alternateDomain.fqdn, bundle, callback);
});
}, callback);
});
@@ -519,7 +549,7 @@ function renewCerts(options, auditSource, progressCallback, callback) {
var appDomains = [];
// add webadmin domain
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, constants.NGINX_ADMIN_CONFIG_FILE_NAME) });
appDomains.push({ domain: config.adminDomain(), fqdn: config.adminFqdn(), type: 'webadmin', nginxConfigFilename: path.join(paths.NGINX_APPCONFIG_DIR, `${config.adminFqdn()}.conf`) });
// add app main
allApps.forEach(function (app) {
@@ -549,9 +579,9 @@ function renewCerts(options, auditSource, progressCallback, callback) {
// reconfigure since the cert changed
var configureFunc;
if (appDomain.type === 'webadmin') configureFunc = writeAdminConfig.bind(null, bundle, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
if (appDomain.type === 'webadmin') configureFunc = writeAdminNginxConfig.bind(null, bundle, `${config.adminFqdn()}.conf`, config.adminFqdn());
else if (appDomain.type === 'main') configureFunc = writeAppNginxConfig.bind(null, appDomain.app, bundle);
else if (appDomain.type === 'alternate') configureFunc = writeAppRedirectNginxConfig.bind(null, appDomain.app, appDomain.fqdn, bundle);
else return iteratorCallback(new Error(`Unknown domain type for ${appDomain.fqdn}. This should never happen`));
configureFunc(function (ignoredError) {
@@ -575,7 +605,7 @@ function renewAll(auditSource, callback) {
function removeAppConfigs() {
for (let appConfigFile of fs.readdirSync(paths.NGINX_APPCONFIG_DIR)) {
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && appConfigFile !== constants.NGINX_ADMIN_CONFIG_FILE_NAME) {
if (appConfigFile !== constants.NGINX_DEFAULT_CONFIG_FILE_NAME && !appConfigFile.startsWith(constants.ADMIN_LOCATION)) {
fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR, appConfigFile));
}
}
@@ -597,7 +627,7 @@ function configureDefaultServer(callback) {
}
}
writeAdminConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
writeAdminNginxConfig({ certFilePath, keyFilePath }, constants.NGINX_DEFAULT_CONFIG_FILE_NAME, '', function (error) {
if (error) return callback(error);
debug('configureDefaultServer: done');
+7 -4
View File
@@ -210,6 +210,8 @@ function configureApp(req, res, next) {
if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings'));
}
if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
debug('Configuring app id:%s data:%j', req.params.id, data);
apps.configure(req.params.id, data, req.user, auditSource(req), function (error) {
@@ -367,7 +369,7 @@ function getLogStream(req, res, next) {
debug('Getting logstream of app id:%s', req.params.id);
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
@@ -376,7 +378,8 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true
follow: true,
format: 'json'
};
apps.getLogs(req.params.id, options, function (error, logStream) {
@@ -405,7 +408,7 @@ function getLogStream(req, res, next) {
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug('Getting logs of app id:%s', req.params.id);
@@ -413,7 +416,7 @@ function getLogs(req, res, next) {
var options = {
lines: lines,
follow: false,
format: req.query.format
format: req.query.format || 'json'
};
apps.getLogs(req.params.id, options, function (error, logStream) {
+13 -34
View File
@@ -7,18 +7,15 @@ exports = module.exports = {
getDisks: getDisks,
getUpdateInfo: getUpdateInfo,
update: update,
feedback: feedback,
checkForUpdates: checkForUpdates,
getLogs: getLogs,
getLogStream: getLogStream,
getStatus: getStatus,
setDashboardDomain: setDashboardDomain,
prepareDashboardDomain: prepareDashboardDomain,
renewCerts: renewCerts
};
var appstore = require('../appstore.js'),
AppstoreError = require('../appstore.js').AppstoreError,
assert = require('assert'),
let assert = require('assert'),
async = require('async'),
cloudron = require('../cloudron.js'),
CloudronError = cloudron.CloudronError,
@@ -26,8 +23,7 @@ var appstore = require('../appstore.js'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
updater = require('../updater.js'),
updateChecker = require('../updatechecker.js'),
UpdaterError = require('../updater.js').UpdaterError,
_ = require('underscore');
UpdaterError = require('../updater.js').UpdaterError;
function auditSource(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
@@ -91,36 +87,16 @@ function checkForUpdates(req, res, next) {
});
}
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
next(new HttpSuccess(201, {}));
});
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.unit, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format
format: req.query.format || 'json'
};
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
@@ -140,7 +116,7 @@ function getLogs(req, res, next) {
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.unit, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
@@ -150,7 +126,7 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true,
format: req.query.format
format: req.query.format || 'json'
};
cloudron.getLogs(req.params.unit, options, function (error, logStream) {
@@ -186,11 +162,14 @@ function setDashboardDomain(req, res, next) {
});
}
function getStatus(req, res, next) {
cloudron.getStatus(function (error, status) {
function prepareDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
cloudron.prepareDashboardDomain(req.body.domain, auditSource(req), function (error, taskId) {
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
next(new HttpSuccess(202, { taskId }));
});
}
+12 -1
View File
@@ -1,14 +1,25 @@
'use strict';
exports = module.exports = {
get: get
get: get,
list: list
};
var eventlog = require('../eventlog.js'),
EventLogError = eventlog.EventLogError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function get(req, res, next) {
eventlog.get(req.params.eventId, function (error, result) {
if (error && error.reason === EventLogError.NOT_FOUND) return next(new HttpError(404, 'no such eventlog'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { event: result }));
});
}
function list(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
+2 -1
View File
@@ -13,12 +13,13 @@ exports = module.exports = {
groups: require('./groups.js'),
oauth2: require('./oauth2.js'),
mail: require('./mail.js'),
notifications: require('./notifications.js'),
profile: require('./profile.js'),
provision: require('./provision.js'),
services: require('./services.js'),
settings: require('./settings.js'),
support: require('./support.js'),
sysadmin: require('./sysadmin.js'),
ssh: require('./ssh.js'),
tasks: require('./tasks.js'),
users: require('./users.js')
};
+64
View File
@@ -0,0 +1,64 @@
'use strict';
exports = module.exports = {
verifyOwnership: verifyOwnership,
get: get,
list: list,
ack: ack
};
let assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
notifications = require('../notifications.js'),
NotificationsError = notifications.NotificationsError;
function verifyOwnership(req, res, next) {
if (!req.params.notificationId) return next(); // skip for listing
notifications.get(req.params.notificationId, function (error, result) {
if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification'));
if (error) return next(new HttpError(500, error));
if (result.userId !== req.user.id) return next(new HttpError(401, 'Unauthorized'));
req.notification = result;
next();
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.notification, 'object');
next(new HttpSuccess(200, { notification: req.notification }));
}
function list(req, res, next) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
var acknowledged = null;
if (req.query.acknowledged && !(req.query.acknowledged === 'true' || req.query.acknowledged === 'false')) return next(new HttpError(400, 'acknowledged must be a true or false'));
else if (req.query.acknowledged) acknowledged = req.query.acknowledged === 'true' ? true : false;
notifications.getAllPaged(req.user.id, acknowledged, page, perPage, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { notifications: result }));
});
}
function ack(req, res, next) {
assert.strictEqual(typeof req.params.notificationId, 'string');
notifications.ack(req.params.notificationId, function (error) {
if (error && error.reason === NotificationsError.NOT_FOUND) return next(new HttpError(404, 'No such notification'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(204, {}));
});
}
+10 -1
View File
@@ -5,7 +5,8 @@ exports = module.exports = {
setupTokenAuth: setupTokenAuth,
setup: setup,
activate: activate,
restore: restore
restore: restore,
getStatus: getStatus
};
var assert = require('assert'),
@@ -156,3 +157,11 @@ function restore(req, res, next) {
next(new HttpSuccess(200));
});
}
function getStatus(req, res, next) {
provision.getStatus(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
+5 -4
View File
@@ -56,7 +56,7 @@ function configure(req, res, next) {
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug(`Getting logs of service ${req.params.service}`);
@@ -64,7 +64,7 @@ function getLogs(req, res, next) {
var options = {
lines: lines,
follow: false,
format: req.query.format
format: req.query.format || 'json'
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
@@ -87,7 +87,7 @@ function getLogStream(req, res, next) {
debug(`Getting logstream of service ${req.params.service}`);
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
@@ -96,7 +96,8 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true
follow: true,
format: 'json'
};
addons.getServiceLogs(req.params.service, options, function (error, logStream) {
-54
View File
@@ -1,54 +0,0 @@
'use strict';
exports = module.exports = {
getAuthorizedKeys: getAuthorizedKeys,
getAuthorizedKey: getAuthorizedKey,
addAuthorizedKey: addAuthorizedKey,
delAuthorizedKey: delAuthorizedKey
};
var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
ssh = require('../ssh.js'),
SshError = ssh.SshError;
function getAuthorizedKeys(req, res, next) {
ssh.getAuthorizedKeys(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { keys: result }));
});
}
function getAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.params.identifier, 'string');
ssh.getAuthorizedKey(req.params.identifier, function (error, result) {
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { identifier: result.identifier, key: result.key }));
});
}
function addAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.key !== 'string' || !req.body.key) return next(new HttpError(400, 'key must be a non empty'));
ssh.addAuthorizedKey(req.body.key, function (error) {
if (error && error.reason === SshError.INVALID_KEY) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function delAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.params.identifier, 'string');
ssh.delAuthorizedKey(req.params.identifier, function (error) {
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
+55
View File
@@ -0,0 +1,55 @@
'use strict';
exports = module.exports = {
feedback: feedback,
getRemoteSupport: getRemoteSupport,
enableRemoteSupport: enableRemoteSupport
};
var appstore = require('../appstore.js'),
AppstoreError = require('../appstore.js').AppstoreError,
assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
support = require('../support.js'),
_ = require('underscore');
function feedback(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
if (VALID_TYPES.indexOf(req.body.type) === -1) return next(new HttpError(400, 'unknown type'));
if (typeof req.body.subject !== 'string' || !req.body.subject) return next(new HttpError(400, 'subject must be string'));
if (typeof req.body.description !== 'string' || !req.body.description) return next(new HttpError(400, 'description must be string'));
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
appstore.sendFeedback(_.extend(req.body, { email: req.user.email, displayName: req.user.displayName }), function (error) {
if (error && error.reason === AppstoreError.BILLING_REQUIRED) return next(new HttpError(402, 'Login to App Store to create support tickets. You can also email support@cloudron.io'));
if (error) return next(new HttpError(503, 'Error contacting cloudron.io. Please email support@cloudron.io'));
next(new HttpSuccess(201, {}));
});
}
function enableRemoteSupport(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
support.enableRemoteSupport(req.body.enable, function (error) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}
function getRemoteSupport(req, res, next) {
support.getRemoteSupport(function (error, status) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, status));
});
}
+5 -4
View File
@@ -59,13 +59,13 @@ function list(req, res, next) {
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : 100;
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
var options = {
lines: lines,
follow: false,
format: req.query.format
format: req.query.format || 'json'
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
@@ -86,7 +86,7 @@ function getLogs(req, res, next) {
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.taskId, 'string');
var lines = req.query.lines ? parseInt(req.query.lines, 10) : -10; // we ignore last-event-id
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; }
@@ -95,7 +95,8 @@ function getLogStream(req, res, next) {
var options = {
lines: lines,
follow: true
follow: true,
format: 'json'
};
tasks.getLogs(req.params.taskId, options, function (error, logStream) {
+1 -137
View File
@@ -5,8 +5,7 @@
/* global before:false */
/* global after:false */
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
let async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
@@ -209,141 +208,6 @@ describe('Cloudron', function () {
});
});
describe('feedback', function () {
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
// stash token for further use
token = result.body.token;
callback();
});
},
], done);
});
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: '', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unknown type', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without description', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: '', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty description', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: '' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with ticket type', function (done) {
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
.reply(201, { });
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope2.isDone()).to.be.ok();
done();
});
});
it('succeeds with app type', function (done) {
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
.reply(201, { });
superagent.post(SERVER_URL + '/api/v1/feedback')
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
describe('logs', function () {
before(function (done) {
async.series([
+63
View File
@@ -10,6 +10,7 @@ var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
eventlogdb = require('../../eventlogdb.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js'),
@@ -22,6 +23,17 @@ var token = null;
var USER_1_ID = null, token_1;
var EVENT_0 = {
id: 'event_0',
action: 'foobaraction',
source: {
ip: '127.0.0.1'
},
data: {
something: 'is there'
}
};
function setup(done) {
config._reset();
config.setFqdn('example-eventlog-test.com');
@@ -64,6 +76,10 @@ function setup(done) {
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add(token_1, USER_1_ID, 'test-client-id', Date.now() + 100000, accesscontrol.SCOPE_PROFILE, '', callback);
},
function (callback) {
eventlogdb.add(EVENT_0.id, EVENT_0.action, EVENT_0.source, EVENT_0.data, callback);
}
], done);
@@ -82,6 +98,53 @@ describe('Eventlog API', function () {
after(cleanup);
describe('get', function () {
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for non-admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
done();
});
});
it('fails if not exists', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/doesnotexist')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds for admin', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog/' + EVENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.event).to.be.an('object');
expect(result.body.event.creationTime).to.be.a('string');
delete result.body.event.creationTime;
expect(result.body.event).to.eql(EVENT_0);
done();
});
});
});
describe('list', function () {
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/eventlog')
.query({ access_token: token.toUpperCase() })
+16 -1
View File
@@ -48,7 +48,7 @@ function setup(done) {
function dnsSetup(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config } })
.send({ dnsConfig: { provider: ADMIN_DOMAIN.provider, domain: ADMIN_DOMAIN.domain, config: ADMIN_DOMAIN.config, tlsConfig: { provider: 'fallback' } } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
@@ -57,6 +57,21 @@ function setup(done) {
});
},
function waitForSetup(done) {
async.retry({ times: 5, interval: 4000 }, function (retryCallback) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
.end(function (error, result) {
if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result'));
console.dir(result.body);
if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback();
retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body)));
});
}, done);
},
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
+2 -2
View File
@@ -30,7 +30,7 @@ var accesscontrol = require('../../accesscontrol.js'),
var SERVER_URL = 'http://localhost:' + config.get('port');
let AUDIT_SOURCE = { ip: '1.2.3.4' };
let AUDIT_SOURCE = { ip: '1.2.3.4', userId: 'someuserid' };
describe('OAuth2', function () {
@@ -221,7 +221,7 @@ describe('OAuth2', function () {
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope),
function (callback) {
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, { }, null /* source */, function (error, userObject) {
users.create(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, { }, AUDIT_SOURCE, function (error, userObject) {
expect(error).to.not.be.ok();
// update the global objects to reflect the new user id
+17 -4
View File
@@ -34,6 +34,19 @@ function cleanup(done) {
], done);
}
function waitForSetup(done) {
async.retry({ times: 5, interval: 4000 }, function (retryCallback) {
superagent.get(SERVER_URL + '/api/v1/cloudron/status')
.end(function (error, result) {
if (!result || result.statusCode !== 200) return retryCallback(new Error('Bad result'));
if (!result.body.setup.active && result.body.setup.errorMessage === '' && result.body.adminFqdn) return retryCallback();
retryCallback(new Error('Not done yet: ' + JSON.stringify(result.body)));
});
}, done);
}
describe('REST API', function () {
before(setup);
after(cleanup);
@@ -128,23 +141,23 @@ describe('REST API', function () {
it('dns setup succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {} } })
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, adminFqdn: 'my.' + DOMAIN, config: {}, tlsConfig: { provider: 'fallback' } } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
done();
waitForSetup(done);
});
});
it('dns setup twice succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/setup')
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} } })
.send({ dnsConfig: { provider: 'noop', domain: DOMAIN, DOMAIN, config: {} }, tlsConfig: { provider: 'fallback' } })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
done();
waitForSetup(done);
});
});
-232
View File
@@ -1,232 +0,0 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var ssh = require('../../ssh.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var INVALID_KEY_TYPE = 'ssh-foobar AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
var INVALID_KEY_VALUE = 'ssh-rsa foobar nebulon@nebulon';
var INVALID_KEY_IDENTIFIER = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N';
var VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
var VALID_KEY_1 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N muchmore';
var token = null;
function setup(done) {
config._reset();
config.setFqdn('example-ssh-test.com');
async.series([
server.start.bind(server),
ssh._clear,
database._clear,
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('SSH API', function () {
before(setup);
after(cleanup);
describe('add authorized_keys', function () {
it('fails due to missing key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to empty key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: 'foobar' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key type', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_TYPE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key value', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_VALUE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key identifier', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_IDENTIFIER })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
describe('get authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.identifier).to.be.a('string');
expect(res.body.identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.key).to.equal(VALID_KEY);
done();
});
});
});
describe('list authorized_keys', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(1);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY);
done();
});
});
it('succeeds with two keys', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(2);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY_1.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY_1);
expect(res.body.keys[1]).to.be.an('object');
expect(res.body.keys[1].identifier).to.be.a('string');
expect(res.body.keys[1].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[1].key).to.equal(VALID_KEY);
done();
});
});
});
});
describe('delete authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
});
});
});
+263
View File
@@ -0,0 +1,263 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
nock = require('nock'),
path = require('path'),
safe = require('safetydance'),
settings = require('../../settings.js'),
settingsdb = require('../../settingsdb.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var AUTHORIZED_KEYS_FILE = path.join(config.baseDir(), 'authorized_keys');
var token = null;
function setup(done) {
nock.cleanAll();
config._reset();
config.setFqdn('example-ssh-test.com');
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILE);
async.series([
server.start.bind(server),
database._clear,
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
callback();
});
},
settingsdb.set.bind(null, settings.APPSTORE_CONFIG_KEY, JSON.stringify({ userId: 'USER_ID', cloudronId: 'CLOUDRON_ID', token: 'ACCESS_TOKEN' }))
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
config._reset();
server.stop(done);
});
}
describe('Support API', function () {
describe('remote support', function () {
before(setup);
after(cleanup);
it('get remote support', function (done) {
superagent.get(SERVER_URL + '/api/v1/support/remote_support')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.enabled).to.be(false);
done();
});
});
it('enable remote support', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/remote_support')
.query({ access_token: token })
.send({ enable: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8');
let count = (data.match(/support@cloudron.io/g) || []).length;
expect(count).to.be(1);
done();
});
});
it('returns true when remote support enabled', function (done) {
superagent.get(SERVER_URL + '/api/v1/support/remote_support')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.enabled).to.be(true);
done();
});
});
it('enable remote support (again)', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/remote_support')
.query({ access_token: token })
.send({ enable: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8');
let count = (data.match(/support@cloudron.io/g) || []).length;
expect(count).to.be(1);
done();
});
});
it('disable remote support', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/remote_support')
.query({ access_token: token })
.send({ enable: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8');
let count = (data.match(/support@cloudron.io/g) || []).length;
expect(count).to.be(0);
done();
});
});
it('disable remote support (again)', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/remote_support')
.query({ access_token: token })
.send({ enable: false })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
let data = safe.fs.readFileSync(AUTHORIZED_KEYS_FILE, 'utf8');
let count = (data.match(/support@cloudron.io/g) || []).length;
expect(count).to.be(0);
done();
});
});
});
describe('feedback', function () {
before(setup);
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without type', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty type', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: '', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unknown type', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: 'foobar', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without description', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: 'ticket', subject: 'some subject' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: 'ticket', subject: '', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty description', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: 'ticket', subject: 'some subject', description: '' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without subject', function (done) {
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: 'ticket', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with ticket type', function (done) {
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
.reply(201, { });
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: 'ticket', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope2.isDone()).to.be.ok();
done();
});
});
it('succeeds with app type', function (done) {
var scope2 = nock(config.apiServerOrigin())
.filteringRequestBody(function (/* unusedBody */) { return ''; }) // strip out body
.post('/api/v1/users/USER_ID/cloudrons/CLOUDRON_ID/feedback?accessToken=ACCESS_TOKEN')
.reply(201, { });
superagent.post(SERVER_URL + '/api/v1/support/feedback')
.send({ type: 'app_missing', subject: 'some subject', description: 'some description' })
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(scope2.isDone()).to.be.ok();
done();
});
});
});
});
+23
View File
@@ -135,4 +135,27 @@ describe('Tasks API', function () {
});
});
});
it('can list tasks', function (done) {
let taskId = null;
let task = tasks.startTask(tasks._TASK_IDENTITY, [ 'ping' ]);
task.on('error', done);
task.on('start', (tid) => { taskId = tid; });
task.on('finish', function () {
superagent.get(SERVER_URL + '/api/v1/tasks?type=' + tasks._TASK_IDENTITY)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.tasks.length >= 1).to.be(true);
expect(res.body.tasks[0].id).to.be(taskId);
expect(res.body.tasks[0].percent).to.be(100);
expect(res.body.tasks[0].args).to.be(undefined);
expect(res.body.tasks[0].active).to.be(false); // finished
expect(res.body.tasks[0].result).to.be('ping');
expect(res.body.tasks[0].errorMessage).to.be(null);
done();
});
});
});
});
+1 -2
View File
@@ -455,8 +455,7 @@ describe('Users API', function () {
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
// two mails for user creation
checkMails(2, done);
done();
});
});
});
+9 -1
View File
@@ -85,7 +85,15 @@ function update(req, res, next) {
}
function list(req, res, next) {
users.list(function (error, results) {
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (req.query.search && typeof req.query.search !== 'string') return next(new HttpError(400, 'search must be a string'));
users.getAllPaged(req.query.search || null, page, perPage, function (error, results) {
if (error) return next(new HttpError(500, error));
results = results.map(users.removeRestrictedFields);
-32
View File
@@ -1,32 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
# verify argument count
if [[ $# -lt 3 ]]; then
echo "Usage: authorized_keys.sh <user> <source> <destination>"
exit 1
fi
if [[ -f "$2" ]]; then
# on some vanilla ubuntu installs, the .ssh directory does not exist
mkdir -p "$(dirname $3)"
cp "$2" "$3"
chown "$1":"$1" "$3"
fi
+5 -5
View File
@@ -21,11 +21,11 @@ function initialize(callback) {
}
// Main process starts here
var backupId = process.argv[2];
var format = process.argv[3];
var dataDir = process.argv[4];
const backupId = process.argv[2];
const format = process.argv[3];
const dataLayoutString = process.argv[4];
debug(`Backing up ${dataDir} to ${backupId}`);
debug(`Backing up ${dataLayoutString} to ${backupId}`);
process.on('SIGTERM', function () {
process.exit(0);
@@ -40,7 +40,7 @@ process.on('disconnect', function () {
initialize(function (error) {
if (error) throw error;
backups.upload(backupId, format, dataDir, (progress) => process.send(progress), function resultHandler(error) {
backups.upload(backupId, format, dataLayoutString, (progress) => process.send(progress), function resultHandler(error) {
if (error) debug('upload completed with error', error);
debug('upload completed');
+40
View File
@@ -0,0 +1,40 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
cmd="$1"
volume_dir="$2"
if [[ "${BOX_ENV}" == "test" ]]; then
# be careful not to nuke some random directory when testing
[[ "${volume_dir}" != *"./cloudron_test/"* ]] && exit 1
fi
if [[ -d "${volume_dir}" ]]; then
# this removes hidden files
find "${volume_dir}" -maxdepth 1 -mindepth 1 -exec rm -rf '{}' \;
fi
if [[ "${cmd}" == "clear" ]]; then
mkdir -p "${volume_dir}"
# set it up so that we can restore here as normal user
chown $SUDO_USER:$SUDO_USER "${volume_dir}"
else
# this make not succeed if volume is a mount point
rmdir "${volume_dir}" || true
fi
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
volume_dir="$1"
mkdir -p "${volume_dir}"
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
source_dir="$1"
target_dir="$2"
if [[ "${BOX_ENV}" == "test" ]]; then
# be careful not to nuke some random directory when testing
[[ "${source_dir}" != *"./cloudron_test/"* ]] && exit 1
[[ "${target_dir}" != *"./cloudron_test/"* ]] && exit 1
fi
# copy and remove - this way if the copy fails, the original is intact
# the find logic is so that move to a subdir works (and we also move hidden files)
find "${source_dir}" -maxdepth 1 -mindepth 1 -not -wholename "${target_dir}" -exec cp -ar '{}' "${target_dir}" \;
find "${source_dir}" -maxdepth 1 -mindepth 1 -not -wholename "${target_dir}" -exec rm -rf '{}' \;
# this will fail if target is a subdir or if source is a mountpoint
rmdir "${source_dir}" || true
+45
View File
@@ -0,0 +1,45 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
CLOUDRON_SUPPORT_PUBLIC_KEY='ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io'
cmd="$1"
keys_file="$2"
user="${3:-1000}"
if [[ "$1" == "is-enabled" ]]; then
if grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
echo "true"
else
echo "false"
fi
elif [[ "$1" == "enable" ]]; then
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
touch "${keys_file}" # required for concat to work
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
chmod 600 "${keys_file}"
chown "${user}" "${keys_file}"
fi
elif [[ "$1" == "disable" ]]; then
if [[ -f "${keys_file}" ]]; then
sed -e "/ support@cloudron.io$/d" -i "${keys_file}"
fi
fi
-32
View File
@@ -1,32 +0,0 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
# this script is called from redis addon as well!
appid="$1"
subdir="$2"
if [[ "${BOX_ENV}" == "cloudron" ]]; then
readonly volume_dir="${HOME}/appsdata/${appid}/${subdir}"
else
readonly volume_dir="${HOME}/.cloudron_test/appsdata/${appid}/${subdir}"
fi
rm -rf "${volume_dir}"
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/env node
'use strict';
require('supererror')({ splatchError: true });
var tar = require('tar-fs');
var sourceDir = process.argv[2];
if (sourceDir === '--check') return console.log('OK');
process.stderr.write('Packing ' + sourceDir + '\n');
tar.pack('/', {
dereference: false, // pack the symlink and not what it points to
entries: [ sourceDir ],
map: function(header) {
header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative
return header;
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
}).pipe(process.stdout);
+1 -1
View File
@@ -43,7 +43,7 @@ else
echo "=> starting service (ubuntu 16.04) ${UPDATER_SERVICE}. see logs using journalctl -u ${UPDATER_SERVICE}"
fi
if ! systemd-run --unit "${UPDATER_SERVICE}" $update_service_options ${installer_path}; then
if ! systemd-run --property=OOMScoreAdjust=-1000 --unit "${UPDATER_SERVICE}" $update_service_options ${installer_path}; then
echo "Failed to install cloudron. See log for details"
exit 1
fi
+13 -7
View File
@@ -96,6 +96,7 @@ function initializeExpressSync() {
var appsManageScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_APPS_MANAGE), routes.apps.verifyOwnership ];
var settingsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_SETTINGS);
var mailScope = routes.accesscontrol.scope(accesscontrol.SCOPE_MAIL);
var notificationsScope = [ routes.accesscontrol.scope(accesscontrol.SCOPE_PROFILE), routes.notifications.verifyOwnership ];
var clientsScope = routes.accesscontrol.scope(accesscontrol.SCOPE_CLIENTS);
var domainsReadScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_READ);
var domainsManageScope = routes.accesscontrol.scope(accesscontrol.SCOPE_DOMAINS_MANAGE);
@@ -111,7 +112,7 @@ function initializeExpressSync() {
router.post('/api/v1/cloudron/setup', routes.provision.providerTokenAuth, routes.provision.setup); // only available until no-domain
router.post('/api/v1/cloudron/restore', routes.provision.restore); // only available until activated
router.post('/api/v1/cloudron/activate', routes.provision.setupTokenAuth, routes.provision.activate);
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
router.get ('/api/v1/cloudron/status', routes.provision.getStatus);
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
@@ -121,6 +122,7 @@ function initializeExpressSync() {
// cloudron routes
router.get ('/api/v1/cloudron/update', cloudronScope, routes.cloudron.getUpdateInfo);
router.post('/api/v1/cloudron/update', cloudronScope, routes.cloudron.update);
router.post('/api/v1/cloudron/prepare_dashboard_domain', cloudronScope, routes.cloudron.prepareDashboardDomain);
router.post('/api/v1/cloudron/set_dashboard_domain', cloudronScope, routes.cloudron.setDashboardDomain);
router.post('/api/v1/cloudron/renew_certs', cloudronScope, routes.cloudron.renewCerts);
router.post('/api/v1/cloudron/check_for_updates', cloudronScope, routes.cloudron.checkForUpdates);
@@ -130,11 +132,8 @@ function initializeExpressSync() {
router.get ('/api/v1/cloudron/disks', cloudronScope, routes.cloudron.getDisks);
router.get ('/api/v1/cloudron/logs/:unit', cloudronScope, routes.cloudron.getLogs);
router.get ('/api/v1/cloudron/logstream/:unit', cloudronScope, routes.cloudron.getLogStream);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, isUnmanaged, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, isUnmanaged, routes.ssh.delAuthorizedKey);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.get);
router.get ('/api/v1/cloudron/eventlog', cloudronScope, routes.eventlog.list);
router.get ('/api/v1/cloudron/eventlog/:eventId', cloudronScope, routes.eventlog.get);
// tasks
router.get ('/api/v1/tasks', settingsScope, routes.tasks.list);
@@ -143,6 +142,11 @@ function initializeExpressSync() {
router.get ('/api/v1/tasks/:taskId/logstream', cloudronScope, routes.tasks.getLogStream);
router.post('/api/v1/tasks/:taskId/stop', settingsScope, routes.tasks.stopTask);
// notifications
router.get ('/api/v1/notifications', notificationsScope, routes.notifications.list);
router.get ('/api/v1/notifications/:notificationId', notificationsScope, routes.notifications.get);
router.post('/api/v1/notifications/:notificationId', notificationsScope, routes.notifications.ack);
// backups
router.get ('/api/v1/backups', settingsScope, routes.backups.list);
router.post('/api/v1/backups', settingsScope, routes.backups.startBackup);
@@ -280,7 +284,9 @@ function initializeExpressSync() {
router.del ('/api/v1/mail/:domain/lists/:name', mailScope, routes.mail.removeList);
// feedback
router.post('/api/v1/feedback', cloudronScope, isUnmanaged, routes.cloudron.feedback);
router.post('/api/v1/support/feedback', cloudronScope, isUnmanaged, routes.support.feedback);
router.get ('/api/v1/support/remote_support', cloudronScope, isUnmanaged, routes.support.getRemoteSupport);
router.post('/api/v1/support/remote_support', cloudronScope, isUnmanaged, routes.support.enableRemoteSupport);
// domain routes
router.post('/api/v1/domains', domainsManageScope, routes.domains.add);
-171
View File
@@ -1,171 +0,0 @@
'use strict';
exports = module.exports = {
SshError: SshError,
getAuthorizedKeys: getAuthorizedKeys,
getAuthorizedKey: getAuthorizedKey,
addAuthorizedKey: addAuthorizedKey,
delAuthorizedKey: delAuthorizedKey,
_clear: clear
};
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:ssh'),
path = require('path'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util');
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys');
var AUTHORIZED_KEYS_TMP_FILEPATH = '/tmp/.authorized_keys';
var AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/authorized_keys.sh');
var VALID_KEY_TYPES = ['ssh-rsa']; // TODO add all supported ones
var VALID_MIN_KEY_LENGTH = 370; // TODO verify this length requirement
function SshError(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(SshError, Error);
SshError.NOT_FOUND = 'Not found';
SshError.INVALID_KEY = 'Invalid key';
SshError.INTERNAL_ERROR = 'Internal Error';
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILEPATH);
callback();
}
function saveKeys(keys, callback) {
assert(Array.isArray(keys));
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) {
debug('Error writing to temporary file', safe.error);
return callback(safe.error);
}
if (!safe.fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600')) { // 600 = rw-------
debug('Failed to adjust permissions of %s %s', AUTHORIZED_KEYS_TMP_FILEPATH, safe.error);
return callback(safe.error);
}
var user = config.TEST ? process.env.USER : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root');
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH ], {}, function (error) {
if (error) return callback(error);
callback(null);
});
}
function getKeys(callback) {
assert.strictEqual(typeof callback, 'function');
shell.sudo('authorized_keys', [ AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH ], {}, function (error) {
if (error) return callback(error);
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
if (!content) return callback(null, []);
var keys = content.split('\n')
.filter(function (k) { return !!k.trim(); })
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
.filter(function (k) { return k.identifier && k.key; });
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
return callback(null, keys);
});
}
function getAuthorizedKeys(callback) {
assert.strictEqual(typeof callback, 'function');
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
return callback(null, keys.sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
});
}
function getAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
var key = keys.find(function (k) { return k.identifier === identifier; });
if (!key) return callback(new SshError(SshError.NOT_FOUND));
callback(null, key);
});
}
function addAuthorizedKey(key, callback) {
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var tmp = key.split(' ');
if (tmp.length !== 3) return callback(new SshError(SshError.INVALID_KEY));
if (!VALID_KEY_TYPES.some(function (t) { return tmp[0] === t; })) return callback(new SshError(SshError.INVALID_KEY, 'Invalid key type'));
if (tmp[1].length < VALID_MIN_KEY_LENGTH) return callback(new SshError(SshError.INVALID_KEY));
var identifier = tmp[2];
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index !== -1) keys[index] = { identifier: identifier, key: key };
else keys.push({ identifier: identifier, key: key });
saveKeys(keys, function (error) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
callback(null);
});
});
}
function delAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
getKeys(function (error, keys) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
let index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
// now remove the key
keys.splice(index, 1);
saveKeys(keys, function (error) {
if (error) return callback(new SshError(SshError.INTERNAL_ERROR, error));
callback(null);
});
});
}
+2 -2
View File
@@ -353,12 +353,12 @@ function copy(apiConfig, oldFilePath, newFilePath) {
});
}
var total = 0, concurrency = 4;
var total = 0;
const concurrency = apiConfig.copyConcurrency || (apiConfig.provider === 's3' ? 500 : 10);
listDir(apiConfig, oldFilePath, 1000, function listDirIterator(entries, done) {
total += entries.length;
if (retryCount === 0) concurrency = Math.min(concurrency + 1, 10); else concurrency = Math.max(concurrency - 1, 5);
events.emit('progress', `Copying ${total-entries.length}-${total}. ${retryCount} errors so far. concurrency set to ${concurrency}`);
retryCount = 0;
+66
View File
@@ -0,0 +1,66 @@
'use strict';
exports = module.exports = {
getRemoteSupport: getRemoteSupport,
enableRemoteSupport: enableRemoteSupport,
SupportError: SupportError
};
let assert = require('assert'),
config = require('./config.js'),
shell = require('./shell.js'),
once = require('once'),
path = require('path'),
util = require('util');
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys'),
AUTHORIZED_KEYS_USER = config.TEST ? process.getuid() : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root'),
AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/remotesupport.sh');
function SupportError(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(SupportError, Error);
SupportError.NOT_FOUND = 'Not found';
SupportError.INVALID_KEY = 'Invalid key';
SupportError.INTERNAL_ERROR = 'Internal Error';
function getRemoteSupport(callback) {
assert.strictEqual(typeof callback, 'function');
callback = once(callback); // exit may or may not be called after an 'error'
let result = '';
let cp = shell.sudo('support', [ AUTHORIZED_KEYS_CMD, 'is-enabled', AUTHORIZED_KEYS_FILEPATH ], {}, function (error) {
if (error) callback(new SupportError(SupportError.INTERNAL_ERROR, error));
callback(null, { enabled: result.trim() === 'true' });
});
cp.stdout.on('data', (data) => result = result + data.toString('utf8'));
}
function enableRemoteSupport(enable, callback) {
assert.strictEqual(typeof callback, 'function');
shell.sudo('support', [ AUTHORIZED_KEYS_CMD, enable ? 'enable' : 'disable', AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_USER ], {}, function (error) {
if (error) callback(new SupportError(SupportError.INTERNAL_ERROR, error));
callback();
});
}
+43 -19
View File
@@ -2,6 +2,7 @@
var assert = require('assert'),
async = require('async'),
DataLayout = require('./datalayout.js'),
debug = require('debug')('box:syncer'),
fs = require('fs'),
path = require('path'),
@@ -21,13 +22,36 @@ function readCache(cacheFile) {
return result;
}
function readTree(dir) {
assert.strictEqual(typeof dir, 'string');
function readTree(dirPath) {
assert.strictEqual(typeof dirPath, 'string');
var list = safe.fs.readdirSync(dir).sort();
if (!list) return [ ];
const names = safe.fs.readdirSync(dirPath).sort();
if (!names) return [ ];
return list.map(function (e) { return { stat: safe.fs.lstatSync(path.join(dir, e)), name: e }; });
return names.map((name) => {
let absolutePath = path.join(dirPath, name);
return {
stat: safe.fs.lstatSync(absolutePath),
absolutePath: absolutePath,
name: name
};
});
}
function readDataLayoutTree(dataLayout) {
assert.strictEqual(typeof dataLayout, 'object');
let rootEntries = readTree(dataLayout.localRoot());
for (let l of dataLayout.directoryMap()) {
rootEntries.push({
stat: safe.fs.lstatSync(l.localDir),
absolutePath: l.localDir,
name: l.remoteDir,
});
}
return rootEntries.sort((e1, e2) => { return e1.name < e2.name ? -1 : (e1.name > e2.name ? +1 : 0); });
}
function ISDIR(x) {
@@ -38,16 +62,16 @@ function ISFILE(x) {
return (x & fs.constants.S_IFREG) === fs.constants.S_IFREG;
}
function sync(dir, taskProcessor, concurrency, callback) {
assert.strictEqual(typeof dir, 'string');
function sync(dataLayout, taskProcessor, concurrency, callback) {
assert(dataLayout instanceof DataLayout, 'Expecting dataLayout to be a DataLayout');
assert.strictEqual(typeof taskProcessor, 'function');
assert.strictEqual(typeof concurrency, 'number');
assert.strictEqual(typeof callback, 'function');
var curCacheIndex = 0, addQueue = [ ], delQueue = [ ];
var cacheFile = path.join(paths.BACKUP_INFO_DIR, path.basename(dir) + '.sync.cache'),
newCacheFile = path.join(paths.BACKUP_INFO_DIR, path.basename(dir) + '.sync.cache.new');
var cacheFile = path.join(paths.BACKUP_INFO_DIR, dataLayout.getBasename() + '.sync.cache'),
newCacheFile = path.join(paths.BACKUP_INFO_DIR, dataLayout.getBasename() + '.sync.cache.new');
var cache = [ ];
@@ -80,12 +104,10 @@ function sync(dir, taskProcessor, concurrency, callback) {
}
}
function traverse(relpath) {
var entries = readTree(path.join(dir, relpath));
for (var i = 0; i < entries.length; i++) {
var entryPath = path.join(relpath, entries[i].name);
var entryStat = entries[i].stat;
function traverse(entries, relpath) {
for (const entry of entries) {
let entryPath = path.join(relpath, entry.name);
let entryStat = entry.stat;
if (!entryStat) continue; // some stat error. prented it doesn't exist
if (!entryStat.isDirectory() && !entryStat.isFile()) continue; // ignore non-files and dirs
@@ -102,13 +124,15 @@ function sync(dir, taskProcessor, concurrency, callback) {
if (cachePath === null || cachePath > entryPath) { // new files appeared
if (entryStat.isDirectory()) {
traverse(entryPath);
traverse(readTree(entry.absolutePath), entryPath);
} else {
addQueue.push({ operation: 'add', path: entryPath, reason: 'new', position: addQueue.length });
}
} else if (ISDIR(cacheStat.mode) && entryStat.isDirectory()) { // dir names match
++curCacheIndex;
traverse(entryPath);
// if we just pass path, have to keep looking into data layout!!! so pass an object
// the object needs to have the path where we are traversing...
traverse(readTree(entry.absolutePath), entryPath);
} else if (ISFILE(cacheStat.mode) && entryStat.isFile()) { // file names match
if (entryStat.mtime.getTime() !== cacheStat.mtime || entryStat.size != cacheStat.size || entryStat.inode !== cacheStat.inode) { // file changed
addQueue.push({ operation: 'add', path: entryPath, reason: 'changed', position: addQueue.length });
@@ -117,7 +141,7 @@ function sync(dir, taskProcessor, concurrency, callback) {
} else if (entryStat.isDirectory()) { // was a file, now a directory
delQueue.push({ operation: 'remove', path: cachePath, reason: 'wasfile' });
++curCacheIndex;
traverse(entryPath);
traverse(readTree(entry.absolutePath), entryPath);
} else { // was a dir, now a file
delQueue.push({ operation: 'removedir', path: cachePath, reason: 'wasdir' });
while (curCacheIndex !== cache.length && cache[curCacheIndex].path.startsWith(cachePath)) ++curCacheIndex;
@@ -126,7 +150,7 @@ function sync(dir, taskProcessor, concurrency, callback) {
}
}
traverse('');
traverse(readDataLayoutTree(dataLayout), '');
advanceCache(''); // remove rest of the cache entries
safe.fs.closeSync(newCacheFd);
+9 -8
View File
@@ -34,19 +34,20 @@ function SysInfoError(reason, errorOrMessage) {
}
util.inherits(SysInfoError, Error);
SysInfoError.INTERNAL_ERROR = 'Internal Error';
SysInfoError.EXTERNAL_ERROR = 'External Error';
function getApi(callback) {
assert.strictEqual(typeof callback, 'function');
switch (config.provider()) {
case '': return callback(null, caas); // current fallback for caas
case 'caas': return callback(null, caas);
case 'digitalocean': return callback(null, generic);
case 'ec2': return callback(null, ec2);
case 'lightsail': return callback(null, ec2);
case 'ami': return callback(null, ec2);
case 'scaleway': return callback(null, scaleway);
default: return callback(null, generic);
case '': return callback(null, caas); // current fallback for caas
case 'caas': return callback(null, caas);
case 'digitalocean': return callback(null, generic);
case 'ec2': return callback(null, ec2);
case 'lightsail': return callback(null, ec2);
case 'ami': return callback(null, ec2);
case 'scaleway': return callback(null, scaleway);
default: return callback(null, generic);
}
}
+2 -2
View File
@@ -17,14 +17,14 @@ function getPublicIp(callback) {
superagent.get('http://169.254.169.254/metadata/v1.json').timeout(30 * 1000).end(function (error, result) {
if (error || result.statusCode !== 200) {
console.error('Error getting metadata', error);
return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'Could not detect public IP from metadata'));
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Could not detect public IP from metadata'));
}
// Note that we do not use a floating IP for 3 reasons:
// The PTR record is not set to floating IP, the outbound interface is not changeable to floating IP
// and there are reports that port 25 on floating IP is blocked.
var ip = safe.query(result.body, 'interfaces.public[0].ipv4.ip_address');
if (!ip) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'Could not detect public IP from interface'));
if (!ip) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Could not detect public IP from interface'));
callback(null, ip);
});
+2 -2
View File
@@ -19,11 +19,11 @@ function getPublicIp(callback) {
superagent.get(config.apiServerOrigin() + '/api/v1/helper/public_ip').timeout(30 * 1000).end(function (error, result) {
if (error || result.statusCode !== 200) {
console.error('Error getting IP', error);
return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'Unable to contact api server'));
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Unable to detect IP. API server unreachable'));
}
if (!result.body && !result.body.ip) {
console.error('Unexpected answer. No "ip" found in response body.', result.body);
return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'No IP found in body'));
return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, 'Unable to detect IP. No IP found in response'));
}
callback(null, result.body.ip);
+4 -2
View File
@@ -5,13 +5,15 @@ exports = module.exports = {
};
var assert = require('assert'),
superagent = require('superagent');
superagent = require('superagent'),
SysInfoError = require('../sysinfo.js').SysInfoError,
util = require('util');
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get('http://169.254.42.42/conf').timeout(30 * 1000).end(function (error, result) {
if (error) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, error.status ? 'Request failed: ' + error.status : 'Network failure'));
if (error) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, error.status ? 'Request failed: ' + error.status : 'Network failure'));
if (result.statusCode !== 200) return callback(new SysInfoError(SysInfoError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
var kv = result.text.split('\n').filter(function (line) { return line.startsWith('PUBLIC_IP_ADDRESS='); });
+18 -10
View File
@@ -13,16 +13,19 @@ let assert = require('assert'),
DatabaseError = require('./databaseerror'),
safe = require('safetydance');
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorMessage', 'creationTime', 'result', 'ts' ];
const TASKS_FIELDS = [ 'id', 'type', 'argsJson', 'percent', 'message', 'errorMessage', 'creationTime', 'resultJson', 'ts' ];
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
function postProcess(task) {
assert.strictEqual(typeof task, 'object');
assert(result.argsJson === null || typeof result.argsJson === 'string');
result.args = safe.JSON.parse(result.argsJson) || [];
delete result.argsJson;
assert(task.argsJson === null || typeof task.argsJson === 'string');
task.args = safe.JSON.parse(task.argsJson) || [];
delete task.argsJson;
result.id = String(result.id);
task.id = String(task.id);
task.result = JSON.parse(task.resultJson);
delete task.resultJson;
}
function add(task, callback) {
@@ -47,8 +50,13 @@ function update(id, data, callback) {
let args = [ ];
let fields = [ ];
for (let k in data) {
fields.push(k + ' = ?');
args.push(data[k]);
if (k === 'result') {
fields.push('resultJson = ?');
args.push(JSON.stringify(data[k]));
} else {
fields.push(k + ' = ?');
args.push(data[k]);
}
}
args.push(id);
@@ -100,7 +108,7 @@ function listByTypePaged(type, page, perPage, callback) {
data.push(type);
}
query += ' ORDER BY creationTime DESC LIMIT ?,?';
query += ' ORDER BY creationTime DESC, id DESC LIMIT ?,?'; // put latest task first
data.push((page-1)*perPage);
data.push(perPage);
+3 -3
View File
@@ -23,7 +23,7 @@ var appdb = require('./appdb.js'),
mkdirp = require('mkdirp'),
path = require('path'),
paths = require('./paths.js'),
sendFailureLogs = require('./logcollector.js').sendFailureLogs,
eventlog = require('./eventlog.js'),
util = require('util'),
_ = require('underscore');
@@ -150,10 +150,10 @@ function startAppTask(appId, callback) {
debug('Task for %s pid %s completed with status %s', appId, pid, code);
if (code === null /* signal */ || (code !== 0 && code !== 50)) { // apptask crashed
debug('Apptask crashed with code %s and signal %s', code, signal);
sendFailureLogs('apptask', { unit: 'box' });
appdb.update(appId, { installationState: appdb.ISTATE_ERROR, installationProgress: 'Apptask crashed with code ' + code + ' and signal ' + signal }, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_APP_TASK_CRASH, { appId: appId }, { crashLogFile: logFilePath }, NOOP_CALLBACK);
} else if (code === 50) {
sendFailureLogs('apptask', { unit: 'box' });
eventlog.add(eventlog.ACTION_APP_TASK_CRASH, { appId: appId }, { crashLogFile: logFilePath }, NOOP_CALLBACK);
}
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
+10 -7
View File
@@ -18,6 +18,8 @@ exports = module.exports = {
TASK_BACKUP: 'backup',
TASK_UPDATE: 'update',
TASK_RENEW_CERTS: 'renewcerts',
TASK_PREPARE_DASHBOARD_DOMAIN: 'prepareDashboardDomain',
TASK_CLEAN_BACKUPS: 'cleanBackups',
// testing
_TASK_IDENTITY: '_identity',
@@ -105,7 +107,7 @@ function startTask(type, args) {
if (error) return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error));
const logFile = `${paths.TASKS_LOG_DIR}/${taskId}.log`;
let fd = safe.fs.openSync(logFile, 'a'); // will autoclose
let fd = safe.fs.openSync(logFile, 'w'); // will autoclose
if (!fd) {
debug(`startTask: unable to get log filedescriptor ${safe.error.message}`);
return events.emit('error', new TaskError(TaskError.INTERNAL_ERROR, error.message));
@@ -119,7 +121,7 @@ function startTask(type, args) {
get(taskId, function (error, task) {
if (!error && task.percent !== 100) { // task crashed or was killed by us (code 50)
error = code === 0 ? new Error(`${taskId} task stopped`) : new Error(`${taskId} task crashed with code ${code} and signal ${signal}`);
error = code === 0 ? new Error(`task ${taskId} stopped`) : new Error(`task ${taskId} crashed with code ${code} and signal ${signal}`);
update(taskId, { percent: 100, errorMessage: error.message }, NOOP_CALLBACK);
} else if (!error && task.errorMessage) {
error = new Error(task.errorMessage);
@@ -174,14 +176,15 @@ function getLogs(taskId, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
assert.strictEqual(typeof options.follow, 'boolean');
debug(`Getting logs for ${taskId}`);
var lines = options.lines || 100,
var lines = options.lines === -1 ? '+1' : options.lines,
format = options.format || 'json',
follow = !!options.follow;
assert.strictEqual(typeof lines, 'number');
assert.strictEqual(typeof format, 'string');
follow = options.follow;
let cmd = '/usr/bin/tail';
var args = [ '--lines=' + lines ];
+3
View File
@@ -6,6 +6,7 @@ var assert = require('assert'),
backups = require('./backups.js'),
database = require('./database.js'),
debug = require('debug')('box:taskworker'),
domains = require('./domains.js'),
reverseProxy = require('./reverseproxy.js'),
tasks = require('./tasks.js'),
updater = require('./updater.js');
@@ -16,6 +17,8 @@ const TASKS = { // indexed by task type
backup: backups.backupBoxAndApps,
update: updater.update,
renewcerts: reverseProxy.renewCerts,
prepareDashboardDomain: domains.prepareDashboardDomain,
cleanBackups: backups.cleanup,
_identity: (arg, progressCallback, callback) => callback(null, arg),
_error: (arg, progressCallback, callback) => callback(new Error(`Failed for arg: ${arg}`)),

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