Compare commits

...

400 Commits

Author SHA1 Message Date
Girish Ramakrishnan
9c0f983ce1 backups: fix failure notitification 2021-06-24 01:44:46 -07:00
Girish Ramakrishnan
b24cf78bc0 certs: fix renewal notification 2021-06-24 01:12:33 -07:00
Girish Ramakrishnan
2b13593630 notifications: only send backup failure email on 3 consecutive fails 2021-06-24 00:48:59 -07:00
Girish Ramakrishnan
6da7218d34 certs: show daysLeft in the logs 2021-06-24 00:48:59 -07:00
Girish Ramakrishnan
7d3270e51a notifications: do not jump json blob for out of disk space 2021-06-23 23:41:55 -07:00
Girish Ramakrishnan
54dec7ae08 notifications: delete obsolete alerts 2021-06-23 22:51:38 -07:00
Girish Ramakrishnan
89607d2c64 remove the backup check notification and route
it seems we decided that instead of a notification, we display a warning in
the backups view itself (see #719).
2021-06-23 22:09:23 -07:00
Girish Ramakrishnan
3eb5a26c46 prefix translatable strings with tr: 2021-06-23 22:02:07 -07:00
Girish Ramakrishnan
ebab671f68 remove slash from container name 2021-06-23 17:20:11 -07:00
Girish Ramakrishnan
5129465e59 aws: const correctness 2021-06-23 14:30:00 -07:00
Girish Ramakrishnan
02263e8921 add back mountpoint check 2021-06-22 15:52:50 -07:00
Girish Ramakrishnan
da6478272d provision: call done instead of callback 2021-06-22 14:40:58 -07:00
Girish Ramakrishnan
15ff43369f mount: if unmount failed, do not proceed 2021-06-22 13:03:44 -07:00
Girish Ramakrishnan
5040b4f3f9 backups: chown and preserve attribs on ext4 and sshfs 2021-06-22 09:27:11 -07:00
Girish Ramakrishnan
20fe04c0cf chown for sshfs as well 2021-06-21 23:21:40 -07:00
Girish Ramakrishnan
ceddabd691 Fix tryAddMount usage 2021-06-21 23:13:37 -07:00
Girish Ramakrishnan
3ba2f96d51 volume: remove private fields 2021-06-21 16:35:08 -07:00
Girish Ramakrishnan
6ace8d1ac5 volumes: fix various mount related issues
Various notes on mounting:

* The permissions come from the mounted file system and not the mount point.
This means that if we change the perms before mounting, it is overridden by
whatever is in the actual file system.

* uid/gid only works for permission-less file systems

SFTP container notes:

* Assumes that nothing changed if the host path hasn't changed. This means that
if a user changes the disk uuid, reload doesn't work.

* Not sure how/why, but even after unmounting the container can still access the old
mount files (!). With ext4 on disk change or nfs after root path change, the file manager
continues to be able to access the old mounts (despite umount succeeding).

All this led to following changes:

* Remove editing of volumes. Just allow editing username/password.
* edit UI then just also provides a way to re-mount.
* Change mode of mountpoint to be 777 post mounting for ease of use. Otherwise, we have to
make the user do this by ssh. this can always become options later.
2021-06-21 16:11:48 -07:00
Girish Ramakrishnan
f433146484 volumes: reload sftp on update
when diskPath changes, docker is busy holding on to the previous mount!
I guess this is because this is all somehow inode based.
2021-06-21 11:53:27 -07:00
Girish Ramakrishnan
c16a7c1f45 do not block for service to restart 2021-06-21 10:05:22 -07:00
Girish Ramakrishnan
79ec7fb245 volumes: make sshfs work 2021-06-20 23:39:35 -07:00
Girish Ramakrishnan
87c22a4670 mount: mount as 777 for max compat
for cifs, file_mode can be 666
2021-06-20 22:48:37 -07:00
Girish Ramakrishnan
90657af7f2 mount: fix nfs re-mounting 2021-06-18 23:48:39 -07:00
Girish Ramakrishnan
c23b935cea volumes: hostPath -> mount point 2021-06-18 23:31:11 -07:00
Girish Ramakrishnan
ecf2ff9e15 mount: better error message detection 2021-06-18 23:02:53 -07:00
Girish Ramakrishnan
55950c7e2d better description for mount files 2021-06-18 18:03:07 -07:00
Girish Ramakrishnan
5f509f802f install sshfs 2021-06-18 14:46:54 -07:00
Girish Ramakrishnan
0a3a7cb1a3 debug: replace newline in args 2021-06-18 14:33:50 -07:00
Girish Ramakrishnan
e6e875814e systemctl show --value does not work on ubuntu 16 2021-06-18 14:29:03 -07:00
Girish Ramakrishnan
406b3394cb mail: fix issue where spam to internal lists was not blocked 2021-06-18 10:35:42 -07:00
Girish Ramakrishnan
5cad4d1ebd do not capitalize title words 2021-06-17 13:51:29 -07:00
Girish Ramakrishnan
21ec89a38a add note on dhparams.pem removal 2021-06-17 11:41:44 -07:00
Girish Ramakrishnan
77989893df remove boxdata/well-known directory
this has already moved into the domains table
2021-06-17 11:37:03 -07:00
Girish Ramakrishnan
7ca86cc96d cloudron-setup: do not cat to stdout 2021-06-17 10:03:59 -07:00
Girish Ramakrishnan
bf1c7eedb7 clone: copy over the enableMailbox flag 2021-06-16 23:17:26 -07:00
Girish Ramakrishnan
f2e0ee12a2 vultr: object storage 2021-06-16 22:36:01 -07:00
Girish Ramakrishnan
ef04253288 print the task options in the logs 2021-06-16 14:21:19 -07:00
Girish Ramakrishnan
fa81491bf3 Fix uninstall of apps with bad docker images names
Some day we can implement https://github.com/distribution/distribution/blob/main/reference/regexp.go
2021-06-16 11:55:23 -07:00
Johannes Zellner
45236aa78d Fix error message 2021-06-16 19:25:05 +02:00
Johannes Zellner
9851eb0817 We now use the delay module outside of the tests 2021-06-16 19:24:43 +02:00
Girish Ramakrishnan
9436dc688b omit icon when creating install/clone eventlog entries
Otherwise, we hit "Error: ER_DATA_TOO_LONG: Data too long for column 'data'"
2021-06-15 11:12:52 -07:00
Girish Ramakrishnan
28c908b126 appstore: nothing to update when not registered yet 2021-06-05 22:21:07 -07:00
Girish Ramakrishnan
1de006b053 lint 2021-06-05 22:13:25 -07:00
Girish Ramakrishnan
b2856bc8e0 vultr: fix out of bounds access 2021-06-05 22:12:11 -07:00
Girish Ramakrishnan
b579f7ae90 better error messages for 401 2021-06-05 21:26:43 -07:00
Girish Ramakrishnan
eb16e8a8ee eventlog: fix cleanup 2021-06-05 21:20:32 -07:00
Girish Ramakrishnan
579c046944 test: app token tests to api-test 2021-06-05 15:39:34 -07:00
Girish Ramakrishnan
b778f1e616 test: move server-test into provision-test 2021-06-05 15:26:35 -07:00
Girish Ramakrishnan
fe8358c3e3 test: remove tokendb from users-test 2021-06-05 15:14:07 -07:00
Girish Ramakrishnan
9c49ca5d2e test: move the 2fa tests into profile 2021-06-05 10:43:41 -07:00
Girish Ramakrishnan
9e34a95732 postgresql: fix backup hogging connections
This fixes the "FATAL: remaining connection slots are reserved for non-replication superuser connections"
2021-06-05 09:43:53 -07:00
Girish Ramakrishnan
9228f0cc12 move around the export to avoid circular dep when running tests 2021-06-04 23:53:30 -07:00
Girish Ramakrishnan
ed7514e4ba typo 2021-06-04 19:47:18 -07:00
Girish Ramakrishnan
ee7cddfbbc acme: fix http challenge 2021-06-04 17:51:26 -07:00
Girish Ramakrishnan
cdbc51b208 openssl: older openssl (1.0.2g/ubuntu 16) requires distinguished_name 2021-06-04 16:48:56 -07:00
Girish Ramakrishnan
dd3600b13c test: fix groups test 2021-06-04 14:55:14 -07:00
Girish Ramakrishnan
9fa63b4ef8 acme: openssl -ext is not recognized on ubuntu 16 2021-06-04 14:54:39 -07:00
Girish Ramakrishnan
7bee7b9ef8 tokens: async'ify 2021-06-04 13:06:38 -07:00
Girish Ramakrishnan
593038907c unbound: on ubuntu 16, sd_notify is not working
not clear, when unbound added support for this.

on ubuntu 16, unbound is 1.5.8.
on ubuntu 20, unbound is 1.9.4
2021-06-04 09:41:54 -07:00
Girish Ramakrishnan
64dcdb5e84 user cert may not exist 2021-06-03 22:51:28 -07:00
Girish Ramakrishnan
0208e3d3a2 test: make appstore-test use common.js 2021-06-03 22:39:26 -07:00
Girish Ramakrishnan
acfb4d8553 remove empty test file 2021-06-03 22:23:48 -07:00
Girish Ramakrishnan
d78df9405d Fix dashboardFqdn not set correctly because of typo 2021-06-03 21:33:46 -07:00
Girish Ramakrishnan
4937cbbc0b shell: add promises test 2021-06-03 19:36:37 -07:00
Girish Ramakrishnan
a0c4ef9d0f more test fixes 2021-06-03 16:29:56 -07:00
Girish Ramakrishnan
8da4eaf4a3 fix tests 2021-06-03 16:08:39 -07:00
Girish Ramakrishnan
c90a9e43cf Fix usage of eventlog.add 2021-06-03 11:42:32 -07:00
Girish Ramakrishnan
2c1bedd38a delay is a normal dep on not dev dep 2021-06-03 10:11:29 -07:00
Girish Ramakrishnan
7aac4455a9 eventlog: async'ify 2021-06-01 16:37:32 -07:00
Girish Ramakrishnan
bdbda9b80e transaction now returns a promise 2021-06-01 16:11:01 -07:00
Girish Ramakrishnan
e9ace613e2 cert: only inform user if renewal fails and only 10 days left 2021-06-01 09:09:16 -07:00
Girish Ramakrishnan
380fe7c17a domains: add vultr dns 2021-05-29 22:58:18 -07:00
Girish Ramakrishnan
9e7dd3f397 notifications: acknowledged can be null 2021-05-29 21:56:35 -07:00
Girish Ramakrishnan
73917e95c9 rework notifications
notifications are now system level instead of user level.

To clarify the use events/notifications/email:
* eventlog - everything that is happenning on server
* notifications - specific important events (alerts)
* email - these are really urgent things that require immediate attention. this is for
  the case where an admin does not visit the dashboard often. can also be alerts like
  bad backup config or reboot required which are not events per-se.

Notes on notifications
* oom - notification only
* appUpdated - notification only
* cert renewal failure - only raise when < 10 days to go. also send email thereafter (todo).
* Backup failure - only if last 5 backups failed (todo).
* Box update - notification only. we anyway send newsletter.
* box update available - we raise a notification. no email.
* app update available - we already have update indicator on dashboard. so, no notification or email.

Alerts:
* backup config
* disk space
* mail status
* reboot
* box updated
* ubuntu update required
2021-05-28 15:29:53 -07:00
Girish Ramakrishnan
3ba62f2ba1 mail: do not forward spam 2021-05-27 22:21:17 -07:00
Girish Ramakrishnan
9d664a7d7c typo 2021-05-27 15:15:29 -07:00
Girish Ramakrishnan
b278056941 typo in backup filename 2021-05-27 15:08:51 -07:00
Girish Ramakrishnan
a34bdb9ddf backups: fix mounting logic of backup settings and cloudron restore 2021-05-27 13:52:05 -07:00
Girish Ramakrishnan
98988202a1 sftp: make the key unreadble by group/others 2021-05-26 15:59:07 -07:00
Girish Ramakrishnan
0342865129 sieve: redirects do not do SRS 2021-05-26 15:22:10 -07:00
Girish Ramakrishnan
c605395885 app import: restore icon, tag, label, proxy configs etc 2021-05-26 09:49:00 -07:00
Girish Ramakrishnan
098cff08f7 make import a task of it's own
this allows us to distinguish it in the eventlog and apptask logic
2021-05-26 09:27:15 -07:00
Girish Ramakrishnan
431e2a6ab9 clone: save app config
clone also clones the tags, labels and icon. this is not done for
restore or import since it's not clear if this is a good idea or not.
for example, if user had some custom tags and label set and then restores
to some old backup, is it expected to reset the labels and tags?
2021-05-26 09:03:05 -07:00
Girish Ramakrishnan
2fb6be81fc cloudron-setup: check if box.service exists instead 2021-05-24 19:05:49 -07:00
Girish Ramakrishnan
0a5a24ba2e add tryAddMount
we try to add a mount. if it fails, it will revert to the previous mount config.

there was a plan to make this work based on systemd-mount but we hit this bug - https://bugzilla.redhat.com/show_bug.cgi?id=1708996
2021-05-21 22:32:51 -07:00
Girish Ramakrishnan
59db625ad9 volumes: wait for mount during add/update
this is a better feedback mechanism for the user
2021-05-19 11:08:33 -07:00
Girish Ramakrishnan
449d6b2de4 add missing callback 2021-05-18 14:49:15 -07:00
Girish Ramakrishnan
91df8df92d add alert for ubuntu 16 2021-05-18 14:48:01 -07:00
Girish Ramakrishnan
a5e34cf775 delete certs that have long expired (6 months)
fixes #783
2021-05-18 13:37:35 -07:00
Girish Ramakrishnan
76d0abae43 postgresql: set max conn limit per db 2021-05-18 09:04:29 -07:00
Johannes Zellner
1785b0352a Add initial sshfs support 2021-05-18 17:27:32 +02:00
Girish Ramakrishnan
14bb928d41 backups: fix various mount issues 2021-05-17 22:58:40 -07:00
Girish Ramakrishnan
599b604dca tests: make volumes routes test pass 2021-05-17 22:23:24 -07:00
Girish Ramakrishnan
c7474511aa fix volume test 2021-05-17 16:23:37 -07:00
Girish Ramakrishnan
124954d490 migrate old providers as generic mountpoint provider 2021-05-17 13:23:32 -07:00
Girish Ramakrishnan
53dce1e7aa users: rename createdAt to creationTime 2021-05-17 07:54:54 -07:00
Girish Ramakrishnan
2421536c23 add indexes for ORDER BY fields used in code
we hit ER_OUT_OF_SORTMEMORY with large tables
2021-05-17 07:06:11 -07:00
Girish Ramakrishnan
aae40f506b backups: add mounting config 2021-05-14 15:27:07 -07:00
Girish Ramakrishnan
24dbf53c5d fix error handling 2021-05-14 14:46:16 -07:00
Girish Ramakrishnan
a56766ab0e ensure nss-lookup.target is hit after unbound starts
https://github.com/NLnetLabs/unbound/issues/296

this fixes volume hostname resolution on reboot
2021-05-14 12:07:05 -07:00
Girish Ramakrishnan
43642b2d60 volumes: better options for ext4 2021-05-14 11:38:32 -07:00
Girish Ramakrishnan
8cb7c8cd1c volumes fixes 2021-05-14 10:26:57 -07:00
Girish Ramakrishnan
00cd10742f cifs: set uid/gid
because the mode is 0777, we should be fine with any valid uid/gid
2021-05-14 10:08:44 -07:00
Girish Ramakrishnan
88a5526e9b starttask: set NODE_OPTIONS instead of env -S 2021-05-14 09:36:00 -07:00
Girish Ramakrishnan
06b7cb962b do not remove mount file if mountType was no-op
we might end up removing user's systemd file
2021-05-14 08:44:44 -07:00
Johannes Zellner
6f2382d5ff Set correct esversion for linter 2021-05-14 12:27:59 +02:00
Johannes Zellner
5e48b69d3b Revert "taskworker: Use --unhandled-rejections=strict"
Breaks the task argument count

This reverts commit b7643ae3b3.
2021-05-14 11:13:52 +02:00
Johannes Zellner
a43e804ee2 Revert "taskworker: put the arg in shebang line"
Not supported on ubuntu 18

This reverts commit e6edc4e999.
2021-05-14 10:51:37 +02:00
Johannes Zellner
170efbcb5e Remove unused require 2021-05-14 10:47:54 +02:00
Johannes Zellner
fe34c158eb Fix cifs mount point syntax and options 2021-05-14 10:30:11 +02:00
Girish Ramakrishnan
8fc4a8abf7 volume: use mountpoint command to check if it is mounted 2021-05-13 23:21:15 -07:00
Girish Ramakrishnan
c2fc978ffd better heuristic to find mount error 2021-05-13 23:03:25 -07:00
Girish Ramakrishnan
938b88d61b Fix crash 2021-05-13 22:52:41 -07:00
Girish Ramakrishnan
f927b9b5b2 make taskworker console.* log to file and not stdout
this is similar to code in box.js
2021-05-13 22:49:47 -07:00
Girish Ramakrishnan
e6edc4e999 taskworker: put the arg in shebang line
otherwise, it gets passed as an arg to the script and is visible in process.argv!
2021-05-13 22:49:15 -07:00
Girish Ramakrishnan
b7643ae3b3 taskworker: Use --unhandled-rejections=strict
this way, those tasks crash and do not hang on bad code
2021-05-13 22:32:12 -07:00
Girish Ramakrishnan
0c4b7f3202 do not use %s to print error object
this ends up suppressing the backtrace
2021-05-13 22:31:58 -07:00
Girish Ramakrishnan
65e114437b volume: always send a message 2021-05-13 17:50:27 -07:00
Girish Ramakrishnan
238073fe48 volume: get status 2021-05-13 16:08:55 -07:00
Girish Ramakrishnan
2c8e83dc6d volumes: update route 2021-05-13 10:48:30 -07:00
Girish Ramakrishnan
ac4fa83080 status is already an object 2021-05-13 00:02:21 -07:00
Girish Ramakrishnan
50407eba0b volumes: generate systemd mount files based on mount type 2021-05-12 23:57:12 -07:00
Girish Ramakrishnan
4c938b5e77 shell: expose promises variant 2021-05-12 17:30:29 -07:00
Girish Ramakrishnan
52da431388 misplaced await 2021-05-12 13:30:22 -07:00
Girish Ramakrishnan
fc52cd7e0c volumes: async'ify 2021-05-12 11:46:04 -07:00
Girish Ramakrishnan
3a252fe10e boxerror: override the properties directly 2021-05-11 17:50:40 -07:00
Johannes Zellner
7dcc904af9 Fix new login location detection 2021-05-09 10:19:19 +02:00
Girish Ramakrishnan
91a7a9e43c backups: change app backup filename
change from app_appid_timestamp_vVersion to app_fqdn_vVersion

Fixes #782
2021-05-08 17:17:11 -07:00
Girish Ramakrishnan
4482da6148 move acme2.js one level up 2021-05-07 23:21:45 -07:00
Girish Ramakrishnan
302ea60b8d consolidate acme paths in the reverseproxy code 2021-05-07 23:21:42 -07:00
Girish Ramakrishnan
dea31109e2 remove debug 2021-05-07 22:59:53 -07:00
Girish Ramakrishnan
b3a805faff ensureCertificate: copy certs from db to disk as needed 2021-05-07 22:07:14 -07:00
Girish Ramakrishnan
593a61f51b apps: get user certificate 2021-05-07 21:37:23 -07:00
Girish Ramakrishnan
84af9580a6 migrate certs into the blobs database
use platformdata/nginx/cert to store the certs
2021-05-07 21:26:49 -07:00
Girish Ramakrishnan
182918b13d add note 2021-05-07 20:20:15 -07:00
Girish Ramakrishnan
d8422ea976 fix safe() error handling 2021-05-07 15:56:43 -07:00
Girish Ramakrishnan
cc684b4ea0 acme: async'ify 2021-05-06 22:52:49 -07:00
Girish Ramakrishnan
31503e2625 postgresql: bump max connections 2021-05-06 10:58:48 -07:00
Girish Ramakrishnan
39e7d9cc7a Further rename of admin -> dashboard 2021-05-05 13:14:48 -07:00
Girish Ramakrishnan
9418e93428 reverseproxy: adminOrigin is not used in the ejs 2021-05-05 13:13:04 -07:00
Girish Ramakrishnan
16dc008702 Fix failing test 2021-05-05 12:52:11 -07:00
Girish Ramakrishnan
44ac406e57 admin -> dashboard 2021-05-05 12:29:04 -07:00
Girish Ramakrishnan
cc9b43450c configureAdmin is never used 2021-05-05 12:16:25 -07:00
Girish Ramakrishnan
7f6a0555b2 store custom app certificates in subdomains table
the REST route and model code is still ununsed as before since there
is no way to set the certs from the UI.
2021-05-05 10:58:20 -07:00
Girish Ramakrishnan
963e92b517 store fallback certs in the database 2021-05-04 22:30:28 -07:00
Girish Ramakrishnan
7de454911e migrate firewall configuration into database
the ports.json is for the moment server specific
2021-05-04 15:55:54 -07:00
Girish Ramakrishnan
d8e464d9c7 Fix sftp paths 2021-05-04 15:55:37 -07:00
Girish Ramakrishnan
fc2e2665b9 restore: write secrets into platformdata on start
this is required when cloudron is restored and we have to then write
keys from the db into the platformdata.
2021-05-04 14:56:25 -07:00
Johannes Zellner
5cc5c1923a If user-agent is not known pass the agent as incoming string 2021-05-04 20:36:41 +02:00
Johannes Zellner
aa86174d6b We can have toplevel arrays just fine 2021-05-04 20:30:52 +02:00
Johannes Zellner
fed8ba95f0 Fallback to unkown useragent and don't stash such login attempts 2021-05-04 20:07:28 +02:00
Johannes Zellner
bec42c69c8 Do not use toplevel arrays 2021-05-04 19:52:21 +02:00
Girish Ramakrishnan
7d8d6d4913 better error messages 2021-05-04 10:45:36 -07:00
Johannes Zellner
5ab925e284 Fix login location stash 2021-05-04 15:00:09 +02:00
Girish Ramakrishnan
f016f3d3e1 use rmdir instead of unlink 2021-05-04 00:28:44 -07:00
Girish Ramakrishnan
dcea55cd81 chown the sftp directory 2021-05-04 00:27:45 -07:00
Johannes Zellner
e10b7b59dc Only use simplified user agent for login detection 2021-05-04 09:11:16 +02:00
Girish Ramakrishnan
885647f484 fix proxyauth icon 2021-05-03 23:00:51 -07:00
Girish Ramakrishnan
c17743d869 migrate secrets into the database
the infra version is bumped because the nginx's dhparams path has changed
and the sftp server key path has changed.
2021-05-03 22:11:18 -07:00
Girish Ramakrishnan
4015f8fdf2 update safetydance 2021-05-03 15:55:27 -07:00
Girish Ramakrishnan
035f356dff add async support to database.query() 2021-05-02 23:18:07 -07:00
Girish Ramakrishnan
199eda82d1 Use Array.isArray instead 2021-05-02 11:26:47 -07:00
Girish Ramakrishnan
442110a437 lint 2021-05-01 11:21:09 -07:00
Girish Ramakrishnan
907ae4f233 secrets -> blobs
this will also have certs which are not really "secrets"
2021-04-30 22:34:27 -07:00
Girish Ramakrishnan
130ef72c9a Add secrets table
this will hold keys, certs etc
2021-04-30 22:07:51 -07:00
Girish Ramakrishnan
a33fdee659 remove unused dir 2021-04-30 16:34:05 -07:00
Girish Ramakrishnan
6e7716e992 Update ts when setting task as well 2021-04-30 16:26:19 -07:00
Girish Ramakrishnan
bad77fd99e apps: update ts in code instead of database
ts is useful as a versioning mechanism (for example, icon changed). update the timestamp explicity in code instead of db.
this way health and healthTime can be updated without changing ts.
2021-04-30 16:14:29 -07:00
Girish Ramakrishnan
0062e6d9fe apps: add icon and appStoreIcon to database 2021-04-30 14:35:21 -07:00
Girish Ramakrishnan
64414eb932 new login mail: minor adjustments to text 2021-04-30 12:01:57 -07:00
Girish Ramakrishnan
698ab93cc9 Add to CHANGES 2021-04-30 10:34:10 -07:00
Girish Ramakrishnan
8ff68331a8 proxyAuth: use default expiry time in cookie (1 year) 2021-04-30 10:31:09 -07:00
Girish Ramakrishnan
6fe8974a2d location -> loginLocations 2021-04-30 10:28:34 -07:00
Girish Ramakrishnan
44027f61e6 Fix failing tests 2021-04-30 09:48:00 -07:00
Johannes Zellner
549b2f2a6b Improve new login location email 2021-04-30 16:20:50 +02:00
Johannes Zellner
fb5c2a5e52 Properly detect new user agents and location 2021-04-30 15:22:10 +02:00
Girish Ramakrishnan
af2c096975 branding: move logo into database
initially, i tried to put this in the current value field but that
is TEXT and has a size limit of 64K. TEXT also stores things with
character encoding, so we have to stash it as base64
2021-04-29 18:28:03 -07:00
Girish Ramakrishnan
3c09416e44 Use Buffer.isBuffer instead 2021-04-29 15:37:32 -07:00
Girish Ramakrishnan
6df5a4f79b Remove unused FIREWALL_CONFIG_FILE 2021-04-29 15:35:42 -07:00
Girish Ramakrishnan
df0532714e Fix various debugs 2021-04-29 15:25:19 -07:00
Girish Ramakrishnan
6a32291609 Move updatechecker.json into platform data 2021-04-29 14:01:24 -07:00
Girish Ramakrishnan
b8ea9de439 move profile icons into the database 2021-04-29 13:57:24 -07:00
Girish Ramakrishnan
7b8fd3596e well known is now stored in the database 2021-04-29 12:17:44 -07:00
Girish Ramakrishnan
6a294f6cd6 Add 6.2.8 changes 2021-04-28 10:40:14 -07:00
Girish Ramakrishnan
fe6ee45645 typo 2021-04-27 15:25:11 -07:00
Girish Ramakrishnan
cd300bb6e2 graphite: carbon crash fix
https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=923464
https://forum.cloudron.io/topic/4797/graphite-keeps-crashing-oom/34
2021-04-27 14:25:12 -07:00
Girish Ramakrishnan
cb573c0a37 reverseproxy: identify LE staging correctly 2021-04-27 12:55:11 -07:00
Girish Ramakrishnan
38425e75b5 tests: create firewall directory 2021-04-26 14:15:39 -07:00
Johannes Zellner
70f2337b09 Allow apps to override the Referrer-Policy header 2021-04-26 11:48:18 +02:00
Girish Ramakrishnan
f3d870978b add tests for inactive mailbox and list 2021-04-21 12:39:18 -07:00
Girish Ramakrishnan
d437acebe2 notifications: can also mark it as unread 2021-04-21 12:20:58 -07:00
Girish Ramakrishnan
bb3f9744fb notifications: fix pagination of listByUserIdPaged
we have to filter in sql query, otherwise we don't get consistent per page count
2021-04-21 10:55:31 -07:00
Girish Ramakrishnan
fbceb67df9 notifications: remove app up/down 2021-04-21 10:55:31 -07:00
Johannes Zellner
de8d861e56 Add basic .jshintrc 2021-04-21 16:15:01 +02:00
Johannes Zellner
61e51c7875 Send new login location notification mail 2021-04-21 16:14:49 +02:00
Girish Ramakrishnan
8b99af952a turn: turn off verbose logging 2021-04-20 11:30:31 -07:00
Johannes Zellner
d74f2b8506 Stop using deprecated developer/login route in tests 2021-04-20 17:52:53 +02:00
Girish Ramakrishnan
727e6720e8 schema.sql: fix appPasswords constraint 2021-04-19 21:02:14 -07:00
Girish Ramakrishnan
142af8e700 Fix notifications schema 2021-04-19 21:00:31 -07:00
Girish Ramakrishnan
0c8e0c4715 notifications: send backup fail only to owner
only superadmin has access to server and can adjust backup config
2021-04-19 20:57:10 -07:00
Girish Ramakrishnan
613da5fff9 notifications: remove user add/edit/update notifications
these just clutter the real notifications. these are in the eventlog
anyways.
2021-04-19 20:44:35 -07:00
Girish Ramakrishnan
355de5b0a4 notifications: fix update notification
the notification wasn't working because this was in apptask and the apptask died
before it could send out the email. we now move the notification to box process
and also remove the email notification.
2021-04-19 15:14:04 -07:00
Girish Ramakrishnan
3ab0a25ec9 Update npm packages 2021-04-17 23:12:33 -07:00
Girish Ramakrishnan
482169c805 Remove superfluous brackets 2021-04-17 22:23:15 -07:00
Girish Ramakrishnan
bba9b7e24e add weblate badge 2021-04-17 22:21:56 -07:00
Girish Ramakrishnan
7a7223a261 OCSP: do not set must-staple in certificate request
On first visit in firefox, must-staple certs (unlike chrome which ignores must-staple) always fail.
Investigating, it turns out, nginx does not fetch OCSP responses on reload or restart - https://trac.nginx.org/nginx/ticket/812 .
So, one has to prime the OCSP cache using curl requests. Alternately, one can use `openssl ocsp -noverify -no_nonce` and
then set `ssl_stapling_file`. Both approaches won't work if the OCSP servers are down and then we have to have some retry logic.
Also, the cache is per nginx worker, so I have no clue how many times one has to call curl. The `ssl_stapling_file` approach
requires some refresh logic as well. All very messy.

For the moment, do not set must-staple in the cert. Instead, check if the cert has a CSP URL and then enable
stapling in nginx accordingly.
2021-04-16 13:33:32 -07:00
Girish Ramakrishnan
4d919127a7 implement OCSP stapling
can verify stapling using openssl s_client -connect hostname:443 -status

status_request is RFC6066. there is also status_request_v2 (RFC6961) but this is
not implemented even in openssl libs yet
2021-04-16 12:13:54 -07:00
Girish Ramakrishnan
5d2fd81c0d Add missing callback() 2021-04-15 16:33:21 -07:00
Girish Ramakrishnan
ef476f74bf notifications: no email for app up/down/oom events
emails will not be used for self monitoring events. these are best done
from the outside. we just log everything in eventlog and raise notifications
as well.
2021-04-15 15:29:25 -07:00
Girish Ramakrishnan
d29d46d812 mail: add active flag to mailboxes and lists 2021-04-15 11:49:19 -07:00
Girish Ramakrishnan
00856b79dd firewall: Set BOX_ENV 2021-04-14 23:01:08 -07:00
Girish Ramakrishnan
c3e14cd11f user: return 2fa status for the UI 2021-04-14 21:46:35 -07:00
Girish Ramakrishnan
5833d6ed5d Fix failing dns and network test 2021-04-14 21:43:51 -07:00
Girish Ramakrishnan
f15714182b users: add route to disable 2fa 2021-04-14 20:45:35 -07:00
Girish Ramakrishnan
6d214cf0f2 2fa: fix routes to not have a slash
otherwise, it feels like it is some sort of resource
2021-04-14 19:59:46 -07:00
Girish Ramakrishnan
f9a72b530c Fix coding style 2021-04-14 15:54:09 -07:00
Girish Ramakrishnan
e983b0d385 more changes 2021-04-14 15:54:01 -07:00
Girish Ramakrishnan
0712eb1250 namecheap: fix del 2021-04-13 22:27:38 -07:00
Girish Ramakrishnan
564409d8b7 namecheap: Send it as POST 2021-04-13 22:17:01 -07:00
Girish Ramakrishnan
1c9c8e8e2b namecheap: refactor 2021-04-13 15:10:24 -07:00
Girish Ramakrishnan
04398c9b16 appstore: on dashboard domain change, update cloudron label 2021-04-13 14:19:45 -07:00
Girish Ramakrishnan
9a9c406fbe appstore: remove track begin/end
we used these to track error rates which we don't need anymore since
it's quite reliable
2021-04-13 14:10:30 -07:00
Johannes Zellner
8757e5ba42 print dashboard domain on --owner-login 2021-04-13 15:49:42 +02:00
Girish Ramakrishnan
131711ef5c mysql: bump connection limit to 200 2021-04-09 10:55:31 -07:00
Johannes Zellner
5ae5566ce8 Fix blocklist setting when source and list have mixed ip versions 2021-04-07 17:31:04 +02:00
Johannes Zellner
114a5ee2b1 Ensure we have a valid but unused iptables blocklist for testing 2021-04-07 17:30:19 +02:00
Johannes Zellner
c2c8e92d24 Allow to skip docker container cleanup when running tests 2021-04-07 16:46:12 +02:00
Girish Ramakrishnan
6d044bfbf3 mysql: Fix "mbind: Operation not permitted" warning"
https://github.com/docker-library/mysql/issues/303#issuecomment-643154859
2021-04-05 15:28:46 -07:00
Girish Ramakrishnan
d161fe9ebd add progress message for restoring addons 2021-04-05 11:35:47 -07:00
Girish Ramakrishnan
919f510796 linode object storage: update aws sdk
https://github.com/aws/aws-sdk-js/pull/3674
2021-04-02 11:54:22 -07:00
Girish Ramakrishnan
e613452058 mysql: remove use of mysql_upgrade 2021-04-01 11:50:03 -07:00
Johannes Zellner
5ccb1d44fe Send translation keys instead of raw english string for backup checks 2021-04-01 16:35:50 +02:00
Girish Ramakrishnan
84dfd4aa84 firewall: no need to keep 25 always open 2021-03-30 15:56:01 -07:00
Girish Ramakrishnan
726c028360 clone: copy services config 2021-03-30 12:45:28 -07:00
Girish Ramakrishnan
f211de1ff4 apphealthmonitor: 403 is ok 2021-03-30 11:57:30 -07:00
Girish Ramakrishnan
c1ee3dcbd4 collectd: cache du values and send it every Interval (20)
collectd plugin ordering matters. the write_graphite plugin establishes
a TCP connection but there is a race between that and the df/du values that
get reported. du is especially problematic since we report this only every 12 hours.

so, instead we cache the values and report it every 20 seconds. on the carbon side,
it will just retain every 12 hours (since that is the whisper retention period).

there is also FlushInterval which I am not 100% sure has any effect. by default, the
write_graphite plugin waits for 1428 bytes to be accumulated. (https://manpages.debian.org/unstable/collectd-core/collectd.conf.5.en.html)

https://github.com/collectd/collectd/issues/2672
https://github.com/collectd/collectd/pull/1044

I found this syntax hidden deep inside https://www.cisco.com/c/en/us/td/docs/net_mgmt/virtual_topology_system/2_6_3/user_guide/Cisco_VTS_2_6_3_User_Guide/Cisco_VTS_2_6_1_User_Guide_chapter_01111.pdf
2021-03-26 00:21:38 -07:00
Johannes Zellner
0402dce1ee Invite token should be valid for 7 days 2021-03-25 17:25:56 +01:00
Girish Ramakrishnan
c1b61bc56b add note 2021-03-24 20:30:02 -07:00
Girish Ramakrishnan
2d771d7c44 6.2.7 changes 2021-03-24 19:37:18 -07:00
Girish Ramakrishnan
d277f8137b redis: backup before upgrade 2021-03-24 19:27:24 -07:00
Girish Ramakrishnan
7ae79fe3a5 graphite: restart collectd on upgrade 2021-03-24 14:10:31 -07:00
Girish Ramakrishnan
407dda5c25 Add 6.2.6 changes
(cherry picked from commit 6cc07cd005)
2021-03-24 10:37:22 -07:00
Girish Ramakrishnan
1f59974e83 give graphite more time to start before restarting collectd 2021-03-24 10:26:19 -07:00
Girish Ramakrishnan
8e8e90b390 Add changes for 6.2.5 2021-03-24 09:45:58 -07:00
Girish Ramakrishnan
0447dce0d6 graphite: restart collectd as well 2021-03-23 16:34:36 -07:00
Girish Ramakrishnan
32f385741a graphite: implement upgrade
for the moment, we wipe out the old data and start afresh. this is because
the graphite web app keeps changing quite drastically.
2021-03-23 16:34:32 -07:00
Girish Ramakrishnan
91a4ae90f2 better logs 2021-03-23 13:06:37 -07:00
Girish Ramakrishnan
3201c5bda3 remove CLOUDRON_MAIL_SMTP_SERVER_HOST from sendmail
let's keep it in email addon because that will trigger reconfigure of apps
on server name change
2021-03-23 10:40:47 -07:00
Girish Ramakrishnan
c6920bd860 HSTS: bump the max-age to 2 years
Side note: https://hstspreload.org/ . This is what the chromium project expects
for preloading.
2021-03-22 19:04:28 -07:00
Girish Ramakrishnan
66ff2a9eb7 Revert "make box code send emails with STARTTLS"
This reverts commit ca496df535.

2525 has no TLS anymore
2021-03-22 14:34:07 -07:00
Girish Ramakrishnan
c3d30a1d99 mail: rework STARTTLS strategy
instead of fixing all apps which is a royal pain, we instead make Haraka
offer STARTTLS for 2587 and no STARTTLS for 2525.
2021-03-21 20:38:05 -07:00
Girish Ramakrishnan
7df89e66c8 request has no retry method
i thought it was using superagent
2021-03-20 11:19:45 -07:00
Girish Ramakrishnan
4954b94d4a acme2: add a retry to getDirectory, since users are reporting a 429 2021-03-19 09:59:09 -07:00
Girish Ramakrishnan
f3d9b81942 check for autofs mounts
autofs mounts are "mounts on demand". this way, instead of mounting
lots of things on startup, you can mount it on first access.
2021-03-19 09:59:09 -07:00
Girish Ramakrishnan
93510654a5 nfs: also check for nfs4 mount type
it seems in some version of ubuntu you mount with "-t nfs4".
this still doesn't handle autofs yet.

https://help.ubuntu.com/community/NFSv4Howto
2021-03-19 09:54:09 -07:00
Girish Ramakrishnan
39a0b9c351 typo 2021-03-18 21:35:18 -07:00
Girish Ramakrishnan
8048e68eb6 graphite: disable tagdb 2021-03-18 18:03:45 -07:00
Girish Ramakrishnan
60bdc34ad0 typo 2021-03-18 12:59:51 -07:00
Girish Ramakrishnan
2ff1f70eb8 Add to changes 2021-03-17 14:22:50 -07:00
Girish Ramakrishnan
67d9b50a16 Fix tests 2021-03-17 12:14:36 -07:00
Girish Ramakrishnan
f7bd47888a Fix issue where df output is not parsed correctly
LANG is the default locale i.e when LC_* are not specificall
LC_ALL will override them all

https://forum.cloudron.io/topic/4681/going-to-system-info-triggers-assertion-error
2021-03-17 11:14:07 -07:00
Girish Ramakrishnan
9960729d6b Add optional mailbox support 2021-03-16 22:40:38 -07:00
Girish Ramakrishnan
4cba5ca405 sftp: only rebuild when app task queue is empty
when multiple apptasks are scheduled, we end up with a sequence like this:
    - task1 finishes
    - task2 (uninstall) removes  appdata directory
    - sftp rebuild (from task1 finish)
    - task2 fails because sftp rebuild created empty appdata directory

a fix is to delay the sftp rebuild until all tasks are done. of course,
the same race is still there, if a user initiated another task immediately
but this seems unlikely. if that happens often, we can further add a sftpRebuildInProgress
flag inside apptaskmanager.
2021-03-16 18:29:01 -07:00
Girish Ramakrishnan
098da7426c Add CLOUDRON_MAIL_SMTP_STARTTLS env
starting 6.3, the internal mail server will do STARTTLS. this env
allows apps to configure themselves appropriately for pre 6.3 and
post 6.3 appropriately.

we trigger a re-configure which ensures that the new env gets put
in the database and then in the container.
2021-03-16 16:20:08 -07:00
Girish Ramakrishnan
a3ee79ccbd More 6.3 changes 2021-03-16 16:07:37 -07:00
Girish Ramakrishnan
176388111c tokens: add lastUsedTime 2021-03-16 16:04:17 -07:00
Girish Ramakrishnan
750f313c6a update: set memory limit properly 2021-03-15 19:25:16 -07:00
Girish Ramakrishnan
ca496df535 make box code send emails with STARTTLS 2021-03-14 12:19:37 -07:00
Girish Ramakrishnan
79d37cf361 update redis 2021-03-12 14:29:57 -08:00
Girish Ramakrishnan
8cc9fe5504 addons: better error handling 2021-03-12 14:17:19 -08:00
Girish Ramakrishnan
ec5966b2f5 6.3.0 changes 2021-03-12 10:54:39 -08:00
Girish Ramakrishnan
825835b3d1 mail: allow TLS from internal hosts
We need to only provide a cert that matches the MX record

https://serverfault.com/questions/389413/what-host-name-should-the-ssl-certificate-for-an-smtp-server-contain
2021-03-12 10:44:42 -08:00
Girish Ramakrishnan
1e96606110 error.code is a number which causes crash at times in BoxError 2021-03-12 10:10:49 -08:00
Girish Ramakrishnan
3ee3786936 6.2.4 changes 2021-03-11 19:00:34 -08:00
Girish Ramakrishnan
c4d60bde83 another export crash fix
we export using the old addon containers, which has a bug that it crashes
when db is missing. so, we have to skip them already. the crash then causes
future exports to also fail because it is restarting.
2021-03-11 18:55:37 -08:00
Girish Ramakrishnan
4aae663b2e typo 2021-03-10 15:32:46 -08:00
Girish Ramakrishnan
da00bce4b7 6.2.3 changes 2021-03-10 15:11:03 -08:00
Girish Ramakrishnan
0067766284 Fix addon crashes with missing databases
this happens because we have some bug in sftp container causing uninstall(s) to
fail. the database of those apps are gone but the export logic then tries to export
them and it all fails.
2021-03-10 15:09:15 -08:00
Girish Ramakrishnan
bb0b5550e0 Update mail container for LMTP cert fix 2021-03-10 09:50:09 -08:00
Girish Ramakrishnan
1db1f3faf4 Make it 30MB for good measure 2021-03-09 19:41:36 -08:00
Girish Ramakrishnan
9650a55c85 bump request timeouts 2021-03-09 14:45:22 -08:00
Girish Ramakrishnan
9451bcd38b services: start mail first to reduce downtime 2021-03-05 19:31:38 -08:00
Girish Ramakrishnan
aa7dbdd1fa Add 6.2.2 changes 2021-03-05 16:13:34 -08:00
Girish Ramakrishnan
ac18fb47b4 Fix ENOBUFS with large number of executable files 2021-03-05 15:09:56 -08:00
Girish Ramakrishnan
91a229305d missing backups: check if the s3 end point is valid
s3 api never return NotFound or ENOENT - https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html

Sadly, DO/OVH etc just return NotFound instead of NoSuchKey. And we cannot
distinguish easily if we are talking to some s3 server or some random server.
This is applicable for things like say minio where maybe there is something
apache now just giving out 404 / NotFound.
2021-03-05 01:24:16 -08:00
Girish Ramakrishnan
70b0da9e38 ovh: revert incorrect URL migration
https://forum.cloudron.io/topic/4584/issue-with-backups-listings-and-saving-backup-config-in-6-2
2021-03-05 00:15:17 -08:00
Girish Ramakrishnan
4275114d28 s3: remove retry options for exists check 2021-03-04 23:40:23 -08:00
Girish Ramakrishnan
83872a0a1d installer: is_update is not set correctly 2021-03-04 23:14:00 -08:00
Girish Ramakrishnan
4d4aad084c remove hard dep on systemd-resolved
the start.sh script does a "systemctl restart systemd-resolved". this
ends up restarting the box code prematurely! and then later when mysql
restarts, the box code loses connection and bad things happen (tm)
especially during a platform update.

we don't log to journald anymore, so not sure if EPIPE is still an issue
2021-03-04 21:07:52 -08:00
Girish Ramakrishnan
8994a12117 6.2.1 changes 2021-03-04 15:53:40 -08:00
Girish Ramakrishnan
28b6a340f0 restore: skip dns setup 2021-03-04 15:50:02 -08:00
Girish Ramakrishnan
1724607433 apphealth: clamp health time to first run
the platform.start can take forever. this means that we start the
clock to include platform.start and this sends a lot of spurious
up/down notifications.

also, bump the down threshold to 20 mins.
2021-03-04 15:03:08 -08:00
Girish Ramakrishnan
39864fbbb9 use the curl that retries 2021-03-04 12:09:23 -08:00
Girish Ramakrishnan
94dcec9df1 while...do 2021-03-04 12:09:23 -08:00
Girish Ramakrishnan
10ca889de0 apphealthmonitor: better debugs 2021-03-04 11:42:43 -08:00
Girish Ramakrishnan
cfcc210f9c try pulling images in a loop 2021-03-03 21:54:08 -08:00
Girish Ramakrishnan
38e5d2286e typo 2021-03-03 14:34:55 -08:00
Girish Ramakrishnan
149e176cfd better logs 2021-03-03 13:49:22 -08:00
Girish Ramakrishnan
3a19ab6866 better error message when update-info.json is old 2021-03-03 10:21:52 -08:00
Girish Ramakrishnan
aa71a734b9 Fix issue where mysql was restarting after new box code has started up
not 100% sure because of missing log timestamps, but mysql restarts after the box
has started up. As seen from logs below, we try to mark the apps for restart on
platform update. But this failed because mysql was restarting at that time.
This ended up with e2e test failing.

box:apps restartAppsUsingAddons: marking nc4801.autoupdatetest.domain.io for restart
box:apps restartAppsUsingAddons: error marking nc4801.autoupdatetest.domain.io for restart: {"name":"BoxError","reason":"Database Error","details":{"fatal":true,"code":"PROTOCOL_CONNECTION_LOST"},"message":"Connection lost: The server closed the connection.","nestedError":{"fatal":true,"code":"PROTOCOL_CONNECTION_LOST"}}
box:apps restartAppsUsingAddons: marking wekan1398.autoupdatetest.domain.io for restart
box:database Connection 51 error: Connection lost: The server closed the connection. PROTOCOL_CONNECTION_LOST
box:database Connection 52 error: Connection lost: The server closed the connection. PROTOCOL_CONNECTION_LOST
Box GET /api/v1/cloudron/status 500 Internal Server Error connect ECONNREFUSED 127.0.0.1:3306 41.251 ms - 217
2021-03-02 23:27:31 -08:00
Girish Ramakrishnan
d81ee7d99a timestamp the setup and installer logs
at some point, mysql disconnects the box code and it becomes hard to
debug without the timestamps
2021-03-02 23:06:37 -08:00
Girish Ramakrishnan
2946657889 stopAllTasks: the box dir might disappear
during update, we stop the box code which ends up trying to stop all tasks.
this gives warning like below:

box:shell stopTask (stdout): shell-init: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
box:shell stopTask (stdout): job-working-directory: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
box:shell stopTask (stdout): box-task-8.service loaded active running /home/yellowtent/box/src/scripts/../taskworker.js 8 /home/yellowtent/platformdata/logs/tasks/8.log
box:shell stopTask (stdout): job-working-directory: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
box:shell stopTask (stdout): job-working-directory: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
box:shell stopTask (stdout): job-working-directory: error retrieving current directory: getcwd: cannot access parent directories: No such file or directory
2021-03-02 22:26:43 -08:00
Girish Ramakrishnan
fc6f91157d Fix progress indicator 2021-03-02 21:25:23 -08:00
Girish Ramakrishnan
315d721174 Fix accumulation logic 2021-03-02 21:23:20 -08:00
Girish Ramakrishnan
ed7f2e7bb5 more changes 2021-03-02 19:11:56 -08:00
Girish Ramakrishnan
53cb9b1f7a fix registry config setter
* default registry provider is noop
* when testing config, skip noop provider
2021-03-02 18:34:06 -08:00
Girish Ramakrishnan
cccdf68cec backups: preserve symlinks in rsync mode 2021-03-02 18:11:59 -08:00
Girish Ramakrishnan
f04654022a add to changes 2021-03-02 13:01:49 -08:00
Girish Ramakrishnan
2b92310d24 call exitHandler to remove motd before reboot 2021-03-02 13:01:19 -08:00
Girish Ramakrishnan
c21155f07b Add to changes 2021-03-02 08:15:27 -08:00
Girish Ramakrishnan
baded52c96 return BoxError and not Error 2021-03-01 11:31:22 -08:00
Girish Ramakrishnan
476f348693 restore: resolve any boxdata directory symilnk before downloading
the tar-fs module cannot handle symlinks and must be given a resolved directory
since it uses lstat()
2021-03-01 11:02:43 -08:00
Girish Ramakrishnan
dd58c174a8 change default referrer policy to same-origin
https://forum.cloudron.io/topic/4546/referrer-policy-header-is-overwritten
2021-03-01 09:34:23 -08:00
Girish Ramakrishnan
376e070b72 update mail container
new solr and higher concurrency
2021-02-28 18:45:43 -08:00
Girish Ramakrishnan
f0e0372127 Update addons (move code to /app/code convention) 2021-02-28 15:52:06 -08:00
Girish Ramakrishnan
5e2c655ccb update mongodb
fixes #767
2021-02-28 12:49:44 -08:00
Girish Ramakrishnan
4a158c559e Fix typo: overwrite -> overwriteDns 2021-02-26 11:43:00 -08:00
Girish Ramakrishnan
03a59cd500 mysql: disable binlogs altogether
this is useful primarily for replication

http://dimitrik.free.fr/blog/archives/2018/04/mysql-performance-testing-80-with-less-blood.html
2021-02-26 09:53:37 -08:00
Girish Ramakrishnan
b71ab187ff mysql: update binlog in addon 2021-02-25 19:10:28 -08:00
Girish Ramakrishnan
bbed7c1d8a stack scripts: add hint that cloudron is installing
with linode, user has no clue that cloudron is installing when they SSH in.
2021-02-25 13:36:57 -08:00
Girish Ramakrishnan
c496d994c0 remove unused createAMI and digitalocean.sh 2021-02-25 10:33:41 -08:00
Girish Ramakrishnan
7a6a170451 remove retire.sh 2021-02-25 10:32:53 -08:00
Girish Ramakrishnan
5a6b261ba2 add to changes 2021-02-24 22:38:40 -08:00
Girish Ramakrishnan
70fbcf8ce4 add route to sync dns records
merge the mail dns route with this one as well

fixes #737
2021-02-24 22:37:59 -08:00
Girish Ramakrishnan
93712c0f03 emit progress message in register/unregister locations 2021-02-24 18:32:28 -08:00
Girish Ramakrishnan
e78abe2fab move register* to domains 2021-02-24 17:54:19 -08:00
Girish Ramakrishnan
e190076f1a apptask: skip waiting for dns propagation
part of #737
2021-02-24 16:57:51 -08:00
Girish Ramakrishnan
4a85207dba remove debug 2021-02-24 16:39:41 -08:00
Girish Ramakrishnan
b0e80de9ec add missing arg 2021-02-24 16:36:13 -08:00
Girish Ramakrishnan
a546914796 mysql: keep binlog to couple of days 2021-02-24 16:00:46 -08:00
Girish Ramakrishnan
3af6012779 typo 2021-02-24 15:03:49 -08:00
Girish Ramakrishnan
5b51f73be4 restore: add skipDnsSetup flag
part of #737
2021-02-24 14:56:09 -08:00
Girish Ramakrishnan
d74537868a apps: add skipDnsSetup to install/restore/clone routes
these are not used in the UI but added for completeness

part of #737
2021-02-24 14:51:18 -08:00
Girish Ramakrishnan
2056ede942 apptask: add skipDnsSetup flag to skip dns setup
Part of #737
2021-02-24 14:47:05 -08:00
Girish Ramakrishnan
f2d366c35d dkim: use a hash for the selector instead of domain name directory
we use a hash instead of random so that it is the same (unless admin domain changed)
within the same server. hash also ensures one cannot reverse it.

fixes #770
2021-02-24 11:41:58 -08:00
Girish Ramakrishnan
0bb2da8a04 better error message 2021-02-24 09:53:57 -08:00
Girish Ramakrishnan
38607048ee mysql: make binlog have 5 day expiry 2021-02-24 09:19:26 -08:00
Girish Ramakrishnan
9c413ffe3d do not overwrite existing dmarc
fixes #769
2021-02-24 09:08:56 -08:00
Girish Ramakrishnan
14e1cb5ad6 Update packages 2021-02-24 09:08:22 -08:00
Girish Ramakrishnan
aaf93cb772 proxyAuth: check for basicAuth flag to permit basic auth
fixes #765
2021-02-23 21:54:49 -08:00
Girish Ramakrishnan
8f08c52103 not required anymore to uninstall gnome-shell 2021-02-23 18:57:15 -08:00
Girish Ramakrishnan
9ccd82ce4e set binlog config in mysql
keep max binlog file size to 100M. and rotate then in 10 days
2021-02-23 14:24:58 -08:00
Girish Ramakrishnan
013669e872 Update mail container
this disables TLSv1 and 1.1 in dovecot
2021-02-22 14:16:55 -08:00
Girish Ramakrishnan
9ebdeca3ad add another changelog 2021-02-22 11:50:47 -08:00
Johannes Zellner
8823487bc1 Rebuild lock file with npm version 6.14.10 2021-02-22 10:43:52 +01:00
Girish Ramakrishnan
c4dffa393b backups: remove entries from database that don't exist in storage
fixes #772
2021-02-19 11:34:22 -08:00
Girish Ramakrishnan
a5c4b5d8a1 tls addon: restart apps on cert change 2021-02-18 09:44:13 -08:00
Girish Ramakrishnan
2f58092af2 Fix .well-known not served up properly for redirection 2021-02-18 09:30:39 -08:00
Johannes Zellner
1f7877e0e5 Do not specify random node engines in package.json 2021-02-18 11:07:49 +01:00
Girish Ramakrishnan
a304c7f4a5 implement tls addon 2021-02-17 23:20:08 -08:00
Girish Ramakrishnan
601fc9a202 it is uuid.v4() now 2021-02-17 23:18:36 -08:00
Girish Ramakrishnan
32e00bdf47 cloudron-support: print the admin fqdn 2021-02-17 20:29:56 -08:00
Girish Ramakrishnan
83fa83a709 cloudron-support: typo 2021-02-17 20:04:43 -08:00
Girish Ramakrishnan
895ccdb549 allow port 853 for DoT 2021-02-17 13:11:00 -08:00
Girish Ramakrishnan
fd8741be16 add to changes 2021-02-17 09:24:50 -08:00
Johannes Zellner
3206afcd7c Do not remove accessRestriction from install app listing 2021-02-17 14:43:25 +01:00
Girish Ramakrishnan
ab2d246945 Update graphite to base image 2021-02-16 16:56:33 -08:00
Girish Ramakrishnan
41ec22e8c3 clear timeout when getting service status 2021-02-16 11:13:41 -08:00
Johannes Zellner
af54142997 Add ldap debug for unhandled routes 2021-02-16 17:20:41 +01:00
Girish Ramakrishnan
c8c4f99849 Reduce gzip_min_length to keep tools like semrush happy 2021-02-15 11:46:36 -08:00
Girish Ramakrishnan
48c52533c4 firewall: syntax cleanup 2021-02-12 08:13:47 -08:00
Johannes Zellner
1a98d6d2bd iptables --dports only supports up to 15 ports apparently 2021-02-12 15:56:19 +01:00
Girish Ramakrishnan
615198cd36 mail: use latest base image 2021-02-11 15:35:04 -08:00
Girish Ramakrishnan
664b3ab958 sftp: multiparty fix for node 14 2021-02-09 23:35:32 -08:00
Girish Ramakrishnan
dac677df06 sftp: force rebuild when infra changes 2021-02-09 22:57:21 -08:00
Girish Ramakrishnan
fd2087d7e4 Fix mysql auth issue
only PHP 7.4 supports the caching_sha2_password mechanism. so we
make the default as mysql_native_password
2021-02-09 17:31:45 -08:00
Girish Ramakrishnan
d5087ff0c2 registry config: add provider 2021-02-09 14:33:20 -08:00
Girish Ramakrishnan
1d0ad3cb47 proxyAuth: Fix docker UA detection 2021-02-09 13:45:00 -08:00
Girish Ramakrishnan
30c3acaed9 change debug string 2021-02-08 23:20:45 -08:00
Girish Ramakrishnan
afd938abdf update more modules 2021-02-08 23:14:32 -08:00
Girish Ramakrishnan
38ca8926af createReleaseTarball: bump node version 2021-02-06 22:00:13 -08:00
Girish Ramakrishnan
283f1aac21 Update base image because of mongodb issue 2021-02-06 21:57:37 -08:00
Girish Ramakrishnan
8ba1f3914c Update postgresql for latest base image 2021-02-06 11:14:23 -08:00
Girish Ramakrishnan
a262b08887 Update redis for latest base image 2021-02-06 10:26:54 -08:00
Girish Ramakrishnan
925408ffcd Update turn image to use latest base image 2021-02-06 10:20:31 -08:00
Girish Ramakrishnan
04d4375297 Update sftp image to use latest base image 2021-02-06 10:10:03 -08:00
Girish Ramakrishnan
691b15363a base image: fix yq typo 2021-02-05 21:15:07 -08:00
Girish Ramakrishnan
caadb1d418 new base image 3.0 2021-02-05 20:25:17 -08:00
Girish Ramakrishnan
382ae7424d async 3: the whilst and doWhilst test funcs are async 2021-02-04 16:39:47 -08:00
Girish Ramakrishnan
6073d2ba7e Use new base image 3.0.0 2021-02-04 16:22:23 -08:00
Girish Ramakrishnan
6ecbd4a0fd update packages 2021-02-04 11:01:32 -08:00
Girish Ramakrishnan
92c43e58c7 update docker to 20.10.3 2021-02-04 11:01:30 -08:00
Girish Ramakrishnan
dc91abb800 update node to 14.15.4 2021-02-04 11:01:08 -08:00
Girish Ramakrishnan
e19ab45e81 ovh: add url migration from s3. to storage. 2021-02-04 10:21:54 -08:00
Girish Ramakrishnan
72daaa9ff0 ionos: add profitbricks object storage 2021-02-04 10:14:35 -08:00
Girish Ramakrishnan
8106fa3b7d Add to changes 2021-02-03 16:34:14 -08:00
Girish Ramakrishnan
282040ed1b gcs: use delete concurrency 2021-02-01 14:23:15 -08:00
Girish Ramakrishnan
bcd04715c0 updater: set the backup memory limit 2021-02-01 14:07:23 -08:00
Johannes Zellner
14b2fa55c3 Update sftp 3.1.0 addon image 2021-02-01 19:20:58 +01:00
Johannes Zellner
04e103a32d Do not bump infra version 2021-02-01 19:06:13 +01:00
Johannes Zellner
0b0c02e421 Update sftp image for copy function 2021-02-01 16:13:46 +01:00
Girish Ramakrishnan
196a5cfb42 Add missing require 2021-01-31 20:47:33 -08:00
Girish Ramakrishnan
fc408b8288 Fix app auto-update breakage 2021-01-31 20:46:55 -08:00
Girish Ramakrishnan
e2c342f242 apptaskmanager: Fix crash 2021-01-30 21:16:41 -08:00
Girish Ramakrishnan
19fcabd32b mail: data.headers is now headers 2021-01-29 00:02:03 -08:00
Girish Ramakrishnan
a842d77b6d Fix SOGo login
listAllMailboxes query was mangled
2021-01-28 22:21:44 -08:00
Girish Ramakrishnan
ef68cb70c0 email autoconfig 2021-01-28 16:58:37 -08:00
Girish Ramakrishnan
adfb506af4 Fix disk usage graphs 2021-01-27 21:48:06 -08:00
Girish Ramakrishnan
1d188297f9 6.1.1 changes 2021-01-27 13:10:40 -08:00
Girish Ramakrishnan
141a32315f ignore any applyServiceConfig failures when starting services 2021-01-27 11:33:27 -08:00
230 changed files with 9897 additions and 10354 deletions

5
.jshintrc Normal file
View File

@@ -0,0 +1,5 @@
{
"node": true,
"unused": true,
"esversion": 8
}

99
CHANGES
View File

@@ -2183,3 +2183,102 @@
* netcup: add dns provider
* Container swap size is now dynamically determined based on system RAM/swap ratio
[6.1.1]
* Fix bug where platform does not start if memory limits could not be applied
[6.1.2]
* App disk usage was not shown in graphs
* Email autoconfig
* Fix SOGo login
[6.2.0]
* ovh: object storage URL has changed from s3 to storage subdomain
* ionos: add profit bricks object storage
* update node to 14.15.4
* update docker to 20.10.3
* new base image 3.0.0
* postgresql updated to 12.5
* redis updated to 5.0.7
* dovecot updated to 2.3.7
* proxyAuth: fix docker UA detection
* registry config: add UI to disable it
* update solr to 8.8.1
* firewall: fix issue where script errored when having more than 15 wl/bl ports
* If groups are used, do not allow app installation without choosing the access settings
* tls addon
* Do not overwrite existing DMARC record
* Sync dns records
* Dry run restore
* linode: show cloudron is installing when user SSHs
* mysql: disable bin logs
* Show cancel task button if task is still running after 2 minutes
* filemanager: fix various bugs involving file names with spaces
* Change Referrer-policy default to 'same-origin'
* rsync: preserve and restore symlinks
* Clean up backups function now removes missing backups
[6.2.1]
* Avoid updown notifications on full restore
* Add retries to downloader logic in installer
[6.2.2]
* Fix ENOBUFS issue with backups when collecting fs metadata
[6.2.3]
* Fix addon crashes with missing databases
* Update mail container for LMTP cert fix
* Fix services view showing yellow icon
[6.2.4]
* Another addon crash fix
[6.2.5]
* update: set memory limit properly
* Fix bug where renew certs button did not work
* sftp: fix rebuild condition
* Fix display of user management/dashboard visiblity for email apps
* graphite: disable tagdb and reduce log noise
[6.2.6]
* Fix issue where collectd is restarted too quickly before graphite
[6.2.7]
* redis: backup before upgrade
[6.2.8]
* linode object storage: update aws sdk to make it work again
* Fix crash in blocklist setting when source and list have mixed ip versions
* mysql: bump connection limit to 200
* namecheap: fix issue where DNS updates and del were not working
* turn: turn off verbose logging
* Fix crash when parsing df output (set LC_ALL for box service)
[6.3.0]
* mail: allow TLS from internal hosts
* tokens: add lastUsedTime
* update: set memory limit properly
* addons: better error handling
* filemanager: various enhancements
* sftp: fix rebuild condition
* app mailbox is now optional
* Fix display of user management/dashboard visiblity for email apps
* graphite: disable tagdb and reduce log noise
* hsts: change max-age to 2 years
* clone: copy over redis memory limit
* namecheap: fix bug where records were not removed
* add UI to disable 2FA of a user
* mail: add active flag to mailboxes and lists
* Implement OCSP stapling
* security: send new browser login location notification email
* backups: add fqdn to the backup filename
* import all boxdata settings into the database
* volumes: generate systemd mount configs based on type
* postgresql: set max conn limit per db
* ubuntu 16: add alert about EOL
* clone: save and restore app config
* app import: restore icon, tag, label, proxy configs etc
* sieve: fix redirects to not do SRS
* notifications are now system level instead of per-user
* vultr DNS
* vultr object storage
* mail: do not forward spam to mailing lists

View File

@@ -1,3 +1,5 @@
![Translation status](https://translate.cloudron.io/widgets/cloudron/-/svg-badge.svg)
# Cloudron
[Cloudron](https://cloudron.io) is the best way to run apps on your server.
@@ -70,8 +72,13 @@ Just to give some heads up, we are a bit restrictive in merging changes. We are
would like to keep our maintenance burden low. It might be best to discuss features first in the [forum](https://forum.cloudron.io),
to also figure out how many other people will use it to justify maintenance for a feature.
# Localization
![Translation status](https://translate.cloudron.io/widgets/cloudron/-/287x66-white.png)
## Support
* [Documentation](https://docs.cloudron.io/)
* [Forum](https://forum.cloudron.io/)

View File

@@ -1,193 +0,0 @@
#!/bin/bash
set -eu -o pipefail
assertNotEmpty() {
: "${!1:? "$1 is not set."}"
}
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
INSTANCE_TYPE="t2.micro"
BLOCK_DEVICE="DeviceName=/dev/sda1,Ebs={VolumeSize=20,DeleteOnTermination=true,VolumeType=gp2}"
SSH_KEY_NAME="id_rsa_yellowtent"
revision=$(git rev-parse HEAD)
ami_name=""
server_id=""
server_ip=""
destroy_server="yes"
deploy_env="prod"
image_id=""
args=$(getopt -o "" -l "revision:,name:,no-destroy,env:,region:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--env) deploy_env="$2"; shift 2;;
--revision) revision="$2"; shift 2;;
--name) ami_name="$2"; shift 2;;
--no-destroy) destroy_server="no"; shift 2;;
--region)
case "$2" in
"us-east-1")
image_id="ami-6edd3078"
security_group="sg-a5e17fd9"
subnet_id="subnet-b8fbc0f1"
;;
"eu-central-1")
image_id="ami-5aee2235"
security_group="sg-19f5a770" # everything open on eu-central-1
subnet_id=""
;;
*)
echo "Unknown aws region $2"
exit 1
;;
esac
export AWS_DEFAULT_REGION="$2" # used by the aws cli tool
shift 2
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
# TODO fix this
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY}"
export AWS_SECRET_ACCESS_KEY="${AWS_ACCESS_SECRET}"
readonly ssh_keys="${HOME}/.ssh/id_rsa_yellowtent"
readonly SSH="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
if [[ ! -f "${ssh_keys}" ]]; then
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
exit 1
fi
if [[ -z "${image_id}" ]]; then
echo "--region is required (us-east-1 or eu-central-1)"
exit 1
fi
function get_pretty_revision() {
local git_rev="$1"
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
echo "${sha1}"
}
function wait_for_ssh() {
echo "=> Waiting for ssh connection"
while true; do
echo -n "."
if $SSH ubuntu@${server_ip} echo "hello"; then
echo ""
break
fi
sleep 5
done
}
now=$(date "+%Y-%m-%d-%H%M%S")
pretty_revision=$(get_pretty_revision "${revision}")
if [[ -z "${ami_name}" ]]; then
ami_name="box-${deploy_env}-${pretty_revision}-${now}"
fi
echo "=> Create EC2 instance"
id=$(aws ec2 run-instances --image-id "${image_id}" --instance-type "${INSTANCE_TYPE}" --security-group-ids "${security_group}" --block-device-mappings "${BLOCK_DEVICE}" --key-name "${SSH_KEY_NAME}" --subnet-id "${subnet_id}" --associate-public-ip-address \
| $JSON Instances \
| $JSON 0.InstanceId)
[[ -z "$id" ]] && exit 1
echo "Instance created ID $id"
echo "=> Waiting for instance to get a public IP"
while true; do
server_ip=$(aws ec2 describe-instances --instance-ids ${id} \
| $JSON Reservations.0.Instances \
| $JSON 0.PublicIpAddress)
if [[ ! -z "${server_ip}" ]]; then
echo ""
break
fi
echo -n "."
sleep 1
done
echo "Got public IP ${server_ip}"
wait_for_ssh
echo "=> Fetching cloudron-setup"
while true; do
if $SSH ubuntu@${server_ip} wget "https://cloudron.io/cloudron-setup" -O "cloudron-setup"; then
echo ""
break
fi
echo -n "."
sleep 5
done
echo "=> Running cloudron-setup"
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ami" --skip-reboot
wait_for_ssh
echo "=> Removing ssh key"
$SSH ubuntu@${server_ip} sudo rm /home/ubuntu/.ssh/authorized_keys /root/.ssh/authorized_keys
echo "=> Creating AMI"
image_id=$(aws ec2 create-image --instance-id "${id}" --name "${ami_name}" | $JSON ImageId)
[[ -z "$id" ]] && exit 1
echo "Creating AMI with Id ${image_id}"
echo "=> Waiting for AMI to be created"
while true; do
state=$(aws ec2 describe-images --image-ids ${image_id} \
| $JSON Images \
| $JSON 0.State)
if [[ "${state}" == "available" ]]; then
echo ""
break
fi
echo -n "."
sleep 5
done
if [[ "${destroy_server}" == "yes" ]]; then
echo "=> Deleting EC2 instance"
while true; do
state=$(aws ec2 terminate-instances --instance-id "${id}" \
| $JSON TerminatingInstances \
| $JSON 0.CurrentState.Name)
if [[ "${state}" == "shutting-down" ]]; then
echo ""
break
fi
echo -n "."
sleep 5
done
fi
echo ""
echo "Done."
echo ""
echo "New AMI is: ${image_id}"
echo ""

View File

@@ -1,261 +0,0 @@
#!/bin/bash
if [[ -z "${DIGITAL_OCEAN_TOKEN}" ]]; then
echo "Script requires DIGITAL_OCEAN_TOKEN env to be set"
exit 1
fi
if [[ -z "${JSON}" ]]; then
echo "Script requires JSON env to be set to path of JSON binary"
exit 1
fi
readonly CURL="curl --retry 5 -s -u ${DIGITAL_OCEAN_TOKEN}:"
function debug() {
echo "$@" >&2
}
function get_ssh_key_id() {
id=$($CURL "https://api.digitalocean.com/v2/account/keys" \
| $JSON ssh_keys \
| $JSON -c "this.name === \"$1\"" \
| $JSON 0.id)
[[ -z "$id" ]] && exit 1
echo "$id"
}
function create_droplet() {
local ssh_key_id="$1"
local box_name="$2"
local image_region="sfo2"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="1gb"
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
id=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets" | $JSON droplet.id)
[[ -z "$id" ]] && exit 1
echo "$id"
}
function get_droplet_ip() {
local droplet_id="$1"
ip=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}" | $JSON "droplet.networks.v4[0].ip_address")
[[ -z "$ip" ]] && exit 1
echo "$ip"
}
function get_droplet_id() {
local droplet_name="$1"
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=200" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
[[ -z "$id" ]] && exit 1
echo "$id"
}
function power_off_droplet() {
local droplet_id="$1"
local data='{"type":"power_off"}'
local response=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions")
local event_id=`echo "${response}" | $JSON action.id`
if [[ -z "${event_id}" ]]; then
debug "Got no event id, assuming already powered off."
debug "Response: ${response}"
return
fi
debug "Powered off droplet. Event id: ${event_id}"
debug -n "Waiting for droplet to power off"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
}
function power_on_droplet() {
local droplet_id="$1"
local data='{"type":"power_on"}'
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
debug "Powered on droplet. Event id: ${event_id}"
if [[ -z "${event_id}" ]]; then
debug "Got no event id, assuming already powered on"
return
fi
debug -n "Waiting for droplet to power on"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
}
function get_image_id() {
local snapshot_name="$1"
local image_id=""
if ! response=$($CURL "https://api.digitalocean.com/v2/images?per_page=200"); then
echo "Failed to get image listing. ${response}"
return 1
fi
if ! image_id=$(echo "$response" \
| $JSON images \
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id); then
echo "Failed to parse curl response: ${response}"
return 1
fi
if [[ -z "${image_id}" ]]; then
echo "Failed to get image id of ${snapshot_name}. reponse: ${response}"
return 1
fi
echo "${image_id}"
}
function snapshot_droplet() {
local droplet_id="$1"
local snapshot_name="$2"
local data="{\"type\":\"snapshot\",\"name\":\"${snapshot_name}\"}"
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
debug "Droplet snapshotted as ${snapshot_name}. Event id: ${event_id}"
debug -n "Waiting for snapshot to complete"
while true; do
if ! response=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}"); then
echo "Could not get action status. ${response}"
continue
fi
if ! event_status=$(echo "${response}" | $JSON action.status); then
echo "Could not parse action.status from response. ${response}"
continue
fi
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug "! done"
if ! image_id=$(get_image_id "${snapshot_name}"); then
return 1
fi
echo "${image_id}"
}
function destroy_droplet() {
local droplet_id="$1"
# TODO: check for 204 status
$CURL -X DELETE "https://api.digitalocean.com/v2/droplets/${droplet_id}"
debug "Droplet destroyed"
debug ""
}
function transfer_image() {
local image_id="$1"
local region_slug="$2"
local data="{\"type\":\"transfer\",\"region\":\"${region_slug}\"}"
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/images/${image_id}/actions" | $JSON action.id`
echo "${event_id}"
}
function wait_for_image_event() {
local image_id="$1"
local event_id="$2"
debug -n "Waiting for ${event_id}"
while true; do
local event_status=`$CURL "https://api.digitalocean.com/v2/images/${image_id}/actions/${event_id}" | $JSON action.status`
if [[ "${event_status}" == "completed" ]]; then
break
fi
debug -n "."
sleep 10
done
debug ""
}
function transfer_image_to_all_regions() {
local image_id="$1"
xfer_events=()
image_regions=(ams2) ## sfo1 is where the image is created
for image_region in ${image_regions[@]}; do
xfer_event=$(transfer_image ${image_id} ${image_region})
echo "Image transfer to ${image_region} initiated. Event id: ${xfer_event}"
xfer_events+=("${xfer_event}")
sleep 1
done
echo "Image transfer initiated, but they will take some time to get transferred."
for xfer_event in ${xfer_events[@]}; do
$vps wait_for_image_event "${image_id}" "${xfer_event}"
done
}
if [[ $# -lt 1 ]]; then
debug "<command> <params...>"
exit 1
fi
case $1 in
get_ssh_key_id)
get_ssh_key_id "${@:2}"
;;
create)
create_droplet "${@:2}"
;;
get_id)
get_droplet_id "${@:2}"
;;
get_ip)
get_droplet_ip "${@:2}"
;;
power_on)
power_on_droplet "${@:2}"
;;
power_off)
power_off_droplet "${@:2}"
;;
snapshot)
snapshot_droplet "${@:2}"
;;
destroy)
destroy_droplet "${@:2}"
;;
transfer_image_to_all_regions)
transfer_image_to_all_regions "${@:2}"
;;
*)
echo "Unknown command $1"
exit 1
esac

View File

@@ -48,9 +48,11 @@ apt-get -y install --no-install-recommends \
linux-generic \
logrotate \
$mysql_package \
nfs-common \
openssh-server \
pwgen \
resolvconf \
sshfs \
swaks \
tzdata \
unattended-upgrades \
@@ -72,10 +74,11 @@ apt-get -o Dpkg::Options::="--force-confold" install -y --no-install-recommends
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
echo "==> Installing node.js"
mkdir -p /usr/local/node-10.18.1
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-10.18.1
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
readonly node_version=14.15.4
mkdir -p /usr/local/node-${node_version}
curl -sL https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-${node_version}
ln -sf /usr/local/node-${node_version}/bin/node /usr/bin/node
ln -sf /usr/local/node-${node_version}/bin/npm /usr/bin/npm
apt-get install -y --no-install-recommends python # Install python which is required for npm rebuild
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
@@ -87,9 +90,10 @@ mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
# there are 3 packages for docker - containerd, CLI and the daemon
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.13-2_amd64.deb" -o /tmp/containerd.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
readonly docker_version=20.10.3
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.4.3-1_amd64.deb" -o /tmp/containerd.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb

2
box.js
View File

@@ -15,7 +15,7 @@ const NOOP_CALLBACK = function () { };
function setupLogging(callback) {
if (process.env.BOX_ENV === 'test') return callback();
var logfileStream = fs.createWriteStream(paths.BOX_LOG_FILE, { flags:'a' });
const logfileStream = fs.createWriteStream(paths.BOX_LOG_FILE, { flags:'a' });
process.stdout.write = process.stderr.write = logfileStream.write.bind(logfileStream);
callback();

View File

@@ -0,0 +1,10 @@
'use strict';
exports.up = function(db, callback) {
/* this contained an invalid migration of OVH URLs from s3 subdomain to storage subdomain. See https://forum.cloudron.io/topic/4584/issue-with-backups-listings-and-saving-backup-config-in-6-2 */
callback();
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="registry_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var registryConfig = JSON.parse(results[0].value);
if (!registryConfig.provider) registryConfig.provider = 'other';
db.runSql('UPDATE settings SET value=? WHERE name="registry_config"', [ JSON.stringify(registryConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE tokens ADD COLUMN lastUsedTime TIMESTAMP NULL', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE tokens DROP COLUMN lastUsedTime', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

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

View File

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

View File

@@ -0,0 +1,37 @@
'use strict';
const async = require('async'),
fs = require('fs'),
path = require('path');
const AVATAR_DIR = '/home/yellowtent/boxdata/profileicons';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN avatar MEDIUMBLOB', function (error) {
if (error) return callback(error);
fs.readdir(AVATAR_DIR, function (error, filenames) {
if (error && error.code === 'ENOENT') return callback();
if (error) return callback(error);
async.eachSeries(filenames, function (filename, iteratorCallback) {
const avatar = fs.readFileSync(path.join(AVATAR_DIR, filename));
const userId = filename;
db.runSql('UPDATE users SET avatar=? WHERE id=?', [ avatar, userId ], iteratorCallback);
}, function (error) {
if (error) return callback(error);
fs.rmdir(AVATAR_DIR, { recursive: true }, callback);
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN avatar', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,20 @@
'use strict';
const fs = require('fs');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE settings ADD COLUMN valueBlob MEDIUMBLOB', function (error) {
if (error) return callback(error);
fs.readFile('/home/yellowtent/boxdata/avatar.png', { encoding: 'base64' }, function (error, avatar) {
if (error && error.code === 'ENOENT') return callback();
if (error) return callback(error);
db.runSql('INSERT INTO settings (name, valueBlob) VALUES (?, ?)', [ 'cloudron_avatar', avatar ], callback);
});
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN loginLocationsJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN loginLocationsJson', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,42 @@
'use strict';
const async = require('async'),
fs = require('fs'),
path = require('path');
const APPICONS_DIR = '/home/yellowtent/boxdata/appicons';
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN icon MEDIUMBLOB'),
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN appStoreIcon MEDIUMBLOB'),
function migrateIcons(next) {
fs.readdir(APPICONS_DIR, function (error, filenames) {
if (error && error.code === 'ENOENT') return next();
if (error) return next(error);
async.eachSeries(filenames, function (filename, iteratorCallback) {
const icon = fs.readFileSync(path.join(APPICONS_DIR, filename));
const appId = filename.split('.')[0];
if (filename.endsWith('.user.png')) {
db.runSql('UPDATE apps SET icon=? WHERE id=?', [ icon, appId ], iteratorCallback);
} else {
db.runSql('UPDATE apps SET appStoreIcon=? WHERE id=?', [ icon, appId ], iteratorCallback);
}
}, function (error) {
if (error) return next(error);
fs.rmdir(APPICONS_DIR, { recursive: true }, next);
});
});
}
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN icon'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN appStoreIcon'),
], callback);
};

View File

@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', [], function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,20 @@
'use strict';
exports.up = function(db, callback) {
const cmd = 'CREATE TABLE blobs(' +
'id VARCHAR(128) NOT NULL UNIQUE,' +
'value MEDIUMBLOB,' +
'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 blobs', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,49 @@
'use strict';
const async = require('async'),
fs = require('fs'),
safe = require('safetydance');
const BOX_DATA_DIR = '/home/yellowtent/boxdata';
const PLATFORM_DATA_DIR = '/home/yellowtent/platformdata';
exports.up = function (db, callback) {
let funcs = [];
const acmeKey = safe.fs.readFileSync(`${BOX_DATA_DIR}/acme/acme.key`);
if (acmeKey) {
funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'acme_account_key', acmeKey ]));
funcs.push(fs.rmdir.bind(fs, `${BOX_DATA_DIR}/acme`, { recursive: true }));
}
const dhparams = safe.fs.readFileSync(`${BOX_DATA_DIR}/dhparams.pem`);
if (dhparams) {
safe.fs.writeFileSync(`${PLATFORM_DATA_DIR}/dhparams.pem`, dhparams);
funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'dhparams', dhparams ]));
// leave the dhparms here for the moment because startup code regenerates box nginx config and reloads nginx. at that point,
// nginx config of apps has not been re-generated yet and the reload fails. post 6.3, this file can be removed in start.sh
// funcs.push(fs.unlink.bind(fs, `${BOX_DATA_DIR}/dhparams.pem`));
}
const turnSecret = safe.fs.readFileSync(`${BOX_DATA_DIR}/addon-turn-secret`);
if (turnSecret) {
funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'addon_turn_secret', turnSecret ]));
funcs.push(fs.unlink.bind(fs, `${BOX_DATA_DIR}/addon-turn-secret`));
}
// sftp keys get moved to platformdata in start.sh
const sftpPublicKey = safe.fs.readFileSync(`${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key.pub`);
const sftpPrivateKey = safe.fs.readFileSync(`${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key`);
if (sftpPublicKey) {
safe.fs.writeFileSync(`${PLATFORM_DATA_DIR}/sftp/ssh/ssh_host_rsa_key.pub`, sftpPublicKey);
safe.fs.writeFileSync(`${PLATFORM_DATA_DIR}/sftp/ssh/ssh_host_rsa_key`, sftpPrivateKey);
safe.fs.chmodSync(`${PLATFORM_DATA_DIR}/sftp/ssh/ssh_host_rsa_key`, 0o600);
funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'sftp_public_key', sftpPublicKey ]));
funcs.push(db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'sftp_private_key', sftpPrivateKey ]));
funcs.push(fs.rmdir.bind(fs, `${BOX_DATA_DIR}/sftp`, { recursive: true }));
}
async.series(funcs, callback);
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,31 @@
'use strict';
const async = require('async'),
fs = require('fs'),
safe = require('safetydance');
const BOX_DATA_DIR = '/home/yellowtent/boxdata';
const PLATFORM_DATA_DIR = '/home/yellowtent/platformdata';
exports.up = function (db, callback) {
if (!fs.existsSync(`${BOX_DATA_DIR}/firewall`)) return callback();
const ports = safe.fs.readFileSync(`${BOX_DATA_DIR}/firewall/ports.json`);
if (ports) {
safe.fs.writeFileSync(`${PLATFORM_DATA_DIR}/firewall/ports.json`, ports);
}
const blocklist = safe.fs.readFileSync(`${BOX_DATA_DIR}/firewall/blocklist.txt`);
async.series([
(next) => {
if (!blocklist) return next();
db.runSql('INSERT INTO settings (name, valueBlob) VALUES (?, ?)', [ 'firewall_blocklist', blocklist ], next);
},
fs.writeFile.bind(fs, `${PLATFORM_DATA_DIR}/firewall/blocklist.txt`, blocklist || ''),
fs.rmdir.bind(fs, `${BOX_DATA_DIR}/firewall`, { recursive: true })
], callback);
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,30 @@
'use strict';
const async = require('async'),
fs = require('fs');
const CERTS_DIR = '/home/yellowtent/boxdata/certs';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE domains ADD COLUMN fallbackCertificateJson MEDIUMTEXT', function (error) {
if (error) return callback(error);
db.all('SELECT * FROM domains', [ ], function (error, domains) {
if (error) return callback(error);
async.eachSeries(domains, function (domain, iteratorDone) {
const cert = fs.readFileSync(`${CERTS_DIR}/${domain.domain}.host.cert`, 'utf8');
const key = fs.readFileSync(`${CERTS_DIR}/${domain.domain}.host.key`, 'utf8');
const fallbackCertificate = { cert, key };
db.runSql('UPDATE domains SET fallbackCertificateJson=? WHERE domain=?', [ JSON.stringify(fallbackCertificate), domain.domain ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
async.series([
db.runSql.run(db, 'ALTER TABLE domains DROP COLUMN fallbackCertificateJson')
], callback);
};

View File

@@ -0,0 +1,34 @@
'use strict';
const async = require('async'),
fs = require('fs'),
safe = require('safetydance');
const CERTS_DIR = '/home/yellowtent/boxdata/certs';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE subdomains ADD COLUMN certificateJson MEDIUMTEXT', function (error) {
if (error) return callback(error);
db.all('SELECT * FROM subdomains', [ ], function (error, subdomains) {
if (error) return callback(error);
async.eachSeries(subdomains, function (subdomain, iteratorDone) {
const cert = safe.fs.readFileSync(`${CERTS_DIR}/${subdomain.subdomain}.${subdomain.domain}.user.cert`, 'utf8');
const key = safe.fs.readFileSync(`${CERTS_DIR}/${subdomain.subdomain}.${subdomain.domain}.user.key`, 'utf8');
if (!cert || !key) return iteratorDone();
const certificate = { cert, key };
db.runSql('UPDATE subdomains SET certificateJson=? WHERE domain=? AND subdomain=?', [ JSON.stringify(certificate), subdomain.domain, subdomain.subdomain ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
async.series([
db.runSql.run(db, 'ALTER TABLE subdomains DROP COLUMN certificateJson')
], callback);
};

View File

@@ -0,0 +1,43 @@
'use strict';
const async = require('async'),
child_process = require('child_process'),
fs = require('fs'),
path = require('path');
const OLD_CERTS_DIR = '/home/yellowtent/boxdata/certs';
const NEW_CERTS_DIR = '/home/yellowtent/platformdata/nginx/cert';
exports.up = function(db, callback) {
fs.readdir(OLD_CERTS_DIR, function (error, filenames) {
if (error && error.code === 'ENOENT') return callback();
if (error) return callback(error);
filenames = filenames.filter(f => f.endsWith('.key') && !f.endsWith('.host.key') && !f.endsWith('.user.key')); // ignore fallback and user keys
async.eachSeries(filenames, function (filename, iteratorCallback) {
const privateKeyFile = filename;
const privateKey = fs.readFileSync(path.join(OLD_CERTS_DIR, filename));
const certificateFile = filename.replace(/\.key$/, '.cert');
const certificate = fs.readFileSync(path.join(OLD_CERTS_DIR, certificateFile));
const csrFile = filename.replace(/\.key$/, '.csr');
const csr = fs.readFileSync(path.join(OLD_CERTS_DIR, csrFile));
async.series([
db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', `cert-${privateKeyFile}`, privateKey),
db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', `cert-${certificateFile}`, certificate),
db.runSql.bind(db, 'INSERT INTO blobs (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', `cert-${csrFile}`, csr),
], iteratorCallback);
}, function (error) {
if (error) return callback(error);
child_process.execSync(`cp ${OLD_CERTS_DIR}/* ${NEW_CERTS_DIR}`); // this way we copy the non-migrated ones like .host, .user etc as well
fs.rmdir(OLD_CERTS_DIR, { recursive: true }, callback);
});
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,17 @@
'use strict';
const async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE volumes ADD COLUMN mountType VARCHAR(16) DEFAULT "noop"'),
db.runSql.bind(db, 'ALTER TABLE volumes ADD COLUMN mountOptionsJson TEXT')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE volumes DROP COLUMN mountType'),
db.runSql.bind(db, 'ALTER TABLE volumes DROP COLUMN mountOptionsJson')
], callback);
};

View File

@@ -0,0 +1,21 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE backups ADD INDEX creationTime_index (creationTime)'),
db.runSql.bind(db, 'ALTER TABLE eventlog ADD INDEX creationTime_index (creationTime)'),
db.runSql.bind(db, 'ALTER TABLE notifications ADD INDEX creationTime_index (creationTime)'),
db.runSql.bind(db, 'ALTER TABLE tasks ADD INDEX creationTime_index (creationTime)'),
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE backups DROP INDEX creationTime_index'),
db.runSql.bind(db, 'ALTER TABLE eventlog DROP INDEX creationTime_index'),
db.runSql.bind(db, 'ALTER TABLE notifications DROP INDEX creationTime_index'),
db.runSql.bind(db, 'ALTER TABLE tasks DROP INDEX creationTime_index'),
], callback);
};

View File

@@ -0,0 +1,33 @@
'use strict';
const async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE users ADD INDEX creationTime_index (creationTime)', function (error) {
if (error) return callback(error);
db.all('SELECT id, createdAt FROM users', function (error, results) {
if (error) return callback(error);
async.eachSeries(results, function (r, iteratorDone) {
const creationTime = new Date(r.createdAt);
db.runSql('UPDATE users SET creationTime=? WHERE id=?', [ creationTime, r.id ], iteratorDone);
}, function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE users DROP COLUMN createdAt', callback);
});
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN creationTime', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,27 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
const backupConfig = JSON.parse(results[0].value);
if (backupConfig.provider === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.externalDisk) {
backupConfig.chown = backupConfig.provider === 'nfs' || backupConfig.provider === 'sshfs' || backupConfig.externalDisk;
backupConfig.preserveAttributes = !!backupConfig.externalDisk;
backupConfig.provider = 'mountpoint';
if (backupConfig.externalDisk) {
backupConfig.mountPoint = backupConfig.backupFolder;
backupConfig.prefix = '';
delete backupConfig.backupFolder;
delete backupConfig.externalDisk;
}
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [JSON.stringify(backupConfig)], callback);
} else {
callback();
}
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,13 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE notifications DROP COLUMN userId', function (error) {
if (error) return callback(error);
db.runSql('DELETE FROM notifications', callback); // just clear notifications table
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE notifications ADD COLUMN userId VARCHAR(128) NOT NULL', callback);
};

View File

@@ -6,7 +6,7 @@
#### Strict mode is enabled
#### VARCHAR - stored as part of table row (use for strings)
#### TEXT - stored offline from table row (use for strings)
#### BLOB - stored offline from table row (use for binary data)
#### BLOB (64KB), MEDIUMBLOB (16MB), LONGBLOB (4GB) - stored offline from table row (use for binary data)
#### https://dev.mysql.com/doc/refman/5.0/en/storage-requirements.html
#### Times are stored in the database in UTC. And precision is seconds
@@ -20,7 +20,7 @@ CREATE TABLE IF NOT EXISTS users(
email VARCHAR(254) NOT NULL UNIQUE,
password VARCHAR(1024) NOT NULL,
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
@@ -31,7 +31,10 @@ CREATE TABLE IF NOT EXISTS users(
resetToken VARCHAR(128) DEFAULT "",
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT 1,
avatar MEDIUMBLOB,
locationJson TEXT, // { locations: [{ ip, userAgent, city, country, ts }] }
INDEX creationTime_index (creationTime),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS userGroups(
@@ -55,6 +58,7 @@ CREATE TABLE IF NOT EXISTS tokens(
clientId VARCHAR(128),
scope VARCHAR(512) NOT NULL,
expires BIGINT NOT NULL, // FIXME: make this a timestamp
lastUsedTime TIMESTAMP NULL,
PRIMARY KEY(accessToken));
CREATE TABLE IF NOT EXISTS apps(
@@ -78,6 +82,7 @@ CREATE TABLE IF NOT EXISTS apps(
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
enableMailbox BOOLEAN DEFAULT 1, // whether sendmail addon is enabled
mailboxName VARCHAR(128), // mailbox of this app
mailboxDomain VARCHAR(128), // mailbox domain of this apps
label VARCHAR(128), // display name
@@ -87,6 +92,8 @@ CREATE TABLE IF NOT EXISTS apps(
errorJson TEXT,
servicesConfigJson TEXT, // app services configuration
containerIp VARCHAR(16) UNIQUE, // this is not-null because of ip allocation fails, user can 'repair'
appStoreIcon MEDIUMBLOB,
icon MEDIUMBLOB,
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
@@ -103,6 +110,7 @@ CREATE TABLE IF NOT EXISTS appPortBindings(
CREATE TABLE IF NOT EXISTS settings(
name VARCHAR(128) NOT NULL UNIQUE,
value TEXT,
valueBlob MEDIUMBLOB,
PRIMARY KEY(name));
CREATE TABLE IF NOT EXISTS appAddonConfigs(
@@ -131,6 +139,7 @@ CREATE TABLE IF NOT EXISTS backups(
format VARCHAR(16) DEFAULT "tgz",
preserveSecs INTEGER DEFAULT 0,
INDEX creationTime_index (creationTime),
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS eventlog(
@@ -140,6 +149,7 @@ CREATE TABLE IF NOT EXISTS eventlog(
data TEXT, /* free flowing json based on action */
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX creationTime_index (creationTime),
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS domains(
@@ -150,6 +160,8 @@ CREATE TABLE IF NOT EXISTS domains(
tlsConfigJson TEXT, /* JSON containing the tls provider config */
wellKnownJson TEXT, /* JSON containing well known docs for this domain */
fallbackCertificateJson MEDIUMTEXT,
PRIMARY KEY (domain))
/* the default db collation is utf8mb4_unicode_ci but for the app table domain constraint we have to use the old one */
@@ -189,6 +201,7 @@ CREATE TABLE IF NOT EXISTS mailboxes(
membersOnly BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
active BOOLEAN DEFAULT 1,
FOREIGN KEY(domain) REFERENCES mail(domain),
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
@@ -200,6 +213,8 @@ CREATE TABLE IF NOT EXISTS subdomains(
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL, /* primary or redirect */
certificateJson MEDIUMTEXT,
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
UNIQUE (subdomain, domain));
@@ -213,17 +228,20 @@ CREATE TABLE IF NOT EXISTS tasks(
resultJson TEXT,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX creationTime_index (creationTime),
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS notifications(
id int NOT NULL AUTO_INCREMENT,
userId VARCHAR(128) NOT NULL,
eventId VARCHAR(128), // reference to eventlog. can be null
title VARCHAR(512) NOT NULL,
message TEXT,
acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY appPasswords_name_appId_identifier (name, userId, identifier),
INDEX creationTime_index (creationTime),
FOREIGN KEY(eventId) REFERENCES eventlog(id),
PRIMARY KEY (id)
);
@@ -234,6 +252,7 @@ CREATE TABLE IF NOT EXISTS appPasswords(
identifier VARCHAR(128) NOT NULL, // resourceId: app id or mail or webadmin
hashedPassword VARCHAR(1024) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY appPasswords_name_appId_identifier (name, userId, identifier)
FOREIGN KEY(userId) REFERENCES users(id),
PRIMARY KEY (id)
@@ -244,6 +263,8 @@ CREATE TABLE IF NOT EXISTS volumes(
name VARCHAR(256) NOT NULL UNIQUE,
hostPath VARCHAR(1024) NOT NULL UNIQUE,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
mountType VARCHAR(16) DEFAULT "noop",
mountOptionsJson TEXT,
PRIMARY KEY (id)
);
@@ -255,4 +276,9 @@ CREATE TABLE IF NOT EXISTS appMounts(
FOREIGN KEY(appId) REFERENCES apps(id),
FOREIGN KEY(volumeId) REFERENCES volumes(id));
CREATE TABLE IF NOT EXISTS blobs(
id VARCHAR(128) NOT NULL UNIQUE,
value TEXT,
PRIMARY KEY(id));
CHARACTER SET utf8 COLLATE utf8_bin;

2511
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,20 +10,17 @@
"type": "git",
"url": "https://git.cloudron.io/cloudron/box.git"
},
"engines": {
"node": ">=4.0.0 <=4.1.1"
},
"dependencies": {
"@google-cloud/dns": "^1.2.9",
"@google-cloud/storage": "^2.5.0",
"@google-cloud/dns": "^2.1.0",
"@google-cloud/storage": "^5.8.5",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^2.6.3",
"aws-sdk": "^2.828.0",
"async": "^3.2.0",
"aws-sdk": "^2.906.0",
"basic-auth": "^2.0.1",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.10.1",
"cloudron-manifestformat": "^5.10.2",
"connect": "^3.7.0",
"connect-lastmile": "^2.0.0",
"connect-lastmile": "^2.1.0",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.5",
"cookie-session": "^1.4.0",
@@ -31,59 +28,61 @@
"db-migrate": "^0.11.12",
"db-migrate-mysql": "^2.1.2",
"debug": "^4.3.1",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"delay": "^5.0.0",
"dockerode": "^3.3.0",
"delay": "^5.0.0",
"ejs": "^3.1.6",
"ejs-cli": "^2.2.1",
"express": "^4.17.1",
"ipaddr.js": "^2.0.0",
"js-yaml": "^3.14.0",
"json": "^9.0.6",
"js-yaml": "^4.1.0",
"json": "^11.0.0",
"jsonwebtoken": "^8.5.1",
"ldapjs": "^2.2.3",
"lodash": "^4.17.20",
"ldapjs": "^2.2.4",
"lodash": "^4.17.21",
"lodash.chunk": "^4.2.0",
"mime": "^2.5.0",
"mime": "^2.5.2",
"moment": "^2.29.1",
"moment-timezone": "^0.5.32",
"moment-timezone": "^0.5.33",
"morgan": "^1.10.0",
"multiparty": "^4.2.2",
"mustache-express": "^1.3.0",
"mysql": "^2.18.1",
"nodemailer": "^6.4.17",
"nodemailer": "^6.6.0",
"nodemailer-smtp-transport": "^2.7.4",
"once": "^1.4.0",
"pretty-bytes": "^5.5.0",
"pretty-bytes": "^5.6.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.4.4",
"readdirp": "^3.5.0",
"readdirp": "^3.6.0",
"request": "^2.88.2",
"rimraf": "^2.6.3",
"rimraf": "^3.0.2",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^1.1.1",
"semver": "^6.1.1",
"showdown": "^1.9.1",
"safetydance": "^2.0.1",
"semver": "^7.3.5",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^5.3.1",
"superagent": "^6.1.0",
"supererror": "^0.7.2",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.2.0",
"tldjs": "^2.3.1",
"underscore": "^1.12.0",
"uuid": "^3.4.0",
"validator": "^11.0.0",
"ws": "^7.4.2",
"ua-parser-js": "^0.7.28",
"underscore": "^1.13.1",
"uuid": "^8.3.2",
"validator": "^13.6.0",
"ws": "^7.4.5",
"xml2js": "^0.4.23"
},
"devDependencies": {
"expect.js": "*",
"hock": "^1.4.1",
"js2xmlparser": "^4.0.1",
"mocha": "^6.2.3",
"mocha": "^8.4.0",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.14.1",
"nock": "^13.0.11",
"node-sass": "^6.0.0",
"recursive-readdir": "^2.2.2"
},
"scripts": {

View File

@@ -22,8 +22,8 @@ fi
mkdir -p ${DATA_DIR}
cd ${DATA_DIR}
mkdir -p appsdata
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com boxdata/sftp/ssh
mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
mkdir -p boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks platformdata/sftp/ssh platformdata/firewall platformdata/update
sudo mkdir -p /mnt/cloudron-test-music /media/cloudron-test-music # volume test
# translations
@@ -34,12 +34,14 @@ cp -r ${source_dir}/../dashboard/dist/translation/* box/dashboard/dist/translati
echo "=> Generating a localhost selfsigned cert"
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
# generate legacy key format for sftp
ssh-keygen -m PEM -t rsa -f boxdata/sftp/ssh/ssh_host_rsa_key -q -N ""
# clear out any containers
echo "=> Delete all docker containers first"
docker ps -qa | xargs --no-run-if-empty docker rm -f
# clear out any containers if FAST is unset
if [[ -z ${FAST+x} ]]; then
echo "=> Delete all docker containers first"
docker ps -qa | xargs --no-run-if-empty docker rm -f
echo "==> To skip this run with: FAST=1 ./runTests"
else
echo "==> WARNING!! Skipping docker container cleanup, the database might not be pristine!"
fi
# create docker network (while the infra code does this, most tests skip infra setup)
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
@@ -60,6 +62,9 @@ while ! mysqladmin ping -h"${MYSQL_IP}" --silent; do
sleep 1
done
echo "=> Create iptables blocklist"
sudo ipset create cloudron_blocklist hash:net || true
echo "=> Starting cloudron-syslog"
cloudron-syslog --logdir "${DATA_DIR}/platformdata/logs/" &

View File

@@ -2,6 +2,12 @@
set -eu -o pipefail
function exitHandler() {
rm -f /etc/update-motd.d/91-cloudron-install-in-progress
}
trap exitHandler EXIT
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
@@ -35,7 +41,8 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
exit 1
fi
if systemctl -q is-active box; then
# do not use is-active in case box service is down and user attempts to re-install
if systemctl cat box.service >/dev/null 2>&1; then
echo "Error: Cloudron is already installed. To reinstall, start afresh"
exit 1
fi
@@ -92,6 +99,26 @@ if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubu
exit 1
fi
# Install MOTD file for stack script style installations. this is removed by the trap exit handler. Heredoc quotes prevents parameter expansion
cat > /etc/update-motd.d/91-cloudron-install-in-progress <<'EOF'
#!/bin/bash
printf "**********************************************************************\n\n"
printf "\t\t\tWELCOME TO CLOUDRON\n"
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Cloudron is installing. Run 'tail -f /var/log/cloudron-setup.log' to view progress."
printf "Cloudron overview - https://docs.cloudron.io/ \n"
printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n"
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
printf "**********************************************************************\n"
EOF
chmod +x /etc/update-motd.d/91-cloudron-install-in-progress
# Can only write after we have confirmed script has root access
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
@@ -193,7 +220,7 @@ if [[ "${rebootServer}" == "true" ]]; then
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;;
[Yy]* ) exitHandler; systemctl reboot;;
* ) exit;;
esac
fi

View File

@@ -39,10 +39,11 @@ while true; do
--owner-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' AND username IS NOT NULL ORDER BY createdAt LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
dashboard_domain=$(mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" 2>/dev/nul)
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > "${ghost_file}"
chown yellowtent:yellowtent "${ghost_file}" && chmod o-r,g-r "${ghost_file}"
echo "Login as ${admin_username} / ${admin_password} . This password may only be used once. ${ghost_file} will be automatically removed after use."
echo "Login at https://${dashboard_domain} as ${admin_username} / ${admin_password} . This password may only be used once. ${ghost_file} will be automatically removed after use."
exit 0
;;
--) break;;
@@ -73,6 +74,9 @@ echo -n "Generating Cloudron Support stats..."
# clear file
rm -rf $OUT
echo -e $LINE"DASHBOARD DOMAIN"$LINE >> $OUT
mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" &>> $OUT 2>/dev/null || true
echo -e $LINE"PROVIDER"$LINE >> $OUT
cat /etc/cloudron/PROVIDER &>> $OUT || true
@@ -99,7 +103,7 @@ systemctl status --lines=100 box mysql unbound cloudron-syslog nginx collectd do
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT
echo -e $LINE"Interface Info"$LINE >> $OUT
ip addr &>> $OUT
echo -e $LINE"Firewall chains"$LINE >> $OUT

View File

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

View File

@@ -11,6 +11,10 @@ if [[ ${EUID} -ne 0 ]]; then
exit 1
fi
function log() {
echo -e "$(date +'%Y-%m-%dT%H:%M:%S')" "==> installer: $1"
}
readonly user=yellowtent
readonly box_src_dir=/home/${user}/box
@@ -21,36 +25,37 @@ readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
readonly ubuntu_version=$(lsb_release -rs)
readonly ubuntu_codename=$(lsb_release -cs)
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
readonly is_update=$(systemctl is-active -q box && echo "yes" || echo "no")
echo "==> installer: Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION) <=="
log "Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION)"
echo "==> installer: updating docker"
log "updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "19.03.12" ]]; then
readonly docker_version=20.10.3
if [[ $(docker version --format {{.Client.Version}}) != "${docker_version}" ]]; then
# there are 3 packages for docker - containerd, CLI and the daemon
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.13-2_amd64.deb" -o /tmp/containerd.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_19.03.12~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.4.3-1_amd64.deb" -o /tmp/containerd.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
echo "==> installer: Waiting for all dpkg tasks to finish..."
log "Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
while ! dpkg --force-confold --configure -a; do
echo "==> installer: Failed to fix packages. Retry"
log "Failed to fix packages. Retry"
sleep 1
done
# the latest docker might need newer packages
while ! apt update -y; do
echo "==> installer: Failed to update packages. Retry"
log "Failed to update packages. Retry"
sleep 1
done
while ! apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb; do
echo "==> installer: Failed to install docker. Retry"
log "Failed to install docker. Retry"
sleep 1
done
@@ -59,24 +64,31 @@ fi
readonly nginx_version=$(nginx -v 2>&1)
if [[ "${nginx_version}" != *"1.18."* ]]; then
echo "==> installer: installing nginx 1.18"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
log "installing nginx 1.18"
$curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
rm /tmp/nginx.deb
fi
# Cloudron 6 on ubuntu 20 installed recommended packages of collectd -> libinotify -> gnome->shell
apt remove -y gnome-shell || true
apt -y autoremove || true
if ! which mount.nfs; then
log "installing nfs-common"
apt install -y nfs-common
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.18.1" ]]; then
mkdir -p /usr/local/node-10.18.1
$curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.18.1
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
rm -rf /usr/local/node-10.15.1
if ! which sshfs; then
log "installing sshfs"
apt install -y sshfs
fi
log "updating node"
readonly node_version=14.15.4
if [[ "$(node --version)" != "v${node_version}" ]]; then
mkdir -p /usr/local/node-${node_version}
$curl -sL https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-${node_version}
ln -sf /usr/local/node-${node_version}/bin/node /usr/bin/node
ln -sf /usr/local/node-${node_version}/bin/npm /usr/bin/npm
rm -rf /usr/local/node-10.18.1
fi
# this is here (and not in updater.js) because rebuild requires the above node
@@ -87,31 +99,31 @@ for try in `seq 1 10`; do
# however by default npm drops privileges for npm rebuild
# https://docs.npmjs.com/misc/config#unsafe-perm
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
echo "==> installer: Failed to rebuild, trying again"
log "Failed to rebuild, trying again"
sleep 5
done
if [[ ${try} -eq 10 ]]; then
echo "==> installer: npm rebuild failed, giving up"
log "npm rebuild failed, giving up"
exit 4
fi
echo "==> installer: downloading new addon images"
log "downloading new addon images"
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
log "\tPulling docker images: ${images}"
for image in ${images}; do
if ! docker pull "${image}"; then # this pulls the image using the sha256
echo "==> installer: Could not pull ${image}"
exit 5
fi
if ! docker pull "${image%@sha256:*}"; then # this will tag the image for readability
echo "==> installer: Could not pull ${image%@sha256:*}"
exit 6
fi
while ! docker pull "${image}"; do # this pulls the image using the sha256
log "Could not pull ${image}"
sleep 5
done
while ! docker pull "${image%@sha256:*}"; do # this will tag the image for readability
log "Could not pull ${image%@sha256:*}"
sleep 5
done
done
echo "==> installer: update cloudron-syslog"
log "update cloudron-syslog"
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
CLOUDRON_SYSLOG_VERSION="1.0.3"
@@ -119,7 +131,7 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
rm -rf "${CLOUDRON_SYSLOG_DIR}"
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@${CLOUDRON_SYSLOG_VERSION}; then break; fi
echo "===> installer: Failed to install cloudron-syslog, trying again"
log "Failed to install cloudron-syslog, trying again"
sleep 5
done
@@ -128,17 +140,17 @@ if ! id "${user}" 2>/dev/null; then
fi
if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop box service for update"
log "stop box service for update"
${box_src_dir}/setup/stop.sh
fi
# ensure we are not inside the source directory, which we will remove now
cd /root
echo "==> installer: switching the box code"
log "switching the box code"
rm -rf "${box_src_dir}"
mv "${box_src_tmp_dir}" "${box_src_dir}"
chown -R "${user}:${user}" "${box_src_dir}"
echo "==> installer: calling box setup script"
log "calling box setup script"
"${box_src_dir}/setup/start.sh"

View File

@@ -5,14 +5,19 @@ set -eu -o pipefail
# This script is run after the box code is switched. This means that this script
# should pretty much always succeed. No network logic/download code here.
echo "==> Cloudron Start"
function log() {
echo -e "$(date +'%Y-%m-%dT%H:%M:%S')" "==> start: $1"
}
log "Cloudron Start"
readonly USER="yellowtent"
readonly HOME_DIR="/home/${USER}"
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 PLATFORM_DATA_DIR="${HOME_DIR}/platformdata"
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata"
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata"
readonly MAIL_DATA_DIR="${HOME_DIR}/boxdata/mail"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
@@ -26,7 +31,7 @@ if ! getent group media; then
addgroup --gid 500 --system media
fi
echo "==> Configuring docker"
log "Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl enable apparmor
systemctl restart apparmor
@@ -37,9 +42,10 @@ docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron |
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
mkdir -p "${MAIL_DATA_DIR}/dkim"
# keep these in sync with paths.js
echo "==> Ensuring directories"
log "Ensuring directories"
mkdir -p "${PLATFORM_DATA_DIR}/graphite"
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
@@ -57,21 +63,18 @@ mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
"${PLATFORM_DATA_DIR}/logs/crash" \
"${PLATFORM_DATA_DIR}/logs/collectd"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
mkdir -p "${BOX_DATA_DIR}/firewall"
mkdir -p "${BOX_DATA_DIR}/profileicons"
mkdir -p "${BOX_DATA_DIR}/certs"
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
mkdir -p "${BOX_DATA_DIR}/well-known" # .well-known documents
mkdir -p "${BOX_DATA_DIR}/sftp/ssh" # sftp keys
mkdir -p "${PLATFORM_DATA_DIR}/sftp/ssh" # sftp keys
mkdir -p "${PLATFORM_DATA_DIR}/firewall"
# ensure backups folder exists and is writeable
mkdir -p /var/backups
chmod 777 /var/backups
echo "==> Configuring journald"
# can be removed after 6.3
[[ -f "${BOX_DATA_DIR}/updatechecker.json" ]] && mv "${BOX_DATA_DIR}/updatechecker.json" "${PLATFORM_DATA_DIR}/update/updatechecker.json"
rm -rf "${BOX_DATA_DIR}/well-known"
log "Configuring journald"
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
-i /etc/systemd/journald.conf
@@ -92,7 +95,7 @@ setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
# Give user access to nginx logs (uses adm group)
usermod -a -G adm ${USER}
echo "==> Setting up unbound"
log "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
@@ -102,9 +105,10 @@ cp -f "${script_dir}/start/unbound.conf" /etc/unbound/unbound.conf.d/cloudron-ne
# 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"
log "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
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/Type=notify/Type=simple/g' -i /etc/systemd/system/unbound.service
systemctl daemon-reload
systemctl enable --now cloudron-syslog
systemctl enable unbound
@@ -124,11 +128,11 @@ systemctl restart unbound
# ensure cloudron-syslog runs
systemctl restart cloudron-syslog
echo "==> Configuring sudoers"
log "Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
echo "==> Configuring collectd"
log "Configuring collectd"
rm -rf /etc/collectd /var/log/collectd.log
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
cp "${script_dir}/start/collectd/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
@@ -140,7 +144,7 @@ if [[ "${ubuntu_version}" == "20.04" ]]; then
fi
systemctl restart collectd
echo "==> Configuring logrotate"
log "Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
@@ -150,10 +154,10 @@ cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/"
echo "==> Adding motd message for admins"
log "Adding motd message for admins"
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
echo "==> Configuring nginx"
log "Configuring nginx"
# link nginx config to system config
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
ln -s "${PLATFORM_DATA_DIR}/nginx" /etc/nginx
@@ -181,18 +185,26 @@ if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf"
cp "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf
while true; do
if ! systemctl list-jobs | grep mysql; then break; fi
echo "Waiting for mysql jobs..."
log "Waiting for mysql jobs..."
sleep 1
done
while true; do
if systemctl restart mysql; then break; fi
echo "Restarting MySql again after sometime since this fails randomly"
log "Stopping mysql"
systemctl stop mysql
while mysqladmin ping 2>/dev/null; do
log "Waiting for mysql to stop..."
sleep 1
done
else
systemctl start mysql
fi
# the start/stop of mysql is separate to make sure it got reloaded with latest config and it's up and running before we start the new box code
# when using 'system restart mysql', it seems to restart much later and the box code loses connection during platform startup (dangerous!)
log "Starting mysql"
systemctl start mysql
while ! mysqladmin ping 2>/dev/null; do
log "Waiting for mysql to start..."
sleep 1
done
readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
if [[ "${ubuntu_version}" == "20.04" ]]; then
@@ -203,50 +215,33 @@ mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
# set HOME explicity, because it's not set when the installer calls it. this is done because
# paths.js uses this env var and some of the migrate code requires box code
echo "==> Migrating data"
log "Migrating data"
cd "${BOX_SRC_DIR}"
if ! HOME=${HOME_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; then
echo "DB migration failed"
log "DB migration failed"
exit 1
fi
rm -f /etc/cloudron/cloudron.conf
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
echo "==> Generating dhparams (takes forever)"
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
else
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
fi
if [[ ! -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" ]]; then
# the key format in Ubuntu 20 changed, so we create keys in legacy format. for older ubuntu, just re-use the host keys
# see https://github.com/proftpd/proftpd/issues/793
if [[ "${ubuntu_version}" == "20.04" ]]; then
ssh-keygen -m PEM -t rsa -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" -q -N ""
else
cp /etc/ssh/ssh_host_rsa_key* ${BOX_DATA_DIR}/sftp/ssh
fi
fi
echo "==> Changing ownership"
log "Changing ownership"
# note, change ownership after db migrate. this allow db migrate to move files around as root and then we can fix it up here
# 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}" -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" "${PLATFORM_DATA_DIR}/sftp" "${PLATFORM_DATA_DIR}/firewall"
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
# do not chown the boxdata/mail directory; dovecot gets upset
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
chown "${USER}:${USER}" "${BOX_DATA_DIR}/mail"
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${MAIL_DATA_DIR}" -exec chown -R "${USER}:${USER}" {} \;
chown "${USER}:${USER}" "${MAIL_DATA_DIR}"
chown "${USER}:${USER}" -R "${MAIL_DATA_DIR}/dkim" # this is owned by box currently since it generates the keys
echo "==> Starting Cloudron"
log "Starting Cloudron"
systemctl start box
sleep 2 # give systemd sometime to start the processes
echo "==> Almost done"
log "Almost done"

View File

@@ -18,16 +18,22 @@ fi
# allow related and establisted connections
iptables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443 -j ACCEPT # 202 is the alternate ssh port
iptables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,80,202,443 -j ACCEPT # 202 is the alternate ssh port
# whitelist any user ports
ports_json="/home/yellowtent/boxdata/firewall/ports.json"
# whitelist any user ports. we used to use --dports but it has a 15 port limit (XT_MULTI_PORTS)
ports_json="/home/yellowtent/platformdata/firewall/ports.json"
if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_tcp_ports.join(','))" 2>/dev/null); then
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
IFS=',' arr=(${allowed_tcp_ports})
for p in "${arr[@]}"; do
iptables -A CLOUDRON -p tcp -m tcp --dport "${p}" -j ACCEPT
done
fi
if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_udp_ports.join(','))" 2>/dev/null); then
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p udp -m udp -m multiport --dports "${allowed_tcp_ports}" -j ACCEPT
IFS=',' arr=(${allowed_udp_ports})
for p in "${arr[@]}"; do
iptables -A CLOUDRON -p udp -m udp --dport "${p}" -j ACCEPT
done
fi
# turn and stun service
@@ -64,7 +70,7 @@ for port in 80 443; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
done
# ssh smtp ssh msa imap sieve
# ssh
for port in 22 202; do
iptables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --set --name "public-${port}"
iptables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --update --name "public-${port}" --seconds 10 --hitcount 5 -j CLOUDRON_RATELIMIT_LOG
@@ -92,3 +98,5 @@ fi
# Workaround issue where Docker insists on adding itself first in FORWARD table
iptables -D FORWARD -j CLOUDRON_RATELIMIT || true
iptables -I FORWARD 1 -j CLOUDRON_RATELIMIT
echo "==> Setting up firewall done"

View File

@@ -1,5 +1,7 @@
#!/bin/bash
[[ -f /etc/update-motd.d/91-cloudron-install-in-progress ]] && exit
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then

View File

@@ -164,7 +164,9 @@ LoadPlugin swap
#LoadPlugin vmem
#LoadPlugin vserver
#LoadPlugin wireless
LoadPlugin write_graphite
<LoadPlugin write_graphite>
FlushInterval 20
</LoadPlugin>
#LoadPlugin write_http
#LoadPlugin write_riemann

View File

@@ -34,4 +34,5 @@ def read():
val.dispatch(values=[used], type_instance='used')
collectd.register_init(init)
# see Interval setting in collectd.conf for polling interval
collectd.register_read(read)

View File

@@ -6,19 +6,26 @@ PATHS = [] # { name, dir, exclude }
# there is a pattern in carbon/storage-schemas.conf which stores values every 12h for a year
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
# we used to pass the INTERVAL as a parameter to register_read. however, collectd write_graphite
# takes a bit to load (tcp connection) and drops the du data. this then means that we have to wait
# for INTERVAL secs for du data. instead, we just cache the value for INTERVAL instead
CACHE = dict()
CACHE_TIME = 0
def du(pathinfo):
# -B1 makes du print block sizes and not apparent sizes (to match df which also uses block sizes)
cmd = 'timeout 1800 du -DsB1 "{}"'.format(pathinfo['dir'])
dirname = pathinfo['dir']
cmd = 'timeout 1800 du -DsB1 "{}"'.format(dirname)
if pathinfo['exclude'] != '':
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
collectd.info('computing size with command: %s' % cmd);
try:
size = subprocess.check_output(cmd, shell=True).split()[0].decode('utf-8')
collectd.info('\tsize of %s is %s (time: %i)' % (pathinfo['dir'], size, int(time.time())))
collectd.info('\tsize of %s is %s (time: %i)' % (dirname, size, int(time.time())))
return size
except Exception as e:
collectd.info('\terror getting the size of %s: %s' % (pathinfo['dir'], str(e)))
collectd.info('\terror getting the size of %s: %s' % (dirname, str(e)))
return 0
def parseSize(size):
@@ -64,19 +71,35 @@ def init():
collectd.info('custom du plugin initialized with %s %s' % (PATHS, sys.version))
def read():
global CACHE, CACHE_TIME
# read from cache if < 12 hours
read_cache = (time.time() - CACHE_TIME) < INTERVAL
if not read_cache:
CACHE_TIME = time.time()
for pathinfo in PATHS:
size = du(pathinfo)
dirname = pathinfo['dir']
if read_cache and dirname in CACHE:
size = CACHE[dirname]
else:
size = du(pathinfo)
CACHE[dirname] = size
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
val = collectd.Values(type='capacity', plugin='du', plugin_instance=pathinfo['name'])
val.dispatch(values=[size], type_instance='usage')
size = dockerSize()
if read_cache and 'docker' in CACHE:
size = CACHE['docker']
else:
size = dockerSize()
CACHE['docker'] = size
val = collectd.Values(type='capacity', plugin='du', plugin_instance='docker')
val.dispatch(values=[size], type_instance='usage')
collectd.register_init(init)
collectd.register_config(configure)
collectd.register_read(read, INTERVAL)
collectd.register_read(read)

View File

@@ -4,7 +4,7 @@
# http://bugs.mysql.com/bug.php?id=68514
[mysqld]
performance_schema=OFF
max_connections=50
max_connections=200
# on ec2, without this we get a sporadic connection drop when doing the initial migration
max_allowed_packet=64M
@@ -15,6 +15,15 @@ collation-server = utf8mb4_unicode_ci
# set timezone to UTC
default_time_zone='+00:00'
# disable bin logs. they are only useful in replication mode
skip-log-bin
# this is used when creating an index using ALTER command
innodb_sort_buffer_size=2097152
# this is a per session sort (ORDER BY) variable for non-indexed fields
sort_buffer_size = 4M
[mysqldump]
quick
quote-names

View File

@@ -13,9 +13,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mkdirvolume.sh
Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
Defaults!/home/yellowtent/box/src/scripts/reboot.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
@@ -25,9 +22,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurecollec
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
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
@@ -44,11 +38,8 @@ yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/backupup
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
Defaults!/home/yellowtent/box/src/scripts/restartdocker.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.sh
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
Defaults!/home/yellowtent/box/src/scripts/restartservice.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartservice.sh
Defaults!/home/yellowtent/box/src/scripts/rmmailbox.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmailbox.sh
@@ -62,3 +53,9 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/stoptask.sh
Defaults!/home/yellowtent/box/src/scripts/setblocklist.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setblocklist.sh
Defaults!/home/yellowtent/box/src/scripts/addmount.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/addmount.sh
Defaults!/home/yellowtent/box/src/scripts/rmmount.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmount.sh

View File

@@ -1,8 +1,6 @@
[Unit]
Description=Cloudron Admin
OnFailure=crashnotifier@%n.service
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service
After=mysql.service nginx.service
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
Wants=cloudron-resize-fs.service
@@ -15,7 +13,8 @@ Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
ExecStart=/home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
; we run commands like df which will parse properly only with correct locale
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production" "LC_ALL=C"
; 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

View File

@@ -5,6 +5,7 @@ PartOf=docker.service
[Service]
Type=oneshot
Environment="BOX_ENV=cloudron"
ExecStart="/home/yellowtent/box/setup/start/cloudron-firewall.sh"
RemainAfterExit=yes

View File

@@ -2,13 +2,17 @@
[Unit]
Description=Unbound DNS Resolver
After=network.target docker.service
After=network-online.target docker.service
Before=nss-lookup.target
Wants=network-online.target nss-lookup.target
[Service]
PIDFile=/run/unbound.pid
ExecStart=/usr/sbin/unbound -d
ExecReload=/bin/kill -HUP $MAINPID
Restart=always
# On ubuntu 16, this doesn't work for some reason
Type=notify
[Install]
WantedBy=multi-user.target

View File

@@ -1,29 +1,31 @@
'use strict';
exports = module.exports = {
verifyToken: verifyToken
verifyToken
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('./boxerror.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js');
safe = require('safetydance'),
tokens = require('./tokens.js'),
users = require('./users.js'),
util = require('util');
function verifyToken(accessToken, callback) {
const userGet = util.promisify(users.get);
async function verifyToken(accessToken) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.getByAccessToken(accessToken, function (error, token) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(error);
const token = await tokens.getByAccessToken(accessToken);
if (!token) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'No such token');
users.get(token.identifier, function (error, user) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(error);
const [error, user] = await safe(userGet(token.identifier));
if (error && error.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not found');
if (error) throw error;
if (!user.active) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (!user.active) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not active');
callback(null, user);
});
});
await safe(tokens.update(token.id, { lastUsedTime: new Date() })); // ignore any error
return user;
}

547
src/acme2.js Normal file
View File

@@ -0,0 +1,547 @@
'use strict';
exports = module.exports = {
getCertificate,
// testing
_name: 'acme',
_getChallengeSubdomain: getChallengeSubdomain
};
const assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('./domains.js'),
fs = require('fs'),
os = require('os'),
path = require('path'),
promiseRetry = require('./promise-retry.js'),
superagent = require('superagent'),
safe = require('safetydance'),
_ = require('underscore');
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = options.accountKeyPem; // Buffer
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
this.directory = {};
this.performHttpAuthorization = !!options.performHttpAuthorization;
this.wildcard = !!options.wildcard;
}
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(Buffer.isBuffer(pem));
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme2.prototype.sendSignedRequest = async function (url, payload) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert(Buffer.isBuffer(this.accountKeyPem));
const that = this;
let header = {
url: url,
alg: 'RS256'
};
// keyId is null when registering account
if (this.keyId) {
header.kid = this.keyId;
} else {
header.jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
}
const payload64 = b64(payload);
let [error, response] = await safe(superagent.get(this.directory.newNonce).timeout(30000).ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`);
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
if (!nonce) throw new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response');
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
const protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
const signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
const signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
const data = {
protected: protected64,
payload: payload64,
signature: signature64
};
[error, response] = await safe(superagent.post(url).send(data).set('Content-Type', 'application/jose+json').set('User-Agent', 'acme-cloudron').timeout(30000).ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`);
return response;
};
// https://tools.ietf.org/html/rfc8555#section-6.3
Acme2.prototype.postAsGet = async function (url) {
return await this.sendSignedRequest(url, '');
};
Acme2.prototype.updateContact = async function (registrationUri) {
assert.strictEqual(typeof registrationUri, 'string');
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
// https://github.com/ietf-wg-acme/acme/issues/30
const payload = {
contact: [ 'mailto:' + this.email ]
};
const result = await this.sendSignedRequest(registrationUri, JSON.stringify(payload));
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
debug(`updateContact: contact of user updated to ${this.email}`);
};
Acme2.prototype.registerUser = async function () {
const payload = {
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
const result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
// 200 if already exists. 201 for new accounts
if (result.status !== 200 && result.status !== 201) return new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
debug(`registerUser: user registered keyid: ${result.headers.location}`);
this.keyId = result.headers.location;
await this.updateContact(result.headers.location);
};
Acme2.prototype.newOrder = async function (domain) {
assert.strictEqual(typeof domain, 'string');
const payload = {
identifiers: [{
type: 'dns',
value: domain
}]
};
debug(`newOrder: ${domain}`);
const result = await this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload));
if (result.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`);
if (result.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`);
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order');
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order');
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header');
return { order, orderUrl };
};
Acme2.prototype.waitForOrder = async function (orderUrl) {
assert.strictEqual(typeof orderUrl, 'string');
debug(`waitForOrder: ${orderUrl}`);
return await promiseRetry({ times: 15, interval: 20000 }, async () => {
debug('waitForOrder: getting status');
const result = await this.postAsGet(orderUrl);
if (result.status !== 200) {
debug(`waitForOrder: invalid response code getting uri ${result.status}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response code: ${result.status}`);
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`);
else if (result.body.status === 'valid' && result.body.certificate) return result.body.certificate;
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status or invalid response: ${JSON.stringify(result.body)}`);
});
};
Acme2.prototype.getKeyAuthorization = function (token) {
assert(Buffer.isBuffer(this.accountKeyPem));
let jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
let shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
let thumbprint = urlBase64Encode(shasum.digest('base64'));
return token + '.' + thumbprint;
};
Acme2.prototype.notifyChallengeReady = async function (challenge) {
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
debug('notifyChallengeReady: %s was met', challenge.url);
const keyAuthorization = this.getKeyAuthorization(challenge.token);
const payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
const result = await this.sendSignedRequest(challenge.url, JSON.stringify(payload));
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
};
Acme2.prototype.waitForChallenge = async function (challenge) {
assert.strictEqual(typeof challenge, 'object');
debug('waitingForChallenge: %j', challenge);
await promiseRetry({ times: 15, interval: 20000 }, async () => {
debug('waitingForChallenge: getting status');
const result = await this.postAsGet(challenge.url);
if (result.status !== 200) {
debug(`waitForChallenge: invalid response code getting uri ${result.status}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode);
}
debug(`waitForChallenge: status is "${result.body.status}" "${JSON.stringify(result.body)}"`);
if (result.body.status === 'pending') throw new BoxError(BoxError.TRY_AGAIN);
else if (result.body.status === 'valid') return;
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status: ${result.body.status}`);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDer) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof finalizationUrl, 'string');
assert(Buffer.isBuffer(csrDer));
const payload = {
csr: b64(csrDer)
};
debug('signCertificate: sending sign request');
const result = await this.sendSignedRequest(finalizationUrl, JSON.stringify(payload));
// 429 means we reached the cert limit for this domain
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
};
Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFilePath) {
assert.strictEqual(typeof hostname, 'string');
if (safe.fs.existsSync(keyFilePath)) {
debug('createKeyAndCsr: reuse the key for renewal at %s', keyFilePath);
} else {
let key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
if (!key) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(keyFilePath, key)) throw new BoxError(BoxError.FS_ERROR, safe.error);
debug('createKeyAndCsr: key file saved at %s', keyFilePath);
}
const [error, tmpdir] = await safe(fs.promises.mkdtemp(path.join(os.tmpdir(), 'acme-')));
if (error) throw new BoxError(BoxError.FS_ERROR, `Error creating temporary directory for openssl config: ${error.message}`);
// OCSP must-staple is currently disabled because nginx does not provide staple on the first request (https://forum.cloudron.io/topic/4917/ocsp-stapling-for-tls-ssl/)
// ' -addext "tlsfeature = status_request"'; // this adds OCSP must-staple
// we used to use -addext to the CLI to add these but that arg doesn't work on Ubuntu 16.04
// empty distinguished_name section is required for Ubuntu 16 openssl
const conf = '[req]\nreq_extensions = v3_req\ndistinguished_name = req_distinguished_name\n'
+ '[req_distinguished_name]\n\n'
+ '[v3_req]\nbasicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName = @alt_names\n'
+ `[alt_names]\nDNS.1 = ${hostname}\n`;
const opensslConfigFile = path.join(tmpdir, 'openssl.conf');
if (!safe.fs.writeFileSync(opensslConfigFile, conf)) throw new BoxError(BoxError.FS_ERROR, `Failed to write openssl config: ${safe.error.message}`);
// while we pass the CN anyways, subjectAltName takes precedence
const csrDer = safe.child_process.execSync(`openssl req -new -key ${keyFilePath} -outform DER -subj /CN=${hostname} -config ${opensslConfigFile}`);
if (!csrDer) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(csrFilePath, csrDer)) throw new BoxError(BoxError.FS_ERROR, safe.error); // bookkeeping. inspect with openssl req -text -noout -in hostname.csr -inform der
await safe(fs.promises.rmdir(tmpdir, { recursive: true }));
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFilePath);
return csrDer;
};
Acme2.prototype.downloadCertificate = async function (hostname, certUrl, certFilePath) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof certUrl, 'string');
await promiseRetry({ times: 5, interval: 20000 }, async () => {
debug('downloadCertificate: downloading certificate');
const result = await this.postAsGet(certUrl);
if (result.statusCode === 202) throw new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate');
if (result.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
const fullChainPem = result.body; // buffer
if (!safe.fs.writeFileSync(certFilePath, fullChainPem)) throw new BoxError(BoxError.FS_ERROR, safe.error);
debug(`downloadCertificate: cert file for ${hostname} saved at ${certFilePath}`);
});
};
Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authorization, acmeChallengesDir) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof acmeChallengesDir, 'string');
debug('prepareHttpChallenge: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges');
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(acmeChallengesDir, challenge.token));
if (!safe.fs.writeFileSync(path.join(acmeChallengesDir, challenge.token), keyAuthorization)) throw new BoxError(BoxError.FS_ERROR, `Error writing challenge: ${safe.error.message}`);
return challenge;
};
Acme2.prototype.cleanupHttpChallenge = async function (hostname, domain, challenge, acmeChallengesDir) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof acmeChallengesDir, 'string');
debug('cleanupHttpChallenge: unlinking %s', path.join(acmeChallengesDir, challenge.token));
if (!safe.fs.unlinkSync(path.join(acmeChallengesDir, challenge.token))) throw new BoxError(BoxError.FS_ERROR, `Error unlinking challenge: ${safe.error.message}`);
};
function getChallengeSubdomain(hostname, domain) {
let challengeSubdomain;
if (hostname === domain) {
challengeSubdomain = '_acme-challenge';
} else if (hostname.includes('*')) { // wildcard
let subdomain = hostname.slice(0, -domain.length - 1);
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
} else {
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
}
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
return challengeSubdomain;
}
Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authorization) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
debug('prepareDnsChallenge: challenges: %j', authorization);
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges');
const challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
const shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
const challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
return new Promise((resolve, reject) => {
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return reject(error);
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
if (error) return reject(error);
resolve(challenge);
});
});
});
};
Acme2.prototype.cleanupDnsChallenge = async function (hostname, domain, challenge) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
return new Promise((resolve, reject) => {
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return reject(error);
resolve(null);
});
});
};
Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizationUrl, acmeChallengesDir) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof acmeChallengesDir, 'string');
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
const response = await this.postAsGet(authorizationUrl);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code getting authorization : ${response.status}`);
const authorization = response.body;
if (this.performHttpAuthorization) {
return await this.prepareHttpChallenge(hostname, domain, authorization, acmeChallengesDir);
} else {
return await this.prepareDnsChallenge(hostname, domain, authorization);
}
};
Acme2.prototype.cleanupChallenge = async function (hostname, domain, challenge, acmeChallengesDir) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof acmeChallengesDir, 'string');
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
if (this.performHttpAuthorization) {
await this.cleanupHttpChallenge(hostname, domain, challenge, acmeChallengesDir);
} else {
await this.cleanupDnsChallenge(hostname, domain, challenge);
}
};
Acme2.prototype.acmeFlow = async function (hostname, domain, paths) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof paths, 'object');
const { certFilePath, keyFilePath, csrFilePath, acmeChallengesDir } = paths;
await this.registerUser();
const { order, orderUrl } = await this.newOrder(hostname);
for (let i = 0; i < order.authorizations.length; i++) {
const authorizationUrl = order.authorizations[i];
debug(`acmeFlow: authorizing ${authorizationUrl}`);
const challenge = await this.prepareChallenge(hostname, domain, authorizationUrl, acmeChallengesDir);
await this.notifyChallengeReady(challenge);
await this.waitForChallenge(challenge);
const csrDer = await this.createKeyAndCsr(hostname, keyFilePath, csrFilePath);
await this.signCertificate(hostname, order.finalize, csrDer);
const certUrl = await this.waitForOrder(orderUrl);
await this.downloadCertificate(hostname, certUrl, certFilePath);
try {
await this.cleanupChallenge(hostname, domain, challenge, acmeChallengesDir);
} catch (cleanupError) {
debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
}
}
};
Acme2.prototype.loadDirectory = async function () {
await promiseRetry({ times: 3, interval: 20000 }, async () => {
const response = await superagent.get(this.caDirectory).timeout(30000).ok(() => true);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching directory : ${response.status}`);
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response body : ${response.body}`);
this.directory = response.body;
});
};
Acme2.prototype.getCertificate = async function (vhost, domain, paths) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof paths, 'object');
debug(`getCertificate: start acme flow for ${vhost} from ${this.caDirectory}`);
if (vhost !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
vhost = domains.makeWildcard(vhost);
debug(`getCertificate: will get wildcard cert for ${vhost}`);
}
await this.loadDirectory();
await this.acmeFlow(vhost, domain, paths);
};
function getCertificate(vhost, domain, paths, options, callback) {
assert.strictEqual(typeof vhost, 'string'); // this can also be a wildcard domain (for alias domains)
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof paths, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let attempt = 1;
async.retry({ times: 3, interval: 0 }, function (retryCallback) {
debug(`getCertificate: attempt ${attempt++}`);
let acme = new Acme2(options || { });
acme.getCertificate(vhost, domain, paths).then(callback).catch(retryCallback);
}, callback);
}

View File

@@ -19,6 +19,8 @@ exports = module.exports = {
getAppIdByAddonConfigValue,
getByIpAddress,
getIcons,
setHealth,
setTask,
getAppStoreIds,
@@ -31,7 +33,7 @@ exports = module.exports = {
_clear: clear
};
var assert = require('assert'),
const assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
@@ -42,8 +44,8 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationS
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
'apps.creationTime', 'apps.updateTime', 'apps.enableMailbox', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(',');
const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -83,10 +85,13 @@ function postProcess(result) {
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
delete result.accessRestrictionJson;
result.sso = !!result.sso; // make it bool
result.enableBackup = !!result.enableBackup; // make it bool
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
result.sso = !!result.sso;
result.enableBackup = !!result.enableBackup;
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate;
result.enableMailbox = !!result.enableMailbox;
result.proxyAuth = !!result.proxyAuth;
result.hasIcon = !!result.hasIcon;
result.hasAppStoreIcon = !!result.hasAppStoreIcon;
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
@@ -215,15 +220,18 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
const mailboxName = data.mailboxName || null;
const mailboxDomain = data.mailboxDomain || null;
const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null;
const servicesConfigJson = data.servicesConfig ? JSON.stringify(data.servicesConfig) : null;
const enableMailbox = data.enableMailbox || false;
const icon = data.icon || null;
var queries = [];
let queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, '
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares,
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ]
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson, servicesConfigJson, icon, enableMailbox ]
});
queries.push({
@@ -291,7 +299,7 @@ function getPortBindings(id, callback) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
var portBindings = { };
for (var i = 0; i < results.length; i++) {
for (let i = 0; i < results.length; i++) {
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
}
@@ -299,6 +307,17 @@ function getPortBindings(id, callback) {
});
}
function getIcons(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT icon, appStoreIcon FROM apps WHERE id = ?', [ id ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, { icon: results[0].icon, appStoreIcon: results[0].appStoreIcon });
});
}
function delPortBinding(hostPort, type, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
@@ -349,6 +368,9 @@ function clear(callback) {
}
function update(id, app, callback) {
// ts is useful as a versioning mechanism (for example, icon changed). update the timestamp explicity in code instead of db.
// this way health and healthTime can be updated without changing ts
app.ts = new Date();
updateWithConstraints(id, app, '', callback);
}
@@ -412,7 +434,7 @@ function updateWithConstraints(id, app, constraints, callback) {
}
var fields = [ ], values = [ ];
for (var p in app) {
for (let p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
@@ -439,10 +461,10 @@ function updateWithConstraints(id, app, constraints, callback) {
function setHealth(appId, health, healthTime, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
assert(util.isDate(healthTime));
assert(util.types.isDate(healthTime));
assert.strictEqual(typeof callback, 'function');
var values = { health, healthTime };
const values = { health, healthTime };
updateWithConstraints(appId, values, '', callback);
}
@@ -453,7 +475,9 @@ function setTask(appId, values, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
values.ts = new Date();
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
if (options.requiredState === null) {
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
@@ -475,7 +499,7 @@ function getAppStoreIds(callback) {
function setAddonConfig(appId, addonId, env, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert(util.isArray(env));
assert(Array.isArray(env));
assert.strictEqual(typeof callback, 'function');
unsetAddonConfig(appId, addonId, function (error) {

View File

@@ -1,60 +1,58 @@
'use strict';
var appdb = require('./appdb.js'),
const appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:apphealthmonitor'),
docker = require('./docker.js'),
eventlog = require('./eventlog.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util');
superagent = require('superagent');
exports = module.exports = {
run
};
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
const UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
const UNHEALTHY_THRESHOLD = 20 * 60 * 1000; // 20 minutes
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // 60 minutes
const gStartTime = new Date(); // time when apphealthmonitor was started
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // will only raise 1 oom event every hour
let gStartTime = null; // time when apphealthmonitor was started
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
function debugApp(app) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + app.id);
}
function setHealth(app, health, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof health, 'string');
assert.strictEqual(typeof callback, 'function');
let now = new Date(), curHealth = app.health;
// app starts out with null health
// if it became healthy, we update immediately. this is required for ui to say "running" etc
// if it became unhealthy/error/dead, wait for a threshold before updating db
const now = new Date(), lastHealth = app.health;
let healthTime = gStartTime > app.healthTime ? gStartTime : app.healthTime; // on box restart, clamp value to start time
if (health === apps.HEALTH_HEALTHY) {
healthTime = now;
if (curHealth && curHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
debugApp(app, 'app switched from %s to healthy', curHealth);
if (lastHealth && lastHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
debug(`setHealth: ${app.id} (${app.fqdn}) switched from ${lastHealth} to healthy`);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, auditSource.HEALTH_MONITOR, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (curHealth === apps.HEALTH_HEALTHY) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
if (lastHealth === apps.HEALTH_HEALTHY) {
debug(`setHealth: marking ${app.id} (${app.fqdn}) as unhealthy since not seen for more than ${UNHEALTHY_THRESHOLD/(60 * 1000)} minutes`);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, auditSource.HEALTH_MONITOR, { app: app });
}
} else {
debugApp(app, 'waiting for %s seconds to update the app health', (UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000);
debug(`setHealth: ${app.id} (${app.fqdn}) waiting for ${(UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000} to update health`);
return callback(null);
}
@@ -63,6 +61,7 @@ function setHealth(app, health, callback) {
if (error) return callback(error);
app.health = health;
app.healthTime = healthTime;
callback(null);
});
@@ -97,7 +96,7 @@ function checkAppHealth(app, callback) {
.end(function (error, res) {
if (error && !error.response) {
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
} else if (res.statusCode > 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
@@ -112,7 +111,7 @@ function getContainerInfo(containerId, callback) {
const appId = safe.query(result, 'Config.Labels.appId', null);
if (!appId) return callback(null, null /* app */, { name: result.Name }); // addon
if (!appId) return callback(null, null /* app */, { name: result.Name.slice(1) }); // addon . Name has a '/' in the beginning for some reason
apps.get(appId, callback); // don't get by container id as this can be an exec container
});
@@ -120,8 +119,7 @@ function getContainerInfo(containerId, callback) {
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:2.0.0 /bin/bash
apt-get update && apt-get install stress
docker run -ti -m 100M cloudron/base:3.0.0 /bin/bash
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents(intervalSecs, callback) {
@@ -144,12 +142,12 @@ function processDockerEvents(intervalSecs, callback) {
const now = Date.now();
const notifyUser = !(app && app.debugMode) && ((now - gLastOomMailTime) > OOM_EVENT_LIMIT);
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now);
debug(`OOM ${program} notifyUser: ${notifyUser}. lastOomTime: ${gLastOomMailTime} (now: ${now})`);
// do not send mails for dev apps
if (notifyUser) {
// app can be null for addon containers
eventlog.add(eventlog.ACTION_APP_OOM, auditSource.HEALTH_MONITOR, { event: event, containerId: containerId, addon: addon || null, app: app || null });
eventlog.add(eventlog.ACTION_APP_OOM, auditSource.HEALTH_MONITOR, { event, containerId, addon: addon || null, app: app || null });
gLastOomMailTime = now;
}
@@ -189,6 +187,10 @@ function run(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
if (constants.TEST) return;
if (!gStartTime) gStartTime = new Date();
async.series([
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs)

View File

@@ -44,6 +44,8 @@ exports = module.exports = {
getLocalLogfilePaths,
getLogs,
getCertificate,
start,
stop,
restart,
@@ -62,12 +64,15 @@ exports = module.exports = {
restartAppsUsingAddons,
getDataDir,
getIconPath,
getIcon,
getMemoryLimit,
downloadFile,
uploadFile,
backupConfig,
restoreConfig,
PORT_TYPE_TCP: 'tcp',
PORT_TYPE_UDP: 'udp',
@@ -82,6 +87,7 @@ exports = module.exports = {
ISTATE_PENDING_DEBUG: 'pending_debug',
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
ISTATE_PENDING_IMPORT: 'pending_import', // import from external backup
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app. this is state because it blocks other operations
ISTATE_PENDING_START: 'pending_start',
@@ -107,7 +113,7 @@ exports = module.exports = {
_MOCK_GET_BY_IP_APP_ID: ''
};
var appdb = require('./appdb.js'),
const appdb = require('./appdb.js'),
appstore = require('./appstore.js'),
appTaskManager = require('./apptaskmanager.js'),
assert = require('assert'),
@@ -115,6 +121,7 @@ var appdb = require('./appdb.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
database = require('./database.js'),
debug = require('debug')('box:apps'),
docker = require('./docker.js'),
domaindb = require('./domaindb.js'),
@@ -182,6 +189,11 @@ function validatePortBindings(portBindings, manifest) {
[50000, 51000] /* turn udp ports */
];
const ALLOWED_PORTS = [
53, // dns 53 is special and adblocker apps can use them
853 // dns over tls
];
if (!portBindings) return null;
for (let portName in portBindings) {
@@ -191,7 +203,7 @@ function validatePortBindings(portBindings, manifest) {
if (!Number.isInteger(hostPort)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not an integer`, { field: 'portBindings', portName: portName });
if (RESERVED_PORTS.indexOf(hostPort) !== -1) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
if (RESERVED_PORT_RANGES.find(range => (hostPort >= range[0] && hostPort <= range[1]))) return new BoxError(BoxError.BAD_FIELD, `Port ${hostPort} is reserved.`, { field: 'portBindings', portName: portName });
if (hostPort !== 53 && (hostPort <= 1023 || hostPort > 65535)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName }); // dns 53 is special and adblocker apps can use them
if (ALLOWED_PORTS.indexOf(hostPort) === -1 && (hostPort <= 1023 || hostPort > 65535)) return new BoxError(BoxError.BAD_FIELD, `${hostPort} is not in permitted range`, { field: 'portBindings', portName: portName });
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
@@ -406,44 +418,30 @@ function removeInternalFields(app) {
'location', 'domain', 'fqdn', 'mailboxName', 'mailboxDomain',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts');
'label', 'alternateDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts', 'enableMailbox');
}
// non-admins can only see these
function removeRestrictedFields(app) {
return _.pick(app,
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'alternateDomains', 'aliasDomains', 'sso',
'id', 'appStoreId', 'installationState', 'error', 'runState', 'health', 'taskId', 'accessRestriction', 'alternateDomains', 'aliasDomains', 'sso',
'location', 'domain', 'fqdn', 'manifest', 'portBindings', 'iconUrl', 'creationTime', 'ts', 'tags', 'label', 'enableBackup');
}
function getIconUrlSync(app) {
const iconUrl = '/api/v1/apps/' + app.id + '/icon';
const userIconPath = `${paths.APP_ICONS_DIR}/${app.id}.user.png`;
if (safe.fs.existsSync(userIconPath)) return iconUrl;
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${app.id}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return iconUrl;
return null;
}
function getIconPath(app, options, callback) {
function getIcon(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const appId = app.id;
appdb.getIcons(app.id, function (error, icons) {
if (error) return callback(error);
if (!options.original) {
const userIconPath = `${paths.APP_ICONS_DIR}/${appId}.user.png`;
if (safe.fs.existsSync(userIconPath)) return callback(null, userIconPath);
}
if (!options.original && icons.icon) return callback(null, icons.icon);
const appstoreIconPath = `${paths.APP_ICONS_DIR}/${appId}.png`;
if (safe.fs.existsSync(appstoreIconPath)) return callback(null, appstoreIconPath);
if (icons.appStoreIcon) return callback(null, icons.appStoreIcon);
callback(new BoxError(BoxError.NOT_FOUND, 'No icon'));
callback(new BoxError(BoxError.NOT_FOUND, 'No icon'));
});
}
function getMemoryLimit(app) {
@@ -466,8 +464,7 @@ function postProcess(app, domainObjectMap) {
result[portName] = app.portBindings[portName].hostPort;
}
app.portBindings = result;
app.iconUrl = getIconUrlSync(app);
app.iconUrl = app.hasIcon || app.hasAppStoreIcon ? `/api/v1/apps/${app.id}/icon` : null;
app.fqdn = domains.fqdn(app.location, domainObjectMap[app.domain]);
app.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
app.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
@@ -624,7 +621,8 @@ function scheduleTask(appId, installationState, taskId, callback) {
if (error) return callback(error);
let memoryLimit = 400;
if (installationState === exports.ISTATE_PENDING_BACKUP || installationState === exports.ISTATE_PENDING_CLONE || installationState === exports.ISTATE_PENDING_RESTORE) {
if (installationState === exports.ISTATE_PENDING_BACKUP || installationState === exports.ISTATE_PENDING_CLONE || installationState === exports.ISTATE_PENDING_RESTORE
|| installationState === exports.ISTATE_PENDING_IMPORT || installationState === exports.ISTATE_PENDING_UPDATE) {
memoryLimit = 'memoryLimit' in backupConfig ? Math.max(backupConfig.memoryLimit/1024/1024, 400) : 400;
}
@@ -643,6 +641,8 @@ function scheduleTask(appId, installationState, taskId, callback) {
appdb.update(appId, { installationState: exports.ISTATE_ERROR, error: boxError.toPlainObject(), taskId: null }, callback);
} else if (!(installationState === exports.ISTATE_PENDING_UNINSTALL && !error)) { // clear out taskId except for successful uninstall
appdb.update(appId, { taskId: null }, callback);
} else {
callback(null);
}
});
});
@@ -667,7 +667,7 @@ function addTask(appId, installationState, task, callback) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_STATE, 'Another task is scheduled for this app')); // could be because app went away OR a taskId exists
if (error) return callback(error);
if (scheduleNow) scheduleTask(appId, installationState, taskId, NOOP_CALLBACK);
if (scheduleNow) scheduleTask(appId, installationState, taskId, task.onFinished || NOOP_CALLBACK);
callback(null, { taskId });
});
@@ -685,12 +685,12 @@ function checkAppState(app, state) {
if (app.error.installationState === state) return null;
// allow uninstall from any state
if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
if (state !== exports.ISTATE_PENDING_UNINSTALL && state !== exports.ISTATE_PENDING_RESTORE && state !== exports.ISTATE_PENDING_IMPORT) return new BoxError(BoxError.BAD_STATE, 'Not allowed in error state');
}
if (app.runState === exports.RSTATE_STOPPED) {
// can't backup or restore since app addons are down. can't update because migration scripts won't run
if (state === exports.ISTATE_PENDING_UPDATE || state === exports.ISTATE_PENDING_BACKUP || state === exports.ISTATE_PENDING_RESTORE) return new BoxError(BoxError.BAD_STATE, 'Not allowed in stopped state');
if (state === exports.ISTATE_PENDING_UPDATE || state === exports.ISTATE_PENDING_BACKUP || state === exports.ISTATE_PENDING_RESTORE || state === exports.ISTATE_PENDING_IMPORT) return new BoxError(BoxError.BAD_STATE, 'Not allowed in stopped state');
}
return null;
@@ -731,13 +731,11 @@ function install(data, auditSource, callback) {
assert.strictEqual(typeof data.manifest, 'object'); // manifest is already downloaded
var location = data.location.toLowerCase(),
let location = data.location.toLowerCase(),
domain = data.domain.toLowerCase(),
portBindings = data.portBindings || null,
accessRestriction = data.accessRestriction || null,
icon = data.icon || null,
cert = data.cert || null,
key = data.key || null,
memoryLimit = data.memoryLimit || 0,
sso = 'sso' in data ? data.sso : null,
debugMode = data.debugMode || null,
@@ -749,7 +747,9 @@ function install(data, auditSource, callback) {
label = data.label || null,
tags = data.tags || [],
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false,
skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false,
appStoreId = data.appStoreId,
enableMailbox = 'enabledMailbox' in data ? data.enableMailbox : true,
manifest = data.manifest;
let error = manifestFormat.parse(manifest);
@@ -791,10 +791,7 @@ function install(data, auditSource, callback) {
if (icon) {
if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.png'), Buffer.from(icon, 'base64'))) {
return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message));
}
icon = Buffer.from(icon, 'base64');
}
const locations = [{ subdomain: location, domain, type: 'primary' }]
@@ -804,14 +801,9 @@ function install(data, auditSource, callback) {
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
if (cert && key) {
error = reverseProxy.validateCertificate(location, domainObjectMap[domain], { cert, key });
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' }));
}
debug('Will install app with id : ' + appId);
var data = {
const data = {
accessRestriction,
memoryLimit,
sso,
@@ -825,6 +817,8 @@ function install(data, auditSource, callback) {
env,
label,
tags,
icon,
enableMailbox,
runState: exports.RSTATE_RUNNING,
installationState: exports.ISTATE_PENDING_INSTALL
};
@@ -836,14 +830,8 @@ function install(data, auditSource, callback) {
purchaseApp({ appId: appId, appstoreId: appStoreId, manifestId: manifest.id || 'customapp' }, function (error) {
if (error) return callback(error);
// save cert to boxdata/certs
if (cert && key) {
let error = reverseProxy.setAppCertificateSync(location, domainObjectMap[domain], { cert, key });
if (error) return callback(error);
}
const task = {
args: { restoreConfig: null, overwriteDns },
args: { restoreConfig: null, skipDnsSetup, overwriteDns },
values: { },
requiredState: data.installationState
};
@@ -851,7 +839,7 @@ function install(data, auditSource, callback) {
addTask(appId, data.installationState, task, function (error, result) {
if (error) return callback(error);
const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings });
const newApp = _.extend({}, _.omit(data, 'icon'), { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
@@ -875,7 +863,7 @@ function setAccessRestriction(app, accessRestriction, auditSource, callback) {
let error = validateAccessRestriction(accessRestriction);
if (error) return callback(error);
appdb.update(appId, { accessRestriction: accessRestriction }, function (error) {
appdb.update(appId, { accessRestriction }, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, accessRestriction });
@@ -894,7 +882,7 @@ function setLabel(app, label, auditSource, callback) {
let error = validateLabel(label);
if (error) return callback(error);
appdb.update(appId, { label: label }, function (error) {
appdb.update(appId, { label }, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, label });
@@ -913,7 +901,7 @@ function setTags(app, tags, auditSource, callback) {
let error = validateTags(tags);
if (error) return callback(error);
appdb.update(appId, { tags: tags }, function (error) {
appdb.update(appId, { tags }, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, tags });
@@ -932,17 +920,15 @@ function setIcon(app, icon, auditSource, callback) {
if (icon) {
if (!validator.isBase64(icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(icon, 'base64'))) {
return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
icon = Buffer.from(icon, 'base64');
}
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, icon });
appdb.update(appId, { icon }, function (error) {
if (error) return callback(error);
callback();
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, iconChanged: true });
callback();
});
}
function setMemoryLimit(app, memoryLimit, auditSource, callback) {
@@ -1073,13 +1059,18 @@ function setDebugMode(app, debugMode, auditSource, callback) {
});
}
function setMailbox(app, mailboxName, mailboxDomain, auditSource, callback) {
function setMailbox(app, data, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert(mailboxName === null || typeof mailboxName === 'string');
assert.strictEqual(typeof mailboxDomain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { enable, mailboxDomain } = data;
let mailboxName = data.mailboxName;
assert.strictEqual(typeof enable, 'boolean');
assert(mailboxName === null || typeof mailboxName === 'string');
assert.strictEqual(typeof mailboxDomain, 'string');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_RECREATE_CONTAINER);
if (error) return callback(error);
@@ -1098,7 +1089,7 @@ function setMailbox(app, mailboxName, mailboxDomain, auditSource, callback) {
const task = {
args: {},
values: { mailboxName, mailboxDomain }
values: { enableMailbox: enable, mailboxName, mailboxDomain }
};
addTask(appId, exports.ISTATE_PENDING_RECREATE_CONTAINER, task, function (error, result) {
if (error) return callback(error);
@@ -1170,28 +1161,30 @@ function setReverseProxyConfig(app, reverseProxyConfig, auditSource, callback) {
});
}
function setCertificate(app, bundle, auditSource, callback) {
function setCertificate(app, data, auditSource, callback) {
assert.strictEqual(typeof app, 'object');
assert(bundle && typeof bundle === 'object');
assert(data && typeof data === 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const appId = app.id;
const { location, domain, cert, key } = data;
domains.get(app.domain, function (error, domainObject) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
if (bundle.cert && bundle.key) {
error = reverseProxy.validateCertificate(app.location, domainObject, { cert: bundle.cert, key: bundle.key });
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error.message, { field: 'cert' }));
if (cert && key) {
error = reverseProxy.validateCertificate(location, domainObject, { cert, key });
if (error) return callback(error);
}
error = reverseProxy.setAppCertificateSync(app.location, domainObject, { cert: bundle.cert, key: bundle.key });
if (error) return callback(error);
reverseProxy.setAppCertificateSync(location, domainObject, { cert, key }, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert: bundle.cert, key: bundle.key });
eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, cert, key });
callback();
callback();
});
});
}
@@ -1245,6 +1238,7 @@ function setLocation(app, data, auditSource, callback) {
const task = {
args: {
oldConfig: _.pick(app, 'location', 'domain', 'fqdn', 'alternateDomains', 'aliasDomains', 'portBindings'),
skipDnsSetup: !!data.skipDnsSetup,
overwriteDns: !!data.overwriteDns
},
values
@@ -1333,13 +1327,9 @@ function update(app, data, auditSource, callback) {
if ('icon' in data) {
if (data.icon) {
if (!validator.isBase64(data.icon)) return callback(new BoxError(BoxError.BAD_FIELD, 'icon is not base64', { field: 'icon' }));
if (!safe.fs.writeFileSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'), Buffer.from(data.icon, 'base64'))) {
return callback(new BoxError(BoxError.FS_ERROR, 'Error saving icon:' + safe.error.message));
}
} else {
safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, appId + '.user.png'));
data.icon = Buffer.from(data.icon, 'base64');
}
values.icon = data.icon;
}
// do not update apps in debug mode
@@ -1360,7 +1350,10 @@ function update(app, data, auditSource, callback) {
const task = {
args: { updateConfig },
values
values,
onFinished: (error) => {
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource, { app, success: !error, errorMessage: error ? error.message : null }, () => {}); // ignore error
}
};
addTask(appId, exports.ISTATE_PENDING_UPDATE, task, function (error, result) {
if (error) return callback(error);
@@ -1431,6 +1424,15 @@ function getLogs(app, options, callback) {
return callback(null, transformStream);
}
async function getCertificate(subdomain, domain) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
const result = await database.query('SELECT certificateJson FROM subdomains WHERE subdomain=? AND domain=?', [ subdomain, domain ]);
if (result.length === 0) return null;
return JSON.parse(result[0].certificateJson);
}
// does a re-configure when called from most states. for install/clone errors, it re-installs with an optional manifest
// re-configure can take a dockerImage but not a manifest because re-configure does not clean up addons
function repair(app, data, auditSource, callback) {
@@ -1450,7 +1452,7 @@ function repair(app, data, auditSource, callback) {
// maybe split this into a separate route like reinstall?
if (errorState === exports.ISTATE_PENDING_INSTALL || errorState === exports.ISTATE_PENDING_CLONE) {
task.args = { overwriteDns: true };
task.args = { skipDnsSetup: false, overwriteDns: true };
if (data.manifest) {
let error = manifestFormat.parse(data.manifest);
if (error) return callback(new BoxError(BoxError.BAD_FIELD, `manifest error: ${error.message}`));
@@ -1523,6 +1525,7 @@ function restore(app, backupId, auditSource, callback) {
args: {
restoreConfig,
oldManifest: app.manifest,
skipDnsSetup: !!backupId, // if this is a restore, just skip dns setup. only re-installs should setup dns
overwriteDns: true
},
values
@@ -1554,7 +1557,7 @@ function importApp(app, data, auditSource, callback) {
let error = backupFormat ? validateBackupFormat(backupFormat) : null;
if (error) return callback(error);
error = checkAppState(app, exports.ISTATE_PENDING_RESTORE);
error = checkAppState(app, exports.ISTATE_PENDING_IMPORT);
if (error) return callback(error);
// TODO: make this smarter to do a read-only test and check if the file exists in the storage backend
@@ -1578,14 +1581,15 @@ function importApp(app, data, auditSource, callback) {
args: {
restoreConfig,
oldManifest: app.manifest,
skipDnsSetup: false,
overwriteDns: true
},
values: {}
};
addTask(appId, exports.ISTATE_PENDING_RESTORE, task, function (error, result) {
addTask(appId, exports.ISTATE_PENDING_IMPORT, task, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId: result.taskId });
eventlog.add(eventlog.ACTION_APP_IMPORT, auditSource, { app: app, backupId, fromManifest: app.manifest, toManifest: app.manifest, taskId: result.taskId });
callback(null, { taskId: result.taskId });
});
@@ -1635,11 +1639,12 @@ function clone(app, data, user, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
var location = data.location.toLowerCase(),
const location = data.location.toLowerCase(),
domain = data.domain.toLowerCase(),
portBindings = data.portBindings || null,
backupId = data.backupId,
overwriteDns = 'overwriteDns' in data ? data.overwriteDns : false,
skipDnsSetup = 'skipDnsSetup' in data ? data.skipDnsSetup : false,
appId = app.id;
assert.strictEqual(typeof backupId, 'string');
@@ -1670,47 +1675,58 @@ function clone(app, data, user, auditSource, callback) {
validateLocations(locations, function (error, domainObjectMap) {
if (error) return callback(error);
var newAppId = uuid.v4();
const newAppId = uuid.v4();
var data = {
installationState: exports.ISTATE_PENDING_CLONE,
runState: exports.RSTATE_RUNNING,
memoryLimit: app.memoryLimit,
accessRestriction: app.accessRestriction,
sso: !!app.sso,
mailboxName: mailboxName,
mailboxDomain: mailboxDomain,
enableBackup: app.enableBackup,
reverseProxyConfig: app.reverseProxyConfig,
env: app.env,
alternateDomains: [],
aliasDomains: []
};
appdb.add(newAppId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, locations, domainObjectMap, portBindings));
appdb.getIcons(app.id, function (error, icons) {
if (error) return callback(error);
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id || 'customapp' }, function (error) {
const data = {
installationState: exports.ISTATE_PENDING_CLONE,
runState: exports.RSTATE_RUNNING,
memoryLimit: app.memoryLimit,
cpuShares: app.cpuShares,
accessRestriction: app.accessRestriction,
sso: !!app.sso,
mailboxName: mailboxName,
mailboxDomain: mailboxDomain,
enableBackup: app.enableBackup,
reverseProxyConfig: app.reverseProxyConfig,
env: app.env,
alternateDomains: [],
aliasDomains: [],
servicesConfig: app.servicesConfig,
label: app.label ? `${app.label}-clone` : '',
tags: app.tags,
enableAutomaticUpdate: app.enableAutomaticUpdate,
icon: icons.icon,
enableMailbox: app.enableMailbox
};
appdb.add(newAppId, appStoreId, manifest, location, domain, translatePortBindings(portBindings, manifest), data, function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(error.message, locations, domainObjectMap, portBindings));
if (error) return callback(error);
const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format };
const task = {
args: { restoreConfig, overwriteDns, oldManifest: null },
values: {},
requiredState: exports.ISTATE_PENDING_CLONE
};
addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, function (error, result) {
purchaseApp({ appId: newAppId, appstoreId: app.appStoreId, manifestId: manifest.id || 'customapp' }, function (error) {
if (error) return callback(error);
const newApp = _.extend({}, data, { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
const restoreConfig = { backupId: backupId, backupFormat: backupInfo.format };
const task = {
args: { restoreConfig, overwriteDns, skipDnsSetup, oldManifest: null },
values: {},
requiredState: exports.ISTATE_PENDING_CLONE
};
addTask(newAppId, exports.ISTATE_PENDING_CLONE, task, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: newApp, taskId: result.taskId });
const newApp = _.extend({}, _.omit(data, 'icon'), { appStoreId, manifest, location, domain, portBindings });
newApp.fqdn = domains.fqdn(newApp.location, domainObjectMap[newApp.domain]);
newApp.alternateDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = domains.fqdn(ad.subdomain, domainObjectMap[ad.domain]); });
callback(null, { id: newAppId, taskId: result.taskId });
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, oldApp: app, newApp: newApp, taskId: result.taskId });
callback(null, { id: newAppId, taskId: result.taskId });
});
});
});
});
@@ -1834,8 +1850,8 @@ function exec(app, options, callback) {
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = options.cmd || [ '/bin/bash' ];
assert(util.isArray(cmd) && cmd.length > 0);
let cmd = options.cmd || [ '/bin/bash' ];
assert(Array.isArray(cmd) && cmd.length > 0);
if (app.installationState !== exports.ISTATE_INSTALLED || app.runState !== exports.RSTATE_RUNNING) {
return callback(new BoxError(BoxError.BAD_STATE, 'App not installed or running'));
@@ -1910,8 +1926,6 @@ function autoupdateApps(updateInfo, auditSource, callback) { // updateInfo is {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (!updateInfo) return callback(null);
async.eachSeries(Object.keys(updateInfo), function iterator(appId, iteratorDone) {
get(appId, function (error, app) {
if (error) {
@@ -1971,7 +1985,8 @@ function listBackups(app, page, perPage, callback) {
});
}
function restoreInstalledApps(callback) {
function restoreInstalledApps(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getAll(function (error, apps) {
@@ -1994,7 +2009,7 @@ function restoreInstalledApps(callback) {
}
const task = {
args: { restoreConfig, overwriteDns: true, oldManifest },
args: { restoreConfig, skipDnsSetup: options.skipDnsSetup, overwriteDns: true, oldManifest },
values: {},
scheduleNow: false, // task will be scheduled by autoRestartTasks when platform is ready
requireNullTaskId: false // ignore existing stale taskId
@@ -2191,3 +2206,34 @@ function uploadFile(app, sourceFilePath, destFilePath, callback) {
readFile.pipe(stream);
});
}
function backupConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) {
return callback(new BoxError(BoxError.FS_ERROR, 'Error creating config.json: ' + safe.error.message));
}
appdb.getIcons(app.id, function (error, icons) {
if (!error && icons.icon) safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png'), icons.icon);
callback(null);
});
}
function restoreConfig(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const appConfig = safe.JSON.parse(safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json')));
let data = {};
if (appConfig) {
data = _.pick(appConfig, 'memoryLimit', 'cpuShares', 'enableBackup', 'reverseProxyConfig', 'env', 'servicesConfig', 'label', 'tags', 'enableAutomaticUpdate');
}
const icon = safe.fs.readFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/icon.png'));
if (icon) data.icon = icon;
appdb.update(app.id, data, callback);
}

View File

@@ -7,10 +7,8 @@ exports = module.exports = {
getApp,
getAppVersion,
trackBeginSetup,
trackFinishedSetup,
registerWithLoginCredentials,
updateCloudron,
purchaseApp,
unpurchaseApp,
@@ -343,30 +341,29 @@ function registerCloudron(data, callback) {
});
}
// This works without a Cloudron token as this Cloudron was not yet registered
let gBeginSetupAlreadyTracked = false;
function trackBeginSetup() {
// avoid browser reload double tracking, not perfect since box might restart, but covers most cases and is simple
if (gBeginSetupAlreadyTracked) return;
gBeginSetupAlreadyTracked = true;
function updateCloudron(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
getCloudronToken(function (error, token) {
if (error && error.reason === BoxError.LICENSE_ERROR) return callback(null); // missing token. not registered yet
if (error) return callback(error);
superagent.post(url).send({}).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return debug(`trackBeginSetup: ${error.message}`);
if (result.statusCode !== 200) return debug(`trackBeginSetup: ${result.statusCode} ${error.message}`);
});
}
const url = `${settings.apiServerOrigin()}/api/v1/update_cloudron`;
const query = {
accessToken: token
};
// This works without a Cloudron token as this Cloudron was not yet registered
function trackFinishedSetup(domain) {
assert.strictEqual(typeof domain, 'string');
superagent.post(url).query(query).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
debug(`updateCloudron: Cloudron updated with data ${JSON.stringify(data)}`);
superagent.post(url).send({ domain }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return debug(`trackFinishedSetup: ${error.message}`);
if (result.statusCode !== 200) return debug(`trackFinishedSetup: ${result.statusCode} ${error.message}`);
callback();
});
});
}
@@ -389,7 +386,7 @@ function registerWithLoginCredentials(options, callback) {
login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, version: constants.VERSION, purpose: options.purpose || '' }, callback);
registerCloudron({ domain: settings.dashboardDomain(), accessToken: result.accessToken, version: constants.VERSION, purpose: options.purpose || '' }, callback);
});
});
});

View File

@@ -3,16 +3,12 @@
'use strict';
exports = module.exports = {
run: run,
run,
// exported for testing
_configureReverseProxy: configureReverseProxy,
_unconfigureReverseProxy: unconfigureReverseProxy,
_createAppDir: createAppDir,
_deleteAppDir: deleteAppDir,
_verifyManifest: verifyManifest,
_registerSubdomains: registerSubdomains,
_unregisterSubdomains: unregisterSubdomains,
_waitForDnsPropagation: waitForDnsPropagation
};
@@ -20,7 +16,6 @@ const appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
collectd = require('./collectd.js'),
@@ -30,7 +25,6 @@ const appdb = require('./appdb.js'),
docker = require('./docker.js'),
domains = require('./domains.js'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
iputils = require('./iputils.js'),
manifestFormat = require('cloudron-manifestformat'),
@@ -75,8 +69,6 @@ function updateApp(app, values, callback) {
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'updating app with values: %j', values);
appdb.update(app.id, values, function (error) {
if (error) return callback(error);
@@ -271,7 +263,7 @@ function cleanupLogs(app, callback) {
// note that redis container logs are cleaned up by the addon
rimraf(path.join(paths.LOG_DIR, app.id), function (error) {
if (error) debugApp(app, 'cannot cleanup logs: %s', error);
if (error) debugApp(app, 'cannot cleanup logs:', error);
callback(null);
});
@@ -297,9 +289,9 @@ function downloadIcon(app, callback) {
// nothing to download if we dont have an appStoreId
if (!app.appStoreId) return callback(null);
debugApp(app, 'Downloading icon of %s@%s', app.appStoreId, app.manifest.version);
debugApp(app, `Downloading icon of ${app.appStoreId}@${app.manifest.version}`);
var iconUrl = settings.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
const iconUrl = settings.apiServerOrigin() + '/api/v1/apps/' + app.appStoreId + '/versions/' + app.manifest.version + '/icon';
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
superagent
@@ -310,105 +302,11 @@ function downloadIcon(app, callback) {
if (error && !error.response) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error downloading icon : ${error.message}`));
if (res.statusCode !== 200) return retryCallback(null); // ignore error. this can also happen for apps installed with cloudron-cli
const iconPath = path.join(paths.APP_ICONS_DIR, app.id + '.png');
if (!safe.fs.writeFileSync(iconPath, res.body)) return retryCallback(new BoxError(BoxError.FS_ERROR, `Error saving icon to ${iconPath}: ${safe.error.message}`));
retryCallback(null);
updateApp(app, { appStoreIcon: res.body }, retryCallback);
});
}, callback);
}
function removeIcon(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove icon : %s', safe.error);
}
if (!safe.fs.unlinkSync(path.join(paths.APP_ICONS_DIR, app.id + '.user.png'))) {
if (safe.error.code !== 'ENOENT') debugApp(app, 'cannot remove user icon : %s', safe.error);
}
callback(null);
}
function registerSubdomains(app, overwrite, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains);
debugApp(app, `registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
async.eachSeries(allDomains, function (domain, iteratorDone) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
// get the current record before updating it
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
if (error && error.reason === BoxError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
if (error && error.reason === BoxError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain }));
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain }));
if (error) return retryCallback(null, new BoxError(BoxError.EXTERNAL_ERROR, error.message, domain)); // give up for other errors
if (values.length !== 0 && values[0] === ip) return retryCallback(null); // up-to-date
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwrite) return retryCallback(null, new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain }));
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
debugApp(app, 'registerSubdomains: Upsert error. Will retry.', error.message);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, domain) : null);
});
});
}, function (error, result) {
if (error || result) return iteratorDone(error || result);
iteratorDone(null);
});
}, callback);
});
}
function unregisterSubdomains(app, allDomains, callback) {
assert.strictEqual(typeof app, 'object');
assert(Array.isArray(allDomains));
assert.strictEqual(typeof callback, 'function');
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(allDomains, function (domain, iteratorDone) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
debugApp(app, 'registerSubdomains: Remove error. Will retry.', error.message);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
}
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain }) : null);
});
}, function (error, result) {
if (error || result) return iteratorDone(error || result);
iteratorDone();
});
}, callback);
});
}
function waitForDnsPropagation(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -492,6 +390,7 @@ function install(app, args, progressCallback, callback) {
const restoreConfig = args.restoreConfig; // has to be set when restoring
const overwriteDns = args.overwriteDns;
const skipDnsSetup = args.skipDnsSetup;
const oldManifest = args.oldManifest;
async.series([
@@ -533,8 +432,15 @@ function install(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
registerSubdomains.bind(null, app, overwriteDns),
function setupDnsIfNeeded(done) {
if (skipDnsSetup) return done();
async.series([
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
domains.registerLocations.bind(null, [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback)
], done);
},
progressCallback.bind(null, { percent: 40, message: 'Downloading image' }),
downloadImage.bind(null, app.manifest),
@@ -553,6 +459,7 @@ function install(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 60, message: 'Importing addons in-place' }),
services.setupAddons.bind(null, app, app.manifest.addons),
services.clearAddons.bind(null, app, _.omit(app.manifest.addons, 'localstorage')),
apps.restoreConfig.bind(null, app),
services.restoreAddons.bind(null, app, app.manifest.addons),
], next);
} else {
@@ -563,18 +470,26 @@ function install(app, args, progressCallback, callback) {
backups.downloadApp.bind(null, app, restoreConfig, (progress) => {
progressCallback({ percent: 65, message: progress.message });
}),
(done) => { if (app.installationState === apps.ISTATE_PENDING_IMPORT) apps.restoreConfig(app, done); else done(); },
progressCallback.bind(null, { percent: 70, message: 'Restoring addons' }),
services.restoreAddons.bind(null, app, app.manifest.addons)
], next);
}
},
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
progressCallback.bind(null, { percent: 80, message: 'Creating container' }),
createContainer.bind(null, app),
startApp.bind(null, app),
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
function waitForDns(done) {
if (skipDnsSetup) return done();
async.series([
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
], done);
},
progressCallback.bind(null, { percent: 95, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
@@ -583,7 +498,7 @@ function install(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error installing app: %s', error);
debugApp(app, 'error installing app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
@@ -606,7 +521,7 @@ function backup(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error backing up app: %s', error);
debugApp(app, 'error backing up app:', error);
// return to installed state intentionally. the error is stashed only in the task and not the app (the UI shows error state otherwise)
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null }, callback.bind(null, makeTaskError(error, app)));
}
@@ -637,7 +552,7 @@ function create(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error creating : %s', error);
debugApp(app, 'error creating :', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
@@ -652,6 +567,7 @@ function changeLocation(app, args, progressCallback, callback) {
const oldConfig = args.oldConfig;
const locationChanged = oldConfig.fqdn !== app.fqdn;
const skipDnsSetup = args.skipDnsSetup;
const overwriteDns = args.overwriteDns;
async.series([
@@ -673,11 +589,17 @@ function changeLocation(app, args, progressCallback, callback) {
if (obsoleteDomains.length === 0) return next();
unregisterSubdomains(app, obsoleteDomains, next);
domains.unregisterLocations(obsoleteDomains, progressCallback, next);
},
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
registerSubdomains.bind(null, app, overwriteDns),
function setupDnsIfNeeded(done) {
if (skipDnsSetup) return done();
async.series([
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
domains.registerLocations.bind(null, [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback)
], done);
},
// re-setup addons since they rely on the app's fqdn (e.g oauth)
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
@@ -688,8 +610,14 @@ function changeLocation(app, args, progressCallback, callback) {
startApp.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
function waitForDns(done) {
if (skipDnsSetup) return done();
async.series([
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
], done);
},
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),
@@ -698,7 +626,7 @@ function changeLocation(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error changing location : %s', error);
debugApp(app, 'error changing location:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
@@ -737,7 +665,7 @@ function migrateDataDir(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, dataDir: newDataDir })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error migrating data dir : %s', error);
debugApp(app, 'error migrating data dir:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
@@ -782,7 +710,7 @@ function configure(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error reconfiguring : %s', error);
debugApp(app, 'error reconfiguring:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
@@ -893,10 +821,10 @@ function update(app, args, progressCallback, callback) {
debugApp(app, 'update aborted because backup failed', error);
updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback.bind(null, error));
} else if (error) {
debugApp(app, 'Error updating app: %s', error);
debugApp(app, 'Error updating app:', error);
updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
} else {
eventlog.add(eventlog.ACTION_APP_UPDATE_FINISH, auditSource.APP_TASK, { app: app, success: true }, () => callback()); // ignore error
callback(null);
}
});
}
@@ -925,7 +853,7 @@ function start(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app: %s', error);
debugApp(app, 'error starting app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
@@ -952,7 +880,7 @@ function stop(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app: %s', error);
debugApp(app, 'error starting app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
@@ -973,7 +901,7 @@ function restart(app, args, progressCallback, callback) {
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
if (error) {
debugApp(app, 'error starting app: %s', error);
debugApp(app, 'error starting app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
@@ -1003,10 +931,7 @@ function uninstall(app, args, progressCallback, callback) {
docker.deleteImage.bind(null, app.manifest),
progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }),
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains).concat(app.aliasDomains)),
progressCallback.bind(null, { percent: 80, message: 'Cleanup icon' }),
removeIcon.bind(null, app),
domains.unregisterLocations.bind(null, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains).concat(app.aliasDomains), progressCallback),
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
cleanupLogs.bind(null, app),
@@ -1015,7 +940,7 @@ function uninstall(app, args, progressCallback, callback) {
appdb.del.bind(null, app.id)
], function seriesDone(error) {
if (error) {
debugApp(app, 'error uninstalling app: %s', error);
debugApp(app, 'error uninstalling app:', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
@@ -1032,12 +957,13 @@ function run(appId, args, progressCallback, callback) {
apps.get(appId, function (error, app) {
if (error) return callback(error);
debugApp(app, 'startTask installationState: %s runState: %s', app.installationState, app.runState);
debugApp(app, `startTask installationState: ${app.installationState} runState: ${app.runState}`);
switch (app.installationState) {
case apps.ISTATE_PENDING_INSTALL:
case apps.ISTATE_PENDING_CLONE:
case apps.ISTATE_PENDING_RESTORE:
case apps.ISTATE_PENDING_IMPORT:
return install(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_CONFIGURE:
return configure(app, args, progressCallback, callback);

View File

@@ -52,7 +52,7 @@ function scheduleTask(appId, taskId, options, callback) {
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug(`Reached concurrency limit, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
gPendingTasks.push({ appId, taskId, options, callback });
return;
}
@@ -61,7 +61,7 @@ function scheduleTask(appId, taskId, options, callback) {
if (lockError) {
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, callback });
gPendingTasks.push({ appId, taskId, options, callback });
return;
}
@@ -79,18 +79,20 @@ function scheduleTask(appId, taskId, options, callback) {
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
// post app task hooks
services.rebuildService('sftp', error => { if (error) debug('Unable to rebuild sftp:', error); });
scheduler.resumeJobs(appId);
});
}
function startNextTask() {
if (gPendingTasks.length === 0) return;
if (gPendingTasks.length === 0) {
// rebuild sftp when task queue is empty. this minimizes risk of sftp rebuild overlapping with other app tasks
services.rebuildService('sftp', error => { if (error) debug('Unable to rebuild sftp:', error); });
return;
}
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
const t = gPendingTasks.shift();
scheduleTask(t.appId, t.taskId, t.callback);
scheduleTask(t.appId, t.taskId, t.options, t.callback);
}

View File

@@ -3,14 +3,13 @@
exports = module.exports = {
CRON: { userId: null, username: 'cron' },
HEALTH_MONITOR: { userId: null, username: 'healthmonitor' },
APP_TASK: { userId: null, username: 'apptask' },
EXTERNAL_LDAP_TASK: { userId: null, username: 'externalldap' },
EXTERNAL_LDAP_AUTO_CREATE: { userId: null, username: 'externalldap' },
fromRequest: fromRequest
fromRequest
};
function fromRequest(req) {
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return { ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}

29
src/autoconfig.xml.ejs Normal file
View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<clientConfig version="1.1">
<emailProvider id="<%= domain %>">
<domain><%= domain %></domain>
<displayName>Cloudron Mail</displayName>
<displayShortName>Cloudron</displayShortName>
<incomingServer type="imap">
<hostname><%= mailFqdn %></hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname><%= mailFqdn %></hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
<addThisServer>true</addThisServer>
</outgoingServer>
<documentation url="http://cloudron.io/email/#autodiscover">
<descr lang="en">Cloudron Email</descr>
</documentation>
</emailProvider>
</clientConfig>

View File

@@ -1,12 +1,11 @@
'use strict';
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
safe = require('safetydance'),
util = require('util');
safe = require('safetydance');
var BACKUPS_FIELDS = [ 'id', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
const BACKUPS_FIELDS = [ 'id', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
exports = module.exports = {
add,
@@ -18,6 +17,7 @@ exports = module.exports = {
get,
del,
update,
list,
_clear: clear
};
@@ -80,6 +80,21 @@ function getByIdentifierPaged(identifier, page, perPage, callback) {
});
}
function list(page, perPage, callback) {
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups ORDER BY creationTime DESC LIMIT ?,?',
[ (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -103,7 +118,7 @@ function add(id, data, callback) {
assert.strictEqual(typeof data.type, 'string');
assert.strictEqual(typeof data.identifier, 'string');
assert.strictEqual(typeof data.state, 'string');
assert(util.isArray(data.dependsOn));
assert(Array.isArray(data.dependsOn));
assert.strictEqual(typeof data.manifest, 'object');
assert.strictEqual(typeof data.format, 'string');
assert.strictEqual(typeof callback, 'function');

View File

@@ -26,11 +26,10 @@ exports = module.exports = {
injectPrivateFields,
removePrivateFields,
checkConfiguration,
configureCollectd,
generateEncryptionKeysSync,
isMountProvider,
BACKUP_IDENTIFIER_BOX: 'box',
@@ -96,6 +95,8 @@ function api(provider) {
case 'nfs': return require('./storage/filesystem.js');
case 'cifs': return require('./storage/filesystem.js');
case 'sshfs': return require('./storage/filesystem.js');
case 'mountpoint': return require('./storage/filesystem.js');
case 'ext4': return require('./storage/filesystem.js');
case 's3': return require('./storage/s3.js');
case 'gcs': return require('./storage/gcs.js');
case 'filesystem': return require('./storage/filesystem.js');
@@ -108,11 +109,17 @@ function api(provider) {
case 'backblaze-b2': return require('./storage/s3.js');
case 'linode-objectstorage': return require('./storage/s3.js');
case 'ovh-objectstorage': return require('./storage/s3.js');
case 'ionos-objectstorage': return require('./storage/s3.js');
case 'vultr-objectstorage': return require('./storage/s3.js');
case 'noop': return require('./storage/noop.js');
default: return null;
}
}
function isMountProvider(provider) {
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'ext4';
}
function injectPrivateFields(newConfig, currentConfig) {
if ('password' in newConfig) {
if (newConfig.password === constants.SECRET_PLACEHOLDER) {
@@ -138,7 +145,7 @@ function testConfig(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
var func = api(backupConfig.provider);
const func = api(backupConfig.provider);
if (!func) return callback(new BoxError(BoxError.BAD_FIELD, 'unknown storage provider', { field: 'provider' }));
if (backupConfig.format !== 'tgz' && backupConfig.format !== 'rsync') return callback(new BoxError(BoxError.BAD_FIELD, 'unknown format', { field: 'format' }));
@@ -549,21 +556,29 @@ function saveFsMetadata(dataLayout, metadataFile, callback) {
// contains paths prefixed with './'
let metadata = {
emptyDirs: [],
execFiles: []
execFiles: [],
symlinks: []
};
// we assume small number of files. spawnSync will raise a ENOBUFS error after maxBuffer
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);
const emptyDirs = safe.child_process.execSync(`find ${lp} -type d -empty`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 30 });
if (emptyDirs === null) return callback(new BoxError(BoxError.FS_ERROR, `Error finding empty dirs: ${safe.error.message}`));
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);
const execFiles = safe.child_process.execSync(`find ${lp} -type f -executable`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 30 });
if (execFiles === null) return callback(new BoxError(BoxError.FS_ERROR, `Error finding executables: ${safe.error.message}`));
if (execFiles.length) metadata.execFiles = metadata.execFiles.concat(execFiles.trim().split('\n').map((ef) => dataLayout.toRemotePath(ef)));
const symlinks = safe.child_process.execSync(`find ${lp} -type l`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 30 });
if (symlinks === null) return callback(new BoxError(BoxError.FS_ERROR, `Error finding symlinks: ${safe.error.message}`));
if (symlinks.length) metadata.symlinks = metadata.symlinks.concat(symlinks.trim().split('\n').map((sl) => {
const target = safe.fs.readlinkSync(sl);
return { path: dataLayout.toRemotePath(sl), target };
}));
}
if (!safe.fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 4))) return callback(safe.error);
if (!safe.fs.writeFileSync(metadataFile, JSON.stringify(metadata, null, 4))) return callback(new BoxError(BoxError.FS_ERROR, `Error writing fs metadata: ${safe.error.message}`));
callback();
}
@@ -691,7 +706,19 @@ function restoreFsMetadata(dataLayout, metadataFile, callback) {
}, function (error) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `unable to chmod: ${error.message}`));
callback();
async.eachSeries(metadata.symlinks || [], function createSymlink(symlink, iteratorDone) {
if (!symlink.target) return iteratorDone();
// the path may not exist if we had a directory full of symlinks
fs.mkdir(path.dirname(dataLayout.toLocalPath(symlink.path)), { recursive: true }, function (error) {
if (error) return iteratorDone(error);
fs.symlink(symlink.target, dataLayout.toLocalPath(symlink.path), 'file', iteratorDone);
});
}, function (error) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `unable to symlink: ${error.message}`));
callback();
});
});
});
}
@@ -805,7 +832,9 @@ function restore(backupConfig, backupId, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const dataLayout = new DataLayout(paths.BOX_DATA_DIR, []);
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
if (!boxDataDir) return callback(new BoxError(BoxError.FS_ERROR, `Error resolving boxdata: ${safe.error.message}`));
const dataLayout = new DataLayout(boxDataDir, []);
download(backupConfig, backupId, backupConfig.format, dataLayout, progressCallback, function (error) {
if (error) return callback(error);
@@ -829,7 +858,7 @@ function downloadApp(app, restoreConfig, progressCallback, callback) {
assert.strictEqual(typeof callback, 'function');
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
if (!appDataDir) return callback(safe.error);
if (!appDataDir) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
const startTime = new Date();
@@ -932,7 +961,7 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) {
if (error) return callback(error);
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
if (!boxDataDir) return callback(safe.error);
if (!boxDataDir) return callback(new BoxError(BoxError.FS_ERROR, `Error resolving boxdata: ${safe.error.message}`));
const uploadConfig = {
backupId: 'snapshot/box',
@@ -963,10 +992,7 @@ function rotateBoxBackup(backupConfig, tag, options, appBackupIds, progressCallb
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var snapshotInfo = getSnapshotInfo('box');
const snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); // add this to filename to make it unique, so it's easy to download them
const backupId = util.format('%s/box_%s_v%s', tag, snapshotTime, constants.VERSION);
const backupId = `${tag}/box_v${constants.VERSION}`;
const format = backupConfig.format;
debug(`Rotating box backup to id ${backupId}`);
@@ -1041,16 +1067,16 @@ function snapshotApp(app, progressCallback, callback) {
const startTime = new Date();
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(app))) {
return callback(new BoxError(BoxError.FS_ERROR, 'Error creating config.json: ' + safe.error.message));
}
apps.backupConfig(app, function (error) {
if (error) return callback(error);
services.backupAddons(app, app.manifest.addons, function (error) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
services.backupAddons(app, app.manifest.addons, function (error) {
if (error) return callback(error);
debugApp(app, `snapshotApp: took ${(new Date() - startTime)/1000} seconds`);
debugApp(app, `snapshotApp: took ${(new Date() - startTime)/1000} seconds`);
return callback(null);
return callback(null);
});
});
}
@@ -1064,11 +1090,10 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
const startTime = new Date();
var snapshotInfo = getSnapshotInfo(app.id);
const snapshotInfo = getSnapshotInfo(app.id);
var manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
const snapshotTime = snapshotInfo.timestamp.replace(/[T.]/g, '-').replace(/[:Z]/g,''); // add this for unique filename which helps when downloading them
const backupId = util.format('%s/app_%s_%s_v%s', tag, app.id, snapshotTime, manifest.version);
const manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
const backupId = `${tag}/app_${app.fqdn}_v${manifest.version}`;
const format = backupConfig.format;
debug(`Rotating app backup of ${app.id} to id ${backupId}`);
@@ -1087,7 +1112,7 @@ function rotateAppBackup(backupConfig, app, tag, options, progressCallback, call
backupdb.add(backupId, data, function (error) {
if (error) return callback(error);
var copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
const copy = api(backupConfig.provider).copy(backupConfig, getBackupFilePath(backupConfig, `snapshot/app_${app.id}`, format), getBackupFilePath(backupConfig, backupId, format));
copy.on('progress', (message) => progressCallback({ message: `${message} (${app.fqdn})` }));
copy.on('done', function (copyBackupError) {
const state = copyBackupError ? exports.BACKUP_STATE_ERROR : exports.BACKUP_STATE_NORMAL;
@@ -1115,7 +1140,7 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) {
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);
if (!appDataDir) return callback(new BoxError(BoxError.FS_ERROR, `Error resolving appsdata: ${safe.error.message}`));
const dataLayout = new DataLayout(appDataDir, app.dataDir ? [{ localDir: app.dataDir, remoteDir: 'data' }] : []);
@@ -1245,13 +1270,13 @@ function startBackupTask(auditSource, callback) {
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
tasks.startTask(taskId, { timeout: 24 * 60 * 60 * 1000 /* 24 hours */, nice: 15, memoryLimit }, function (error, backupId) {
tasks.startTask(taskId, { timeout: 24 * 60 * 60 * 1000 /* 24 hours */, nice: 15, memoryLimit }, async function (error, backupId) {
locker.unlock(locker.OP_FULL_BACKUP);
const errorMessage = error ? error.message : '';
const timedOut = error ? error.code === tasks.ETIMEOUT : false;
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId });
await safe(eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, errorMessage, timedOut, backupId }));
});
callback(null, taskId);
@@ -1323,7 +1348,7 @@ function cleanupBackup(backupConfig, backup, progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
var backupFilePath = getBackupFilePath(backupConfig, backup.id, backup.format);
const backupFilePath = getBackupFilePath(backupConfig, backup.id, backup.format);
function done(error) {
if (error) {
@@ -1427,6 +1452,48 @@ function cleanupBoxBackups(backupConfig, progressCallback, callback) {
});
}
function cleanupMissingBackups(backupConfig, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
let page = 1, perPage = 1000, more = false, missingBackupIds = [];
if (constants.TEST) return callback(null, missingBackupIds);
async.doWhilst(function (whilstCallback) {
backupdb.list(page, perPage, function (error, result) {
if (error) return whilstCallback(error);
async.eachSeries(result, function (backup, next) {
let backupFilePath = getBackupFilePath(backupConfig, backup.id, backup.format);
if (backup.format === 'rsync') backupFilePath = backupFilePath + '/'; // add trailing slash to indicate directory
api(backupConfig.provider).exists(backupConfig, backupFilePath, function (error, exists) {
if (error || exists) return next();
progressCallback({ message: `Removing missing backup ${backup.id}`});
backupdb.del(backup.id, function (error) {
if (error) debug(`cleanupBackup: error removing ${backup.id} from database`, error);
missingBackupIds.push(backup.id);
next();
});
});
}, function () {
more = result.length === perPage;
whilstCallback();
});
});
}, function (testDone) { return testDone(null, more); }, function (error) {
if (error) return callback(error);
return callback(null, missingBackupIds);
});
}
function cleanupCacheFilesSync() {
var files = safe.fs.readdirSync(path.join(paths.BACKUP_INFO_DIR));
if (!files) return;
@@ -1498,12 +1565,18 @@ function cleanup(progressCallback, callback) {
cleanupAppBackups(backupConfig, referencedAppBackupIds, progressCallback, function (error, removedAppBackupIds) {
if (error) return callback(error);
progressCallback({ percent: 90, message: 'Cleaning snapshots' });
progressCallback({ percent: 70, message: 'Cleaning missing backups' });
cleanupSnapshots(backupConfig, function (error) {
cleanupMissingBackups(backupConfig, progressCallback, function (error, missingBackupIds) {
if (error) return callback(error);
callback(null, { removedBoxBackupIds, removedAppBackupIds });
progressCallback({ percent: 90, message: 'Cleaning snapshots' });
cleanupSnapshots(backupConfig, function (error) {
if (error) return callback(error);
callback(null, { removedBoxBackupIds, removedAppBackupIds, missingBackupIds });
});
});
});
});
@@ -1515,12 +1588,13 @@ function startCleanupTask(auditSource, callback) {
tasks.add(tasks.TASK_CLEAN_BACKUPS, [], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackups, removedAppBackups }
tasks.startTask(taskId, {}, (error, result) => { // result is { removedBoxBackupIds, removedAppBackupIds, missingBackupIds }
eventlog.add(eventlog.ACTION_BACKUP_CLEANUP_FINISH, auditSource, {
taskId,
errorMessage: error ? error.message : null,
removedBoxBackups: result ? result.removedBoxBackups : [],
removedAppBackups: result ? result.removedAppBackups : []
removedBoxBackupIds: result ? result.removedBoxBackupIds : [],
removedAppBackupIds: result ? result.removedAppBackupIds : [],
missingBackupIds: result ? result.missingBackupIds : []
});
});
@@ -1528,23 +1602,6 @@ function startCleanupTask(auditSource, callback) {
});
}
function checkConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
let message = '';
if (backupConfig.provider === 'noop') {
message = 'Cloudron backups are disabled. Please ensure this server is backed up using alternate means. See https://docs.cloudron.io/backups/#storage-providers for more information.';
} else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) {
message = 'Cloudron backups are currently on the same disk as the Cloudron server instance. This is dangerous and can lead to complete data loss if the disk fails. See https://docs.cloudron.io/backups/#storage-providers for storing backups in an external location.';
}
callback(null, message);
});
}
function configureCollectd(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');

102
src/blobs.js Normal file
View File

@@ -0,0 +1,102 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
get,
set,
del,
initSecrets,
ACME_ACCOUNT_KEY: 'acme_account_key',
ADDON_TURN_SECRET: 'addon_turn_secret',
DHPARAMS: 'dhparams',
SFTP_PUBLIC_KEY: 'sftp_public_key',
SFTP_PRIVATE_KEY: 'sftp_private_key',
CERT_PREFIX: 'cert',
_clear: clear
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
database = require('./database.js'),
debug = require('debug')('box:blobs'),
paths = require('./paths.js'),
safe = require('safetydance');
const BLOBS_FIELDS = [ 'id', 'value' ].join(',');
async function get(id) {
assert.strictEqual(typeof id, 'string');
const result = await database.query(`SELECT ${BLOBS_FIELDS} FROM blobs WHERE id = ?`, [ id ]);
if (result.length === 0) return null;
return result[0].value;
}
async function set(id, value) {
assert.strictEqual(typeof id, 'string');
assert(value === null || Buffer.isBuffer(value));
await database.query('INSERT INTO blobs (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', [ id, value ]);
}
async function del(id) {
await database.query('DELETE FROM blobs WHERE id=?', [ id ]);
}
async function clear() {
await database.query('DELETE FROM blobs');
}
async function initSecrets() {
let acmeAccountKey = await get(exports.ACME_ACCOUNT_KEY);
if (!acmeAccountKey) {
acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096');
if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`);
await set(exports.ACME_ACCOUNT_KEY, acmeAccountKey);
}
let turnSecret = await get(exports.ADDON_TURN_SECRET);
if (!turnSecret) {
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
await set(exports.ADDON_TURN_SECRET, Buffer.from(turnSecret));
}
if (!constants.TEST) {
let dhparams = await get(exports.DHPARAMS);
if (!dhparams) {
debug('initSecrets: generating dhparams.pem. this takes forever');
dhparams = safe.child_process.execSync('openssl dhparam 2048');
if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
await set(exports.DHPARAMS, dhparams);
} else if (!safe.fs.existsSync(paths.DHPARAMS_FILE)) {
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
}
}
let sftpPrivateKey = await get(exports.SFTP_PRIVATE_KEY);
let sftpPublicKey = await get(exports.SFTP_PUBLIC_KEY);
if (!sftpPrivateKey || !sftpPublicKey) {
debug('initSecrets: generate sftp keys');
if (constants.TEST) {
safe.fs.unlinkSync(paths.SFTP_PUBLIC_KEY_FILE);
safe.fs.unlinkSync(paths.SFTP_PRIVATE_KEY_FILE);
}
if (!safe.child_process.execSync(`ssh-keygen -m PEM -t rsa -f "${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key" -q -N ""`)) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate sftp ssh keys: ${safe.error.message}`);
sftpPublicKey = safe.fs.readFileSync(paths.SFTP_PUBLIC_KEY_FILE);
await set(exports.SFTP_PUBLIC_KEY, sftpPublicKey);
sftpPrivateKey = safe.fs.readFileSync(paths.SFTP_PRIVATE_KEY_FILE);
await set(exports.SFTP_PRIVATE_KEY, sftpPrivateKey);
} else if (!safe.fs.existsSync(paths.SFTP_PUBLIC_KEY_FILE) || !safe.fs.existsSync(paths.SFTP_PRIVATE_KEY_FILE)) {
if (!safe.fs.writeFileSync(paths.SFTP_PUBLIC_KEY_FILE, sftpPublicKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp public key: ${safe.error.message}`);
if (!safe.fs.writeFileSync(paths.SFTP_PRIVATE_KEY_FILE, sftpPrivateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp private key: ${safe.error.message}`);
}
}

View File

@@ -9,17 +9,17 @@ const assert = require('assert'),
exports = module.exports = BoxError;
function BoxError(reason, errorOrMessage, details) {
function BoxError(reason, errorOrMessage, override) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
assert(typeof details === 'object' || typeof details === 'undefined');
assert(typeof override === 'object' || typeof override === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
this.details = details || {};
this.details = {};
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
@@ -28,7 +28,7 @@ function BoxError(reason, errorOrMessage, details) {
} else { // error object
this.message = errorOrMessage.message;
this.nestedError = errorOrMessage;
_.extend(this.details, errorOrMessage); // copy enumerable properies
_.extend(this, override); // copy enumerable properies
}
}
util.inherits(BoxError, Error);
@@ -47,13 +47,14 @@ BoxError.DOCKER_ERROR = 'Docker Error';
BoxError.EXTERNAL_ERROR = 'External Error'; // use this for external API errors
BoxError.FEATURE_DISABLED = 'Feature Disabled';
BoxError.FS_ERROR = 'FileSystem Error';
BoxError.INACTIVE = 'Inactive';
BoxError.INACTIVE = 'Inactive'; // service/volume/mount
BoxError.INTERNAL_ERROR = 'Internal Error';
BoxError.INVALID_CREDENTIALS = 'Invalid Credentials';
BoxError.IPTABLES_ERROR = 'IPTables Error';
BoxError.LICENSE_ERROR = 'License Error';
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
BoxError.MAIL_ERROR = 'Mail Error';
BoxError.MOUNT_ERROR = 'Mount Error';
BoxError.NETWORK_ERROR = 'Network Error';
BoxError.NGINX_ERROR = 'Nginx Error';
BoxError.NOT_FOUND = 'Not found';
@@ -90,6 +91,7 @@ BoxError.toHttpError = function (error) {
case BoxError.EXTERNAL_ERROR:
case BoxError.NETWORK_ERROR:
case BoxError.FS_ERROR:
case BoxError.MOUNT_ERROR:
case BoxError.MAIL_ERROR:
case BoxError.DOCKER_ERROR:
case BoxError.ADDONS_ERROR:

View File

@@ -1,628 +0,0 @@
'use strict';
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('../domains.js'),
fs = require('fs'),
path = require('path'),
paths = require('../paths.js'),
request = require('request'),
safe = require('safetydance'),
util = require('util'),
_ = require('underscore');
const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
CA_STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'acme',
_getChallengeSubdomain: getChallengeSubdomain
};
// http://jose.readthedocs.org/en/latest/
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
function Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = null; // Buffer
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
this.directory = {};
this.performHttpAuthorization = !!options.performHttpAuthorization;
this.wildcard = !!options.wildcard;
}
// urlsafe base64 encoding (jose)
function urlBase64Encode(string) {
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
function b64(str) {
var buf = util.isBuffer(str) ? str : Buffer.from(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(util.isBuffer(pem));
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
if (!match) return null;
return Buffer.from(match[1], 'hex');
}
Acme2.prototype.sendSignedRequest = function (url, payload, callback) {
assert.strictEqual(typeof url, 'string');
assert.strictEqual(typeof payload, 'string');
assert.strictEqual(typeof callback, 'function');
assert(util.isBuffer(this.accountKeyPem));
const that = this;
let header = {
url: url,
alg: 'RS256'
};
// keyId is null when registering account
if (this.keyId) {
header.kid = this.keyId;
} else {
header.jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
}
var payload64 = b64(payload);
request.get(this.directory.newNonce, { json: true, timeout: 30000 }, function (error, response) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`));
if (response.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching nonce : ' + response.statusCode));
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
if (!nonce) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response'));
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
var signer = crypto.createSign('RSA-SHA256');
signer.update(protected64 + '.' + payload64, 'utf8');
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
var data = {
protected: protected64,
payload: payload64,
signature: signature64
};
request.post(url, { headers: { 'Content-Type': 'application/jose+json', 'User-Agent': 'acme-cloudron' }, body: JSON.stringify(data), timeout: 30000 }, function (error, response) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`)); // network error
// we don't set json: true in request because it ends up mangling the content-type
// we don't set json: true in request because it ends up mangling the content-type
if (response.headers['content-type'] === 'application/json') response.body = safe.JSON.parse(response.body);
callback(null, response);
});
});
};
// https://tools.ietf.org/html/rfc8555#section-6.3
Acme2.prototype.postAsGet = function (url, callback) {
this.sendSignedRequest(url, '', callback);
};
Acme2.prototype.updateContact = function (registrationUri, callback) {
assert.strictEqual(typeof registrationUri, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`updateContact: registrationUri: ${registrationUri} email: ${this.email}`);
// https://github.com/ietf-wg-acme/acme/issues/30
const payload = {
contact: [ 'mailto:' + this.email ]
};
const that = this;
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to update contact. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
debug(`updateContact: contact of user updated to ${that.email}`);
callback();
});
};
Acme2.prototype.registerUser = function (callback) {
assert.strictEqual(typeof callback, 'function');
var payload = {
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
var that = this;
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
// 200 if already exists. 201 for new accounts
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.statusCode} ${JSON.stringify(result.body)}`));
debug(`registerUser: user registered keyid: ${result.headers.location}`);
that.keyId = result.headers.location;
that.updateContact(result.headers.location, callback);
});
};
Acme2.prototype.newOrder = function (domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
var payload = {
identifiers: [{
type: 'dns',
value: domain
}]
};
debug('newOrder: %s', domain);
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`));
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order'));
if (typeof order.finalize !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order'));
if (typeof orderUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header'));
callback(null, order, orderUrl);
});
};
Acme2.prototype.waitForOrder = function (orderUrl, callback) {
assert.strictEqual(typeof orderUrl, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`waitForOrder: ${orderUrl}`);
const that = this;
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitForOrder: getting status');
that.postAsGet(orderUrl, function (error, result) {
if (error) {
debug('waitForOrder: network error getting uri %s', orderUrl);
return retryCallback(error);
}
if (result.statusCode !== 200) {
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`));
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
});
}, callback);
};
Acme2.prototype.getKeyAuthorization = function (token) {
assert(util.isBuffer(this.accountKeyPem));
let jwk = {
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
kty: 'RSA',
n: b64(getModulus(this.accountKeyPem))
};
let shasum = crypto.createHash('sha256');
shasum.update(JSON.stringify(jwk));
let thumbprint = urlBase64Encode(shasum.digest('base64'));
return token + '.' + thumbprint;
};
Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object'); // { type, status, url, token }
assert.strictEqual(typeof callback, 'function');
debug('notifyChallengeReady: %s was met', challenge.url);
const keyAuthorization = this.getKeyAuthorization(challenge.token);
var payload = {
resource: 'challenge',
keyAuthorization: keyAuthorization
};
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
callback();
});
};
Acme2.prototype.waitForChallenge = function (challenge, callback) {
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('waitingForChallenge: %j', challenge);
const that = this;
async.retry({ times: 15, interval: 20000 }, function (retryCallback) {
debug('waitingForChallenge: getting status');
that.postAsGet(challenge.url, function (error, result) {
if (error) {
debug('waitForChallenge: network error getting uri %s', challenge.url);
return retryCallback(error);
}
if (result.statusCode !== 200) {
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s" %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new BoxError(BoxError.TRY_AGAIN));
else if (result.body.status === 'valid') return retryCallback();
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
});
}, function retryFinished(error) {
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
callback(error);
});
};
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof finalizationUrl, 'string');
assert(util.isBuffer(csrDer));
assert.strictEqual(typeof callback, 'function');
const payload = {
csr: b64(csrDer)
};
debug('signCertificate: sending sign request');
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
if (error) return callback(error);
// 429 means we reached the cert limit for this domain
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
return callback(null);
});
};
Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
const certName = hostname.replace('*.', '_.');
var csrFile = path.join(outdir, `${certName}.csr`);
var privateKeyFile = path.join(outdir, `${certName}.key`);
if (safe.fs.existsSync(privateKeyFile)) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
}
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
if (!csrDer) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); // bookkeeping
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
callback(null, csrDer);
};
Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof certUrl, 'string');
assert.strictEqual(typeof callback, 'function');
var outdir = paths.APP_CERTS_DIR;
const that = this;
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
debug('downloadCertificate: downloading certificate');
that.postAsGet(certUrl, function (error, result) {
if (error) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${error.message}`));
if (result.statusCode === 202) return retryCallback(new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate'));
if (result.statusCode !== 200) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
const fullChainPem = result.body; // buffer
const certName = hostname.replace('*.', '_.');
var certificateFile = path.join(outdir, `${certName}.cert`);
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return retryCallback(new BoxError(BoxError.FS_ERROR, safe.error));
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
retryCallback(null);
});
}, callback);
};
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges'));
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
let keyAuthorization = this.getKeyAuthorization(challenge.token);
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
callback(null, challenge);
});
};
Acme2.prototype.cleanupHttpChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug('cleanupHttpChallenge: unlinking %s', path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
fs.unlink(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), callback);
};
function getChallengeSubdomain(hostname, domain) {
let challengeSubdomain;
if (hostname === domain) {
challengeSubdomain = '_acme-challenge';
} else if (hostname.includes('*')) { // wildcard
let subdomain = hostname.slice(0, -domain.length - 1);
challengeSubdomain = subdomain ? subdomain.replace('*', '_acme-challenge') : '_acme-challenge';
} else {
challengeSubdomain = '_acme-challenge.' + hostname.slice(0, -domain.length - 1);
}
debug(`getChallengeSubdomain: challenge subdomain for hostname ${hostname} at domain ${domain} is ${challengeSubdomain}`);
return challengeSubdomain;
}
Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorization, 'object');
assert.strictEqual(typeof callback, 'function');
debug('acmeFlow: challenges: %j', authorization);
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges'));
let challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(error);
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
if (error) return callback(error);
callback(null, challenge);
});
});
};
Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
const keyAuthorization = this.getKeyAuthorization(challenge.token);
let shasum = crypto.createHash('sha256');
shasum.update(keyAuthorization);
const txtValue = urlBase64Encode(shasum.digest('base64'));
let challengeSubdomain = getChallengeSubdomain(hostname, domain);
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(error);
callback(null);
});
};
Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof authorizationUrl, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
const that = this;
this.postAsGet(authorizationUrl, function (error, response) {
if (error) return callback(error);
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code getting authorization : ' + response.statusCode));
const authorization = response.body;
if (that.performHttpAuthorization) {
that.prepareHttpChallenge(hostname, domain, authorization, callback);
} else {
that.prepareDnsChallenge(hostname, domain, authorization, callback);
}
});
};
Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof challenge, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
if (this.performHttpAuthorization) {
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
} else {
this.cleanupDnsChallenge(hostname, domain, challenge, callback);
}
};
Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
debug('getCertificate: generating acme account key on first run');
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
if (!this.accountKeyPem) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
} else {
debug('getCertificate: using existing acme account key');
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
}
var that = this;
this.registerUser(function (error) {
if (error) return callback(error);
that.newOrder(hostname, function (error, order, orderUrl) {
if (error) return callback(error);
async.eachSeries(order.authorizations, function (authorizationUrl, iteratorCallback) {
debug(`acmeFlow: authorizing ${authorizationUrl}`);
that.prepareChallenge(hostname, domain, authorizationUrl, function (error, challenge) {
if (error) return iteratorCallback(error);
async.waterfall([
that.notifyChallengeReady.bind(that, challenge),
that.waitForChallenge.bind(that, challenge),
that.createKeyAndCsr.bind(that, hostname),
that.signCertificate.bind(that, hostname, order.finalize),
that.waitForOrder.bind(that, orderUrl),
that.downloadCertificate.bind(that, hostname)
], function (error) {
that.cleanupChallenge(hostname, domain, challenge, function (cleanupError) {
if (cleanupError) debug('acmeFlow: ignoring error when cleaning up challenge:', cleanupError);
iteratorCallback(error);
});
});
});
}, callback);
});
});
};
Acme2.prototype.getDirectory = function (callback) {
const that = this;
request.get(this.caDirectory, { json: true, timeout: 30000 }, function (error, response) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error getting directory: ${error.message}`));
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code when fetching directory : ' + response.statusCode));
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response body : ${response.body}`));
that.directory = response.body;
callback(null);
});
};
Acme2.prototype.getCertificate = function (vhost, domain, callback) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getCertificate: start acme flow for ${vhost} from ${this.caDirectory}`);
if (vhost !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
vhost = domains.makeWildcard(vhost);
debug(`getCertificate: will get wildcard cert for ${vhost}`);
}
const that = this;
this.getDirectory(function (error) {
if (error) return callback(error);
that.acmeFlow(vhost, domain, function (error) {
if (error) return callback(error);
var outdir = paths.APP_CERTS_DIR;
const certName = vhost.replace('*.', '_.');
callback(null, path.join(outdir, `${certName}.cert`), path.join(outdir, `${certName}.key`));
});
});
};
function getCertificate(vhost, domain, options, callback) {
assert.strictEqual(typeof vhost, 'string'); // this can also be a wildcard domain (for alias domains)
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let attempt = 1;
async.retry({ times: 3, interval: 0 }, function (retryCallback) {
debug(`getCertificate: attempt ${attempt++}`);
let acme = new Acme2(options || { });
acme.getCertificate(vhost, domain, retryCallback);
}, callback);
}

View File

@@ -5,7 +5,7 @@ let assert = require('assert'),
path = require('path');
exports = module.exports = {
getChanges: getChanges
getChanges
};
function getChanges(version) {

View File

@@ -17,6 +17,7 @@ exports = module.exports = {
setDashboardDomain,
updateDashboardDomain,
renewCerts,
syncDnsRecords,
runSystemChecks
};
@@ -51,16 +52,14 @@ const apps = require('./apps.js'),
tasks = require('./tasks.js'),
users = require('./users.js');
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
async function initialize() {
runStartupTasks();
notifyUpdate(callback);
await notifyUpdate();
}
function uninitialize(callback) {
@@ -72,42 +71,37 @@ function uninitialize(callback) {
], callback);
}
function onActivated(callback) {
function onActivated(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('onActivated: running post activation tasks');
// Starting the platform after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
async.series([
platform.start,
platform.start.bind(null, options),
cron.startJobs,
function checkBackupConfiguration(done) {
backups.checkConfiguration(function (error, message) {
if (error) return done(error);
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, done);
});
},
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
// the UI some time to query the dashboard domain in the restore code path
(done) => setTimeout(() => reverseProxy.writeDefaultConfig({ activated :true }, done), 30000)
], callback);
}
function notifyUpdate(callback) {
assert.strictEqual(typeof callback, 'function');
async function notifyUpdate() {
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
if (version === constants.VERSION) return callback();
if (version === constants.VERSION) return;
eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { errorMessage: '', oldVersion: version || 'dev', newVersion: constants.VERSION }, function (error) {
if (error) return callback(error);
await eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { errorMessage: '', oldVersion: version || 'dev', newVersion: constants.VERSION });
return new Promise((resolve, reject) => {
tasks.setCompletedByType(tasks.TASK_UPDATE, { error: null }, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); // when hotfixing, task may not exist
if (error && error.reason !== BoxError.NOT_FOUND) return reject(error); // when hotfixing, task may not exist
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
callback();
resolve();
});
});
}
@@ -129,9 +123,9 @@ function runStartupTasks() {
// always generate webadmin config since we have no versioning mechanism for the ejs
function (callback) {
if (!settings.adminDomain()) return callback();
if (!settings.dashboardDomain()) return callback();
reverseProxy.writeDashboardConfig(settings.adminDomain(), callback);
reverseProxy.writeDashboardConfig(settings.dashboardDomain(), callback);
},
// check activation state and start the platform
@@ -147,7 +141,7 @@ function runStartupTasks() {
return reverseProxy.writeDefaultConfig({ activated: false }, callback);
}
onActivated(callback);
onActivated({}, callback);
});
}
];
@@ -163,6 +157,10 @@ function runStartupTasks() {
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
const release = safe.fs.readFileSync('/etc/lsb-release', 'utf-8');
if (release === null) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
const ubuntuVersion = release.match(/DISTRIB_DESCRIPTION="(.*)"/)[1];
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
@@ -170,10 +168,11 @@ function getConfig(callback) {
callback(null, {
apiServerOrigin: settings.apiServerOrigin(),
webServerOrigin: settings.webServerOrigin(),
adminDomain: settings.adminDomain(),
adminFqdn: settings.adminFqdn(),
adminDomain: settings.dashboardDomain(),
adminFqdn: settings.dashboardFqdn(),
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
ubuntuVersion,
isDemo: settings.isDemo(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
@@ -203,17 +202,18 @@ function isRebootRequired(callback) {
function runSystemChecks(callback) {
assert.strictEqual(typeof callback, 'function');
debug('runSystemChecks: checking status');
async.parallel([
checkMailStatus,
checkRebootRequired
checkRebootRequired,
checkUbuntuVersion
], callback);
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking mail status');
mail.checkConfiguration(function (error, message) {
if (error) return callback(error);
@@ -224,8 +224,6 @@ function checkMailStatus(callback) {
function checkRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking if reboot required');
isRebootRequired(function (error, rebootRequired) {
if (error) return callback(error);
@@ -233,6 +231,15 @@ function checkRebootRequired(callback) {
});
}
function checkUbuntuVersion(callback) {
assert.strictEqual(typeof callback, 'function');
const isXenial = fs.readFileSync('/etc/lsb-release', 'utf-8').includes('16.04');
if (!isXenial) return callback();
notifications.alert(notifications.ALERT_UPDATE_UBUNTU, 'Ubuntu upgrade required', 'Ubuntu 16.04 has reached end of life and will not receive security and maintenance updates. Please follow https://docs.cloudron.io/guides/upgrade-ubuntu-18/ to upgrade to Ubuntu 18 at the earliest.', callback);
}
function getLogs(unit, options, callback) {
assert.strictEqual(typeof unit, 'string');
assert(options && typeof options === 'object');
@@ -291,7 +298,7 @@ function prepareDashboardDomain(domain, auditSource, callback) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
const fqdn = domains.fqdn(constants.DASHBOARD_LOCATION, domainObject);
apps.getAll(function (error, result) {
if (error) return callback(error);
@@ -299,7 +306,7 @@ function prepareDashboardDomain(domain, auditSource, callback) {
const conflict = result.filter(app => app.fqdn === fqdn);
if (conflict.length) return callback(new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.ADMIN_LOCATION, domain, auditSource ], function (error, taskId) {
tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.DASHBOARD_LOCATION, domain, auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
@@ -324,12 +331,14 @@ function setDashboardDomain(domain, auditSource, callback) {
reverseProxy.writeDashboardConfig(domain, function (error) {
if (error) return callback(error);
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
const fqdn = domains.fqdn(constants.DASHBOARD_LOCATION, domainObject);
settings.setAdminLocation(domain, fqdn, function (error) {
settings.setDashboardLocation(domain, fqdn, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
appstore.updateCloudron({ domain }, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain, fqdn });
callback(null);
});
@@ -361,7 +370,7 @@ function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
tasks.add(tasks.TASK_RENEW_CERTS, [ options, auditSource ], function (error, taskId) {
tasks.add(tasks.TASK_CHECK_CERTS, [ options, auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
@@ -380,17 +389,17 @@ function setupDnsAndCert(subdomain, domain, auditSource, progressCallback, callb
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = domains.fqdn(subdomain, domainObject);
const dashboardFqdn = domains.fqdn(subdomain, domainObject);
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.series([
(done) => { progressCallback({ message: `Updating DNS of ${adminFqdn}` }); done(); },
(done) => { progressCallback({ message: `Updating DNS of ${dashboardFqdn}` }); done(); },
domains.upsertDnsRecords.bind(null, subdomain, domain, 'A', [ ip ]),
(done) => { progressCallback({ message: `Waiting for DNS of ${adminFqdn}` }); done(); },
(done) => { progressCallback({ message: `Waiting for DNS of ${dashboardFqdn}` }); done(); },
domains.waitForDnsRecord.bind(null, subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ message: `Getting certificate of ${adminFqdn}` }); done(); },
(done) => { progressCallback({ message: `Getting certificate of ${dashboardFqdn}` }); done(); },
reverseProxy.ensureCertificate.bind(null, domains.fqdn(subdomain, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
@@ -400,3 +409,16 @@ function setupDnsAndCert(subdomain, domain, auditSource, progressCallback, callb
});
});
}
function syncDnsRecords(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
tasks.add(tasks.TASK_SYNC_DNS_RECORDS, [ options ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
}

View File

@@ -22,7 +22,7 @@ exports = module.exports = {
'admins', 'users' // ldap code uses 'users' pseudo group
],
ADMIN_LOCATION: 'my',
DASHBOARD_LOCATION: 'my',
PORT: CLOUDRON ? 3000 : 5454,
INTERNAL_SMTP_PORT: 2525, // this value comes from the mail container
@@ -32,7 +32,8 @@ exports = module.exports = {
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
DEFAULT_TOKEN_EXPIRATION: 365 * 24 * 60 * 60 * 1000, // 1 year
DEFAULT_TOKEN_EXPIRATION_MSECS: 365 * 24 * 60 * 60 * 1000, // 1 year
DEFAULT_TOKEN_EXPIRATION_DAYS: 365,
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js

View File

@@ -1,10 +1,10 @@
'use strict';
exports = module.exports = {
sendFailureLogs: sendFailureLogs
sendFailureLogs
};
var assert = require('assert'),
const assert = require('assert'),
auditSource = require('./auditsource.js'),
eventlog = require('./eventlog.js'),
safe = require('safetydance'),
@@ -38,7 +38,7 @@ function sendFailureLogs(unitName, callback) {
return callback();
}
collectLogs(unitName, function (error, logs) {
collectLogs(unitName, async function (error, logs) {
if (error) {
console.error('Failed to collect logs.', error);
logs = util.format('Failed to collect logs.', error);
@@ -49,12 +49,11 @@ function sendFailureLogs(unitName, callback) {
if (!safe.fs.writeFileSync(path.join(paths.CRASH_LOG_DIR, `${crashId}.log`), logs)) console.log(`Failed to stash logs to ${crashId}.log:`, safe.error);
eventlog.add(eventlog.ACTION_PROCESS_CRASH, auditSource.HEALTH_MONITOR, { processName: unitName, crashId: crashId }, function (error) {
if (error) console.log(`Error sending crashlog. Logs stashed at ${crashId}.log`);
[error] = await safe(eventlog.add(eventlog.ACTION_PROCESS_CRASH, auditSource.HEALTH_MONITOR, { processName: unitName, crashId: crashId }));
if (error) console.log(`Error sending crashlog. Logs stashed at ${crashId}.log`);
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
callback();
});
callback();
});
}

View File

@@ -16,7 +16,7 @@ exports = module.exports = {
DEFAULT_AUTOUPDATE_PATTERN,
};
var appHealthMonitor = require('./apphealthmonitor.js'),
const appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
@@ -33,9 +33,10 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
settings = require('./settings.js'),
system = require('./system.js'),
updater = require('./updater.js'),
updateChecker = require('./updatechecker.js');
updateChecker = require('./updatechecker.js'),
_ = require('underscore');
var gJobs = {
const gJobs = {
autoUpdater: null,
backup: null,
updateChecker: null,
@@ -51,7 +52,7 @@ var gJobs = {
appHealthMonitor: null
};
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// cron format
// Seconds: 0-59
@@ -100,7 +101,7 @@ function startJobs(callback) {
gJobs.cleanupEventlog = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: eventlog.cleanup,
onTick: eventlog.cleanup.bind(null, new Date(Date.now() - 60 * 60 * 24 * 10 * 1000)), // 10 days ago
start: true
});
@@ -198,9 +199,10 @@ function autoupdatePatternChanged(pattern, tz) {
return;
}
if (updateInfo.apps && Object.keys(updateInfo.apps).length > 0) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, auditSource.CRON, NOOP_CALLBACK);
const appUpdateInfo = _.omit(updateInfo, 'box');
if (Object.keys(appUpdateInfo).length > 0) {
debug('Starting app update to %j', appUpdateInfo);
apps.autoupdateApps(appUpdateInfo, auditSource.CRON, NOOP_CALLBACK);
} else {
debug('No app auto updates available');
}

View File

@@ -1,18 +1,18 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
query: query,
transaction: transaction,
initialize,
uninitialize,
query,
transaction,
importFromFile: importFromFile,
exportToFile: exportToFile,
importFromFile,
exportToFile,
_clear: clear
};
var assert = require('assert'),
const assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
@@ -86,40 +86,52 @@ function clear(callback) {
}
function query() {
const args = Array.prototype.slice.call(arguments);
const callback = args[args.length - 1];
assert.strictEqual(typeof callback, 'function');
assert.notStrictEqual(gConnectionPool, null);
if (constants.TEST && !gConnectionPool) return callback(new BoxError(BoxError.DATABASE_ERROR, 'database.js not initialized'));
return new Promise((resolve, reject) => {
let args = Array.prototype.slice.call(arguments);
const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null;
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
args.push(function queryCallback(error, result) {
if (error) return callback ? callback(error) : reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
callback ? callback(null, result) : resolve(result);
});
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
});
}
function transaction(queries, callback) {
assert(util.isArray(queries));
assert.strictEqual(typeof callback, 'function');
function transaction(queries) {
assert(Array.isArray(queries));
callback = once(callback);
const args = Array.prototype.slice.call(arguments);
const callback = typeof args[args.length - 1] === 'function' ? once(args.pop()) : null;
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback(error);
return new Promise((resolve, reject) => {
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback ? callback(error) : reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
const releaseConnection = (error) => { connection.release(); callback(error); };
const releaseConnection = (error) => {
connection.release();
callback ? callback(error) : reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
};
connection.beginTransaction(function (error) {
if (error) return releaseConnection(error);
connection.beginTransaction(function (error) {
if (error) return releaseConnection(error);
async.mapSeries(queries, function iterator(query, done) {
connection.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return connection.rollback(() => releaseConnection(error));
connection.commit(function (error) {
async.mapSeries(queries, function iterator(query, done) {
connection.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return connection.rollback(() => releaseConnection(error));
connection.release();
connection.commit(function (error) {
if (error) return connection.rollback(() => releaseConnection(error));
callback(null, results);
connection.release();
callback ? callback(null, results) : resolve(results);
});
});
});
});

View File

@@ -110,7 +110,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -206,7 +206,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

View File

@@ -1,16 +1,16 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
@@ -22,7 +22,7 @@ var assert = require('assert'),
util = require('util'),
waitForDns = require('./waitfordns.js');
var DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com';
const DIGITALOCEAN_ENDPOINT = 'https://api.digitalocean.com';
function formatError(response) {
return util.format('DigitalOcean DNS error [%s] %j', response.statusCode, response.body);
@@ -69,7 +69,7 @@ function getInternal(dnsConfig, zoneName, name, type, callback) {
iteratorDone();
});
}, function () { return !!nextPage; }, function (error) {
}, function (testDone) { return testDone(null, !!nextPage); }, function (error) {
debug('getInternal:', error, JSON.stringify(matchingRecords));
if (error) return callback(error);
@@ -82,7 +82,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -185,7 +185,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

View File

@@ -39,7 +39,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -98,7 +98,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

View File

@@ -73,7 +73,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -144,7 +144,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

View File

@@ -45,7 +45,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -118,7 +118,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

View File

@@ -34,7 +34,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
// Result: none
@@ -57,7 +57,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
// Result: none

View File

@@ -1,13 +1,13 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
let async = require('async'),
@@ -99,7 +99,7 @@ function getZoneRecords(dnsConfig, zoneName, name, type, callback) {
iteratorDone();
});
}, function () { return more; }, function (error) {
}, function (testDone) { return testDone(null, more); }, function (error) {
debug('getZoneRecords:', error, JSON.stringify(records));
if (error) return callback(error);
@@ -135,7 +135,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -217,7 +217,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

View File

@@ -31,7 +31,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
@@ -52,7 +52,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
return callback();

View File

@@ -16,10 +16,10 @@ var assert = require('assert'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
querystring = require('querystring'),
safe = require('safetydance'),
superagent = require('superagent'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
xml2js = require('xml2js');
@@ -50,11 +50,9 @@ function getQuery(dnsConfig, callback) {
});
}
function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
function getZone(dnsConfig, zoneName, 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');
getQuery(dnsConfig, function (error, query) {
@@ -89,7 +87,7 @@ function getInternal(dnsConfig, zoneName, subdomain, type, callback) {
});
}
function setInternal(dnsConfig, zoneName, hosts, callback) {
function setZone(dnsConfig, zoneName, hosts, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert(Array.isArray(hosts));
@@ -116,7 +114,10 @@ function setInternal(dnsConfig, zoneName, hosts, callback) {
}
});
superagent.post(ENDPOINT).query(query).end(function (error, result) {
// namecheap recommends sending as POSTDATA with > 10 records
const qs = querystring.stringify(query);
superagent.post(ENDPOINT).set('Content-Type', 'application/x-www-form-urlencoded').send(qs).end(function (error, result) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
var parser = new xml2js.Parser();
@@ -144,7 +145,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
@@ -154,17 +155,17 @@ function upsert(domainObject, subdomain, type, values, callback) {
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getZone(dnsConfig, zoneName, 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++) {
for (let i = 0; i < values.length; i++) {
let curValue = values[i];
let wasUpdate = false;
for (var j = 0; j < result.length; j++) {
for (let j = 0; j < result.length; j++) {
let curHost = result[j];
if (curHost.Type === type && curHost.Name === subdomain) {
@@ -198,9 +199,9 @@ function upsert(domainObject, subdomain, type, values, callback) {
}
}
let toUpsert = result.concat(toInsert);
const hosts = result.concat(toInsert);
setInternal(dnsConfig, zoneName, toUpsert, callback);
setZone(dnsConfig, zoneName, hosts, callback);
});
}
@@ -215,16 +216,16 @@ function get(domainObject, subdomain, type, callback) {
subdomain = domains.getName(domainObject, subdomain, type) || '@';
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getZone(dnsConfig, zoneName, 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);
const 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; });
const tmp = actualHosts.map(function (record) { return record.Address; });
debug('get: %j', tmp);
debug(`get: subdomain: ${subdomain} type:${type} value:${JSON.stringify(tmp)}`);
return callback(null, tmp);
});
@@ -234,7 +235,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
@@ -244,29 +245,19 @@ function del(domainObject, subdomain, type, values, callback) {
debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
getInternal(dnsConfig, zoneName, subdomain, type, function (error, result) {
getZone(dnsConfig, zoneName, function (error, result) {
if (error) return callback(error);
if (result.length === 0) return callback();
const originalLength = result.length;
let removed = false;
for (var i = 0; i < values.length; i++) {
for (let 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
}
}
result = result.filter(curHost => curHost.Type !== type || curHost.Name !== subdomain || curHost.Address !== curValue);
}
// Only set hosts if we actually removed a host
if (removed) return setInternal(dnsConfig, zoneName, result, callback);
if (result.length !== originalLength) return setZone(dnsConfig, zoneName, result, callback);
callback();
});

View File

@@ -157,7 +157,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -200,7 +200,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

View File

@@ -90,7 +90,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -183,7 +183,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

View File

@@ -26,7 +26,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
@@ -47,7 +47,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
return callback();

View File

@@ -97,7 +97,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
@@ -178,7 +178,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,

280
src/dns/vultr.js Normal file
View File

@@ -0,0 +1,280 @@
'use strict';
exports = module.exports = {
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
const async = require('async'),
assert = require('assert'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/vultr'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
const VULTR_ENDPOINT = 'https://api.vultr.com/v2';
function formatError(response) {
return util.format('Vultr DNS error [%s] %j', response.statusCode, response.body);
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getZoneRecords(dnsConfig, zoneName, name, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`);
let per_page = 100, cursor= null;
let records = [];
async.doWhilst(function (iteratorDone) {
const url = `${VULTR_ENDPOINT}/domains/${zoneName}/records?per_page=${per_page}` + (cursor ? `&cursor=${cursor}` : '');
superagent.get(url)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
records = records.concat(result.body.records.filter(function (record) {
return (record.type === type && record.name === name);
}));
cursor = safe.query(result.body, 'meta.links.next');
iteratorDone();
});
}, function (testDone) { return testDone(null, !!cursor); }, function (error) {
debug('getZoneRecords:', error, JSON.stringify(records));
if (error) return callback(error);
callback(null, records);
});
}
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,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, records) {
if (error) return callback(error);
var tmp = records.map(function (record) { return record.data; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
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.strictEqual(typeof callback, 'function');
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', name, zoneName, type, values);
getZoneRecords(dnsConfig, zoneName, name, type, function (error, records) {
if (error) return callback(error);
let i = 0, recordIds = []; // used to track available records to update instead of create
async.eachSeries(values, function (value, iteratorCallback) {
let data = {
type,
ttl: 300 // lowest
};
if (type === 'MX') {
data.priority = parseInt(value.split(' ')[0], 10);
data.data = value.split(' ')[1];
} else if (type === 'TXT') {
data.data = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes
} else {
data.data = value;
}
if (i >= records.length) {
data.name = name; // only set for new records
superagent.post(`${VULTR_ENDPOINT}/domains/${zoneName}/records`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 201) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(result.body.record.id);
return iteratorCallback(null);
});
} else {
superagent.patch(`${VULTR_ENDPOINT}/domains/${zoneName}/records/${records[i].id}`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(records[i-1].id);
return iteratorCallback(null);
});
}
}, function (error) {
if (error) return callback(error);
debug('upsert: completed with recordIds:%j', recordIds);
callback();
});
});
}
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.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, records) {
if (error) return callback(error);
if (records.length === 0) return callback(null);
const tmp = records.filter(function (record) { return values.some(function (value) { return value === record.data; }); });
debug('del: %j', tmp);
if (tmp.length === 0) return callback(null);
// FIXME we only handle the first one currently
superagent.del(`${VULTR_ENDPOINT}/domains/${zoneName}/records/${tmp[0].id}`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
return callback(null);
});
});
}
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 BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.vultr.com') === -1) {
debug('verifyDnsConfig: %j does not contains vultr NS', nameservers);
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Vultr', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}

View File

@@ -31,7 +31,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsert: %s for zone %s of type %s with values %j', location, domainObject.zoneName, type, values);
@@ -52,7 +52,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
return callback();

View File

@@ -43,12 +43,12 @@ const apps = require('./apps.js'),
Docker = require('dockerode'),
os = require('os'),
path = require('path'),
reverseProxy = require('./reverseproxy.js'),
services = require('./services.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
safe = require('safetydance'),
system = require('./system.js'),
util = require('util'),
volumes = require('./volumes.js'),
_ = require('underscore');
@@ -58,11 +58,13 @@ const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
function testRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
function testRegistryConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
gConnection.checkAuth(auth, function (error /*, data */) { // this returns a 500 even for auth errors
if (config.provider === 'noop') return callback();
gConnection.checkAuth(config, function (error /*, data */) { // this returns a 500 even for auth errors
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error, { field: 'serverAddress' }));
callback();
@@ -81,14 +83,14 @@ function removePrivateFields(registryConfig) {
return registryConfig;
}
function setRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
function setRegistryConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
const isLogin = !!auth.password;
const isLogin = !!config.password;
// currently, auth info is not stashed in the db but maybe it should for restore to work?
const cmd = isLogin ? `docker login ${auth.serverAddress} --username ${auth.username} --password ${auth.password}` : `docker logout ${auth.serverAddress}`;
const cmd = isLogin ? `docker login ${config.serverAddress} --username ${config.username} --password ${config.password}` : `docker logout ${config.serverAddress}`;
child_process.exec(cmd, { }, function (error /*, stdout, stderr */) {
if (error) return callback(new BoxError(BoxError.ACCESS_DENIED, error.message));
@@ -134,12 +136,12 @@ function getRegistryConfig(image, callback) {
}
function pullImage(manifest, callback) {
getRegistryConfig(manifest.dockerImage, function (error, authConfig) {
getRegistryConfig(manifest.dockerImage, function (error, config) {
if (error) return callback(error);
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`);
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${config ? 'yes' : 'no'}`);
gConnection.pull(manifest.dockerImage, { authconfig: authConfig }, function (error, stream) {
gConnection.pull(manifest.dockerImage, { authconfig: config }, function (error, stream) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to pull image ${manifest.dockerImage}. message: ${error.message} statusCode: ${error.statusCode}`));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${manifest.dockerImage}. Please check the network or if the image needs authentication. statusCode: ${error.statusCode}`));
@@ -191,25 +193,93 @@ function downloadImage(manifest, callback) {
});
}
function getBinds(app, callback) {
async function getVolumeMounts(app) {
assert.strictEqual(typeof app, 'object');
let mounts = [];
if (app.mounts.length === 0) return [];
const result = await volumes.list();
let volumesById = {};
result.forEach(r => volumesById[r.id] = r);
for (const mount of app.mounts) {
const volume = volumesById[mount.volumeId];
mounts.push({
Source: volume.hostPath,
Target: `/media/${volume.name}`,
Type: 'bind',
ReadOnly: mount.readOnly
});
}
return mounts;
}
function getAddonMounts(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (app.mounts.length === 0) return callback(null);
let mounts = [];
let binds = [];
const addons = app.manifest.addons;
if (!addons) return callback(null, mounts);
volumes.list(function (error, result) {
if (error) return callback(error);
let volumesById = {};
result.forEach(r => volumesById[r.id] = r);
async.eachSeries(Object.keys(addons), function (addon, iteratorDone) {
switch (addon) {
case 'localstorage':
mounts.push({
Target: '/app/data',
Source: `${app.id}-localstorage`,
Type: 'volume',
ReadOnly: false
});
for (const mount of app.mounts) {
const volume = volumesById[mount.volumeId];
binds.push(`${volume.hostPath}:/media/${volume.name}:${mount.readOnly ? 'ro' : 'rw'}`);
return iteratorDone();
case 'tls':
reverseProxy.getCertificatePath(app.fqdn, app.domain, function (error, bundle) {
if (error) return iteratorDone(error);
mounts.push({
Target: '/etc/certs/tls_cert.pem',
Source: bundle.certFilePath,
Type: 'bind',
ReadOnly: true
});
mounts.push({
Target: '/etc/certs/tls_key.pem',
Source: bundle.keyFilePath,
Type: 'bind',
ReadOnly: true
});
iteratorDone();
});
return;
default:
iteratorDone();
}
}, function (error) {
callback(error, mounts);
});
}
callback(null, binds);
async function getMounts(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
const [error, volumeMounts] = await safe(getVolumeMounts(app));
if (error) return callback(error);
getAddonMounts(app, function (error, addonMounts) {
if (error) return callback(error);
callback(null, volumeMounts.concat(addonMounts));
});
}
@@ -228,7 +298,7 @@ function getLowerUpIp() { // see getifaddrs and IFF_LOWER_UP and netdevice
function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
assert(!cmd || util.isArray(cmd));
assert(!cmd || Array.isArray(cmd));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -244,8 +314,8 @@ function createSubcontainer(app, name, cmd, options, callback) {
'CLOUDRON=1',
'CLOUDRON_PROXY_IP=172.18.0.1',
`CLOUDRON_APP_HOSTNAME=${app.id}`,
`${envPrefix}WEBADMIN_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}API_ORIGIN=${settings.adminOrigin()}`,
`${envPrefix}WEBADMIN_ORIGIN=${settings.dashboardOrigin()}`,
`${envPrefix}API_ORIGIN=${settings.dashboardOrigin()}`,
`${envPrefix}APP_ORIGIN=https://${domain}`,
`${envPrefix}APP_DOMAIN=${domain}`
];
@@ -278,7 +348,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
services.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(error);
getBinds(app, function (error, binds) {
getMounts(app, function (error, mounts) {
if (error) return callback(error);
let containerOptions = {
@@ -299,8 +369,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
'isCloudronManaged': String(true)
},
HostConfig: {
Mounts: services.getMountsSync(app, app.manifest.addons),
Binds: binds, // ideally, we have to use 'Mounts' but we have to create volumes then
Mounts: mounts,
LogConfig: {
Type: 'syslog',
Config: {
@@ -496,10 +565,11 @@ function deleteImage(manifest, callback) {
assert(!manifest || typeof manifest === 'object');
assert.strictEqual(typeof callback, 'function');
var dockerImage = manifest ? manifest.dockerImage : null;
const dockerImage = manifest ? manifest.dockerImage : null;
if (!dockerImage) return callback(null);
if (dockerImage.includes('//')) return callback(null); // a common mistake is to paste a https:// as docker image. this results in a crash at runtime in dockerode module
var removeOptions = {
const removeOptions = {
force: false, // might be shared with another instance of this app
noprune: false // delete untagged parents
};

View File

@@ -1,8 +1,8 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
start,
stop
};
var apps = require('./apps.js'),

View File

@@ -3,20 +3,20 @@
'use strict';
exports = module.exports = {
add: add,
get: get,
getAll: getAll,
update: update,
del: del,
clear: clear
add,
get,
getAll,
update,
del,
clear
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson' ].join(',');
const DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson', 'fallbackCertificateJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
@@ -28,6 +28,9 @@ function postProcess(data) {
data.wellKnown = safe.JSON.parse(data.wellKnownJson);
delete data.wellKnownJson;
data.fallbackCertificate = safe.JSON.parse(data.fallbackCertificateJson);
delete data.fallbackCertificateJson;
return data;
}
@@ -62,10 +65,12 @@ function add(name, data, callback) {
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof data.fallbackCertificate, 'object');
assert.strictEqual(typeof callback, 'function');
let queries = [
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig) ] },
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson, fallbackCertificateJson) VALUES (?, ?, ?, ?, ?, ?)',
args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig), JSON.stringify(data.fallbackCertificate) ] },
{ query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] },
];
@@ -84,14 +89,8 @@ function update(name, domain, callback) {
var args = [ ], fields = [ ];
for (var k in domain) {
if (k === 'config') {
fields.push('configJson = ?');
args.push(JSON.stringify(domain[k]));
} else if (k === 'tlsConfig') {
fields.push('tlsConfigJson = ?');
args.push(JSON.stringify(domain[k]));
} else if (k === 'wellKnown') {
fields.push('wellKnownJson = ?');
if (k === 'config' || k === 'tlsConfig' || k === 'wellKnown' || k === 'fallbackCertificate') { // json fields
fields.push(`${k}Json = ?`);
args.push(JSON.stringify(domain[k]));
} else {
fields.push(k + ' = ?');

View File

@@ -26,22 +26,27 @@ module.exports = exports = {
parentDomain,
checkDnsRecords
registerLocations,
unregisterLocations,
checkDnsRecords,
syncDnsRecords
};
var assert = require('assert'),
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -58,6 +63,7 @@ function api(provider) {
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'linode': return require('./dns/linode.js');
case 'vultr': return require('./dns/vultr.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'netcup': return require('./dns/netcup.js');
@@ -114,7 +120,7 @@ function validateHostname(location, domainObject) {
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
if (hostname === settings.adminFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
@@ -184,19 +190,23 @@ function add(domain, data, auditSource, callback) {
let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate);
if (error) return callback(error);
} else {
fallbackCertificate = reverseProxy.generateFallbackCertificateSync({ domain, config });
fallbackCertificate = reverseProxy.generateFallbackCertificateSync(domain);
if (fallbackCertificate.error) return callback(fallbackCertificate.error);
}
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (!dkimSelector) dkimSelector = 'cloudron-' + settings.adminDomain().replace(/\./g, '');
if (!dkimSelector) {
// create a unique suffix. this lets one add this domain can be added in another cloudron instance and not have their dkim selector conflict
const suffix = crypto.createHash('sha256').update(settings.dashboardDomain()).digest('hex').substr(0, 6);
dkimSelector = `cloudron-${suffix}`;
}
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector }, function (error) {
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector, fallbackCertificate }, function (error) {
if (error) return callback(error);
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
@@ -219,17 +229,7 @@ function get(domain, callback) {
domaindb.get(domain, function (error, result) {
if (error) return callback(error);
reverseProxy.getFallbackCertificate(domain, function (_, bundle) { // never returns an error
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
// do not error here. otherwise, there is no way to fix things up from the UI
if (!cert || !key) debug(`Unable to read fallback certificates of ${domain} from disk`);
result.fallbackCertificate = { cert: cert, key: key };
return callback(null, result);
});
return callback(null, result);
});
}
@@ -255,7 +255,7 @@ function update(domain, data, auditSource, callback) {
let { zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown } = data;
if (settings.isDemo() && (domain === settings.adminDomain())) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
if (settings.isDemo() && (domain === settings.dashboardDomain())) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
domaindb.get(domain, function (error, domainObject) {
if (error) return callback(error);
@@ -287,9 +287,11 @@ function update(domain, data, auditSource, callback) {
zoneName,
provider,
tlsConfig,
wellKnown
wellKnown,
};
if (fallbackCertificate) newData.fallbackCertificate = fallbackCertificate;
domaindb.update(domain, newData, function (error) {
if (error) return callback(error);
@@ -312,8 +314,8 @@ function del(domain, auditSource, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain'));
if (domain === settings.mailDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain'));
if (domain === settings.dashboardDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain'));
if (domain === settings.mailDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain. Change the mail server location first'));
domaindb.del(domain, function (error) {
if (error) return callback(error);
@@ -337,6 +339,7 @@ function clear(callback) {
}
// returns the 'name' that needs to be inserted into zone
// eslint-disable-next-line no-unused-vars
function getName(domain, location, type) {
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
@@ -386,7 +389,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values);
@@ -406,7 +409,7 @@ 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(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values);
@@ -463,3 +466,126 @@ function makeWildcard(vhost) {
parts[0] = '*';
return parts.join('.');
}
function registerLocations(locations, options, progressCallback, callback) {
assert(Array.isArray(locations));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`registerLocations: Will register ${JSON.stringify(locations)} with options ${JSON.stringify(options)}`);
const overwriteDns = options.overwriteDns || false;
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(locations, function (location, iteratorDone) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
progressCallback({ message: `Registering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` });
// get the current record before updating it
getDnsRecords(location.subdomain, location.domain, 'A', function (error, values) {
if (error && error.reason === BoxError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again
if (error && error.reason === BoxError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain: location }));
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain: location }));
if (error) return retryCallback(null, new BoxError(BoxError.EXTERNAL_ERROR, error.message, location)); // give up for other errors
if (values.length !== 0 && values[0] === ip) return retryCallback(null); // up-to-date
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwriteDns) return retryCallback(null, new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain: location }));
upsertDnsRecords(location.subdomain, location.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
progressCallback({ message: `registerSubdomains: Upsert error. Will retry. ${error.message}` });
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again
}
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, location) : null);
});
});
}, function (error, result) {
if (error || result) return iteratorDone(error || result);
iteratorDone(null);
});
}, callback);
});
}
function unregisterLocations(locations, progressCallback, callback) {
assert(Array.isArray(locations));
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(locations, function (location, iteratorDone) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
progressCallback({ message: `Unregistering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` });
removeDnsRecords(location.subdomain, location.domain, 'A', [ ip ], function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
progressCallback({ message: `Error unregistering location. Will retry. ${error.message}`});
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again
}
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location }) : null);
});
}, function (error, result) {
if (error || result) return iteratorDone(error || result);
iteratorDone();
});
}, callback);
});
}
function syncDnsRecords(options, progressCallback, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (options.domain && options.type === 'mail') return mail.setDnsRecords(options.domain, callback);
getAll(function (error, domains) {
if (error) return callback(error);
if (options.domain) domains = domains.filter(d => d.domain === options.domain);
const mailSubdomain = settings.mailFqdn().substr(0, settings.mailFqdn().length - settings.mailDomain().length - 1);
apps.getAll(function (error, allApps) {
if (error) return callback(error);
let progress = 1, errors = [];
// we sync by domain only to get some nice progress
async.eachSeries(domains, function (domain, iteratorDone) {
progressCallback({ percent: progress, message: `Updating DNS of ${domain.domain}`});
progress += Math.round(100/(1+domains.length));
let locations = [];
if (domain.domain === settings.dashboardDomain()) locations.push({ subdomain: constants.DASHBOARD_LOCATION, domain: settings.dashboardDomain() });
if (domain.domain === settings.mailDomain() && settings.mailFqdn() !== settings.dashboardFqdn()) locations.push({ subdomain: mailSubdomain, domain: settings.mailDomain() });
allApps.forEach(function (app) {
const appLocations = [{ subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains);
locations = locations.concat(appLocations.filter(al => al.domain === domain.domain));
});
async.series([
registerLocations.bind(null, locations, { overwriteDns: true }, progressCallback),
progressCallback.bind(null, { message: `Updating mail DNS of ${domain.domain}`}),
mail.setDnsRecords.bind(null, domain.domain)
], function (error) {
if (error) errors.push({ domain: domain.domain, message: error.message });
iteratorDone();
});
}, () => callback(null, { errors }));
});
});
}

View File

@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
sync: sync
sync
};
let apps = require('./apps.js'),
@@ -32,7 +32,7 @@ function sync(auditSource, callback) {
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
domains.upsertDnsRecords(constants.ADMIN_LOCATION, settings.adminDomain(), 'A', [ ip ], function (error) {
domains.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('refreshDNS: updated admin location');

View File

@@ -2,11 +2,11 @@
exports = module.exports = {
add,
upsert,
upsertLoginEvent,
get,
getAllPaged,
getByCreationTime,
cleanup,
_clear: clear,
// keep in sync with webadmin index.js filter
ACTION_ACTIVATE: 'cloudron.activate',
@@ -15,6 +15,7 @@ exports = module.exports = {
ACTION_APP_REPAIR: 'app.repair',
ACTION_APP_INSTALL: 'app.install',
ACTION_APP_RESTORE: 'app.restore',
ACTION_APP_IMPORT: 'app.import',
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_APP_UPDATE_FINISH: 'app.update.finish',
@@ -75,94 +76,119 @@ exports = module.exports = {
ACTION_PROCESS_CRASH: 'system.crash'
};
var assert = require('assert'),
const assert = require('assert'),
database = require('./database.js'),
debug = require('debug')('box:eventlog'),
eventlogdb = require('./eventlogdb.js'),
mysql = require('mysql'),
notifications = require('./notifications.js'),
util = require('util'),
safe = require('safetydance'),
uuid = require('uuid');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
const EVENTLOG_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
function add(action, source, data, callback) {
function postProcess(record) {
// usually we have sourceJson and dataJson, however since this used to be the JSON data type, we don't
record.source = safe.JSON.parse(record.source);
record.data = safe.JSON.parse(record.data);
return record;
}
// never throws, only logs because previously code did not take a callback
async function add(action, source, data) {
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
eventlogdb.add(uuid.v4(), action, source, data, function (error, id) {
if (error) return callback(error);
callback(null, { id: id });
notifications.onEvent(id, action, source, data, NOOP_CALLBACK);
});
const id = uuid.v4();
try {
await database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ]);
await notifications.onEvent(id, action, source, data);
return id;
} catch (error) {
debug('add: error adding event', error);
return null;
}
}
function upsert(action, source, data, callback) {
// never throws, only logs because previously code did not take a callback
async function upsertLoginEvent(action, source, data) {
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert(!callback || typeof callback === 'function');
callback = callback || NOOP_CALLBACK;
// can't do a real sql upsert, for frequent eventlog entries we only have to do 2 queries once a day
const queries = [{
query: 'UPDATE eventlog SET creationTime=NOW(), data=? WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()',
args: [ JSON.stringify(data), action, JSON.stringify(source) ]
}, {
query: 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()',
args: [ action, JSON.stringify(source) ]
}];
eventlogdb.upsert(uuid.v4(), action, source, data, function (error, id) {
if (error) return callback(error);
try {
const result = await database.transaction(queries);
if (result[0].affectedRows >= 1) return result[1][0].id;
callback(null, { id: id });
notifications.onEvent(id, action, source, data, NOOP_CALLBACK);
});
// no existing eventlog found, create one
return await add(action, source, data);
} catch (error) {
debug('add: error adding event', error);
return null;
}
}
function get(id, callback) {
async function get(id) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
eventlogdb.get(id, function (error, result) {
if (error) return callback(error);
const result = await database.query('SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE id = ?', [ id ]);
if (result.length === 0) return null;
callback(null, result);
});
return postProcess(result[0]);
}
function getAllPaged(actions, search, page, perPage, callback) {
async function getAllPaged(actions, search, page, perPage) {
assert(Array.isArray(actions));
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlogdb.getAllPaged(actions, search, page, perPage, function (error, events) {
if (error) return callback(error);
let data = [];
let query = `SELECT ${EVENTLOG_FIELDS} FROM eventlog`;
callback(null, events);
if (actions.length || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
if (actions.length && search) query += ' AND ( ';
actions.forEach(function (action, i) {
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
if (i < actions.length-1) query += ' OR ';
});
if (actions.length && search) query += ' ) ';
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);
const results = await database.query(query, data);
results.forEach(postProcess);
return results;
}
function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
async function cleanup(options) {
assert.strictEqual(typeof options, 'object');
eventlogdb.getByCreationTime(creationTime, function (error, events) {
if (error) return callback(error);
const creationTime = options.creationTime;
callback(null, events);
});
const results = await database.query('SELECT * FROM eventlog WHERE creationTime <= ?', [ creationTime ]);
for (const result of results) {
await database.query('DELETE FROM notifications WHERE eventId=?', [ result.id ]); // remove notifications that reference the events as well
await database.query('DELETE FROM eventlog WHERE id=?', [ result.id ]);
}
}
function cleanup(callback) {
callback = callback || NOOP_CALLBACK;
var d = new Date();
d.setDate(d.getDate() - 10); // 10 days ago
eventlogdb.delByCreationTime(d, function (error) {
if (error) return callback(error);
callback(null);
});
async function clear() {
await database.query('DELETE FROM eventlog');
}

View File

@@ -1,171 +0,0 @@
'use strict';
exports = module.exports = {
get: get,
getAllPaged: getAllPaged,
getByCreationTime: getByCreationTime,
add: add,
upsert: upsert,
count: count,
delByCreationTime: delByCreationTime,
_clear: clear
};
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
mysql = require('mysql'),
safe = require('safetydance'),
util = require('util');
var EVENTLOG_FIELDS = [ 'id', 'action', 'source', 'data', 'creationTime' ].join(',');
function postProcess(eventLog) {
// usually we have sourceJson and dataJson, however since this used to be the JSON data type, we don't
eventLog.source = safe.JSON.parse(eventLog.source);
eventLog.data = safe.JSON.parse(eventLog.data);
return eventLog;
}
function get(eventId, callback) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE id = ?', [ eventId ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Eventlog not found'));
callback(null, postProcess(result[0]));
});
}
function getAllPaged(actions, search, page, perPage, callback) {
assert(Array.isArray(actions));
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
var data = [];
var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog';
if (actions.length || search) query += ' WHERE';
if (search) query += ' (source LIKE ' + mysql.escape('%' + search + '%') + ' OR data LIKE ' + mysql.escape('%' + search + '%') + ')';
if (actions.length && search) query += ' AND ( ';
actions.forEach(function (action, i) {
query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') ';
if (i < actions.length-1) query += ' OR ';
});
if (actions.length && search) query += ' ) ';
query += ' ORDER BY creationTime DESC LIMIT ?,?';
data.push((page-1)*perPage);
data.push(perPage);
database.query(query, data, function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function getByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
var query = 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE creationTime >= ? ORDER BY creationTime DESC';
database.query(query, [ creationTime ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function add(id, action, source, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, id);
});
}
// id is only used if we didn't do an update but insert instead
function upsert(id, action, source, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof action, 'string');
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
// can't do a real sql upsert, for frequent eventlog entries we only have to do 2 queries once a day
var queries = [{
query: 'UPDATE eventlog SET creationTime=NOW(), data=? WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()',
args: [ JSON.stringify(data), action, JSON.stringify(source) ]
}, {
query: 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE action = ? AND source LIKE ? AND DATE(creationTime)=CURDATE()',
args: [ action, JSON.stringify(source) ]
}];
database.transaction(queries, function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result[0].affectedRows >= 1) return callback(null, result[1][0].id);
// no existing eventlog found, create one
add(id, action, source, data, callback);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM eventlog', function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result[0].total);
});
}
function clear(callback) {
database.query('DELETE FROM eventlog', function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function delByCreationTime(creationTime, callback) {
assert(util.isDate(creationTime));
assert.strictEqual(typeof callback, 'function');
// remove notifications that reference the events as well
database.query('SELECT * FROM eventlog WHERE creationTime <= ?', [ creationTime ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
async.eachSeries(result, function (item, iteratorCallback) {
async.series([
database.query.bind(null, 'DELETE FROM notifications WHERE eventId=?', [ item.id ]),
database.query.bind(null, 'DELETE FROM eventlog WHERE id=?', [ item.id ])
], iteratorCallback);
}, function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
});
}

View File

@@ -1,17 +1,17 @@
'use strict';
exports = module.exports = {
search: search,
verifyPassword: verifyPassword,
createAndVerifyUserIfNotExist: createAndVerifyUserIfNotExist,
search,
verifyPassword,
createAndVerifyUserIfNotExist,
testConfig: testConfig,
startSyncer: startSyncer,
testConfig,
startSyncer,
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
injectPrivateFields,
removePrivateFields,
sync: sync
sync
};
var assert = require('assert'),

View File

@@ -1,50 +0,0 @@
'use strict';
exports = module.exports = {
start,
DEFAULT_MEMORY_LIMIT: 256 * 1024 * 1024
};
var assert = require('assert'),
async = require('async'),
infra = require('./infra_version.js'),
paths = require('./paths.js'),
shell = require('./shell.js'),
system = require('./system.js');
function start(existingInfra, serviceConfig, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof serviceConfig, 'object');
assert.strictEqual(typeof callback, 'function');
const tag = infra.images.graphite.tag;
const dataDir = paths.PLATFORM_DATA_DIR;
const memoryLimit = serviceConfig.memoryLimit || exports.DEFAULT_MEMORY_LIMIT;
const memory = system.getMemoryAllocation(memoryLimit);
const cmd = `docker run --restart=always -d --name="graphite" \
--hostname graphite \
--net cloudron \
--net-alias graphite \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m ${memory} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8417:8000 \
-v "${dataDir}/graphite:/var/lib/graphite" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
async.series([
shell.exec.bind(null, 'stopGraphite', 'docker stop graphite || true'),
shell.exec.bind(null, 'removeGraphite', 'docker rm -f graphite || true'),
shell.exec.bind(null, 'startGraphite', cmd)
], callback);
}

View File

@@ -1,24 +1,24 @@
'use strict';
exports = module.exports = {
get: get,
getByName: getByName,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
add: add,
update: update,
del: del,
count: count,
get,
getByName,
getWithMembers,
getAll,
getAllWithMembers,
add,
update,
del,
count,
getMembers: getMembers,
addMember: addMember,
removeMember: removeMember,
setMembers: setMembers,
isMember: isMember,
getMembers,
addMember,
removeMember,
setMembers,
isMember,
getMembership: getMembership,
setMembership: setMembership,
getMembership,
setMembership,
_clear: clear
};

View File

@@ -6,22 +6,22 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.18.0',
'version': '48.20.0',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
{ repo: 'cloudron/base', tag: 'cloudron/base:3.0.0@sha256:455c70428723e3a823198c57472785437eb6eab082e79b3ff04ea584faf46e92' }
],
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
'images': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.2.0@sha256:4359aae80050a92bae3be30600fb93ef4dbaec6dc9254bda353c0b131a36f969' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.2@sha256:dd624870c7f8ba9b2759f93ce740d1e092a1ac4b2d6af5007a01b30ad6b316d0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.3.0@sha256:0daf1be5320c095077392bf21d247b93ceaddca46c866c17259a335c80d2f357' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:3.0.0@sha256:59e50b1f55e433ffdf6d678f8c658812b4119f631db8325572a52ee40d3bc562' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.1.0@sha256:18e0d75ad88a3e66849de2c4c01f794e8df9235befd74544838e34b65f487740' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.0.0@sha256:7e0165f17789192fd4f92efb34aa373450fa859e3b502684b2b121a5582965bf' }
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.3.1@sha256:759cafab7625ff538418a1f2ed5558b1d5bff08c576bba577d865d6d02b49091' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:3.0.7@sha256:6679c2fb96f8d6d62349b607748570640a90fc46b50aad80ca2c0161655d07f4' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:4.0.6@sha256:e583082e15e8e41b0e3b80c3efc917ec429f19fa08a19e14fc27144a8bfe446a' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:4.0.2@sha256:9df297ccc3370f38c54f8d614e214e082b363777cd1c6c9522e29663cc8f5362' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.0.3@sha256:37e5222e01ae89bc5a742ce12030631de25a127b5deec8a0e992c68df0fdec10' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.3.2@sha256:d0d153612f478a0ef099809d4c3c72c3e02f43a55a796987d922f16367e7881e' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:3.0.1@sha256:bed9f6b5d06fe2c5289e895e806cfa5b74ad62993d705be55d4554a67d128029' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.2.0@sha256:61e8247ded1e07cf882ca478dab180960357c614472e80b938f1f690a46788c2' }
}
};

View File

@@ -1,35 +1,29 @@
'use strict';
var assert = require('assert'),
const assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:janitor'),
Docker = require('dockerode'),
tokendb = require('./tokendb.js');
safe = require('safetydance'),
tokens = require('./tokens.js');
exports = module.exports = {
cleanupTokens: cleanupTokens,
cleanupDockerVolumes: cleanupDockerVolumes
cleanupTokens,
cleanupDockerVolumes
};
const NOOP_CALLBACK = function () { };
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
function cleanupTokens(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
callback = callback || NOOP_CALLBACK;
async function cleanupTokens() {
debug('Cleaning up expired tokens');
tokendb.delExpired(function (error, result) {
if (error) return debug('cleanupTokens: error removing expired tokens', error);
const [error, result] = await safe(tokens.delExpired());
if (error) return debug('cleanupTokens: error removing expired tokens', error);
debug('Cleaned up %s expired tokens.', result);
callback(null);
});
debug(`Cleaned up ${result} expired tokens`,);
}
function cleanupTmpVolume(containerInfo, callback) {

View File

@@ -264,7 +264,9 @@ function mailboxSearch(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
var obj = {
if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString()));
let obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['mailbox'],
@@ -289,10 +291,11 @@ function mailboxSearch(req, res, next) {
var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
mailboxdb.listMailboxes(domain, 1, 1000, function (error, mailboxes) {
if (error) return next(new ldap.OperationsError(error.toString()));
var results = [];
mailboxes = mailboxes.filter(m => m.active);
let results = [];
// send mailbox objects
mailboxes.forEach(function (mailbox) {
@@ -323,7 +326,9 @@ function mailboxSearch(req, res, next) {
mailboxdb.listAllMailboxes(1, 1000, function (error, mailboxes) {
if (error) return next(new ldap.OperationsError(error.toString()));
var results = [];
mailboxes = mailboxes.filter(m => m.active);
let results = [];
// send mailbox objects
async.eachSeries(mailboxes, function (mailbox, callback) {
@@ -382,10 +387,12 @@ function mailAliasSearch(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
if (!alias.active) return next(new ldap.NoSuchObjectError(req.dn.toString())); // there is no way to disable an alias. this is just here for completeness
// https://wiki.debian.org/LDAP/MigrationTools/Examples
// https://docs.oracle.com/cd/E19455-01/806-5580/6jej518pp/index.html
// member is fully qualified - https://docs.oracle.com/cd/E19957-01/816-6082-10/chap4.doc.html#43314
var obj = {
let obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['nisMailAlias'],
@@ -421,6 +428,8 @@ function mailingListSearch(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
if (!list.active) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// http://ldapwiki.willeke.com/wiki/Original%20Mailgroup%20Schema%20From%20Netscape
// members are fully qualified (https://docs.oracle.com/cd/E19444-01/816-6018-10/groups.htm#13356)
var obj = {
@@ -482,13 +491,13 @@ function authorizeUserForApp(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.app, 'object');
apps.hasAccessTo(req.app, req.user, function (error, hasAccess) {
apps.hasAccessTo(req.app, req.user, async function (error, hasAccess) {
if (error) return next(new ldap.OperationsError(error.toString()));
// we return no such object, to avoid leakage of a users existence
if (!hasAccess) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', appId: req.app.id }, { userId: req.user.id, user: users.removePrivateFields(req.user) });
res.end();
});
@@ -537,12 +546,15 @@ function authenticateUserMailbox(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) {
if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString()));
verifyMailboxPassword(mailbox, req.credentials || '', async function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
@@ -677,12 +689,15 @@ function authenticateMailAddon(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
verifyMailboxPassword(mailbox, req.credentials || '', function (error, result) {
if (!mailbox.active) return next(new ldap.NoSuchObjectError(req.dn.toString()));
verifyMailboxPassword(mailbox, req.credentials || '', async function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
eventlog.upsert(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
eventlog.upsertLoginEvent(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: email }, { userId: result.id, user: users.removePrivateFields(result) });
res.end();
});
});
@@ -740,6 +755,12 @@ function start(callback) {
res.end();
});
// just log that an attempt was made to unknown route, this helps a lot during app packaging
gServer.use(function(req, res, next) {
debug('not handled: dn %s, scope %s, filter %s (from %s)', req.dn ? req.dn.toString() : '-', req.scope, req.filter ? req.filter.toString() : '-', req.connection.ldap.id);
return next();
});
gServer.listen(constants.LDAP_PORT, '0.0.0.0', callback);
}

View File

@@ -39,7 +39,7 @@ exports = module.exports = {
listMailboxes,
getMailbox,
addMailbox,
updateMailboxOwner,
updateMailbox,
removeMailbox,
getAliases,
@@ -633,7 +633,7 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
assert.strictEqual(typeof serviceConfig, 'object');
assert.strictEqual(typeof callback, 'function');
// mail (note: 2525 is hardcoded in mail container and app use this port)
// mail (note: 2587 is hardcoded in mail container and app use this port)
// MAIL_SERVER_NAME is the hostname of the mailserver i.e server uses these certs
// MAIL_DOMAIN is the domain for which this server is relaying mails
// mail container uses /app/data for backed up data and /run for restart-able data
@@ -643,13 +643,14 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
const memory = system.getMemoryAllocation(memoryLimit);
const cloudronToken = hat(8 * 128), relayToken = hat(8 * 128);
reverseProxy.getCertificate(mailFqdn, mailDomain, function (error, bundle) {
reverseProxy.getCertificatePath(mailFqdn, mailDomain, function (error, bundle) {
if (error) return callback(error);
// the setup script copies dhparams.pem to /addons/mail
const dhparamsFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/dhparams.pem');
const mailCertFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_cert.pem');
const mailKeyFilePath = path.join(paths.ADDON_CONFIG_DIR, 'mail/tls_key.pem');
if (!safe.child_process.execSync(`cp ${paths.DHPARAMS_FILE} ${dhparamsFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not copy dhparams:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message));
@@ -662,7 +663,7 @@ function configureMail(mailFqdn, mailDomain, serviceConfig, callback) {
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
if (error) return callback(error);
var ports = allowInbound ? '-p 587:2525 -p 993:9993 -p 4190:4190 -p 25:2525' : '';
var ports = allowInbound ? '-p 587:2587 -p 993:9993 -p 4190:4190 -p 25:2587' : '';
const cmd = `docker run --restart=always -d --name="mail" \
--net cloudron \
@@ -722,8 +723,8 @@ function restartMail(callback) {
services.getServiceConfig('mail', function (error, serviceConfig) {
if (error) return callback(error);
debug(`restartMail: restarting mail container with ${settings.mailFqdn()} ${settings.adminDomain()}`);
configureMail(settings.mailFqdn(), settings.adminDomain(), serviceConfig, callback);
debug(`restartMail: restarting mail container with mailFqdn:${settings.mailFqdn()} dashboardDomain:${settings.dashboardDomain()}`);
configureMail(settings.mailFqdn(), settings.dashboardDomain(), serviceConfig, callback);
});
}
@@ -875,37 +876,40 @@ function upsertDnsRecords(domain, mailFqdn, callback) {
if (process.env.BOX_ENV === 'test') return callback();
var dkimKey = readDkimPublicKeySync(domain);
const dkimKey = readDkimPublicKeySync(domain);
if (!dkimKey) return callback(new BoxError(BoxError.FS_ERROR, 'Failed to read dkim public key'));
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
const dkimRecord = { subdomain: `${mailDomain.dkimSelector}._domainkey`, domain: domain, type: 'TXT', values: [ `"v=DKIM1; t=s; p=${dkimKey}"` ] };
var records = [ ];
let records = [];
records.push(dkimRecord);
if (mailDomain.enabled) {
records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
}
if (mailDomain.enabled) records.push({ subdomain: '', domain: domain, type: 'MX', values: [ '10 ' + mailFqdn + '.' ] });
txtRecordsWithSpf(domain, mailFqdn, function (error, txtRecords) {
if (error) return callback(error);
if (txtRecords) records.push({ subdomain: '', domain: domain, type: 'TXT', values: txtRecords });
debug('upsertDnsRecords: will update %j', records);
domains.getDnsRecords('_dmarc', domain, 'TXT', function (error, dmarcRecords) { // only update dmarc if absent. this allows user to set email for reporting
if (error) return callback(error);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) {
debug(`upsertDnsRecords: failed to update: ${error}`);
return callback(error);
}
if (dmarcRecords.length === 0) records.push({ subdomain: '_dmarc', domain: domain, type: 'TXT', values: [ '"v=DMARC1; p=reject; pct=100"' ] });
debug('upsertDnsRecords: records %j added with changeIds %j', records, changeIds);
debug('upsertDnsRecords: will update %j', records);
callback(null);
async.mapSeries(records, function (record, iteratorCallback) {
domains.upsertDnsRecords(record.subdomain, record.domain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) {
debug(`upsertDnsRecords: failed to update: ${error}`);
return callback(error);
}
debug('upsertDnsRecords: records %j added with changeIds %j', records, changeIds);
callback(null);
});
});
});
});
@@ -1179,14 +1183,18 @@ function getMailbox(name, domain, callback) {
});
}
function addMailbox(name, domain, ownerId, ownerType, auditSource, callback) {
function addMailbox(name, domain, data, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { ownerId, ownerType, active } = data;
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof active, 'boolean');
name = name.toLowerCase();
var error = validateName(name);
@@ -1194,23 +1202,27 @@ function addMailbox(name, domain, ownerId, ownerType, auditSource, callback) {
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
mailboxdb.addMailbox(name, domain, ownerId, ownerType, function (error) {
mailboxdb.addMailbox(name, domain, data, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_ADD, auditSource, { name, domain, ownerId, ownerType, active });
callback(null);
});
}
function updateMailboxOwner(name, domain, ownerId, ownerType, auditSource, callback) {
function updateMailbox(name, domain, data, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { ownerId, ownerType, active } = data;
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof active, 'boolean');
name = name.toLowerCase();
if (ownerType !== exports.OWNERTYPE_USER && ownerType !== exports.OWNERTYPE_GROUP) return callback(new BoxError(BoxError.BAD_FIELD, 'bad owner type'));
@@ -1218,10 +1230,10 @@ function updateMailboxOwner(name, domain, ownerId, ownerType, auditSource, callb
getMailbox(name, domain, function (error, result) {
if (error) return callback(error);
mailboxdb.updateMailboxOwner(name, domain, ownerId, ownerType, function (error) {
mailboxdb.updateMailbox(name, domain, data, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, ownerId, ownerType });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldUserId: result.userId, ownerId, ownerType, active });
callback(null);
});
@@ -1334,14 +1346,18 @@ function getList(name, domain, callback) {
});
}
function addList(name, domain, members, membersOnly, auditSource, callback) {
function addList(name, domain, data, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { members, membersOnly, active } = data;
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof active, 'boolean');
name = name.toLowerCase();
var error = validateName(name);
@@ -1351,23 +1367,27 @@ function addList(name, domain, members, membersOnly, auditSource, callback) {
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
}
mailboxdb.addList(name, domain, members, membersOnly, function (error) {
mailboxdb.addList(name, domain, data, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly });
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly, active });
callback();
});
}
function updateList(name, domain, members, membersOnly, auditSource, callback) {
function updateList(name, domain, data, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const { members, membersOnly, active } = data;
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof active, 'boolean');
name = name.toLowerCase();
var error = validateName(name);
@@ -1380,10 +1400,10 @@ function updateList(name, domain, members, membersOnly, auditSource, callback) {
getList(name, domain, function (error, result) {
if (error) return callback(error);
mailboxdb.updateList(name, domain, members, membersOnly, function (error) {
mailboxdb.updateList(name, domain, data, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly, active });
callback(null);
});
@@ -1421,7 +1441,7 @@ function resolveList(listName, listDomain, callback) {
let result = [], toResolve = list.members.slice(), visited = []; // slice creates a copy of array
async.whilst(() => toResolve.length != 0, function (iteratorCallback) {
async.whilst((testDone) => testDone(null, toResolve.length != 0), function (iteratorCallback) {
const toProcess = toResolve.shift();
const parts = toProcess.split('@');
const memberName = parts[0].split('+')[0], memberDomain = parts[1];

View File

@@ -1,21 +0,0 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
The application '<%= title %>' installed at <%= appFqdn %> is not responding.
This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app by opening the app's web terminal - https://docs.cloudron.io/apps/#web-terminal
* Restore the app to the latest backup - https://docs.cloudron.io/backups/#restoring-an-app
* Contact us via <%= supportEmail %> or https://forum.cloudron.io
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<% } %>

View File

@@ -1,14 +0,0 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
The application '<%= title %>' installed at <%= appFqdn %> is back online
and responding to health checks.
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<% } %>

View File

@@ -1,40 +0,0 @@
<%if (format === 'text') { %>
Dear Cloudron Admin,
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
Changes:
<%= changelog %>
Powered by https://cloudron.io
Sent at: <%= new Date().toUTCString() %>
<% } else { %>
<center>
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>Dear <%= cloudronName %> Admin,</h3>
<br/>
<div style="width: 650px; text-align: left;">
The application '<%= title %>' installed at <%= appFqdn %> was updated to app package version <%= version %>.
<h5>Changelog:</h5>
<%- changelogHTML %>
</div>
<br/>
<br/>
<div style="font-size: 10px; color: #333333; background: #ffffff;">
Powered by <a href="https://cloudron.io">Cloudron</a>.<br/>
Sent at: <%= new Date().toUTCString() %>
</div>
</center>
<% } %>

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