Compare commits

...

1113 Commits

Author SHA1 Message Date
Girish Ramakrishnan
52d2fe6909 data dir: allow sameness of old and new dir
this makes it easy to migrate to a new volume setup

(cherry picked from commit a32166bc9d)
2022-06-10 09:39:24 -07:00
Girish Ramakrishnan
61a1ac6983 7.2.4 changes 2022-06-10 09:33:12 -07:00
Girish Ramakrishnan
67801020ed mailboxDisplayName is optional 2022-06-08 14:25:16 -07:00
Girish Ramakrishnan
037f4195da guard against two level subdir moves
this has never worked since the -wholename check only works for
one level deep
2022-06-08 12:24:11 -07:00
Girish Ramakrishnan
8cf0922401 Fix container creation when migrating data dir 2022-06-08 11:52:22 -07:00
Girish Ramakrishnan
6311c78bcd Fix quoting 2022-06-08 11:25:20 -07:00
Girish Ramakrishnan
544ca6e1f4 initial xfs support 2022-06-08 10:58:00 -07:00
Girish Ramakrishnan
6de198eaad sendmail: check for supportsDisplayName
it seems quite some apps don't support this. so, we need a way for the
ui to hide the field so that users are not confused.
2022-06-08 09:43:58 -07:00
Girish Ramakrishnan
6c67f13d90 Use bind mount instead of volume
see also c76b211ce0
2022-06-06 15:59:59 -07:00
Girish Ramakrishnan
7598cf2baf consolidate storage validation logic 2022-06-06 12:50:21 -07:00
Girish Ramakrishnan
7dba294961 storage: check volume status 2022-06-03 10:43:59 -07:00
Girish Ramakrishnan
4bee30dd83 fix more typos 2022-06-03 09:10:37 -07:00
Girish Ramakrishnan
7952a67ed2 guess the volume type better 2022-06-03 07:54:16 -07:00
Johannes Zellner
50b2eabfde Also fixup userdirectory tests 2022-06-03 13:59:21 +02:00
Johannes Zellner
591067ee22 Fixup ldap group search tests 2022-06-03 13:54:31 +02:00
Johannes Zellner
88f78c01ba Remove virtual groups users and admin exposed via ldap 2022-06-03 13:32:35 +02:00
Girish Ramakrishnan
dddc5a1994 migrate app dataDir to volumes 2022-06-02 16:29:01 -07:00
Girish Ramakrishnan
8fc8128957 Make apps.getDataDir async 2022-06-02 11:19:33 -07:00
Girish Ramakrishnan
c76b211ce0 localstorage: remove usage of docker volumes
just move bind mounts. the initial idea was to use docker volume backends
but we have no plans for this. in addition, usage of volumes means that
files get copied from the image and into volume on first run which is
not desired. people are putting /app/data stuff into images which ideally
should break.
2022-06-02 11:09:27 -07:00
Girish Ramakrishnan
0c13504928 Bump version 2022-06-02 11:02:06 -07:00
Girish Ramakrishnan
26ab7f2767 add mailbox display name to schema 2022-06-01 22:06:34 -07:00
Girish Ramakrishnan
f78dabbf7e mail: add display name validation tests 2022-06-01 22:04:36 -07:00
Girish Ramakrishnan
39c5c44ac3 cloudron-firewall: fix spurious line 2022-06-01 09:28:50 -07:00
Girish Ramakrishnan
2dea7f8fe9 sendmail: restrict few characters in the display name 2022-06-01 08:13:19 -07:00
Girish Ramakrishnan
85af0d96d2 sendmail: allow display name to be set 2022-06-01 01:38:16 -07:00
Girish Ramakrishnan
176e917f51 update 7.2.3 changes 2022-05-31 13:27:00 -07:00
Girish Ramakrishnan
534c8f9c3f collectd: on one system, localhost was missing in /etc/hosts 2022-05-27 16:10:38 -07:00
Girish Ramakrishnan
5ee9feb0d2 If disk name has '.', replace with '_'
graphite uses . as the separator between different metric parts

see #348
2022-05-27 16:00:08 -07:00
Girish Ramakrishnan
723453dd1c 7.2.3 changes 2022-05-27 12:04:01 -07:00
Girish Ramakrishnan
45c9ddeacf appstore: allow re-registration on server side delete 2022-05-26 22:27:58 -07:00
Girish Ramakrishnan
5b075e3918 transfer ownership is not used anymore 2022-05-26 14:30:32 -07:00
Girish Ramakrishnan
c9916c4107 Really disable FQDNLookup 2022-05-25 15:48:25 -07:00
Girish Ramakrishnan
c7956872cb Add to changes 2022-05-25 15:14:01 -07:00
Girish Ramakrishnan
3adf8b5176 collectd: FQDNLookup causes collectd install to fail
this is on ubuntu 20

https://forum.cloudron.io/topic/7091/aws-ubuntu-20-04-installation-issue
2022-05-25 15:10:55 -07:00
Girish Ramakrishnan
40eae601da Update cloudron-manifestformat for new scheduler patterns 2022-05-23 11:02:04 -07:00
Girish Ramakrishnan
3eead2fdbe Fix possible duplicate key issue
console_server_origin in injected by the new setup script even for
7.1.x
2022-05-22 20:48:29 -07:00
Girish Ramakrishnan
9fcd6f9c0a cron: add @service which is probably clearer than @reboot in app context 2022-05-20 10:57:44 -07:00
Girish Ramakrishnan
17910584ca cron: add extensions
https://www.man7.org/linux/man-pages/man5/crontab.5.html#EXTENSIONS
2022-05-20 10:53:30 -07:00
Girish Ramakrishnan
d9a02faf7a make the globals const 2022-05-20 09:38:22 -07:00
Girish Ramakrishnan
d366f3107d net_admin: enable IPv6 forwarding in the container 2022-05-19 17:10:05 -07:00
Girish Ramakrishnan
2596afa7b3 appstore: set utmSource during user registration 2022-05-19 00:00:48 -07:00
Johannes Zellner
aa1e8dc930 Give the dashboard a way to check backgroundImage availability 2022-05-17 15:25:44 +02:00
Johannes Zellner
f3c66056b5 Allow to unset background image 2022-05-17 13:17:05 +02:00
Girish Ramakrishnan
93bacd00da Fix exec web socket/upload/download 2022-05-16 11:46:28 -07:00
Girish Ramakrishnan
b5c2a0ff44 exec: rework API to get exit code 2022-05-16 11:23:58 -07:00
Johannes Zellner
6bd478b8b0 Add profile backgroundImage api 2022-05-15 12:08:11 +02:00
Girish Ramakrishnan
c5c62ff294 Add to changes 2022-05-14 09:36:56 -07:00
Girish Ramakrishnan
7ed8678d50 mongodb: fix import timeout 2022-05-09 17:20:16 -07:00
Girish Ramakrishnan
e19e5423f0 cloudron-support: Remove unused var 2022-05-07 19:25:06 -07:00
Girish Ramakrishnan
622ba01c7a ubuntu 22: collectd disappeared
https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1971093

also, remove the ubuntu 16 hack
2022-05-06 20:02:02 -07:00
Girish Ramakrishnan
935da3ed15 vultr: set ttl to 120
https://www.vultr.com/docs/introduction-to-vultr-dns/#Limitations
2022-05-06 12:29:12 -07:00
Girish Ramakrishnan
ce054820a6 add migration to add consoleServerOrigin 2022-05-05 09:59:22 -07:00
Johannes Zellner
a7668624b4 Ensure we also set the new console server origin during installation 2022-05-05 16:52:11 +02:00
Girish Ramakrishnan
01b36bb37e proxyAuth: make the POST to /logout redirect
for firefly-III
2022-05-03 18:19:22 -07:00
Girish Ramakrishnan
5d1aaf6bc6 cloudron-setup: silent 2022-05-03 10:20:19 -07:00
Girish Ramakrishnan
7ceb307110 Add 7.2.1 changes 2022-05-03 09:15:21 -07:00
Girish Ramakrishnan
6371b7c20d dns: add hetzner 2022-05-02 22:33:30 -07:00
Girish Ramakrishnan
7ec648164e Remove usage of util 2022-05-02 21:32:10 -07:00
Girish Ramakrishnan
6e98f5f36c backuptask: make upload/download async 2022-04-30 16:42:14 -07:00
Girish Ramakrishnan
a098c6da34 noop: removeDir is async 2022-04-30 16:35:39 -07:00
Girish Ramakrishnan
94e70aca33 storage: downloadDir is not part of interface 2022-04-30 16:24:49 -07:00
Girish Ramakrishnan
ea01586b52 storage: make copy async 2022-04-30 16:24:45 -07:00
Girish Ramakrishnan
8ceb80dc44 hush: return BoxError everywhere 2022-04-29 19:02:59 -07:00
Girish Ramakrishnan
2280b7eaf5 Add S3MultipartDownloadStream
This extends the modern Readable class
2022-04-29 18:23:56 -07:00
Girish Ramakrishnan
1c1d247a24 cloudron-support: update key 2022-04-29 12:39:42 -07:00
Girish Ramakrishnan
90a6ad8cf5 support: new keys (ed25519)
rsa keys are slowly going away
2022-04-29 12:37:27 -07:00
Girish Ramakrishnan
80d91e5540 Add missing changelog 2022-04-29 09:58:17 -07:00
Girish Ramakrishnan
26cf084e1c tarPack/tarExtract do not need a callback 2022-04-28 21:58:00 -07:00
Girish Ramakrishnan
8ef730ad9c backuptask: make upload/download async 2022-04-28 21:37:08 -07:00
Girish Ramakrishnan
7123ec433c split up backupformat logic into separate files 2022-04-28 19:10:57 -07:00
Girish Ramakrishnan
c67d9fd082 move crypto code to hush.js 2022-04-28 18:12:17 -07:00
Girish Ramakrishnan
dd8f710605 Fix failing test 2022-04-28 18:03:36 -07:00
Girish Ramakrishnan
e097b79f65 godaddy: do not remove all the records of type 2022-04-28 17:46:03 -07:00
Girish Ramakrishnan
765f6d1b12 Revert "proxyAuth: use default fallback icon when no appstore icon or custom icon"
This reverts commit 045c3917c9.

This was committed by mistake, not sure how. 3d28833c35 is the commit
that fixes this issue.
2022-04-28 17:05:46 -07:00
Girish Ramakrishnan
7cf80ebf69 postgresql: add connection logs
This was an attempt to fix connection leak in postgresql. It turns
out that there was a long running cron task which was holding a db
connection. When that happens, the apptask might fail because postgres
says db is in use. The code in scheduler.js currently does not really
'suspend' task running because of re-entrancy issues.
2022-04-28 16:11:09 -07:00
Johannes Zellner
cc328f3a6e cloudron-support --enable-ssh should only enable ssh not attempt to collect stats, this might fail 2022-04-28 11:31:18 +02:00
Girish Ramakrishnan
045c3917c9 proxyAuth: use default fallback icon when no appstore icon or custom icon 2022-04-28 10:48:25 +02:00
Girish Ramakrishnan
ac2186ccf6 redis: fix cgroup check 2022-04-27 18:46:00 -07:00
Girish Ramakrishnan
a57fe36643 collectd: add cgroup v2 config
Ubuntu 22 has cgroup v2 config by default

https://rootlesscontaine.rs/getting-started/common/cgroup2/#checking-whether-cgroup-v2-is-already-enabled
https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
https://man7.org/training/download/splc_cgroups_v1_slides.pdf
2022-04-27 18:41:20 -07:00
Girish Ramakrishnan
1e711f7928 Ubuntu 22 has private home directories by default (https://discourse.ubuntu.com/t/private-home-directories-for-ubuntu-21-04-onwards/) 2022-04-27 17:49:29 -07:00
Girish Ramakrishnan
eafccde6cb Reset mysql password by detecting version (instead of ubuntu version) 2022-04-27 15:45:53 -07:00
Girish Ramakrishnan
6b85e11a22 update: collectd configuration can be removed 2022-04-27 15:41:28 -07:00
Girish Ramakrishnan
a74de3811b mysqldump: better detection of --column-statistics support
since it's also needed for ubuntu 22
2022-04-27 15:39:53 -07:00
Girish Ramakrishnan
070a425c85 typo 2022-04-27 13:11:20 -07:00
Girish Ramakrishnan
32153ed47d nginx: switch to ubuntu's repo package
ubuntu 18.04 has nginx 1.14
ubuntu 20.04, 22.04 has nginx 1.18

We used a custom nginx for TLSv1.3 support (ssl_protocols TLSv1.3).

OpenSSL itself has TLS 1.3 only from Ubuntu 18.10. This is why we
installed custom packages on Ubuntu 18.04
2022-04-27 10:59:27 -07:00
Girish Ramakrishnan
454f9c4a79 syncer: task processor cannot be async because of asyncjs quirk 2022-04-27 09:14:51 -07:00
Girish Ramakrishnan
3d28833c35 proxyAuth: use default fallback icon when no appstore icon or custom icon 2022-04-26 19:43:22 -07:00
Girish Ramakrishnan
be458020dd use string interpolation 2022-04-26 18:55:02 -07:00
Girish Ramakrishnan
9b6733fd88 godaddy: there is now a delete API 2022-04-26 18:44:50 -07:00
Girish Ramakrishnan
1b34a3e599 proxyAuth: add header spoofing note 2022-04-26 14:59:38 -07:00
Girish Ramakrishnan
67d29dbad8 systemd-detect-virt returns false when none detected 2022-04-26 14:59:26 -07:00
Girish Ramakrishnan
28b0043541 cloudron-setup: add container virtualization check 2022-04-26 08:24:36 -07:00
Girish Ramakrishnan
78824b059e turn off sso flag if an update removes sso options
ff-iii used to have LDAP but we removed it. in the database, 'sso'
is still true. the migration here will reset it back to false.
for future situations like these, we sync the sso flag on app update itself.

this ensures correct behavior when yet another update add back sso support.
in ff-iii case, a future update is bringing in proxyAuth based sso!

we don't store the 'sso' bit in backupdb, so user choice of sso is
lost if restore changes sso addons.
2022-04-25 23:36:58 -07:00
Girish Ramakrishnan
c63709312d proxyAuth: set X-Remote-User, X-Remote-Email headers
Apps like firefly-iii support https://datatracker.ietf.org/doc/html/rfc3875#section-4.1.10
2022-04-25 22:24:41 -07:00
Girish Ramakrishnan
11cf24075b Keep proxyAuth.enabled configs together 2022-04-25 22:09:34 -07:00
Girish Ramakrishnan
5d440d55c3 Add to changes 2022-04-25 09:07:27 -07:00
Johannes Zellner
4c3b81d29c Add make user local tests and fixup route 2022-04-24 22:49:12 +02:00
Johannes Zellner
032218c0fd Add route to make user local 2022-04-24 22:22:25 +02:00
Johannes Zellner
0cd48bd239 Ensure LDAP usernames are always treated lowercase 2022-04-23 11:21:14 +02:00
Girish Ramakrishnan
f5a2e8545b Initial support for ubuntu 22.04 Jammy Jellyfish 2022-04-21 12:30:37 -07:00
Girish Ramakrishnan
4306e20a8e Update docker to 20.10.14 2022-04-21 12:30:14 -07:00
Girish Ramakrishnan
635dd5f10d Update nodejs 2022-04-21 12:28:55 -07:00
Girish Ramakrishnan
7f89dfd261 add once.js 2022-04-15 19:01:35 -05:00
Girish Ramakrishnan
e878e71b20 promisify once 2022-04-15 17:59:41 -05:00
Girish Ramakrishnan
64a2493ca2 Fixup prettyBytes 2022-04-15 17:56:24 -05:00
Girish Ramakrishnan
26f9635a38 taskworker: only support async workers 2022-04-15 17:40:46 -05:00
Girish Ramakrishnan
5f2492558d callback is not needed here 2022-04-15 17:29:15 -05:00
Girish Ramakrishnan
c83c151e10 remove recursive-readdir 2022-04-15 11:34:16 -05:00
Girish Ramakrishnan
801dddc269 inline S3ReadStream 2022-04-15 11:25:15 -05:00
Girish Ramakrishnan
9a886111ad inline chunk.js 2022-04-15 09:25:54 -05:00
Girish Ramakrishnan
bdc9a0cbe3 inline prettyBytes 2022-04-15 09:18:07 -05:00
Girish Ramakrishnan
555f914537 remove lodash.chunk 2022-04-15 08:07:46 -05:00
Girish Ramakrishnan
43f86674b4 Remove delay module 2022-04-15 07:52:35 -05:00
Girish Ramakrishnan
f7ed044a40 fix storage test 2022-04-15 07:49:38 -05:00
Girish Ramakrishnan
72408f2542 Remove proxy-middleware, we have our own copy by now
See aad50fb5b2
2022-04-15 07:43:19 -05:00
Girish Ramakrishnan
0abc6c8844 replace pretty-bytes module 2022-04-15 07:34:16 -05:00
Girish Ramakrishnan
d46de32ffb Update packages 2022-04-15 07:24:50 -05:00
Girish Ramakrishnan
185d5d66ad even more constness 2022-04-14 20:30:00 -05:00
Girish Ramakrishnan
01ce251596 constness 2022-04-14 18:03:43 -05:00
Girish Ramakrishnan
05d7a7f496 constness 2022-04-14 17:50:41 -05:00
Girish Ramakrishnan
685bda35b9 storage: make remove and removeDir async 2022-04-14 16:16:20 -05:00
Girish Ramakrishnan
8d8cdd38a9 Add missing await 2022-04-14 15:40:51 -05:00
Girish Ramakrishnan
d54c03f0a0 storage: make exists async 2022-04-14 12:24:34 -05:00
Girish Ramakrishnan
11f7be2065 storage: verifyConfig is now async 2022-04-14 12:24:30 -05:00
Girish Ramakrishnan
a39e0ab934 storage: make remount async 2022-04-14 09:57:31 -05:00
Girish Ramakrishnan
b51082f7e4 storage: checkPreconditions is now async 2022-04-14 07:40:19 -05:00
Girish Ramakrishnan
9ec76c69ec s3: make callback of getS3Config 2022-04-14 07:35:41 -05:00
Girish Ramakrishnan
b0a09a8a00 restore: fix usage of backupId 2022-04-13 21:23:12 -05:00
Girish Ramakrishnan
5870f949a3 Update changes 2022-04-13 11:39:23 -05:00
Girish Ramakrishnan
87cb90c9b6 Fix crash
ReferenceError: Cannot access 'backups' before initialization
2022-04-08 16:27:11 -07:00
Girish Ramakrishnan
21b900258a backup: fix format of id
the id is used in dependsOn by the UI to find the linked apps. if we
had it as an uuid, we have to query the db a lot
2022-04-08 16:23:27 -07:00
Johannes Zellner
de9f3c10f4 Use new sftp addon 3.6.1
This will copy the uploaded file on conflict with a unique .number
extension
2022-04-07 18:02:11 +02:00
Johannes Zellner
47e45808a3 Give the addons a lot more time to initiate a connection 2022-04-06 13:05:09 +02:00
Girish Ramakrishnan
0280c2baba keep the backup if preserveSecs is -1 2022-04-05 11:08:38 -07:00
Girish Ramakrishnan
2f8f5fcb7d Typo 2022-04-05 10:26:05 -07:00
Girish Ramakrishnan
709d4041b2 backups: fix restore code path after backup id changes 2022-04-05 09:55:57 -07:00
Johannes Zellner
b4b999bd74 Fix await safe usage 2022-04-05 13:17:49 +02:00
Girish Ramakrishnan
ea3fd27123 backups: recursively update the dep preserveSecs
One idea was to compute this at cleanup time, but this has two problems:

* the UI won't reflect this value. can be good or bad
* the cleaner has no easy way to find out the "parent". I guess we should
  change our data structure, if we want to go down this route...
2022-04-04 21:29:35 -07:00
Girish Ramakrishnan
452a4d9a75 backups: add remotePath
the main motivation is that id can be used in REST API routes. previously,
the id was a path and this had a "/" in it. This made /api/v1/backups/:backupId
not work.
2022-04-04 20:40:40 -07:00
Girish Ramakrishnan
54934c41a7 storage: rename getBackupPath to getBasePath 2022-04-04 14:08:24 -07:00
Girish Ramakrishnan
a05e564ae6 Fix expectation in test 2022-04-04 14:03:07 -07:00
Girish Ramakrishnan
57ac94bab6 Fix appstore test 2022-04-04 13:55:23 -07:00
Girish Ramakrishnan
6839ff4cf6 reverseproxy: fix typo
this type was causing nginx configs of the primary domain being re-written
everytime we try to renew certs
2022-04-04 10:30:32 -07:00
Girish Ramakrishnan
993dda9121 rename function 2022-04-03 08:29:59 -07:00
Girish Ramakrishnan
70695b1b0f backups: set label of backup and control it's retention 2022-04-02 19:30:54 -07:00
Girish Ramakrishnan
d47b39d90b eventlog: distinguish install vs update finish 2022-04-01 14:19:53 -07:00
Girish Ramakrishnan
574d3b120f Use hyphens instead of camel case for scripts 2022-04-01 09:51:15 -07:00
Girish Ramakrishnan
3d1f2bf716 move init script into scripts
the baseimage directory was from a time when we used to build a
base image and snapshot it. this is not done anymore.

init-ubuntu.sh - static packages installed one time and managed by ubuntu
installer.sh - packages installed and maintained by cloudron. run before an update.
    this can "fail" and the updater can thus abort
start.sh - configuring packages
2022-04-01 09:48:40 -07:00
Girish Ramakrishnan
bac5edc188 cloudron-setup: remove arguments to init script
this ends support for cloudron 5 installs with this version of the script
2022-04-01 09:37:06 -07:00
Girish Ramakrishnan
7700c56d3e cloudron-setup: remove --skip-baseimage-init, it is unused 2022-04-01 09:22:23 -07:00
Girish Ramakrishnan
9f395f64da accessToken -> cloudronToken 2022-03-31 23:59:42 -07:00
Girish Ramakrishnan
73d029ba4b cloudron-setup: add setup-token to arg list 2022-03-31 23:49:34 -07:00
Girish Ramakrishnan
a292393a43 7.2 changes 2022-03-31 23:45:14 -07:00
Girish Ramakrishnan
37a4e8d5c5 cloudron-setup: add --setup-token 2022-03-31 23:38:54 -07:00
Girish Ramakrishnan
81728f4202 appstore: make the args of updateCloudron clear 2022-03-31 23:27:00 -07:00
Girish Ramakrishnan
2d2ddd1c49 add note on the existing setupToken 2022-03-31 23:02:26 -07:00
Girish Ramakrishnan
bc49f64a0c appstore: it never returns 422
I think I meant 402 which is subscription expired/billing error
2022-03-31 22:51:40 -07:00
Girish Ramakrishnan
52fc031516 Log error message if updateCloudron failed 2022-03-31 22:46:14 -07:00
Girish Ramakrishnan
cae528158c appstore: check login response 2022-03-31 22:43:34 -07:00
Girish Ramakrishnan
566a03cd59 remove unnecessary temp variables 2022-03-31 22:41:48 -07:00
Girish Ramakrishnan
ad2221350f Add appstore web token
* For existing installs, migrate using the soon to be obsoleted user_token route
* For new installs, the token post login is stashed during registration time
2022-03-31 22:35:45 -07:00
Girish Ramakrishnan
656dca7c66 rename cloudron_token to appstore_api_token 2022-03-31 22:18:08 -07:00
Girish Ramakrishnan
638fe2e6c8 ldap: add rootDSE test 2022-03-31 21:18:56 -07:00
Girish Ramakrishnan
3295d2b727 settings: remove licenseKey
this is unused
2022-03-31 12:47:45 -07:00
Johannes Zellner
c4689a8385 Add registerWithSetupToken() to be used 2022-03-31 17:29:44 +02:00
Girish Ramakrishnan
d09d6c21fa sshfs: fix bug where sshfs mounts were generated without unbound dependancy 2022-03-30 21:39:15 -07:00
Girish Ramakrishnan
7ec1594428 create a separate support user
This creates a separate user named 'cloudron-support' using which we
can provide remote support. The hyphen username convention follows the
systemd sytem username convention.

With a separate user, we don't need to ask users to keep changing PermitRootLogin
(and remind them to change it back).

Using a sudo user has various advantages:

* https://askubuntu.com/questions/687249/why-does-ubuntu-have-a-disabled-root-account
* https://wiki.debian.org/sudo
* https://askubuntu.com/questions/16178/why-is-it-bad-to-log-in-as-root

The yellowtent user is also locked down further - no password and no shell login.
2022-03-30 15:08:20 -07:00
Girish Ramakrishnan
529f6fb2cd sftp: fix private key file permissions on restore 2022-03-30 11:58:21 -07:00
Girish Ramakrishnan
724f5643bc suppress grep message 2022-03-30 11:10:00 -07:00
Girish Ramakrishnan
74e849e2a1 backup cleaner: do not clean when provider is not mounted 2022-03-30 10:17:20 -07:00
Girish Ramakrishnan
bfb233eca1 installer.sh: move installation of docker/node/nginx etc
no need to dup the code in two places. i think this will also
fix the unbound/resolvconf DNS resolution issue. this way unbound is configured
and is what gets used when docker is installed.

https://forum.cloudron.io/topic/6660/help-please-failing-setup-of-cloudron
https://forum.cloudron.io/topic/6632/help-me-please-got-error-while-installing-the-cloudron-on-a-fresh-ubuntu-20-04-x64-server
https://forum.cloudron.io/topic/6561/that-install-script-fails-74-times-out-of-75
2022-03-29 22:34:03 -07:00
Girish Ramakrishnan
5b27eb9c54 initializeBaseUbuntuImage: create yellowtent user 2022-03-29 21:41:46 -07:00
Girish Ramakrishnan
faf91d4d00 sshfs and mount.nfs are in base image now 2022-03-29 21:32:48 -07:00
Girish Ramakrishnan
dbb803ff5e cifs: use credentials file
this supports special characters in passwords better

https://forum.cloudron.io/topic/6577/failed-to-mount-inactive-mount-error-13-when-mounting-cifs-from-synology
2022-03-29 21:26:58 -07:00
Girish Ramakrishnan
0dea2d283b move sshfs key write logic to renderMountFile 2022-03-29 20:15:55 -07:00
Girish Ramakrishnan
cbc44da102 create sshfs dir in start.sh 2022-03-29 20:13:41 -07:00
Girish Ramakrishnan
3f633c9779 dns: check for CNAME record
Check if CNAME record exists and remove it if overwrite is set
2022-03-29 13:53:34 -07:00
Girish Ramakrishnan
6933ccefe2 Update nginx to 1.20.0-1 2022-03-28 13:25:05 -07:00
Girish Ramakrishnan
54aeff1419 ldap: send rootDSE response
some apps like osTicket require this
2022-03-25 14:15:18 -07:00
Girish Ramakrishnan
14f9d7fe25 cloudron-setup: add a redo flag to workaround dns failures
temporary hotfix for dns issues some VMs are having:

https://forum.cloudron.io/topic/6660/help-please-failing-setup-of-cloudron
https://forum.cloudron.io/topic/6632/help-me-please-got-error-while-installing-the-cloudron-on-a-fresh-ubuntu-20-04-x64-server
https://forum.cloudron.io/topic/6561/that-install-script-fails-74-times-out-of-75
2022-03-25 10:33:49 -07:00
Girish Ramakrishnan
144e98abab image name cannot start with '/'
https://forum.cloudron.io/topic/6689/cannot-uninstall-custom-app
https://stackoverflow.com/questions/43091075/docker-restrictions-regarding-naming-image
2022-03-24 10:03:40 -07:00
Girish Ramakrishnan
e0e0c049c8 add link to upstream issue 2022-03-23 09:52:17 -07:00
Johannes Zellner
ef0f9c5298 Fixup cn attribute for ldap to be according to spec
Bring back b54c4bb399
2022-03-22 10:19:21 -07:00
Girish Ramakrishnan
d13905377c firewall: do not add duplicate ldap redirect rules 2022-03-21 12:25:30 -07:00
Girish Ramakrishnan
6f1023e0cd Add to changes 2022-03-18 10:27:04 -07:00
Girish Ramakrishnan
eeddc233dd more changes 2022-03-16 09:05:41 -07:00
Girish Ramakrishnan
f48690ee11 dyndns: fix typo 2022-03-15 09:53:54 -07:00
Girish Ramakrishnan
3b0bdd9807 support: send the server IPv4 when remote support enabled 2022-03-14 21:30:54 -07:00
Girish Ramakrishnan
6dc5c4f13b ldap: add dummy apps search route for directus 2022-03-14 09:17:49 -07:00
Girish Ramakrishnan
9bb5096f1c nginx: enable underscores in headers
chatwoot requires this

https://www.chatwoot.com/docs/self-hosted/deployment/caprover#api-requests-failing-with-you-need-to-sign-in-or-sign-up-before-continuing

They are apparently disabled by default since they conflict with some CGI headers:

https://stackoverflow.com/questions/22856136/why-do-http-servers-forbid-underscores-in-http-header-names
https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/?highlight=disappearing%20http%20headers#missing-disappearing-http-headers
2022-03-13 23:04:34 -07:00
Girish Ramakrishnan
af42008fd3 Enable IPv6 on new interfaces with net_admin cap 2022-03-12 09:14:37 -08:00
Johannes Zellner
d6875d4949 Add test coverage support 2022-03-11 00:52:41 +01:00
Girish Ramakrishnan
4396bd3ea7 wildcard: handle ENODATA 2022-03-08 17:14:42 -08:00
Girish Ramakrishnan
db03053e05 cloudflare: remove async 2022-03-08 14:30:27 -08:00
Girish Ramakrishnan
193dff8c30 Better log 2022-03-03 10:08:34 -08:00
Girish Ramakrishnan
59582d081a port25check: log the error message 2022-03-03 09:58:58 -08:00
Girish Ramakrishnan
ef684d32a2 port25checker: Use random tick to not bombard our checker service 2022-03-03 09:57:41 -08:00
Girish Ramakrishnan
fc2a326332 mysql: Fix default collation
https://github.com/mattermost/mattermost-server/issues/19602#issuecomment-1057360142

> SELECT @@character_set_database, @@collation_database;

This will show utf8mb4 and utf8mb4_0900_ai_ci (was utf8mb4_unicode_ci)

To see the table schemas:

> SELECT table_schema, table_name, table_collation FROM information_schema.tables;
2022-03-02 22:34:30 -08:00
Girish Ramakrishnan
e66a804012 ufw may not be installed 2022-03-02 19:36:32 -08:00
Girish Ramakrishnan
5afa7345a5 route53: check permissions to perform route53:ListResourceRecordSets
otherwise, at install time we see "DNS credentials for xx are invalid. Update it in Domains & Certs view"

the exact error from route 53 is:

User: arn:aws:iam::xx:user/yy is not authorized to perform: route53:ListResourceRecordSets on resource: arn:aws:route53:::hostedzone/zz because no identity-based policy allows the route53:ListResourceRecordSets action
2022-03-02 10:44:52 -08:00
Girish Ramakrishnan
c100be4131 dns: filter out link local addresses
Unlike IPv4, IPv6 requires a link-local address on every network interface on which the IPv6 protocol is enabled, even when routable addresses are also assigned
2022-03-01 12:13:59 -08:00
Girish Ramakrishnan
d326d05ad6 sysinfo: add noop provider 2022-03-01 12:05:01 -08:00
Girish Ramakrishnan
eb0662b245 Up the json size to 2mb for block list route
https://forum.cloudron.io/topic/6575/cloudron-7-1-2-firewall-not-ipv6-ready
2022-03-01 11:57:50 -08:00
Johannes Zellner
b92641d1b8 Update ldapjs to 2.3.2 2022-03-01 17:36:09 +01:00
Girish Ramakrishnan
7912d521ca 7.1.3 changes 2022-02-28 14:26:37 -08:00
Johannes Zellner
71dac64c4c Only allow impersonation for equal or less powerful roles 2022-02-28 20:42:33 +01:00
Girish Ramakrishnan
aab6f222b3 better log 2022-02-28 11:04:44 -08:00
Girish Ramakrishnan
1cb1be321c remove usage of deprecated fs.rmdir 2022-02-25 16:43:20 -08:00
Girish Ramakrishnan
2434e81383 backups: fix incorrect mountpoint check with managed mounts 2022-02-25 12:53:05 -08:00
Girish Ramakrishnan
62142c42ea Fix crash 2022-02-25 11:03:16 -08:00
Girish Ramakrishnan
0ae30e6447 disable routes/test/apps-test for now 2022-02-24 20:50:35 -08:00
Girish Ramakrishnan
1a87856655 eventlog: log event on alias update 2022-02-24 20:30:42 -08:00
Girish Ramakrishnan
a3e097d541 add missing awaits for eventlog.add 2022-02-24 20:04:46 -08:00
Girish Ramakrishnan
9a6694286a eventlog: event type typo 2022-02-24 19:59:29 -08:00
Girish Ramakrishnan
a662a60332 eventlog: add event for certificate cleanup 2022-02-24 19:55:43 -08:00
Girish Ramakrishnan
69f3b4e987 better debugs 2022-02-24 12:57:56 -08:00
Girish Ramakrishnan
481586d7b7 add missing return 2022-02-24 12:51:27 -08:00
Girish Ramakrishnan
34c3a2b42d mail: increase pool_timeout 2022-02-24 12:25:38 -08:00
Johannes Zellner
c4a9295d3e Fix typo 2022-02-24 19:10:04 +01:00
Girish Ramakrishnan
993ff50681 cloudron-firewall: fix crash when ports are whitelisted
it failed with:
Feb 22 08:52:30 strawberry cloudron-firewall.sh[14300]: /home/yellowtent/box/setup/start/cloudron-firewall.sh: line 14: iptables --wait 120 --wait-interval 1: command not found

the root cause was that IFS was getting set but not getting reset later.
the IFS=xx line is not line local as it seems to appear (just a bash statement)
2022-02-22 00:56:57 -08:00
Girish Ramakrishnan
ba5c2f623c remove supererror, not really used 2022-02-21 17:34:51 -08:00
Girish Ramakrishnan
24a16cf8b4 redis: fix issue where protected mode was enabled with no password 2022-02-21 12:21:37 -08:00
Girish Ramakrishnan
5d34460e7f typo 2022-02-21 12:02:09 -08:00
Girish Ramakrishnan
64b6187a26 tests: make the network ipv6 2022-02-21 12:01:12 -08:00
Girish Ramakrishnan
c15913a1b2 add to changes 2022-02-20 17:46:37 -08:00
Girish Ramakrishnan
8ef5e35677 cloudron-firewall: add retry for xtables lock
cloudron-firewall.sh[30679]: ==> Setting up firewall
cloudron-firewall.sh[30693]: iptables: Chain already exists.
cloudron-firewall.sh[30694]: ip6tables: Chain already exists.
cloudron-firewall.sh[30699]: ipset v7.5: Set cannot be created: set with the same name already exists
cloudron-firewall.sh[30702]: ipset v7.5: Set cannot be created: set with the same name already exists
cloudron-firewall.sh[30740]: Another app is currently holding the xtables lock. Perhaps you want to use the -w option?
2022-02-20 17:42:20 -08:00
Girish Ramakrishnan
c55d1f6a22 Add to changes 2022-02-19 15:27:51 -08:00
Girish Ramakrishnan
8b5b13af4d leave note on br0ken usage of async 2022-02-19 14:26:48 -08:00
Girish Ramakrishnan
dfd51aad62 ensure dkim keys
a previous migration moved dkim keys into the database but looks like
sometimes the domain has empty dkim keys. this could be because we do not
add mail domain and domain in a transaction, so it's possible dkim was not
generated?
2022-02-19 14:23:30 -08:00
Girish Ramakrishnan
2b81120d43 cloudron-setup: say that it is cloudron we are installing 2022-02-18 13:38:52 -08:00
Girish Ramakrishnan
91dc91a390 fix dns tests 2022-02-18 11:36:14 -08:00
Johannes Zellner
b886a35cff Fixup gcdns calls. The api returns an array as result
https://github.com/googleapis/google-cloud-node/issues/2556
https://github.com/googleapis/google-cloud-node/issues/2896
2022-02-18 19:46:03 +01:00
Girish Ramakrishnan
e59efc7e34 bump free space requirement to 2GB 2022-02-18 09:56:42 -08:00
Johannes Zellner
2160644124 Lets not stretch our luck 2022-02-18 18:40:49 +01:00
Johannes Zellner
b54c4bb399 Fixup cn attribute for ldap to be according to spec 2022-02-18 17:43:47 +01:00
Girish Ramakrishnan
feaa5585e1 mailbox: fix crash when domain not found 2022-02-17 18:03:56 -08:00
Girish Ramakrishnan
6f7bede7bd listen on ipv6 as well for port 53 2022-02-17 11:56:08 -08:00
Girish Ramakrishnan
eb3e87c340 add debug 2022-02-17 11:08:22 -08:00
Girish Ramakrishnan
26a8738b21 make user listing return non-private fields
this was from a time when normal users could install apps
2022-02-16 21:22:38 -08:00
Girish Ramakrishnan
012a3e2984 ensure certificate of secondary domains 2022-02-16 20:32:04 -08:00
Girish Ramakrishnan
dfebda7170 Remove deprecated fs.rmdirSync 2022-02-16 20:30:33 -08:00
Girish Ramakrishnan
149f778652 wildcard: better error message 2022-02-16 20:22:50 -08:00
Girish Ramakrishnan
773dfd9a7b ipv6 support in firewall allow and block lists 2022-02-16 13:39:35 -08:00
Girish Ramakrishnan
426ed435a4 userdirectory: move the validation and apply logic 2022-02-16 13:00:06 -08:00
Girish Ramakrishnan
2ed770affd mountpoint: allow chown flag to be set 2022-02-16 11:48:37 -08:00
Girish Ramakrishnan
9d2d5d16f3 return 200 for immediate setters which require no further processing 2022-02-16 10:09:23 -08:00
Girish Ramakrishnan
9dbb299bb9 user directory: listen on ipv4 and ipv6 2022-02-15 14:27:51 -08:00
Girish Ramakrishnan
661799cd54 typo 2022-02-15 13:25:14 -08:00
Girish Ramakrishnan
0f25458914 rename key to match other json keys 2022-02-15 13:12:34 -08:00
Girish Ramakrishnan
d0c59c1f75 add separate route to get ipv4 and ipv6 2022-02-15 12:47:16 -08:00
Girish Ramakrishnan
c6da8c8167 make ipv4 and ipv6 settings separate 2022-02-15 12:36:05 -08:00
Girish Ramakrishnan
0dbe8ee8f2 godaddy: invalid ipv6 2022-02-15 12:01:52 -08:00
Girish Ramakrishnan
f8b124caa6 do not check if we have ipv6 to enable ipv6 2022-02-15 11:57:27 -08:00
Girish Ramakrishnan
125325721f add mail manager tests 2022-02-15 10:30:26 -08:00
Johannes Zellner
ac57e433b1 Improve errorhandling in netcup dns 2022-02-14 10:57:06 +01:00
Girish Ramakrishnan
de84cbc977 add note on turn container host mode 2022-02-11 23:08:56 -08:00
Girish Ramakrishnan
d6d7bc93e8 firewall: add ipxtables helper 2022-02-11 22:56:23 -08:00
Girish Ramakrishnan
8f4779ad2f Update addons to listen on ipv6
docker sets up the hostname DNS to be ipv4 and ipv6

Part of #264
2022-02-10 10:53:46 -08:00
Girish Ramakrishnan
6aa034ea41 platform: Only re-create docker network on version change 2022-02-10 09:32:22 -08:00
Girish Ramakrishnan
ca83deb761 Docker IPv6 support
Docker's initial IPv6 support is based on allocating public IPv6 to containers.
This approach has many issues:
* The server may not get a block of IPv6 assigned to it
* It's complicated to allocate a block of IPv6 to cloudron server on home setups
* It's unclear how dynamic IPv6 is. If it's dynamic, then should containers be recreated?
* DNS setup is complicated
* Not a issue for Cloudron itself, but with -P, it just exposed the full container into the world

Given these issues, IPv6 NAT is being considered. Even though NAT is not a security mechanism as such,
it does offer benefits that we care about:
* We can allocate some private IPv6 to containers
* Have docker NAT66 the exposed ports
* Works similar to IPv4

Currently, the IPv6 ports are always mapped and exposed. The "Enable IPv6" config option is only whether
to automate AAAA records or not. This way, user can enable it and 'sync' dns and we don't need to
re-create containers etc. There is no inherent benefit is not exposing IPv6 at all everywhere unless we find
it unstable.

Fixes #264
2022-02-09 23:54:53 -08:00
Girish Ramakrishnan
ff664486ff do not start if platform.start does not work 2022-02-09 23:15:37 -08:00
Girish Ramakrishnan
c5f9c80f89 move comment to unbound.conf 2022-02-09 23:15:37 -08:00
Girish Ramakrishnan
852eebac4d move cloudron network creation to platform code
this gives us more control on re-creating the network with different
arguments/options when needed.
2022-02-09 23:15:37 -08:00
Girish Ramakrishnan
f0f9ade972 sftp: listen on ipv6 2022-02-09 23:15:37 -08:00
Girish Ramakrishnan
f3ba1a9702 unbound: always disable ip6 during install
this was br0ken anway because "-s" is always false here. this is because
/proc/net/if_inet6 which has 0 size (but has contents!).
2022-02-09 23:15:37 -08:00
Girish Ramakrishnan
c2f2a70d7f vultr has ufw enabled by default 2022-02-09 23:15:37 -08:00
Girish Ramakrishnan
f18d108467 nginx: add listen note 2022-02-09 23:15:37 -08:00
Girish Ramakrishnan
566def2b64 Disable IPv6 temporary address 2022-02-09 12:17:42 -08:00
Girish Ramakrishnan
c9e3da22ab Revert "Disable userland proxy in new installations"
This reverts commit 430f5e939b.

Too early, apparently there is a bunch of issues and this is why
it's not disabled upstream - https://github.com/moby/moby/issues/14856
2022-02-09 09:45:04 -08:00
Girish Ramakrishnan
430f5e939b Disable userland proxy in new installations
https://github.com/moby/moby/issues/8356

The initial motivation for userland proxy is to enable localhost
connections since the linux kernel did not allow loopback connections
to be routed.

With hairpin NAT support (https://github.com/moby/moby/pull/6810), this
seems to be solved.
2022-02-08 11:51:37 -08:00
Girish Ramakrishnan
7bfa237d26 Update docker to 20.10.12 2022-02-08 10:57:24 -08:00
Girish Ramakrishnan
85964676fa Fix location conflict error message 2022-02-07 16:09:43 -08:00
Girish Ramakrishnan
68c2f6e2bd Fix users test 2022-02-07 14:22:34 -08:00
Girish Ramakrishnan
75c0caaa3d rename subdomains table to locations 2022-02-07 14:04:11 -08:00
Girish Ramakrishnan
46b497d87e rename SUBDOMAIN_ to LOCATION_
location is { subdomain, domain } pair
2022-02-07 13:48:08 -08:00
Girish Ramakrishnan
964c1a5f5a remove field from errors
we have standardized on indexOf in error.message by now
2022-02-07 13:44:29 -08:00
Johannes Zellner
d5481342ed Add ability to filter users by state 2022-02-07 17:18:13 +01:00
Johannes Zellner
e3a0a9e5dc Hack to allow SOGo logins for more than 1k mailboxes 2022-02-07 16:22:05 +01:00
Girish Ramakrishnan
23b3070c52 add percent info when switching dashboard 2022-02-06 11:21:32 -08:00
Girish Ramakrishnan
5048f455a3 Misplaced brackets 2022-02-06 10:58:49 -08:00
Girish Ramakrishnan
e27bad4bdd Fix incorrect brackets 2022-02-06 10:22:04 -08:00
Johannes Zellner
4273c56b44 Add some changes 2022-02-05 21:09:14 +01:00
Girish Ramakrishnan
0af9069f23 make linode async 2022-02-04 16:01:41 -08:00
Girish Ramakrishnan
e1db45ef81 remove callback asserts 2022-02-04 15:47:38 -08:00
Girish Ramakrishnan
59b2bf72f7 make gcdns async 2022-02-04 15:46:17 -08:00
Girish Ramakrishnan
8802b3bb14 make namecheap async 2022-02-04 15:34:02 -08:00
Girish Ramakrishnan
ee0cbb0e42 make route53 async 2022-02-04 15:20:49 -08:00
Girish Ramakrishnan
5d415d4d7d make cloudflare, gandi, manual, noop, wildcard, netcup, godaddy, namecom async 2022-02-04 14:36:30 -08:00
Girish Ramakrishnan
3b3b510343 Check if we get IPv6 when enabling 2022-02-04 11:15:53 -08:00
Girish Ramakrishnan
5c56cdfbc7 Revert "tld.isValid is deprecated"
This reverts commit bd4097098d.

the published library does not have the function :/
2022-02-04 10:49:19 -08:00
Girish Ramakrishnan
7601b4919a make upsert remove the additional records 2022-02-04 10:22:22 -08:00
Girish Ramakrishnan
856b23d940 asyncify the vultr and DO backend 2022-02-04 10:15:35 -08:00
Girish Ramakrishnan
bd4097098d tld.isValid is deprecated 2022-02-04 10:09:24 -08:00
Johannes Zellner
1441c59589 Remove left over assert 2022-02-04 17:35:44 +01:00
Girish Ramakrishnan
0373fb70d5 make waitForDns async
cloudflare is partly broken
2022-02-03 17:35:45 -08:00
Girish Ramakrishnan
da5b5aadbc typo in debug 2022-02-02 15:07:50 -08:00
Girish Ramakrishnan
b75afaf5d5 clone: secondary domains are required 2022-02-01 23:36:41 -08:00
Girish Ramakrishnan
26bfa32c7b Fix display of task error 2022-02-01 21:47:49 -08:00
Girish Ramakrishnan
67fe17d20c Fix crash with alias domains 2022-02-01 21:28:43 -08:00
Girish Ramakrishnan
150f89ae43 proxyauth: on invalid token, redirect user
https://forum.cloudron.io/topic/6425/403-in-proxyauth-apps-after-server-migration
2022-02-01 17:58:05 -08:00
Girish Ramakrishnan
944d364e1a turn: secret is a string 2022-02-01 17:36:51 -08:00
Girish Ramakrishnan
aeef815bf7 proxyAuth: persist the secret token 2022-02-01 17:35:21 -08:00
Girish Ramakrishnan
46144ae07a lint 2022-02-01 17:35:21 -08:00
Girish Ramakrishnan
8f08ed1aed Fix blobs schema 2022-02-01 17:29:25 -08:00
Girish Ramakrishnan
73f637be26 Add 2 more changes 2022-02-01 12:09:22 -08:00
Girish Ramakrishnan
37c8ca7617 mail: use port25check.cloudron.io to check outbound port 25 connectivity 2022-01-31 16:55:56 -08:00
Girish Ramakrishnan
c4bcbb8074 mail: smtp.live.com is not reachable anymore 2022-01-31 11:20:21 -08:00
Girish Ramakrishnan
19ddff058e reverseproxy: fix crash because of missing app property 2022-01-29 16:53:26 -08:00
Girish Ramakrishnan
5382e3d832 remove nginx config of stopped apps
when the cert of a stopped app gets auto-cleaned up, nginx does not
start anymore since the config references the cert.

there are two possible fixes:
* do not cleanup cert of stopped apps
* remove the nginx config of stopped apps

this implements the second approach
2022-01-28 10:23:56 -08:00
Girish Ramakrishnan
ee3d1b3697 remove unused var 2022-01-27 09:16:46 -08:00
Girish Ramakrishnan
a786fad3ee mountPoint is only set for 'mountpoint' (unmanaged)
When restoring, mountPoint field is expected for managed mount points
2022-01-26 13:37:16 -08:00
Girish Ramakrishnan
8b9d821905 isMountProvider -> isManagedProvider 2022-01-26 12:40:28 -08:00
Girish Ramakrishnan
04b7c14fd7 restore: fix crash when using fs/mountpoint 2022-01-26 12:17:46 -08:00
Girish Ramakrishnan
5517d09e45 cloudron-setup: fix curl output capture
not sure why the old method does not work. also move the cache file
2022-01-26 10:22:17 -08:00
Johannes Zellner
50adac3d99 Ensure volume mountpoints are happening before containers start up 2022-01-26 16:33:35 +01:00
Johannes Zellner
8f8a59bd87 Unbound does no longer depend on docker 2022-01-26 16:33:19 +01:00
Johannes Zellner
8e15f27080 Make unbound listen also on future devices
The local network for docker containers might not be up yet
https://unbound.docs.nlnetlabs.nl/en/latest/manpages/unbound.conf.html#term-ip-freebind-yes-or-no
2022-01-26 16:32:48 +01:00
Girish Ramakrishnan
e7977525a0 better error message 2022-01-25 16:41:29 -08:00
Girish Ramakrishnan
be9830d0d4 postgresql: enable postgis 2022-01-21 23:18:40 -08:00
Girish Ramakrishnan
8958b154e9 ldap: do not list inactive users 2022-01-21 21:07:33 -08:00
Girish Ramakrishnan
d21d13afb0 Add to changes 2022-01-21 17:31:26 -08:00
Girish Ramakrishnan
43759061a4 set secondaryDomains environment variables
part of #809
2022-01-21 11:35:01 -08:00
Johannes Zellner
a3efa8db54 Use semicolon instead of comma 2022-01-21 19:42:07 +01:00
Girish Ramakrishnan
f017e297f7 secondaryDomains are always required
they can still become empty after an update but install and change_location
requires them

part of #809
2022-01-21 10:03:30 -08:00
Girish Ramakrishnan
e8577d4d85 more location renaming 2022-01-16 18:56:44 -08:00
Girish Ramakrishnan
e8d08968a1 rename location to subdomain
the primary subdomain was previously called 'location'. but the alias/secondary/redirect
subdomain is called 'subdomain'. this makes it all consistent.

location terminology is now used for { subdomain, domain } pair
2022-01-16 12:48:29 -08:00
Girish Ramakrishnan
1e2f01cc69 reverseProxy: refactor filename logic 2022-01-16 12:22:29 -08:00
Girish Ramakrishnan
b34f66b115 add secondary domains
note that for updates to work, we keep the secondary domain optional,
even though they are really not.

part of #809
2022-01-16 12:10:48 -08:00
Girish Ramakrishnan
d18977ccad reverseProxy: single writeAppNginxConfig()
this prepares for secondary domains
2022-01-16 11:29:21 -08:00
Girish Ramakrishnan
89c3847fb0 reverseProxy: refactor 2022-01-16 10:28:49 -08:00
Girish Ramakrishnan
aeeeaae62a pass domain object to reduce one query 2022-01-16 10:16:14 -08:00
Girish Ramakrishnan
1e98a2affb change argument order to match others 2022-01-16 09:45:59 -08:00
Girish Ramakrishnan
3da19d5fa6 Use constants 2022-01-14 22:57:44 -08:00
Girish Ramakrishnan
d7d46a5a81 rename alternateDomains to redirectDomains 2022-01-14 22:32:34 -08:00
Girish Ramakrishnan
d4369851bf ldap: add organizationalperson and top objectclasses
these are used by firefly-iii ldap atleast
2022-01-14 14:31:33 -08:00
Girish Ramakrishnan
97e439f8a3 more profileConfig rename 2022-01-13 16:49:06 -08:00
Girish Ramakrishnan
e9945d8010 Update cloudron-syslog 2022-01-13 16:29:50 -08:00
Girish Ramakrishnan
d35f948157 rename directory config to profile config 2022-01-13 14:39:49 -08:00
Girish Ramakrishnan
09d3d258b6 do not retry forever if dpkg install fails
https://forum.cloudron.io/topic/6329/cloudron-update-failing
2022-01-13 11:04:45 -08:00
Girish Ramakrishnan
4513b6de70 add a way for admins to set username when profiles are locked 2022-01-12 16:21:00 -08:00
Girish Ramakrishnan
ded5db20e6 mail: use same validation logic as mailbox name for aliases as well 2022-01-10 22:06:37 -08:00
Johannes Zellner
6cf7ae4788 Add changes 2022-01-10 16:45:44 +01:00
Johannes Zellner
0508a04bab Support cifs seal option
https://manpages.debian.org/testing/cifs-utils/mount.cifs.8.en.html#seal
2022-01-10 14:28:09 +01:00
Girish Ramakrishnan
e7983f03d8 Update packages 2022-01-09 16:39:52 -08:00
Girish Ramakrishnan
eada292ef3 email addon: add additional env vars 2022-01-09 16:03:35 -08:00
Girish Ramakrishnan
3a19be5a2e filemanager: fix file delete 2022-01-07 12:03:16 -08:00
Johannes Zellner
52385fcc9c Rename exposed ldap to user directory 2022-01-07 14:06:13 +01:00
Johannes Zellner
cc998ba805 Implement full exposed ldap auth 2022-01-07 13:11:27 +01:00
Girish Ramakrishnan
37d641ec76 waitForDns: support AAAA 2022-01-06 22:08:28 -08:00
Girish Ramakrishnan
3fd45f8537 settings: add route to configure ipv6
part of #264
2022-01-06 21:42:03 -08:00
Girish Ramakrishnan
f4a21bdeb4 sysinfo: fixed provider now takes ipv6 optionally
part of #264
2022-01-06 21:39:28 -08:00
Girish Ramakrishnan
d65ac353fe initial ipv6 support
this adds and waits for AAAA records based on setting. we have to wait
for both A and AAAA because we don't know if the user is accessing via
IPv4 or IPv6. For Let's Encrypt, IPv6 is preferred (but not sure if it
retries if IPv6 is unreachable).

part of #264
2022-01-06 17:22:45 -08:00
Girish Ramakrishnan
7d7539f931 replace the forEach 2022-01-06 17:22:45 -08:00
Girish Ramakrishnan
ac19921ca1 dns: refactor register/unregisterLocation logic
this prepares it for ipv6 support
2022-01-06 16:34:33 -08:00
Girish Ramakrishnan
0654d549db sysinfo: return the ipv4 and ipv6 address 2022-01-06 16:21:30 -08:00
Girish Ramakrishnan
91b1265833 sysinfo: ensure we return 5952 ipv6 format 2022-01-06 12:33:56 -08:00
Girish Ramakrishnan
2bc5c3cb6e Fixes to getServerIPv6() 2022-01-06 12:22:16 -08:00
Girish Ramakrishnan
cc61ee00be settings: add ipv6 2022-01-06 11:38:41 -08:00
Girish Ramakrishnan
c74556fa3b promise-retry: add a retry function to abort early 2022-01-06 11:28:30 -08:00
Girish Ramakrishnan
bf51bc25e9 dnsConfig -> domainConfig
this prepares for the incoming settings.getDnsConfig()
2022-01-05 22:56:10 -08:00
Girish Ramakrishnan
bbf1a5af3d sysinfo: add interface to get IPv6 address 2022-01-05 18:08:15 -08:00
Girish Ramakrishnan
235d18cbb1 add note on the promiseRetry usage 2022-01-05 12:27:03 -08:00
Girish Ramakrishnan
32668b04c6 mail: fix name validation
https://forum.cloudron.io/topic/6229/mailbox-name-can-only-contain-alphanumerals-and-dot/10
2022-01-05 09:55:10 -08:00
Girish Ramakrishnan
9ccf46dc8b Bump year 2022-01-05 09:18:48 -08:00
Girish Ramakrishnan
d049aa1b57 2022 now 2022-01-05 09:17:13 -08:00
Johannes Zellner
44a149d1d9 Add exposed ldap secret for bind auth 2022-01-05 14:35:48 +01:00
Johannes Zellner
38dd7e7414 Update lockfile 2022-01-05 14:33:07 +01:00
Johannes Zellner
fb5d726d42 Ensure tests have sudo access to setldapallowlist 2022-01-05 14:32:50 +01:00
Girish Ramakrishnan
531a6fe0dc Use ipv4.api.cloudron.io endpoint for IPv4 detection 2022-01-04 22:14:53 -08:00
Girish Ramakrishnan
15d0dd93f4 mail: allow underscore in mail address 2022-01-04 14:02:58 -08:00
Girish Ramakrishnan
d8314d335a implement manifest.logPaths 2022-01-04 10:04:40 -08:00
Girish Ramakrishnan
b18626c75c getLocalLogfilePaths -> getLogPaths 2022-01-04 09:14:13 -08:00
Johannes Zellner
a04abf25f4 We now use esversion 11 2022-01-04 14:40:33 +01:00
Girish Ramakrishnan
ebb6a246cb Update cloudron-manifestformat 2022-01-03 18:42:01 -08:00
Girish Ramakrishnan
e672514ec7 update packages
also removes unused mime and mustache-express
2022-01-03 10:05:09 -08:00
Johannes Zellner
b531a10392 Invite links do not depend on resetToken expiration 2021-12-28 16:34:47 +01:00
Johannes Zellner
9a71360346 Only check for PermitRootLogin if we want to enable remote support 2021-12-26 17:51:05 +01:00
Girish Ramakrishnan
5e9a46d71e filemanager: fix mounting of filesystem and mountpoint backends 2021-12-24 15:05:51 -08:00
Girish Ramakrishnan
66fd05ce47 sftp: add note 2021-12-23 22:35:28 -08:00
Johannes Zellner
7117c17777 Add exposed ldap tests 2021-12-23 21:31:48 +01:00
Johannes Zellner
9ad7123da4 Fix exposed ldap bind
the duplicate functions should probably share some code
2021-12-23 17:58:08 +01:00
Johannes Zellner
98fd78159e Do not require app auth for exposed ldap 2021-12-23 10:23:54 +01:00
Girish Ramakrishnan
3d57b2b47c docker: loop through the ip net addr output
all of a sudden, my linux box has the actual address in [1].

ip -f inet -j addr show wlp2s0

[{
        "addr_info": [{}]
    },{
        "ifindex": 3,
        "ifname": "wlp2s0",
        "flags": ["BROADCAST","MULTICAST","UP","LOWER_UP"],
        "mtu": 1500,
        "qdisc": "mq",
        "operstate": "UP",
        "group": "default",
        "txqlen": 1000,
        "addr_info": [{
                "family": "inet",
                "local": "192.168.1.8",
                "prefixlen": 24,
                "broadcast": "192.168.1.255",
                "scope": "global",
                "dynamic": true,
                "noprefixroute": true,
                "label": "wlp2s0",
                "valid_life_time": 78146,
                "preferred_life_time": 78146
            }]
    },{
        "addr_info": [{}]
    },{
        "addr_info": [{}]
    }
]
2021-12-22 16:48:00 -08:00
Girish Ramakrishnan
2bc49682c4 mailproxy: use http 2021-12-21 12:30:28 -08:00
Girish Ramakrishnan
bb2d9fca9b update manifest format for 'upstreamVersion' field 2021-12-21 11:24:04 -08:00
Girish Ramakrishnan
be8ab3578b update mysql container
* remove 'request' module usage entirely
* http based service
2021-12-20 10:52:42 -08:00
Girish Ramakrishnan
43af0e1e3c Update turn base image 2021-12-20 09:02:00 -08:00
Girish Ramakrishnan
43f33a34b8 switch mail container to http 2021-12-19 12:11:47 -08:00
Girish Ramakrishnan
7aded4aed7 switch status api to http as well 2021-12-17 13:39:06 -08:00
Girish Ramakrishnan
d37652d362 postgresql container update
* makes the service http based
* no more request module usage
2021-12-17 13:26:34 -08:00
Girish Ramakrishnan
9590a60c47 Update base image of some addons to 3.2.0 2021-12-17 09:18:22 -08:00
Girish Ramakrishnan
54bb7edf3b asyncify importAppDatabase 2021-12-17 07:47:20 -08:00
Girish Ramakrishnan
34d11f7f6e mongodb container update
* upgrades mongodb to 4.4
* makes the service http based
* no more request module usage
2021-12-16 22:49:38 -08:00
Girish Ramakrishnan
3a956857d2 update package.lock for newer node 2021-12-16 22:43:23 -08:00
Girish Ramakrishnan
08d41f4302 update redis base image 2021-12-16 22:26:43 -08:00
Girish Ramakrishnan
219fafc8e4 Update base image to 3.2.0 (mongodb 4.4) 2021-12-16 16:26:31 -08:00
Girish Ramakrishnan
53593a10a9 redis: fix issue with double headers 2021-12-16 14:06:52 -08:00
Girish Ramakrishnan
26dc63553e update redis addon to use pipeline+http api 2021-12-15 17:54:50 -08:00
Girish Ramakrishnan
83fd3d9ab4 We now require node 16.13.1 2021-12-15 17:54:50 -08:00
Johannes Zellner
d69758e559 Only set ldap allowlist if file exists and is not empty 2021-12-15 19:23:22 +01:00
Johannes Zellner
d6fbe2a1bb Use correct error object 2021-12-15 17:22:16 +01:00
Girish Ramakrishnan
a3280a0e30 Update node to 16.13.1
useful for using stream.promises
2021-12-14 20:49:25 -08:00
Girish Ramakrishnan
e7f94b6748 Update base image to 3.1.0 2021-12-14 20:47:41 -08:00
Girish Ramakrishnan
6492c9b71f nginx: remove combined2 custom log format
collectd does not use this anymore (eb47476c83)

This makes nginx work better with a variety of tools like Wazuh and ossec

https://forum.cloudron.io/topic/6077/nginx-logs-format/
https://forum.cloudron.io/topic/6161/implement-default-nginx-logging
2021-12-13 10:47:12 -08:00
Johannes Zellner
438bd36267 Fixup exposed ldap startup state 2021-12-10 18:06:23 +01:00
Johannes Zellner
1c7eeb6ac6 Handle exposed ldap allowlist 2021-12-10 17:04:30 +01:00
Johannes Zellner
86d642c8a3 Fixup ldap group tests 2021-12-09 17:23:14 +01:00
Johannes Zellner
d02d2dcb80 Expose ldap groups to apps 2021-12-09 15:07:30 +01:00
Girish Ramakrishnan
b5695c98af mailserver: make restart wait for restart 2021-12-08 16:55:44 -08:00
Girish Ramakrishnan
fcdc53f7bd add flag to enable/disable mailbox sharing 2021-12-08 11:05:13 -08:00
Girish Ramakrishnan
5d85fe2577 pass the attempt as argument 2021-12-08 10:19:16 -08:00
Girish Ramakrishnan
013f5d359d pass debug to promise-retry 2021-12-07 11:18:26 -08:00
Girish Ramakrishnan
ae0e572593 promise-retry: debug retry errors 2021-12-07 11:14:24 -08:00
Girish Ramakrishnan
b4ed05c911 disable exim4 2021-12-07 09:42:25 -08:00
Girish Ramakrishnan
683ac9b16e remove support for manifest version 1
this is long untested by now
2021-12-06 17:44:09 -08:00
Girish Ramakrishnan
2415e1ca4b Prefix email addon variables with CLOUDRON_EMAIL instead of CLOUDRON_MAIL
otherwise, it's conflicting with the sendmail and recvmail addons
2021-12-06 17:39:29 -08:00
Girish Ramakrishnan
cefbe7064f Fix crash when changing the location of app with disabled sendmail addon 2021-12-06 13:59:00 -08:00
Girish Ramakrishnan
a687b7da26 netcup: remove debugs flooding the logs 2021-12-06 13:37:14 -08:00
Girish Ramakrishnan
ea2b11e448 Fix tests 2021-12-03 18:33:49 -08:00
Girish Ramakrishnan
39807e6ba4 domain: split the config and wellknown routes
we want to add more stuff to the UI like the jitsi URL
2021-12-03 18:14:46 -08:00
Girish Ramakrishnan
5592dc8a42 schema: add cron to apps table 2021-12-03 13:02:25 -08:00
Girish Ramakrishnan
aab69772e6 mailbox: add app owner type
this is useful when we create mailboxes for the recvmail addon
2021-12-02 22:28:06 -08:00
Girish Ramakrishnan
a5a9fce1eb mail: allow masquerading for mail manager
this is mostly for the UI, it's hard to hide just this
2021-12-02 14:56:37 -08:00
Girish Ramakrishnan
e5fecdaabf Add mail manager role
part of #807
2021-12-02 09:24:09 -08:00
Johannes Zellner
412bb406c0 Do not attempt to start exposed ldap server again 2021-11-26 10:50:14 +01:00
Johannes Zellner
98b28db092 Store allowlist for exposed directory server 2021-11-26 10:43:50 +01:00
Johannes Zellner
63fe75ecd2 Reduce noisy externalldap debug()s 2021-11-26 09:55:59 +01:00
Johannes Zellner
c51a4514f4 start/stop exposed LDAP depending on settings 2021-11-26 09:50:21 +01:00
Girish Ramakrishnan
3dcbeb11b8 mail: use dashboardDomain and not mailDomain
also remove unused mail_domain
2021-11-25 15:04:30 -08:00
Girish Ramakrishnan
e5301fead5 exclude externalldap debugs by default 2021-11-25 14:49:59 -08:00
Johannes Zellner
4a467c4dce Add crud for exposed ldap settings 2021-11-23 18:00:07 +01:00
Johannes Zellner
3a8aaf72ba Expose LDAP via iptables 2021-11-23 12:37:03 +01:00
Johannes Zellner
735737b513 Initial attempt to expose the ldap server 2021-11-22 21:29:23 +01:00
Johannes Zellner
37f066f2b0 Fix user signup when profile is locked and add tests 2021-11-22 20:42:51 +01:00
Johannes Zellner
1a9cfd046a Update invite route tests 2021-11-22 19:32:42 +01:00
Girish Ramakrishnan
31523af5e1 ami: fix instance id check 2021-11-17 19:05:26 -08:00
Girish Ramakrishnan
e71d932de0 eventlog: add Json suffix to json fields 2021-11-17 12:31:46 -08:00
Girish Ramakrishnan
7f45e1db06 send new login location to user email 2021-11-17 11:53:03 -08:00
Girish Ramakrishnan
2ab2255115 fix dhparam generation
it cannot be created in default config creation time since it is
already run pre-VM snapshot time
2021-11-17 11:48:06 -08:00
Girish Ramakrishnan
515b1db9d0 Fix tests 2021-11-17 11:35:44 -08:00
Girish Ramakrishnan
a7fe7b0aa3 boxerror: add acme error code 2021-11-17 10:54:26 -08:00
Girish Ramakrishnan
89389258d7 pass correct auditSource when raising notifications
this fixes the bug where automatic app update notification were not
raised.
2021-11-17 10:42:53 -08:00
Girish Ramakrishnan
1aacf65372 apps: pass the auditSource to addTask()
this is required for the notification logic to know what caused the
task (cron or manual, for example)
2021-11-17 10:38:02 -08:00
Girish Ramakrishnan
7ffcfc5206 auditSource: add PLATFORM 2021-11-17 10:33:28 -08:00
Girish Ramakrishnan
5ab2d9da8a notifications: remove dead code 2021-11-17 10:26:47 -08:00
Girish Ramakrishnan
cd302a7621 add missing await 2021-11-17 09:38:01 -08:00
Girish Ramakrishnan
1c8e699a71 generate dhparams per server
this way we don't need to save/restore it from the database.
2021-11-16 23:03:16 -08:00
Girish Ramakrishnan
c4db0d746d acme: if account key was revoked, generate new account key
the plan was to migrate only specific keys but this allows us the
flexibility to revoke keys after the release (since we have not
gotten response from DO about access to old 1-click images so far).
2021-11-16 22:57:40 -08:00
Girish Ramakrishnan
b7c5c99301 move turn secret generation 2021-11-16 22:37:42 -08:00
Girish Ramakrishnan
132c1872f4 sftp: move key generation to sftp code 2021-11-16 21:52:39 -08:00
Girish Ramakrishnan
0f04933dbf backups: fix issue where mail backups were not cleaned up 2021-11-16 19:52:51 -08:00
Girish Ramakrishnan
6d864d3621 ensure we have atleast 1GB before making an update 2021-11-16 18:20:40 -08:00
Girish Ramakrishnan
b6ee1fb662 mail: add non-tls ports for recvmail addon 2021-11-16 17:21:34 -08:00
Girish Ramakrishnan
649cd896fc throw error and not return 2021-11-16 14:46:58 -08:00
Girish Ramakrishnan
39be267805 restore: secrets must be copied over after downloading box backup 2021-11-16 11:14:41 -08:00
Girish Ramakrishnan
f6356b2dff speed up dhparam creation 2021-11-16 09:53:43 -08:00
Johannes Zellner
48574ce350 Add missing await 2021-11-16 18:48:13 +01:00
Girish Ramakrishnan
40a3145d92 Add more bad account keys and fix fresh cloudron migration 2021-11-16 00:56:59 -08:00
Girish Ramakrishnan
f42430b7c4 regenerate acme key of DO 1-click image
https://community.letsencrypt.org/t/receiving-expiration-emails-for-dozens-of-domains/165441
2021-11-16 00:25:59 -08:00
Girish Ramakrishnan
178d93033f 7.0.4 changes 2021-11-15 23:51:06 -08:00
Girish Ramakrishnan
01a1803625 provision: delay initialization of secrets until provision time
when we create the DO 1-click image, the key also gets snapshotted.

https://community.letsencrypt.org/t/receiving-expiration-emails-for-dozens-of-domains/165441
2021-11-15 23:33:54 -08:00
Girish Ramakrishnan
42eef42cf3 Add to changes 2021-11-15 13:58:59 -08:00
Girish Ramakrishnan
9c096b18e1 demo: limit to 20 apps 2021-11-15 13:55:29 -08:00
Girish Ramakrishnan
aa3ee2e180 cloudron-support: add option to reset account
new cli option --reset-appstore-account
2021-11-15 10:06:18 -08:00
Girish Ramakrishnan
fdefc780b4 docker: hardcode the bridge gateway IP
on some environments like ESXi, the gateway gets the dynamic IP 172.18.0.2.
we have hardcoded 172.18.0.1 in many places in the code

https://forum.cloudron.io/topic/5987/install-cloudron-7-0-3-on-ubuntu-20-04-3-esxi
2021-11-12 09:04:03 -08:00
Johannes Zellner
3826ae64c6 Ensure the main login route is rate-limited 2021-11-12 11:14:21 +01:00
Johannes Zellner
dcdafda124 Remove deprecated developer/login route 2021-11-12 11:12:15 +01:00
Girish Ramakrishnan
fc2cc25861 Update manifest-format (httpPaths) 2021-11-09 21:56:52 -08:00
Girish Ramakrishnan
68db4524f1 remove unused httpPaths from manifest 2021-11-09 21:50:33 -08:00
Girish Ramakrishnan
48b75accdd 7.0.4 changes 2021-11-09 09:31:58 -08:00
Johannes Zellner
0313a60f44 Fix newline stripping when passing the tmp file as path
This fixes the issue where the input data gets too large for the
commandline argument buffer
2021-11-09 16:05:36 +01:00
Girish Ramakrishnan
9897b5d18a appstore: fix crash if account already registered 2021-11-08 10:45:57 -08:00
Girish Ramakrishnan
e4cc431d35 Do not nuke all the logrotate configs on update
this was added many releases ago to migrate to new logrotate configs.
looks like I forgot to remove this.

https://forum.cloudron.io/topic/4381/safe-to-truncate-home-yellowtent-platformdata-logs-when-large-disk-consumer
2021-11-04 09:41:33 -07:00
Girish Ramakrishnan
535a755e74 7.1.0 changes 2021-11-03 15:08:48 -07:00
Johannes Zellner
2ae77a5ab7 Provide dashboardOrigin to proxy auth for stylesheet sourcing 2021-11-03 22:12:30 +01:00
Johannes Zellner
e36d7665fa The profile based password reset does not return a resetLink 2021-11-03 22:03:08 +01:00
Girish Ramakrishnan
786b627bad add 7.0.3 changes 2021-11-03 12:21:12 -07:00
Girish Ramakrishnan
c7ddbea8ed restore: download mail backup in restore phase
if we download it in the platform start phase, there is no way to
give feedback to the user. so it's best to show the restore UI and
not redirect to the dashboard.
2021-11-03 12:10:40 -07:00
Girish Ramakrishnan
af2a8ba07f add retry to platform.start instead
this is because it holds a lock and cannot be re-tried

See also 0c0aeeae4c which tried to
make it for all startup tasks
2021-11-02 23:35:53 -07:00
Girish Ramakrishnan
4ffe03553a database: sqlMessage can be undefined for connection errors 2021-11-02 23:23:59 -07:00
Girish Ramakrishnan
f505fdd5cb remove the space 2021-11-02 18:07:45 -07:00
Girish Ramakrishnan
ce4f5c0ad6 backups: print the app index/total 2021-11-02 18:07:19 -07:00
Girish Ramakrishnan
de2c596394 backups: typo
this resulted in incomplete backups when there is an app with backups disabled
2021-11-02 18:00:04 -07:00
Girish Ramakrishnan
6cb041bcb2 Print readable sizes in the log 2021-11-02 17:51:27 -07:00
Girish Ramakrishnan
0c0aeeae4c retry startup tasks on database error
https://forum.cloudron.io/topic/5909/cloudron-7-0-1-gitlab-stuck-after-update
2021-11-02 14:05:51 -07:00
Girish Ramakrishnan
8bfb3d6b6d mail: save message-id in eventlog 2021-11-02 01:42:07 -07:00
Girish Ramakrishnan
f803754e08 mail: fix eventlog search 2021-11-02 01:00:28 -07:00
Girish Ramakrishnan
09cfce79fb mail: fix direction field in eventlog of deferred mails 2021-11-02 00:48:01 -07:00
Girish Ramakrishnan
6479e333de pop3: fix crash when authenticating non-existent mailbox 2021-11-01 19:54:39 -07:00
Girish Ramakrishnan
28d1d5e960 ldap: make mailbox app passwords work with sogo 2021-11-01 19:17:30 -07:00
Girish Ramakrishnan
15d8f4e89c ldap: remove legacy sogo search route 2021-11-01 17:08:23 -07:00
Girish Ramakrishnan
8fdbd7bd5f 7.0.3 changes 2021-11-01 16:17:35 -07:00
Girish Ramakrishnan
7b5ed0b2a1 support: set filePath when user is root 2021-11-01 12:20:47 -07:00
Girish Ramakrishnan
b69c5f62c0 Add to changes 2021-10-28 10:27:32 -07:00
Johannes Zellner
63f6f065ba Add and fixup invite link related tests 2021-10-28 11:18:31 +02:00
Johannes Zellner
92f0f56fae do not strictly require fallbackEmail on user creation but provide a fallback 2021-10-28 10:29:02 +02:00
Johannes Zellner
cb8aa15e62 Do not allow setting ghost password for user without username 2021-10-27 23:36:44 +02:00
Johannes Zellner
4356d673bc Fix wrong assert and minor typos 2021-10-27 22:31:54 +02:00
Girish Ramakrishnan
5ece159fba sftp: fix crash when creating directory 2021-10-27 13:17:23 -07:00
Johannes Zellner
b59776bf9b fail getting invite link or sending invite if invate was already used 2021-10-27 21:25:43 +02:00
Johannes Zellner
475795a107 Invite is now also separate 2021-10-27 19:58:06 +02:00
Johannes Zellner
9a80049d36 Add two distinct password reset routes 2021-10-27 19:12:18 +02:00
Johannes Zellner
daf212468f fallbackEmail is now independent from email 2021-10-26 22:50:02 +02:00
Girish Ramakrishnan
2f510c2625 capitalize sql keywords 2021-10-26 11:19:30 -07:00
Girish Ramakrishnan
7a977fa76b 7.0.2 changes 2021-10-26 11:17:57 -07:00
Girish Ramakrishnan
f5e025c213 mail: mailbox listing does not return pop3 status 2021-10-26 11:11:07 -07:00
Girish Ramakrishnan
971b73f853 move the bind inside 2021-10-26 11:03:54 -07:00
Girish Ramakrishnan
0103b21724 bump default backup memory limit to 800 2021-10-26 11:03:54 -07:00
Johannes Zellner
cef5c1e78c Use normal bind() 2021-10-26 18:47:51 +02:00
Johannes Zellner
50ff6b99e0 More external ldap fixes after the test tests the correct thing 2021-10-26 18:04:25 +02:00
Johannes Zellner
26dbd50cf2 Ensure we don't crash if mount status does not include some strings 2021-10-26 14:54:56 +02:00
Johannes Zellner
84884b969e Fix external ldap bind
See "Losing context" https://masteringjs.io/tutorials/node/promisify
2021-10-26 11:55:58 +02:00
Girish Ramakrishnan
62174c5328 proxyauth: only log failed requests by default 2021-10-25 09:41:12 -07:00
Girish Ramakrishnan
716951a3f1 dkim: ignore any spurious errors
in one of our cloudrons, we had a random dangling symlink in that directory
2021-10-22 17:26:12 -07:00
Girish Ramakrishnan
fbf6fe22af 7.0.1 changes 2021-10-22 16:39:42 -07:00
Girish Ramakrishnan
b18c4d3426 migration: wellKnown is {} or NULL 2021-10-22 16:29:32 -07:00
Girish Ramakrishnan
26a993abe7 Ubuntu 16 is unsupported 2021-10-22 16:09:43 -07:00
Girish Ramakrishnan
010024dfd7 apps: make downloadFile async 2021-10-21 15:25:15 -07:00
Girish Ramakrishnan
2e3070a5c6 apps: make uploadFile async 2021-10-21 15:15:39 -07:00
Girish Ramakrishnan
fbaee89c7b apps: clear timeout for upload and download routes 2021-10-21 10:44:17 -07:00
Girish Ramakrishnan
e0edfbf621 services: better status for sftp and turn 2021-10-19 16:02:18 -07:00
Girish Ramakrishnan
8cda287838 fix crash when there are multiple quick oom events 2021-10-19 12:25:25 -07:00
Johannes Zellner
80f83ef195 Next release is 7.0.0 2021-10-18 19:00:31 +02:00
Girish Ramakrishnan
d164a428a8 add to features 2021-10-18 09:05:59 -07:00
Girish Ramakrishnan
22e4d956fb mail: add option to force from address for relays 2021-10-16 22:30:28 -07:00
Girish Ramakrishnan
273a833935 mail: chmod the key file, so we can make the config dir readonly 2021-10-16 16:36:53 -07:00
Girish Ramakrishnan
da21e1ffd1 Fix typo in dkim path 2021-10-16 16:28:17 -07:00
Girish Ramakrishnan
4f9975de1b mail: set loglevel in recovery mode 2021-10-16 16:07:35 -07:00
Girish Ramakrishnan
00d6dfbacc Bump the year in license 2021-10-16 15:03:26 -07:00
Girish Ramakrishnan
3988d0d05f mail: add duplication detection for lists 2021-10-15 21:52:16 -07:00
Girish Ramakrishnan
e9edfbc1e6 req.body -> data 2021-10-15 11:20:09 -07:00
Johannes Zellner
c81f40dd8c Ensure mail data dir is still created 2021-10-15 15:02:54 +02:00
Girish Ramakrishnan
c775ec9b9c mail: auto-expunge junk folder (60 days) 2021-10-14 11:26:57 -07:00
Girish Ramakrishnan
98c6d99cad mail: enable vacation-seconds sieve extension 2021-10-14 09:31:57 -07:00
Girish Ramakrishnan
13197a47a9 mail: allow configuring dnsbl zones 2021-10-13 14:53:20 -07:00
Girish Ramakrishnan
419b58b300 mail: implement event log spam filter 2021-10-12 18:42:38 -07:00
Girish Ramakrishnan
272c77e49d mail: better eventlog schema 2021-10-12 17:11:55 -07:00
Girish Ramakrishnan
afdac02ab8 mail: fix the folder structure 2021-10-12 12:30:19 -07:00
Girish Ramakrishnan
405eae4495 Fix installation detection 2021-10-12 10:26:58 -07:00
Johannes Zellner
26e4f05adb Send subscription status for all users 2021-10-12 18:50:40 +02:00
Girish Ramakrishnan
98949d6360 dkim: typo when importing private key 2021-10-12 09:38:33 -07:00
Johannes Zellner
8c9c19d07d Fixup appstore route related tests 2021-10-12 14:55:30 +02:00
Girish Ramakrishnan
004a264993 mail: dkim key update 2021-10-11 22:56:34 -07:00
Girish Ramakrishnan
dc8ec9dcd8 mail: move dkim keys into the database 2021-10-11 20:30:42 -07:00
Girish Ramakrishnan
a63e04359c Fix tests 2021-10-11 20:29:50 -07:00
Girish Ramakrishnan
4fda00e56c mail: update locations 2021-10-11 18:14:22 -07:00
Girish Ramakrishnan
ca9b4ba230 add to changes 2021-10-11 15:44:34 -07:00
Girish Ramakrishnan
b9a11f9c31 filemanager: fix crash in extract 2021-10-11 15:34:11 -07:00
Girish Ramakrishnan
ca252e80d6 Fix usage of await 2021-10-11 10:29:46 -07:00
Girish Ramakrishnan
8e8d2e0182 Update docker to 20.10.7 2021-10-11 10:24:08 -07:00
Johannes Zellner
d1a7172895 Add remount route for mountlike backup storages 2021-10-11 18:12:11 +02:00
Johannes Zellner
9eed3af8b6 add volume remount 2021-10-11 16:22:56 +02:00
Girish Ramakrishnan
f01764617c mail: fix rebuild
also fixes dangerous code that downloads mail backup if infra version is 'none'
2021-10-09 08:15:10 -07:00
Girish Ramakrishnan
54bcfe92b9 recvmail: inject POP3 port 2021-10-08 15:24:38 -07:00
Girish Ramakrishnan
000db4e33d mail: add flag to enable/disable pop3 access per mailbox 2021-10-08 10:43:17 -07:00
Girish Ramakrishnan
9414041ba8 ldap: lookup by addon id and not service id 2021-10-08 09:59:44 -07:00
Girish Ramakrishnan
f17e3b3a62 mail: export pop3 port 2021-10-07 22:06:26 -07:00
Girish Ramakrishnan
92c712ea75 ldap: use service ids when auth'ing email 2021-10-07 21:32:22 -07:00
Johannes Zellner
e13c5c8e1a Do not duplicate sshd_config file path 2021-10-07 17:17:45 +02:00
Johannes Zellner
544825f344 Ensure root login is enabled for enabling remote support 2021-10-07 17:04:20 +02:00
Girish Ramakrishnan
b642bc98a5 ensure fallback certificates of all domains
https://forum.cloudron.io/topic/5683/data-argument-must-be-of-type-received-null-error-during-restore-process
2021-10-06 13:34:06 -07:00
Girish Ramakrishnan
da2f561257 add note in functions used in migrations 2021-10-06 13:09:53 -07:00
Girish Ramakrishnan
4a9d074b50 Use for..of instead of forEach for clarity 2021-10-06 13:01:12 -07:00
Girish Ramakrishnan
93636a7f3a apps: fix log streaming 2021-10-04 10:08:11 -07:00
Girish Ramakrishnan
671e0d1e6f recvmail: check for active mailbox 2021-10-03 23:59:06 -07:00
Girish Ramakrishnan
1743368069 app: clear mailbox fields when sendmail is removed with an update 2021-10-03 23:38:12 -07:00
Girish Ramakrishnan
a3fc5f226a make recvmail work
unlike sendmail, recvmail is always optional. this is the case because
the cloudron may not receive emails at all, so app always has to be
prepared for it.

part of #804
2021-10-02 03:11:47 -07:00
Girish Ramakrishnan
aed84a6ac9 Fix postgresql import issue with long table names 2021-10-01 16:24:38 -07:00
Girish Ramakrishnan
e31cf4cbfe do not wait for container in recovery mode 2021-10-01 14:38:47 -07:00
Girish Ramakrishnan
6a3cec3de8 services: add recoveryMode 2021-10-01 14:01:30 -07:00
Girish Ramakrishnan
54731392ff cannot disable sendmail if not optional 2021-10-01 11:20:13 -07:00
Girish Ramakrishnan
54668c92ba remove asserts when sendmail disabled 2021-10-01 11:16:49 -07:00
Girish Ramakrishnan
7a2b00cfa9 hasMailAddon is really just sendmail 2021-10-01 09:37:42 -07:00
Girish Ramakrishnan
1483dff018 make getLogs async 2021-10-01 09:23:25 -07:00
Girish Ramakrishnan
b34d642490 get rid of debugApp 2021-10-01 09:20:19 -07:00
Johannes Zellner
885ea259d7 Set inviteToken on user creation 2021-10-01 14:52:58 +02:00
Johannes Zellner
4ce21f643e send invite status via user rest api 2021-10-01 14:32:37 +02:00
Johannes Zellner
cb31e5ae8b Separate invite and password reset token 2021-10-01 12:27:22 +02:00
Johannes Zellner
c7b668b3a4 remove unused require 2021-10-01 11:55:35 +02:00
Girish Ramakrishnan
092b55d6ca apps: add backup start and finish events
these can then be used by the UI to show errors

fixes #797
2021-09-30 11:44:11 -07:00
Girish Ramakrishnan
b0bdfbd870 apps: onFinished handler not called across restarts
if box code restarts in the middle of a apptask, the onFinished handlers
are not called for data migration and update. rework the code to hook
the onFinished handlers when the task completes and not where the task
is started.
2021-09-30 10:54:47 -07:00
Girish Ramakrishnan
445c83c8b9 make auditsource a class
this allows us to use AuditSource for the class and auditSource for
the instances!
2021-09-30 10:13:36 -07:00
Girish Ramakrishnan
339fdfbea1 schema: add missing args to tasks table 2021-09-30 09:01:43 -07:00
Johannes Zellner
6bcef05e2a Fixup user route tests 2021-09-30 13:05:18 +02:00
Girish Ramakrishnan
679b813a7a give hint download has started 2021-09-29 23:36:54 -07:00
Girish Ramakrishnan
653496f96f import: validate and create transient mount point
fixes #788
2021-09-29 23:30:16 -07:00
Girish Ramakrishnan
9729d4adb8 backups: move hardcoded mountPoint to backend 2021-09-29 22:40:58 -07:00
Girish Ramakrishnan
ae4a091261 pass debug for safe call 2021-09-29 20:15:54 -07:00
Girish Ramakrishnan
d43209e655 autoconfig: add pop3 as protocol 2021-09-29 19:35:45 -07:00
Girish Ramakrishnan
b57d50d38c remove HOMEPATH and USERPROFILE fallbacks
probably from a time when I had a mac
2021-09-29 19:00:59 -07:00
Girish Ramakrishnan
73315a42fe setup: fix journalctl configuration
/var/log/journal/*/system.journal does not exist on some systems

https://forum.cloudron.io/topic/4068/installation-failed-on-20-04-server
https://forum.cloudron.io/topic/5731/time4vps-installation-error
2021-09-28 19:21:16 -07:00
Girish Ramakrishnan
3bcd32c56d restore: mount all volumes before restoring apps
fixes #786
2021-09-28 11:51:01 -07:00
Girish Ramakrishnan
d79206f978 mounts: volume -> mounts
this code is shared by volume code and backup code
2021-09-28 11:44:09 -07:00
Girish Ramakrishnan
13644624df add crontab tests 2021-09-28 11:08:10 -07:00
Girish Ramakrishnan
74ce00d94d cron -> crontab 2021-09-27 21:41:41 -07:00
Girish Ramakrishnan
b86d5ea0ea apps: add crontab
crontab is a text field, so we can have comments

part of #793
2021-09-27 21:33:00 -07:00
Girish Ramakrishnan
04ff8dab1b Fix progress message 2021-09-27 11:17:10 -07:00
Girish Ramakrishnan
fac48aa977 upcloud: add object storage integration 2021-09-27 10:05:38 -07:00
Johannes Zellner
c568c142c0 Remove unused require 2021-09-27 13:07:11 +02:00
Girish Ramakrishnan
d390495608 provision: download mail backup during restore 2021-09-26 22:55:23 -07:00
Girish Ramakrishnan
7ea9252059 services: simplify startup logic 2021-09-26 22:48:14 -07:00
Girish Ramakrishnan
0415262305 backupcleaner: fix crash 2021-09-26 21:59:48 -07:00
Girish Ramakrishnan
ad3dbe8daa mail: keep mail backups separately from box backups
part of #717
2021-09-26 21:47:24 -07:00
Girish Ramakrishnan
184fc70e97 pass debug for background promises 2021-09-26 21:24:37 -07:00
Girish Ramakrishnan
743597f91e backuptask: better debugs 2021-09-26 18:45:28 -07:00
Girish Ramakrishnan
90482f0263 use realpath to resolve links 2021-09-26 18:36:33 -07:00
Girish Ramakrishnan
9584990d7a remove old migration code 2021-09-26 18:10:39 -07:00
Girish Ramakrishnan
8255623874 mail: mount mail data directory into sftp container
fixes #794
2021-09-26 13:47:45 -07:00
Girish Ramakrishnan
d4edd771b5 sftp: prefix the id with app- and volume-
this helps the backend identify the type of mount
2021-09-25 23:35:44 -07:00
Girish Ramakrishnan
8553b57982 apptask: fix crash in configure 2021-09-25 21:39:54 -07:00
Girish Ramakrishnan
28f7fec44a apptask: remove debugApp 2021-09-25 21:39:54 -07:00
Girish Ramakrishnan
54c6f33e5f Fix broken invitation link 2021-09-25 17:36:56 -07:00
Girish Ramakrishnan
4523dd69c0 sftp: refactor 2021-09-25 17:12:38 -07:00
Girish Ramakrishnan
ddcafdec58 remove obsolete comment 2021-09-25 17:02:22 -07:00
Girish Ramakrishnan
d90beb18d4 eventlog: add service rebuild/restart/configure events 2021-09-24 10:22:45 -07:00
Girish Ramakrishnan
05e8339555 Fix typos in cert renewal 2021-09-23 17:54:54 -07:00
Girish Ramakrishnan
3090307c1d tasks: remove superfluous update code 2021-09-23 17:44:41 -07:00
Girish Ramakrishnan
8644a63919 better debug 2021-09-23 17:38:55 -07:00
Girish Ramakrishnan
b135aec525 pass debug argument to background safe() calls 2021-09-23 17:28:22 -07:00
Girish Ramakrishnan
1aa96f7f76 demo: do not send login notification 2021-09-23 09:13:07 -07:00
Girish Ramakrishnan
6fbf7890cc operator: mailbox route has to be protected
this is because operator cannot list domains
2021-09-22 12:45:13 -07:00
Girish Ramakrishnan
dff2275a9b add a flag to disable ocsp globally
fixes #796
2021-09-22 09:13:16 -07:00
Johannes Zellner
5b70c055cc Fixup accessLevel tests 2021-09-22 12:07:31 +02:00
Johannes Zellner
efa364414f Fix viable app tests and disable currently broken ones 2021-09-22 11:37:27 +02:00
Girish Ramakrishnan
5883857e8c sftp: remove requireAdmin setting. deprecated with operators 2021-09-21 22:43:04 -07:00
Girish Ramakrishnan
629908eb4c operator: add a limits route to determine max app resource limits 2021-09-21 22:29:19 -07:00
Girish Ramakrishnan
214540ebfa operator: add app task status route 2021-09-21 22:19:20 -07:00
Girish Ramakrishnan
d7bd3dfe7c operator: add graphs route 2021-09-21 21:50:33 -07:00
Girish Ramakrishnan
0857378801 operator: add app update checker route 2021-09-21 19:58:38 -07:00
Girish Ramakrishnan
82d4fdf24e operator: add route to get app event log
we cannot go via /cloudron/eventlog since that requires admin
2021-09-21 19:45:29 -07:00
Girish Ramakrishnan
06e5f9baa1 operators: make the terminal work 2021-09-21 18:27:54 -07:00
Girish Ramakrishnan
6c9b8c8fa8 apps: fix various operators issues
part of #791
2021-09-21 18:20:03 -07:00
Girish Ramakrishnan
fabd0323e1 Add missing await 2021-09-21 17:47:42 -07:00
Girish Ramakrishnan
bb2ad0e986 Implement operator role for apps
There are two main use cases:
* A consultant/contractor/external developer is given access to just an app.
* A "service" personnel (say upstream app author) is to be given access to single app
for debugging.

Since, this is an "app admin", they are also given access to apps to be consistent with
the idea that Cloudron admin has access to all apps.

part of #791
2021-09-21 12:30:02 -07:00
Girish Ramakrishnan
f44fa2cf47 apps: hasAccessTo -> canAccess 2021-09-21 10:13:06 -07:00
Johannes Zellner
737412653f Fix renamed function call 2021-09-21 18:58:18 +02:00
Girish Ramakrishnan
0cfc3e03bb Use concrete resource name instead of generic "resource" 2021-09-20 22:42:34 -07:00
Girish Ramakrishnan
d1e8fded65 mail: expose 465 for mail submission
Port 465 is implicit TLS. rfc8314 is now pushing this as a standard
and some mail clients like outlook have already taken this to heart.

Note that this port is sometimes confused with SMTPS. Unlike SMTPS,
this is being used for "submissions" (by a client) as opposed to
server transfer protocol.

This is more secure than port 587+STARTTLS. We reject credentials
on insecure connections but it's too late.

See also:

https://www.fastmail.help/hc/en-us/articles/360058753834
https://www.agwa.name/blog/post/starttls_considered_harmful
https://linuxguideandhints.com/misc/port465.html
2021-09-20 15:42:16 -07:00
Girish Ramakrishnan
2a667cb985 attach debug object for background safe() 2021-09-20 10:36:49 -07:00
Girish Ramakrishnan
a36c51483c no need to re-throw 2021-09-20 10:36:46 -07:00
Girish Ramakrishnan
e2fc785e80 rename getServiceIds to listServices 2021-09-20 09:15:49 -07:00
Johannes Zellner
5a1a439224 Adjust comment about getAll 2021-09-20 18:04:01 +02:00
Johannes Zellner
212d025579 Do not send new login notification if we have ghost user login 2021-09-20 17:56:37 +02:00
Johannes Zellner
7c70b9050d Fixup ghost tests 2021-09-20 14:59:26 +02:00
Johannes Zellner
ca2cc0b86c Make cloudron-support --owner-login use the settings table 2021-09-20 13:20:41 +02:00
Johannes Zellner
c6c62de68a Move ghosts into settings table 2021-09-20 13:05:42 +02:00
Girish Ramakrishnan
f66af19458 page number starts from 1 2021-09-19 18:36:08 -07:00
Girish Ramakrishnan
50c68cd499 notifications: better oom message for redis
fixes #795
2021-09-19 17:34:41 -07:00
Girish Ramakrishnan
05b4f96854 eslint: bump ecmaVersion
we can now use the optional chaining operator ?. that is available
in node 14
2021-09-19 17:32:01 -07:00
Girish Ramakrishnan
8c66ec5d18 tokens: ID_CLI is never used 2021-09-17 15:21:56 -07:00
Girish Ramakrishnan
66a907ef48 Logout users without 2FA when mandatory 2fa is enabled
Fixes #803
2021-09-17 14:52:24 -07:00
Girish Ramakrishnan
e8aaad976b backups: make test config funcs return error 2021-09-17 10:14:26 -07:00
Girish Ramakrishnan
2554c47632 add missing apps.delPortBinding
this got lost in async/db translation
2021-09-17 09:52:21 -07:00
Girish Ramakrishnan
c5794b5ecd get rid of all the NOOP_CALLBACKs 2021-09-17 09:40:26 -07:00
Johannes Zellner
b3fe2a4b84 Set correct default ghost expiration 2021-09-17 16:08:03 +02:00
Johannes Zellner
2ea5786fcc Fix setGhost api usage 2021-09-17 15:52:52 +02:00
Johannes Zellner
f75b0ebff9 Add set ghost route 2021-09-17 12:52:41 +02:00
Johannes Zellner
8fde4e959c Support ghost password expiration in ghost file 2021-09-17 11:48:56 +02:00
Girish Ramakrishnan
ac59a7dcc2 disable col stats in test mode (mysql 5.7) or non-ubuntu 20 2021-09-16 17:25:09 -07:00
Girish Ramakrishnan
9a2ed4f2c8 apptask: asyncify 2021-09-16 17:25:05 -07:00
Girish Ramakrishnan
b5539120f1 tests: cache dhparams in /tmp 2021-09-16 16:39:13 -07:00
Johannes Zellner
7277727307 Fixup some of app route tests 2021-09-16 17:20:19 +02:00
Johannes Zellner
f13e641af4 Also generate dhparams in test to let the platform finish startup 2021-09-16 17:19:59 +02:00
Johannes Zellner
da23bae09e return error if purchase fails 2021-09-16 17:19:38 +02:00
Johannes Zellner
9da18d3acb Fixup user tests 2021-09-16 15:38:06 +02:00
Johannes Zellner
d92f4c2d2b Ensure a whole test run succeeds for me on archlinux 2021-09-16 15:20:26 +02:00
Johannes Zellner
6785253377 Invitation is now also just a single route like password reset 2021-09-16 15:03:48 +02:00
Johannes Zellner
074ce574dd Return password reset link on reset request route 2021-09-16 14:34:56 +02:00
Johannes Zellner
ecd35bd08d Fixup 2fa reset route 2021-09-16 13:18:22 +02:00
Johannes Zellner
df864a8b6e Add missing safe() call 2021-09-16 08:40:01 +02:00
Girish Ramakrishnan
48eab7935c sftp: add missing safe() 2021-09-15 15:31:20 -07:00
Johannes Zellner
4080d111c1 We now map ldap users instead of ignoring them if usernames match 2021-09-15 11:44:39 +02:00
Girish Ramakrishnan
a78178ec47 redact password immediately after verify 2021-09-14 10:36:14 -07:00
Girish Ramakrishnan
d947be8683 Add to changes 2021-09-14 09:16:20 -07:00
Johannes Zellner
48056d7451 If we detect a local user with the same username as found on LDAP/AD we map it 2021-09-13 21:17:41 +02:00
Girish Ramakrishnan
2f0297d97e Use the debug argument 2021-09-13 11:29:55 -07:00
Girish Ramakrishnan
cdf6988156 Update node to 14.17.6 2021-09-10 14:34:11 -07:00
Girish Ramakrishnan
ae13fe60a7 make startBackupTask async 2021-09-10 12:10:10 -07:00
Girish Ramakrishnan
242fad137c update safetydance 2021-09-10 11:51:44 -07:00
Girish Ramakrishnan
bb7eb6d50e database: remove callback support 2021-09-10 11:40:01 -07:00
Johannes Zellner
59cbac0171 Require password for fallback email change 2021-09-09 23:22:00 +02:00
Johannes Zellner
d3d22f0878 Directly use users.verify() instead of another db lookup 2021-09-09 22:50:35 +02:00
Johannes Zellner
2d5eb6fd62 Remove unused require 2021-09-09 22:15:12 +02:00
Girish Ramakrishnan
fefd4abf33 Fix logger to log exceptions
this is similar to the fix in taskworker
2021-09-07 11:23:57 -07:00
Girish Ramakrishnan
7709e155e0 more async'ification 2021-09-07 11:21:06 -07:00
Girish Ramakrishnan
e7f51d992f acme: getCertificate can be async now 2021-09-07 09:34:23 -07:00
Johannes Zellner
5a955429f1 Overlooked one more domains occasion 2021-09-06 09:46:27 +02:00
Johannes Zellner
350a42c202 Fix linter issue of reused variable name 2021-09-05 12:10:37 +02:00
Girish Ramakrishnan
6a6b60412d Fix location change 2021-09-03 13:12:47 -07:00
Girish Ramakrishnan
1df0c12d6f mail: fix location change 2021-09-03 12:57:10 -07:00
Girish Ramakrishnan
e2cb0daec1 sysinfo: add missing return 2021-09-03 09:08:20 -07:00
Girish Ramakrishnan
949b2e2530 postgresql: bump shm size and disable parallel queries
https://forum.cloudron.io/topic/5604/nextcloud-take-very-long-time-to-respond/5
2021-09-03 08:02:06 -07:00
Girish Ramakrishnan
51d067cbe3 sysinfo: async'ify
in the process, provision, dyndns, mail, dns also got further asyncified
2021-09-02 16:19:46 -07:00
Girish Ramakrishnan
1856caf972 externalldap: async'ify
and make the tests work again
2021-09-01 21:33:27 -07:00
Girish Ramakrishnan
167eae5b81 Use safe instead of try/catch 2021-09-01 15:37:04 -07:00
Johannes Zellner
8d43015867 Asyncify some external ldap sync code 2021-09-01 14:47:43 +02:00
Girish Ramakrishnan
b5d6588e3e updater: async'ify 2021-08-31 13:12:14 -07:00
Girish Ramakrishnan
d225a687a5 Fix typo in updater logic 2021-08-31 11:16:58 -07:00
Girish Ramakrishnan
ffc3c94d77 tests: add footer tests 2021-08-31 08:47:01 -07:00
Girish Ramakrishnan
6027397961 Add missing safe() 2021-08-31 08:37:16 -07:00
Girish Ramakrishnan
c8c4ee898d scheduler: inspectByName -> inspect 2021-08-31 07:59:07 -07:00
Girish Ramakrishnan
66fcf92a24 wellknown: asyncify 2021-08-30 23:07:19 -07:00
Girish Ramakrishnan
22231a93c0 Ensure logs are flushed before crash 2021-08-30 22:01:34 -07:00
Girish Ramakrishnan
6754409ee2 Add missing safe() 2021-08-30 18:52:02 -07:00
Girish Ramakrishnan
b1da86c97f rename variable to avoid shadowing 2021-08-30 15:30:50 -07:00
Girish Ramakrishnan
ca4aeadddd prepareDashboardDomain: detect conflicts properly 2021-08-30 15:19:16 -07:00
Girish Ramakrishnan
6dfb328532 Add missing await 2021-08-30 14:00:50 -07:00
Girish Ramakrishnan
7d8cca0ed4 Fix typo 2021-08-30 11:42:46 -07:00
Girish Ramakrishnan
99d8c171b3 apps: return 404 when get returns null 2021-08-30 09:28:21 -07:00
Girish Ramakrishnan
d2c2b8e680 Fix shell.sudo usage 2021-08-30 09:28:16 -07:00
Girish Ramakrishnan
a5d41e33f9 Fix update route to use async 2021-08-27 09:30:52 -07:00
Girish Ramakrishnan
7413ccd22e Fix some more crashes 2021-08-26 21:29:40 -07:00
Girish Ramakrishnan
f5c169f881 Fix service status 2021-08-26 21:18:20 -07:00
Girish Ramakrishnan
42774eac8c docker.js and services.js: async'ify 2021-08-26 18:23:31 -07:00
Girish Ramakrishnan
1cc11fece8 Fix crash in renewCerts() 2021-08-25 15:52:05 -07:00
Girish Ramakrishnan
fc1eabfae4 appstore: fix usage of getCloudronToken 2021-08-25 15:22:24 -07:00
Girish Ramakrishnan
041b5db58b Add changes 2021-08-25 14:35:12 -07:00
Girish Ramakrishnan
3912c18824 cloudron-setup: detect amd64 2021-08-25 13:20:12 -07:00
Girish Ramakrishnan
8d3790d890 Fix grammar 2021-08-24 09:38:51 -07:00
Girish Ramakrishnan
766357567a Add missing safe() 2021-08-23 15:44:23 -07:00
Girish Ramakrishnan
77f5cb183b merge appdb.js into apps.js 2021-08-23 15:35:38 -07:00
Girish Ramakrishnan
b6f2d6d620 Make database.initialize async 2021-08-23 15:20:14 -07:00
Girish Ramakrishnan
1052889795 taskworkers can be async or take a callback 2021-08-23 15:20:14 -07:00
Johannes Zellner
3a0e882d33 Add missing safe() wrapper 2021-08-23 17:47:58 +02:00
Girish Ramakrishnan
37c2b5d739 proxyauth: fix crash 2021-08-22 16:19:22 -07:00
Girish Ramakrishnan
62eb4ab90e Fix addon crash
getAddonConfigByName returns null now when not found
2021-08-22 15:41:42 -07:00
Girish Ramakrishnan
95af5ef138 mailer: fix crash 2021-08-22 09:52:01 -07:00
Johannes Zellner
ba2475dc7e Some images like scaleway bare-metal on 20.04 explicitly require systemd-timesyncd 2021-08-22 17:22:47 +02:00
Girish Ramakrishnan
7ba3203625 users: getAll -> list 2021-08-20 11:31:10 -07:00
Girish Ramakrishnan
dd16866e5a eventlog: getAll -> list 2021-08-20 11:27:35 -07:00
Girish Ramakrishnan
aa6b845c9c make loginLocationsJson mediumtext
it seems we overflow atleast in the demo cloudron
TEXT – 64KB (65,535 characters)
MEDIUMTEXT – 16MB (16,777,215 characters)
2021-08-20 10:30:14 -07:00
Girish Ramakrishnan
a4b5219706 more removal of unused functions 2021-08-20 09:11:38 -07:00
Girish Ramakrishnan
0d87a5d665 remove unused function 2021-08-20 09:02:16 -07:00
Girish Ramakrishnan
ba3a93e648 remove unused function 2021-08-20 08:58:51 -07:00
Girish Ramakrishnan
0494bad90a make settings-test follow the new pattern 2021-08-20 08:58:00 -07:00
Girish Ramakrishnan
c5fff756d1 move addon config db code to addonconfigs.js 2021-08-19 22:08:31 -07:00
Girish Ramakrishnan
411cc7daa1 merge settingsdb into settings code 2021-08-19 17:45:40 -07:00
Girish Ramakrishnan
4cd5137292 mailer: fix error handling
previous mailer code has no callback and thus no way to pass back errors.
now with asyncification it passes back the error
2021-08-19 12:40:53 -07:00
Girish Ramakrishnan
ada7166bf8 translation: asyncify 2021-08-19 11:54:28 -07:00
Girish Ramakrishnan
03e22170da appstore and support: async'ify 2021-08-18 23:38:18 -07:00
Girish Ramakrishnan
200018a022 settings: async'ify
* directory config
* unstable app config
2021-08-18 15:46:08 -07:00
Girish Ramakrishnan
2d1f4ff281 settingsdb.getAll is gone 2021-08-18 15:33:49 -07:00
Girish Ramakrishnan
4671396889 settingsdb: merge blob get/set into settings.js 2021-08-18 15:31:07 -07:00
Girish Ramakrishnan
3806b3b3ff settings: initCache and list are now async 2021-08-18 13:59:57 -07:00
Girish Ramakrishnan
fa9938f50a mailboxdb: merge into mail.js 2021-08-18 12:48:34 -07:00
Girish Ramakrishnan
98ef6dfae9 throw must create a new object 2021-08-17 15:20:30 -07:00
Girish Ramakrishnan
5dd6f85025 reverseproxy: async'ify 2021-08-17 14:34:55 -07:00
Girish Ramakrishnan
5bcf1bc47b merge domaindb.js into domains.js 2021-08-16 14:41:42 -07:00
Girish Ramakrishnan
74febcd30a make ldap tests pass 2021-08-13 16:55:39 -07:00
Girish Ramakrishnan
beb1ab7c5b make users-test work 2021-08-13 14:52:57 -07:00
Girish Ramakrishnan
a8760f6c2c tests: cleanup common variables 2021-08-13 11:34:05 -07:00
Girish Ramakrishnan
aa981da43b tests: bump expiry of token 2021-08-13 10:23:27 -07:00
Girish Ramakrishnan
85e3e4b955 Accomodate redhat client
Patch from @jk at https://forum.cloudron.io/topic/4383/cannot-install-apps-from-docker-registry-because-authentication-fails
2021-08-13 09:36:06 -07:00
Girish Ramakrishnan
ec0d64ac12 tests: complete common'ification of routes tests 2021-08-12 22:49:19 -07:00
Girish Ramakrishnan
ac5b7f8093 tests: more common'ification 2021-08-12 17:20:57 -07:00
Girish Ramakrishnan
05576b5a91 6.4 changes 2021-08-11 22:25:17 -07:00
Girish Ramakrishnan
c7017da770 Add 6.3.6 changes 2021-08-11 22:23:59 -07:00
Girish Ramakrishnan
04d377d20d password reset: require and verify totpToken 2021-08-11 12:08:28 -07:00
Johannes Zellner
5b10cb63f4 sftp: update addon to fix symlink deletion 2021-08-11 09:32:30 +02:00
Girish Ramakrishnan
1e665b6323 Use the addresses of all available interfaces
See https://forum.cloudron.io/topic/5481/special-treatment-of-port-53-does-not-work-in-all-cases
2021-08-10 22:20:35 -07:00
Girish Ramakrishnan
79997d5529 users.add and users.createOwner only returns id now 2021-08-10 13:50:52 -07:00
Girish Ramakrishnan
2c13158265 appstore: remove purpose field 2021-08-10 13:30:51 -07:00
Girish Ramakrishnan
449220eca1 appAddonConfigs: change value to TEXT
since the value is used directly as an environment variable, we have to
allow up to max env var size (32767). Use TEXT which has a size of 64k
2021-08-09 13:40:23 -07:00
Girish Ramakrishnan
1a1f40988e enable all the tests in users-test.js 2021-08-06 23:14:06 -07:00
Johannes Zellner
a6e79c243e Show correct/new app version info in updated finished notification 2021-07-31 14:17:51 +02:00
Girish Ramakrishnan
fee38acc40 Fix crash when setting up user account 2021-07-31 04:39:10 -07:00
Girish Ramakrishnan
e4ce1a9ad3 Fix crash 2021-07-30 11:33:17 -07:00
Girish Ramakrishnan
41c11d50c0 remove m.identity_server
https://forum.cloudron.io/topic/5416/implement-well-known-matrix-client-endpoint/10
2021-07-29 14:37:20 -07:00
Johannes Zellner
768b9af1f9 Fix async usage 2021-07-29 22:21:18 +02:00
Johannes Zellner
635c5f7073 For some reason using df with regular promises breaks and calls catch without error 2021-07-29 22:21:18 +02:00
Girish Ramakrishnan
1273f0a3a4 add matrix client migration 2021-07-29 12:20:20 -07:00
Girish Ramakrishnan
205dab02be wellknown: serve up matrix/client 2021-07-29 12:05:21 -07:00
Johannes Zellner
f11cc7389d owner may be null even without error 2021-07-29 17:08:01 +02:00
Johannes Zellner
8e42423f06 When using await on superagent we should not call end()
https://visionmedia.github.io/superagent/#promise-and-generator-support
2021-07-29 11:26:28 +02:00
Johannes Zellner
eda3cd83ae Make new login email translatable
Fixes #798
2021-07-29 10:54:38 +02:00
Girish Ramakrishnan
ef56bf9888 cloudron-setup: check if nginx/docker is already installed 2021-07-28 07:20:16 -07:00
Girish Ramakrishnan
24eaea3523 add missing await 2021-07-26 22:16:01 -07:00
Girish Ramakrishnan
0b8d9df6e7 taskworker: print exceptions 2021-07-26 22:11:25 -07:00
Girish Ramakrishnan
882a7fce80 redis: suppress password warning 2021-07-24 08:51:00 -07:00
Girish Ramakrishnan
52fa57583e bump up memory limit when setting data directory 2021-07-22 17:18:02 -07:00
Girish Ramakrishnan
6e9b62dfba fix various users-test.js 2021-07-19 23:38:20 -07:00
Girish Ramakrishnan
48585e003d fix reverseproxy test 2021-07-17 09:49:32 -07:00
Girish Ramakrishnan
a1c61facdc merge userdb.js into users.js 2021-07-16 22:33:22 -07:00
Girish Ramakrishnan
2840bba4bf fix the backup tests 2021-07-15 00:09:45 -07:00
Girish Ramakrishnan
004e812d60 merge backupdb into backups.js 2021-07-14 15:10:45 -07:00
Girish Ramakrishnan
ac70350531 tasks.get returns null on not found 2021-07-14 10:59:49 -07:00
Girish Ramakrishnan
e59d0e878d merge taskdb into tasks.js 2021-07-14 10:37:12 -07:00
Girish Ramakrishnan
db685d3a56 notification: app updated message shown despite failure 2021-07-13 14:27:53 -07:00
Johannes Zellner
0947125a03 Some more test fixes 2021-07-13 11:13:16 +02:00
Johannes Zellner
227196138c Fixup database tests 2021-07-13 10:38:47 +02:00
Johannes Zellner
b67dca8a61 Fix docker filter usage in runTests 2021-07-13 10:38:40 +02:00
Johannes Zellner
120ed30878 Update lock file 2021-07-13 10:38:26 +02:00
Girish Ramakrishnan
14000e56b7 Fix notifications.alert (async usage)
this broke the reboot button among other things
2021-07-12 16:11:58 -07:00
Girish Ramakrishnan
cad7d4a78f more changes 2021-07-10 15:46:10 -07:00
Girish Ramakrishnan
3659210c7b typo 2021-07-10 11:13:36 -07:00
Girish Ramakrishnan
eafd72b4e7 eventlog: typo in cleanup 2021-07-10 10:53:21 -07:00
Girish Ramakrishnan
5d836b3f7c sshfs: only chown when auth as root user 2021-07-10 08:36:30 -07:00
Girish Ramakrishnan
fd9964c2cb mount: always use mountpoint for getting mount state
for ssfs.fuse, we get this on ubuntu 18:

root@my:/etc/systemd/system# systemctl status mnt-cloudronbackup.mount
● mnt-cloudronbackup.mount - backup
   Loaded: loaded (/etc/systemd/system/mnt-cloudronbackup.mount; enabled; vendor preset: enabled)
   Active: active (mounted) (Result: exit-code) since Sat 2021-07-10 00:16:53 UTC; 40s ago
    Where: /mnt/cloudronbackup
     What: root@149.28.218.27:/mnt/backups
  Process: 8273 ExecUnmount=/bin/umount /mnt/cloudronbackup -c (code=exited, status=32)
  Process: 8288 ExecMount=/bin/mount root@149.28.218.27:/mnt/backups /mnt/cloudronbackup -t fuse.sshfs -o allow_other,port=22,IdentityFile=/home/yellowtent/platformdata/sshfs/id_rsa_149.28.2
    Tasks: 0 (limit: 2314)
   CGroup: /system.slice/mnt-cloudronbackup.mount

Jul 10 00:16:53 my.cloudron.space systemd[1]: Mounting backup...
Jul 10 00:16:53 my.cloudron.space mount[8288]: read: Connection reset by peer
Jul 10 00:16:53 my.cloudron.space systemd[1]: mnt-cloudronbackup.mount: Mount process exited, code=exited status=1
Jul 10 00:16:53 my.cloudron.space systemd[1]: Mounted backup.

so even though the mount failed, it says active/mounted. sad.
2021-07-09 17:50:29 -07:00
Girish Ramakrishnan
c93284e6fb mount: json parsing of error message 2021-07-09 16:59:57 -07:00
Girish Ramakrishnan
7f4d039e11 backups: remove any old mount point configuration 2021-07-09 16:15:58 -07:00
Girish Ramakrishnan
17a70fdefd sshfs: hide private key 2021-07-09 16:07:45 -07:00
Girish Ramakrishnan
4c08315803 update 6.3.5 changes 2021-07-09 14:48:40 -07:00
Johannes Zellner
b87ba2f873 Fixup some app tests using test/common.js 2021-07-09 17:09:10 +02:00
Johannes Zellner
7a6b765f59 Prevent crash if groupIds is not set 2021-07-09 13:25:27 +02:00
Johannes Zellner
ede72ab05c Add more avatar tests 2021-07-09 12:30:47 +02:00
Johannes Zellner
35dc2141ea Make profile route tests work 2021-07-09 12:07:09 +02:00
Johannes Zellner
8c87f97054 We now explicitly expect a Buffer as avatar 2021-07-09 12:01:09 +02:00
Girish Ramakrishnan
5a4cb00b96 Fix the changelog 2021-07-08 09:09:52 -07:00
Girish Ramakrishnan
01a585aa11 remove safe usage 2021-07-08 08:52:51 -07:00
Johannes Zellner
0db62b4fd8 Make avatar apis buffer based 2021-07-08 11:17:13 +02:00
Girish Ramakrishnan
caa8104dda fix ldap test 2021-07-07 15:30:31 -07:00
Johannes Zellner
bbbfc4da05 Use avatar in userdb.add() 2021-07-07 18:50:51 +02:00
Johannes Zellner
be0c46ad8e Revert "Revert "Add avatar field constraint to not be NULL""
This reverts commit aafc22511b.
2021-07-07 18:50:09 +02:00
Johannes Zellner
aafc22511b Revert "Add avatar field constraint to not be NULL"
This reverts commit ba86802fc0.
2021-07-07 18:41:34 +02:00
Johannes Zellner
38d8bad1e1 Only kill container labeled with isCloudronManaged in runTests 2021-07-07 18:34:00 +02:00
Johannes Zellner
ba86802fc0 Add avatar field constraint to not be NULL 2021-07-07 18:32:05 +02:00
Johannes Zellner
de9d30117f Add gravatar change to changes 2021-07-07 18:15:17 +02:00
Johannes Zellner
16a3c1dd3b Add avatar migration script
Fixes #792
2021-07-07 17:54:25 +02:00
Johannes Zellner
81e6cd6195 Make gravatar support explicit only 2021-07-07 16:16:04 +02:00
Johannes Zellner
cdad2a80d4 Remove unused require 2021-06-30 17:19:30 +02:00
Johannes Zellner
41273640da SSHFS also does not need to chown here 2021-06-30 17:10:34 +02:00
Girish Ramakrishnan
ac484a02f2 merge maildb.js into mail.js 2021-06-29 15:59:02 -07:00
Girish Ramakrishnan
ea430b255b make the tests work 2021-06-29 11:01:46 -07:00
Girish Ramakrishnan
31498afe39 async'ify the groups code 2021-06-29 09:08:45 -07:00
Girish Ramakrishnan
7009c142cb 6.3.4 changes
(cherry picked from commit 700a7637b6)
2021-06-28 12:09:41 -07:00
Girish Ramakrishnan
c052882de9 reverseproxy: remove any old dashboard domain configs 2021-06-27 08:58:33 -07:00
Girish Ramakrishnan
e7d9af5aed users: asyncify and merge userdb.del 2021-06-26 10:13:21 -07:00
Girish Ramakrishnan
147c8df6e3 async'ify avatar and apppassword code 2021-06-25 23:32:21 -07:00
Girish Ramakrishnan
31d742fa67 fix sporadic ETIMEDOUT
it seems when docker is busy deleting images, we get a ETIMEDOUT.
the default was 10000.

2021-06-25T22:18:32.324Z box:apps BoxError: connect ETIMEDOUT
    at /home/yellowtent/box/src/settingsdb.js:26:36
    at Query.queryCallback [as _callback] (/home/yellowtent/box/src/database.js:96:42)
2021-06-25 16:46:49 -07:00
Girish Ramakrishnan
dd5737f948 mail: enable editheader sieve extension and upgrade solr 2021-06-25 16:38:44 -07:00
Girish Ramakrishnan
50d7610bfd cloudron-support: createdAt -> creationTime 2021-06-25 12:51:42 -07:00
Girish Ramakrishnan
e51dd8f530 installer: prepare apt before installing more packages
currently, this is only prepared when needed because don't want this
to happen on every update
2021-06-25 12:14:24 -07:00
Girish Ramakrishnan
bad6e39d59 volume: add filesystem type for shared folders
rename noop to mountpoint
2021-06-25 10:12:28 -07:00
Girish Ramakrishnan
1ce4875db1 volumes: set hostPath based on volume id
this is required for the file browser to work which does operations
based on the id

fixes #789
2021-06-24 17:32:41 -07:00
Girish Ramakrishnan
097a7d6b60 sftp: rework appdata and volume mounting logic
this tries to solve two issues:

* the current approach mounts the data directories of apps/volumes individually.
this causes a problem with volume mounts that mount after the container is started i.e not
network time/delay but systemd ordering. With CIFS, the mount is a hostname. This requires
unbound to be running but unbound can only start after docker because it wants to bind to
the docker network. one way to fix is to not start sftp automatically and only start sftp
container in the box code. This results in the sftp container attaching itself of the
directory before mounting and it appears empty. (on the host, the directory will appear
to have mount data!)

* every time apptask runs we keep rebuilding this sftp container. this results in much race.

the fix is: mount the parent directory of apps and volumes. in addition, then any specialized appdata
paths and volume paths are mounted individually. this greatly minimized rebuilding and also since we don't rely
on binding to the mount point itself. the child directories can mount in leisure. this limits the race
issue to only no-op volume mounts.

part of #789
2021-06-24 16:51:58 -07:00
Girish Ramakrishnan
87b2b63043 sshfs: add StrictHostKeyChecking=no so that it can connect the first time 2021-06-24 15:10:00 -07:00
Girish Ramakrishnan
0b0d552f58 Fix usage of execSync
important thing is to not use encoding: 'utf8' because in that case
it will return a string instead of a Buffer object. '' is false but
Buffer() is not.
2021-06-24 12:59:47 -07:00
Girish Ramakrishnan
5437291177 add to changes 2021-06-24 09:09:39 -07:00
Girish Ramakrishnan
78754f943d read avatar as binary and not base64 2021-06-24 09:09:08 -07:00
Girish Ramakrishnan
27db2c6855 Fix cert migrations 2021-06-24 08:30:51 -07:00
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
325 changed files with 39590 additions and 34281 deletions

View File

@@ -5,7 +5,7 @@
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 8
"ecmaVersion": 2020
},
"rules": {
"indent": [

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
node_modules/
coverage/
.nyc_output/
webadmin/dist/
installer/src/certs/server.key

5
.jshintrc Normal file
View File

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

270
CHANGES
View File

@@ -2232,3 +2232,273 @@
[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
[6.3.1]
* Fix cert migration issues
[6.3.2]
* Avatar was migrated as base64 instead of binary
* Fix issue where filemanager came up empty for CIFS mounts
[6.3.3]
* volumes: add filesystem volume type for shared folders
* mail: enable sieve extension editheader
* mail: update solr to 8.9.0
[6.3.4]
* Fix issue where old nginx configs where not removed before upgrade
[6.3.5]
* Fix permission issues with sshfs
* filemanager: reset selection if directory has changed
* branding: fix error highlight with empty cloudron name
* better text instead of "Cloudron in the wild"
* Make sso login hint translatable
* Give unread notifications a small left border
* Fix issue where clicking update indicator opened app in new tab
* Ensure notifications are only fetched and shown for at least admins
* setupaccount: Show input field errors below input field
* Set focus automatically for new alias or redirect
* eventlog: fix issue where old events are not periodically removed
* ssfs: fix chown
[6.3.6]
* Fix broken reboot button
* app updated notification shown despite failure
* Update translation for sso login information
* Hide groups/tags/state filter in app listing for normal users
* filemanager: Ensure breadcrumbs and hash are correctly updated on folder navigation
* cloudron-setup: check if nginx/docker is already installed
* Use the addresses of all available interfaces for port 53 binding
* refresh config on appstore login
* password reset: check 2fa when enabled
[7.0.0]
* Ubuntu 16 is not supported anymore
* Do not use Gravatar as the default but only an option
* redis: suppress password warning
* setup UI: fix dark mode
* wellknown: response to .wellknown/matrix/client
* purpose field is not required anymore during appstore signup
* sftp: fix symlink deletion
* Show correct/new app version info in updated finished notification
* Make new login email translatable
* Hide ticket form if cloudron.io mail is not verified
* Refactor code to use async/await
* postgresql: bump shm size and disable parallel queries
* update nodejs to 14.17.6
* external ldap: If we detect a local user with the same username as found on LDAP/AD we map it
* add basic eventlog for apps in app view
* Enable sshfs/cifs/nfs in app import UI
* Require password for fallback email change
* Make password reset logic translatable
* support: only verified email address can open support tickets
* Logout users without 2FA when mandatory 2fa is enabled
* notifications: better oom message for redis
* Add way to impersonate users for presetup
* mail: open up port 465 for mail submission (TLS)
* Implement operator role for apps
* sftp: normal users do not have SFTP access anymore. Use operator role instead
* eventlog: add service rebuild/restart/configure events
* upcloud: add object storage integration
* Each app can now have a custom crontab
* services: add recovery mode
* postgresql: fix restore issue with long table names
* recvmail: make the addon work again
* mail: update solr to 8.10.0
* mail: POP3 support
* update docker to 20.10.7
* volumes: add remount button
* mail: add spam eventlog filter type
* mail: configure dnsbl
* mail: add duplication detection for lists
* mail: add SRS for Sieve Forwarding
[7.0.1]
* Fix matrix wellKnown client migration
[7.0.2]
* mail: POP3 flag was not returned correctly
* external ldap: fix crash preventing users from logging in
* volumes: ensure we don't crash if mount status is unexpected
* backups: set default backup memory limit to 800
* users: allow admins to specify password recovery email
* retry startup tasks on database error
[7.0.3]
* support: fix remoe support not working for 'root' user
* Fix cog icon on app grid item hover for darkmode
* Disable password reset and impersonate button for self user instead of hiding them
* pop3: fix crash with auth of non-existent mailbox
* mail: fix direction field in eventlog of deferred mails
* mail: fix eventlog search
* mail: save message-id in eventlog
* backups: fix issue which resulted in incomplete backups when an app has backups disabled
* restore: do not redirect until mail data has been restored
* proxyauth: set viewport meta tag in login view
[7.0.4]
* Add password reveal button to login pages
* appstore: fix crash if account already registered
* Do not nuke all the logrotate configs on update
* Remove unused httpPaths from manifest
* cloudron-support: add option to reset cloudron.io account
* Fix flicker in login page
* Fix LE account key re-use issue in DO 1-click image
* mail: add non-tls ports for recvmail addon
* backups: fix issue where mail backups where not cleaned up
* notifications: fix automatic app update notifications
[7.1.0]
* Add mail manager role
* mailbox: app can be set as owner when recvmail addon enabled
* domains: add well known config UI (for jitsi configuration)
* Prefix email addon variables with CLOUDRON_EMAIL instead of CLOUDRON_MAIL
* remove support for manifest version 1
* Add option to enable/disable mailbox sharing
* base image 3.2.0
* Update node to 16.13.1
* mongodb: update to 4.4
* Add `upstreamVersion` to manifest
* Add `logPaths` to manifest
* Add cifs seal support for backup and volume mounts
* add a way for admins to set username when profiles are locked
* Add support for secondary domains
* postgresql: enable postgis
* remove nginx config of stopped apps
* mail: use port25check.cloudron.io to check outbound port 25 connectivity
* Add import/export of mailboxes and users
* LDAP server can now be exposed
* Update monaco-editor to 0.32.1
* Update xterm.js to 4.17.0
* Update docker to 20.10.12
* IPv6 support
[7.1.1]
* Fix issue where dkimKey of a mail domain is sometimes null
* firewall: add retry for xtables lock
* redis: fix issue where protected mode was enabled with no password
[7.1.2]
* Fix crash in cloudron-firewall when ports are whitelisted
* eventlog: add event for certificate cleanup
* eventlog: log event for mailbox alias update
* backups: fix incorrect mountpoint check with managed mounts
[7.1.3]
* Fix security issue where an admin can impersonate an owner
* block list: can upload up to 2MB
* dns: fix issue where link local address was picked up for ipv6
* setup: ufw may not be installed
* mysql: fix default collation of databases
[7.1.4]
* wildcard dns: fix handling of ENODATA
* cloudflare: fix error handling
* openvpn: ipv6 support
* dyndns: fix issue where eventlog was getting filled with empty entries
* mandatory 2fa: Fix typo in 2FA check
[7.2.0]
* mail: hide log button for non-superadmins
* firewall: do not add duplicate ldap redirect rules
* ldap: respond to RootDSE
* Check if CNAME record exists and remove it if overwrite is set
* cifs: use credentials file for better password support
* installer: rework script to fix DNS resolution issues
* backup cleaner: do not clean if not mounted
* restore: fix sftp private key perms
* support: add a separate system user named cloudron-support
* sshfs: fix bug where sshfs mounts were generated without unbound dependancy
* cloudron-setup: add --setup-token
* notifications: add installation event
* backups: set label of backup and control it's retention
* wasabi: add new regions (London, Frankfurt, Paris, Toronto)
* docker: update to 20.10.14
* Ensure LDAP usernames are always treated lowercase
* Add a way to make LDAP users local
* proxyAuth: set X-Remote-User (rfc3875)
* GoDaddy: there is now a delete API
* nginx: use ubuntu packages for ubuntu 20.04 and 22.04
* Ubuntu 22.04 LTS support
* Add Hetzner DNS
* cron: add support for extensions (@reboot, @weekly etc)
* Add profile backgroundImage api
* exec: rework API to get exit code
* Add update available filter
[7.2.1]
* Refactor backup code to use async/await
* mongodb: fix bug where a small timeout prevented import of large backups
* Add update available filter
* exec: rework API to get exit code
* Add profile backgroundImage api
* cron: add support for extensions (@reboot, @weekly etc)
[7.2.2]
* Update cloudron-manifestformat for new scheduler patterns
* collectd: FQDNLookup causes collectd install to fail
[7.2.3]
* appstore: allow re-registration on server side delete
* transfer ownership route is not used anymore
* graphite: fix issue where disk names with '.' do not render
* dark mode fixes
* sendmail: mail from display name
* Use volumes for app data instead of raw path
* initial xfs support
[7.2.4]
* volumes: Ensure long volume names do not overflow the table
* Move all appstore filter to the left
* app data: allow sameness of old and new dir

View File

@@ -1,5 +1,5 @@
The Cloudron Subscription license
Copyright (c) 2020 Cloudron UG
Copyright (c) 2022 Cloudron UG
With regard to the Cloudron Software:

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,176 +0,0 @@
#!/bin/bash
set -euv -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly arg_infraversionpath="${SOURCE_DIR}/../src"
function die {
echo $1
exit 1
}
export DEBIAN_FRONTEND=noninteractive
readonly ubuntu_codename=$(lsb_release -cs)
readonly ubuntu_version=$(lsb_release -rs)
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
apt-mark unhold grub* >/dev/null
echo "==> Installing required packages"
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
mysql_package=$([[ "${ubuntu_version}" == "20.04" ]] && echo "mysql-server-8.0" || echo "mysql-server-5.7")
apt-get -y install --no-install-recommends \
acl \
apparmor \
build-essential \
cifs-utils \
cron \
curl \
debconf-utils \
dmsetup \
$gpg_package \
ipset \
iptables \
libpython2.7 \
linux-generic \
logrotate \
$mysql_package \
openssh-server \
pwgen \
resolvconf \
swaks \
tzdata \
unattended-upgrades \
unbound \
unzip \
xfsprogs
echo "==> installing nginx for xenial for TLSv3 support"
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 /tmp/nginx.deb
rm /tmp/nginx.deb
# on some providers like scaleway the sudo file is changed and we want to keep the old one
apt-get -o Dpkg::Options::="--force-confold" install -y --no-install-recommends sudo
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
echo "==> Installing node.js"
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"
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
echo "==> Installing Docker"
# create systemd drop-in file. if you channge options here, be sure to fixup installer.sh as well
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
# there are 3 packages for docker - containerd, CLI and the daemon
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
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
if [[ "${storage_driver}" != "overlay2" ]]; then
echo "Docker is using "${storage_driver}" instead of overlay2"
exit 1
fi
# do not upgrade grub because it might prompt user and break this script
echo "==> Enable memory accounting"
apt-get -y --no-upgrade --no-install-recommends install grub2-common
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
update-grub
echo "==> Downloading docker images"
if [ ! -f "${arg_infraversionpath}/infra_version.js" ]; then
echo "No infra_versions.js found"
exit 1
fi
images=$(node -e "var i = require('${arg_infraversionpath}/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
echo -e "\tPulling docker images: ${images}"
for image in ${images}; do
docker pull "${image}"
docker pull "${image%@sha256:*}" # this will tag the image for readability
done
echo "==> Install collectd"
# without this, libnotify4 will install gnome-shell
apt-get install -y libnotify4 --no-install-recommends
if ! apt-get install -y --no-install-recommends libcurl3-gnutls collectd collectd-utils; then
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
fi
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
[[ "${ubuntu_version}" == "20.04" ]] && echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
# some hosts like atlantic install ntp which conflicts with timedatectl. https://serverfault.com/questions/1024770/ubuntu-20-04-time-sync-problems-and-possibly-incorrect-status-information
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
if systemctl is-active ntp; then
systemctl stop ntp
apt purge -y ntp
fi
timedatectl set-ntp 1
# mysql follows the system timezone
timedatectl set-timezone UTC
echo "==> Adding sshd configuration warning"
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://docs.cloudron.io/security/#securing-ssh-access' -i /etc/ssh/sshd_config
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
echo "==> Disabling motd news"
if [ -f "/etc/default/motd-news" ]; then
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
fi
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed)
systemctl stop bind9 || true
systemctl disable bind9 || true
# on ovh images dnsmasq seems to run by default
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
# on ubuntu 18.04 and 20.04, this is the default. this requires resolvconf for DNS to work further after the disable
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
# ubuntu's default config for unbound does not work if ipv6 is disabled. this config is overwritten in start.sh
# we need unbound to work as this is required for installer.sh to do any DNS requests
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl restart unbound

89
box.js
View File

@@ -2,65 +2,76 @@
'use strict';
let async = require('async'),
dockerProxy = require('./src/dockerproxy.js'),
fs = require('fs'),
const fs = require('fs'),
ldap = require('./src/ldap.js'),
paths = require('./src/paths.js'),
proxyAuth = require('./src/proxyauth.js'),
server = require('./src/server.js');
safe = require('safetydance'),
server = require('./src/server.js'),
settings = require('./src/settings.js'),
userdirectory = require('./src/userdirectory.js');
const NOOP_CALLBACK = function () { };
let logFd;
function setupLogging(callback) {
if (process.env.BOX_ENV === 'test') return callback();
async function setupLogging() {
if (process.env.BOX_ENV === 'test') return;
var logfileStream = fs.createWriteStream(paths.BOX_LOG_FILE, { flags:'a' });
process.stdout.write = process.stderr.write = logfileStream.write.bind(logfileStream);
callback();
logFd = fs.openSync(paths.BOX_LOG_FILE, 'a');
// we used to write using a stream before but it caches internally and there is no way to flush it when things crash
process.stdout.write = process.stderr.write = function (...args) {
const callback = typeof args[args.length-1] === 'function' ? args.pop() : function () {}; // callback is required for fs.write
fs.write.apply(fs, [logFd, ...args, callback]);
};
}
async.series([
setupLogging,
server.start, // do this first since it also inits the database
proxyAuth.start,
ldap.start,
dockerProxy.start
], function (error) {
if (error) {
console.log('Error starting server', error);
process.exit(1);
}
// this is also used as the 'uncaughtException' handler which can only have synchronous functions
function exitSync(status) {
if (status.error) fs.write(logFd, status.error.stack + '\n', function () {});
fs.fsyncSync(logFd);
fs.closeSync(logFd);
process.exit(status.code);
}
// require those here so that logging handler is already setup
require('supererror');
async function startServers() {
await setupLogging();
await server.start(); // do this first since it also inits the database
await proxyAuth.start();
await ldap.start();
const conf = await settings.getUserDirectoryConfig();
if (conf.enabled) await userdirectory.start();
}
async function main() {
const [error] = await safe(startServers());
if (error) return exitSync({ error: new Error(`Error starting server: ${JSON.stringify(error)}`), code: 1 });
// require this here so that logging handler is already setup
const debug = require('debug')('box:box');
process.on('SIGINT', function () {
process.on('SIGINT', async function () {
debug('Received SIGINT. Shutting down.');
proxyAuth.stop(NOOP_CALLBACK);
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
await proxyAuth.stop();
await server.stop();
await userdirectory.stop();
await ldap.stop();
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
process.on('SIGTERM', async function () {
debug('Received SIGTERM. Shutting down.');
proxyAuth.stop(NOOP_CALLBACK);
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
await proxyAuth.stop();
await server.stop();
await userdirectory.stop();
await ldap.stop();
setTimeout(process.exit.bind(process), 3000);
});
process.on('uncaughtException', function (error) {
console.error((error && error.stack) ? error.stack : error);
setTimeout(process.exit.bind(process, 1), 3000);
});
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
});
}
main();

View File

@@ -2,27 +2,21 @@
'use strict';
var database = require('./src/database.js');
const database = require('./src/database.js');
var crashNotifier = require('./src/crashnotifier.js');
const crashNotifier = require('./src/crashnotifier.js');
// This is triggered by systemd with the crashed unit name as argument
function main() {
async function main() {
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
var unitName = process.argv[2];
const unitName = process.argv[2];
console.log('Started crash notifier for', unitName);
// eventlog api needs the db
database.initialize(function (error) {
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
await database.initialize();
crashNotifier.sendFailureLogs(unitName, function (error) {
if (error) console.error(error);
process.exit();
});
});
await crashNotifier.sendFailureLogs(unitName);
}
main();

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', 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,38 @@
'use strict';
const async = require('async'),
safe = require('safetydance');
const CERTS_DIR = '/home/yellowtent/boxdata/certs',
PLATFORM_CERTS_DIR = '/home/yellowtent/platformdata/nginx/cert';
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) {
// b94dbf5fa33a6d68d784571721ff44348c2d88aa seems to have moved certs from platformdata to boxdata
let cert = safe.fs.readFileSync(`${CERTS_DIR}/${domain.domain}.host.cert`, 'utf8');
let key = safe.fs.readFileSync(`${CERTS_DIR}/${domain.domain}.host.key`, 'utf8');
if (!cert) {
cert = safe.fs.readFileSync(`${PLATFORM_CERTS_DIR}/${domain.domain}.host.cert`, 'utf8');
key = safe.fs.readFileSync(`${PLATFORM_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,52 @@
'use strict';
const async = require('async'),
child_process = require('child_process'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance');
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 = safe.fs.readFileSync(path.join(OLD_CERTS_DIR, certificateFile));
if (!certificate) {
console.log(`${certificateFile} is missing. skipping migration`);
return iteratorCallback();
}
const csrFile = filename.replace(/\.key$/, '.csr');
const csr = safe.fs.readFileSync(path.join(OLD_CERTS_DIR, csrFile));
if (!csr) {
console.log(`${csrFile} is missing. skipping migration`);
return iteratorCallback();
}
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

@@ -0,0 +1,26 @@
'use strict';
const async = require('async'),
safe = require('safetydance');
exports.up = function(db, callback) {
db.all('SELECT * FROM volumes', function (error, volumes) {
if (error || volumes.length === 0) return callback(error);
async.eachSeries(volumes, function (volume, iteratorDone) {
if (volume.mountType !== 'noop') return iteratorDone();
let mountType;
if (safe.child_process.execSync(`mountpoint -q -- ${volume.hostPath}`)) {
mountType = 'mountpoint';
} else {
mountType = 'filesystem';
}
db.runSql('UPDATE volumes SET mountType=? WHERE id=?', [ mountType, volume.id ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,13 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE users SET avatar="gravatar" WHERE avatar IS NULL', function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE users MODIFY avatar MEDIUMBLOB NOT NULL', callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users MODIFY avatar MEDIUMBLOB', callback);
};

View File

@@ -0,0 +1,30 @@
'use strict';
const async = require('async'),
safe = require('safetydance');
exports.up = function(db, callback) {
db.all('SELECT * from domains', [], function (error, results) {
if (error) return callback(error);
async.eachSeries(results, function (r, iteratorDone) {
if (!r.wellKnownJson) return iteratorDone();
const wellKnown = safe.JSON.parse(r.wellKnownJson);
if (!wellKnown || !wellKnown['matrix/server']) return iteratorDone();
const matrixHostname = JSON.parse(wellKnown['matrix/server'])['m.server'];
wellKnown['matrix/client'] = JSON.stringify({
'm.homeserver': {
'base_url': 'https://' + matrixHostname
}
});
db.runSql('UPDATE domains SET wellKnownJson=? WHERE domain=?', [ JSON.stringify(wellKnown), r.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE appAddonConfigs MODIFY value TEXT NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE appAddonConfigs MODIFY value VARCHAR(512)', [], function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

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

View File

@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN operatorsJson TEXT', callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN operatorsJson', callback);
};

View File

@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN crontab TEXT', callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN crontab', callback);
};

View File

@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN inviteToken VARCHAR(128) DEFAULT ""', callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN inviteToken', callback);
};

View File

@@ -0,0 +1,19 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN enableInbox BOOLEAN DEFAULT 0'),
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN inboxName VARCHAR(128)'),
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN inboxDomain VARCHAR(128)'),
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN enableInbox'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN inboxName'),
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN inboxDomain'),
], callback);
};

View File

@@ -0,0 +1,35 @@
'use strict';
const async = require('async'),
reverseProxy = require('../src/reverseproxy.js'),
safe = require('safetydance');
const NGINX_CERT_DIR = '/home/yellowtent/platformdata/nginx/cert';
// ensure fallbackCertificate of domains are present in database and the cert dir. it seems a bad migration lost them.
// https://forum.cloudron.io/topic/5683/data-argument-must-be-of-type-received-null-error-during-restore-process
exports.up = function(db, callback) {
db.all('SELECT * FROM domains', [ ], function (error, domains) {
if (error) return callback(error);
// this code is br0ken since async 3.x since async functions won't get iteratorDone anymore
// no point fixing this migration though since it won't run again in old cloudrons. and in new cloudron domains will be empty
async.eachSeries(domains, async function (domain, iteratorDone) {
let fallbackCertificate = safe.JSON.parse(domain.fallbackCertificateJson);
if (!fallbackCertificate || !fallbackCertificate.cert || !fallbackCertificate.key) {
let error;
[error, fallbackCertificate] = await safe(reverseProxy.generateFallbackCertificate(domain.domain));
if (error) return iteratorDone(error);
}
if (!safe.fs.writeFileSync(`${NGINX_CERT_DIR}/${domain.domain}.host.cert`, fallbackCertificate.cert, 'utf8')) return iteratorDone(safe.error);
if (!safe.fs.writeFileSync(`${NGINX_CERT_DIR}/${domain.domain}.host.key`, fallbackCertificate.key, 'utf8')) return iteratorDone(safe.error);
db.runSql('UPDATE domains SET fallbackCertificateJson=? WHERE domain=?', [ JSON.stringify(fallbackCertificate), domain.domain ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

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

View File

@@ -0,0 +1,44 @@
'use strict';
const async = require('async'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance');
const MAIL_DATA_DIR = '/home/yellowtent/boxdata/mail';
const DKIM_DIR = `${MAIL_DATA_DIR}/dkim`;
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mail ADD COLUMN dkimKeyJson MEDIUMTEXT', function (error) {
if (error) return callback(error);
fs.readdir(DKIM_DIR, function (error, filenames) {
if (error && error.code === 'ENOENT') return callback();
if (error) return callback(error);
async.eachSeries(filenames, function (filename, iteratorCallback) {
const domain = filename;
const publicKey = safe.fs.readFileSync(path.join(DKIM_DIR, domain, 'public'), 'utf8');
const privateKey = safe.fs.readFileSync(path.join(DKIM_DIR, domain, 'private'), 'utf8');
if (!publicKey || !privateKey) return iteratorCallback();
const dkimKey = {
publicKey,
privateKey
};
db.runSql('UPDATE mail SET dkimKeyJson=? WHERE domain=?', [ JSON.stringify(dkimKey), domain ], iteratorCallback);
}, function (error) {
if (error) return callback(error);
fs.rmdir(DKIM_DIR, { recursive: true }, callback);
});
});
});
};
exports.down = function(db, callback) {
async.series([
db.runSql.run(db, 'ALTER TABLE mail DROP COLUMN dkimKeyJson')
], callback);
};

View File

@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('DELETE FROM blobs WHERE id=?', [ 'dhparams' ], 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 eventlog CHANGE source sourceJson TEXT', []),
db.runSql.bind(db, 'ALTER TABLE eventlog CHANGE data dataJson TEXT', []),
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE eventlog CHANGE sourceJson source TEXT', []),
db.runSql.bind(db, 'ALTER TABLE eventlog CHANGE dataJson data TEXT', []),
], callback);
};

View File

@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('SELECT value FROM settings WHERE name=?', [ 'sysinfo_config' ], function (error, result) {
if (error || result.length === 0) return callback(error);
const sysinfoConfig = JSON.parse(result[0].value);
if (sysinfoConfig.provider !== 'fixed' || !sysinfoConfig.ip) return callback();
sysinfoConfig.ipv4 = sysinfoConfig.ip;
delete sysinfoConfig.ip;
db.runSql('REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'sysinfo_config', JSON.stringify(sysinfoConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE settings SET name=? WHERE name=?', [ 'directory_config', 'profile_config' ], callback);
};
exports.down = function(db, callback) {
db.runSql('UPDATE settings SET name=? WHERE name=?', [ 'profile_config', 'directory_config' ], callback);
};

View File

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

View File

@@ -0,0 +1,19 @@
'use strict';
const safe = require('safetydance');
const PROXY_AUTH_TOKEN_SECRET_FILE = '/home/yellowtent/platformdata/proxy-auth-token-secret';
exports.up = function (db, callback) {
const token = safe.fs.readFileSync(PROXY_AUTH_TOKEN_SECRET_FILE);
if (!token) return callback();
db.runSql('INSERT INTO blobs (id, value) VALUES (?, ?)', [ 'proxy_auth_token_secret', token ], function (error) {
if (error) return callback(error);
safe.fs.unlinkSync(PROXY_AUTH_TOKEN_SECRET_FILE);
callback();
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,12 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('RENAME TABLE subdomains TO locations', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,27 @@
'use strict';
const async = require('async'),
mail = require('../src/mail.js'),
safe = require('safetydance'),
util = require('util');
// it seems some mail domains do not have dkimKey in the database for some reason because of some previous bad migration
exports.up = function(db, callback) {
db.all('SELECT * FROM mail', [ ], function (error, mailDomains) {
if (error) return callback(error);
async.eachSeries(mailDomains, function (mailDomain, iteratorDone) {
let dkimKey = safe.JSON.parse(mailDomain.dkimKeyJson);
if (dkimKey && dkimKey.publicKey && dkimKey.privateKey) return iteratorDone();
console.log(`${mailDomain.domain} has no dkim key in the database. generating a new one`);
util.callbackify(mail.generateDkimKey)(function (error, dkimKey) {
if (error) return iteratorDone(error);
db.runSql('UPDATE mail SET dkimKeyJson=? WHERE domain=?', [ JSON.stringify(dkimKey), mailDomain.domain ], iteratorDone);
});
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('DELETE FROM settings WHERE name=?', [ 'license_key' ], callback);
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('UPDATE settings SET name=? WHERE name=?', [ 'appstore_api_token', 'cloudron_token' ], callback);
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,37 @@
'use strict';
const superagent = require('superagent');
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="api_server_origin"', function (error, results) {
if (error || results.length === 0) return callback(error);
const apiServerOrigin = results[0].value;
db.all('SELECT value FROM settings WHERE name="appstore_api_token"', function (error, results) {
if (error || results.length === 0) return callback(error);
const apiToken = results[0].value;
console.log(`Getting appstore web token from ${apiServerOrigin}`);
superagent.post(`${apiServerOrigin}/api/v1/user_token`)
.send({})
.query({ accessToken: apiToken })
.timeout(30 * 1000).end(function (error, response) {
if (error && !error.response) {
console.log('Network error getting web token', error);
return callback();
}
if (response.statusCode !== 201 || !response.body.accessToken) {
console.log(`Bad status getting web token: ${response.status} ${response.text}`);
return callback();
}
db.runSql('INSERT settings (name, value) VALUES(?, ?)', [ 'appstore_web_token', response.body.accessToken ], callback);
});
});
});
};
exports.down = function(db, callback) {
callback();
};

View File

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

View File

@@ -0,0 +1,51 @@
'use strict';
const async = require('async'),
hat = require('../src/hat.js');
exports.up = function(db, callback) {
db.all('SELECT * from backups', function (error, allBackups) {
if (error) return callback(error);
console.log(`Fixing up ${allBackups.length} backup entries`);
const idMap = {};
allBackups.forEach(b => {
b.remotePath = b.id;
b.id = `${b.type}_${b.identifier}_v${b.packageVersion}_${hat(256)}`; // id is used by the UI to derive dependent packages. making this a UUID will require a lot of db querying
idMap[b.remotePath] = b.id;
});
db.runSql('ALTER TABLE backups ADD COLUMN remotePath VARCHAR(256)', function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE backups CHANGE COLUMN dependsOn dependsOnJson TEXT', function (error) {
if (error) return callback(error);
async.eachSeries(allBackups, function (backup, iteratorDone) {
const dependsOnPaths = backup.dependsOn ? backup.dependsOn.split(',') : []; // previously, it was paths
let dependsOnIds = [];
dependsOnPaths.forEach(p => { if (idMap[p]) dependsOnIds.push(idMap[p]); });
db.runSql('UPDATE backups SET id = ?, remotePath = ?, dependsOnJson = ? WHERE id = ?', [ backup.id, backup.remotePath, JSON.stringify(dependsOnIds), backup.remotePath ], iteratorDone);
}, function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE backups MODIFY COLUMN remotePath VARCHAR(256) NOT NULL UNIQUE', callback);
});
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN remotePath', function (error) {
if (error) console.error(error);
db.runSql('ALTER TABLE backups RENAME COLUMN dependsOnJson to dependsOn', function (error) {
if (error) return callback(error);
callback(error);
});
});
};

View File

@@ -0,0 +1,22 @@
'use strict';
const async = require('async');
exports.up = function(db, callback) {
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
const manifest = JSON.parse(app.manifestJson);
const hasSso = !!manifest.addons['proxyAuth'] || !!manifest.addons['ldap'];
if (hasSso || !app.sso) return iteratorDone();
console.log(`Unsetting sso flag of ${app.id}`);
db.runSql('UPDATE apps SET sso=? WHERE id=?', [ 0, app.id ], iteratorDone);
}, callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,20 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT * FROM settings WHERE name = ?', [ 'api_server_origin' ], function (error, result) {
if (error || result.length === 0) return callback(error);
let consoleOrigin;
switch (result[0].value) {
case 'https://api.dev.cloudron.io': consoleOrigin = 'https://console.dev.cloudron.io'; break;
case 'https://api.staging.cloudron.io': consoleOrigin = 'https://console.staging.cloudron.io'; break;
default: consoleOrigin = 'https://console.cloudron.io'; break;
}
db.runSql('REPLACE INTO settings (name, value) VALUES (?, ?)', [ 'console_server_origin', consoleOrigin ], callback);
});
};
exports.down = function(db, callback) {
callback();
};

View File

@@ -0,0 +1,9 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN backgroundImage MEDIUMBLOB', callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN backgroundImage', callback);
};

View File

@@ -0,0 +1,12 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN mailboxDisplayName VARCHAR(128) DEFAULT "" NOT NULL', [], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN mailboxDisplayName', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,51 @@
'use strict';
const path = require('path'),
safe = require('safetydance'),
uuid = require('uuid');
function getMountPoint(dataDir) {
const output = safe.child_process.execSync(`df --output=target "${dataDir}" | tail -1`, { encoding: 'utf8' });
if (!output) return dataDir;
const mountPoint = output.trim();
if (mountPoint === '/') return dataDir;
return mountPoint;
}
exports.up = async function(db) {
await db.runSql('ALTER TABLE apps ADD storageVolumeId VARCHAR(128), ADD FOREIGN KEY(storageVolumeId) REFERENCES volumes(id)');
await db.runSql('ALTER TABLE apps ADD storageVolumePrefix VARCHAR(128)');
await db.runSql('ALTER TABLE apps ADD CONSTRAINT apps_storageVolume UNIQUE (storageVolumeId, storageVolumePrefix)');
const apps = await db.runSql('SELECT * FROM apps WHERE dataDir IS NOT NULL');
const allVolumes = await db.runSql('SELECT * FROM volumes');
for (const app of apps) {
console.log(`data-dir (${app.id}): migrating data dir ${app.dataDir}`);
const mountPoint = getMountPoint(app.dataDir);
const prefix = path.relative(mountPoint, app.dataDir);
console.log(`data-dir (${app.id}): migrating to mountpoint ${mountPoint} and prefix ${prefix}`);
const volume = allVolumes.find(v => v.hostPath === mountPoint);
if (volume) {
console.log(`data-dir (${app.id}): using existing volume ${volume.id}`);
await db.runSql('UPDATE apps SET storageVolumeId=?, storageVolumePrefix=? WHERE id=?', [ volume.id, prefix, app.id ]);
continue;
}
const id = uuid.v4().replace(/-/g, ''); // to make systemd mount file names more readable
const name = `app-${app.id}`;
const type = app.dataDir === mountPoint ? 'filesystem' : 'mountpoint';
console.log(`data-dir (${app.id}): creating new volume ${id}`);
await db.runSql('INSERT INTO volumes (id, name, hostPath, mountType, mountOptionsJson) VALUES (?, ?, ?, ?, ?)', [ id, name, mountPoint, type, JSON.stringify({}) ]);
await db.runSql('UPDATE apps SET storageVolumeId=?, storageVolumePrefix=? WHERE id=?', [ id, prefix, app.id ]);
}
await db.runSql('ALTER TABLE apps DROP COLUMN dataDir');
};
exports.down = async function(/*db*/) {
};

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 "",
@@ -28,10 +28,15 @@ CREATE TABLE IF NOT EXISTS users(
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
role VARCHAR(32),
inviteToken VARCHAR(128) DEFAULT "",
resetToken VARCHAR(128) DEFAULT "",
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT 1,
avatar MEDIUMBLOB NOT NULL,
backgroundImage MEDIUMBLOB,
loginLocationsJson MEDIUMTEXT, // { locations: [{ ip, userAgent, city, country, ts }] }
INDEX creationTime_index (creationTime),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS userGroups(
@@ -55,6 +60,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,18 +84,29 @@ 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
mailboxDomain VARCHAR(128), // mailbox domain of this app
mailboxDisplayName VARCHAR(128), // mailbox display name
enableInbox BOOLEAN DEFAULT 0, // whether recvmail addon is enabled
inboxName VARCHAR(128), // mailbox of this app
inboxDomain VARCHAR(128), // mailbox domain of this app
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
storageVolumeId VARCHAR(128),
storageVolumePrefix VARCHAR(128),
taskId INTEGER, // current task
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,
crontab TEXT,
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
FOREIGN KEY(storageVolumeId) REFERENCES volumes(id),
UNIQUE (storageVolumeId, storageVolumePrefix),
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS appPortBindings(
@@ -103,13 +120,14 @@ 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(
appId VARCHAR(128) NOT NULL,
addonId VARCHAR(32) NOT NULL,
name VARCHAR(128) NOT NULL,
value VARCHAR(512) NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS appEnvVars(
@@ -120,26 +138,30 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
remotePath VARCHAR(256) NOT NULL UNIQUE,
label VARCHAR(128) DEFAULT "",
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
packageVersion VARCHAR(128) NOT NULL, /* app version or box version */
encryptionVersion INTEGER, /* when null, unencrypted backup */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
identifier VARCHAR(128) NOT NULL, /* 'box' or the app id */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
dependsOnJson TEXT, /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
format VARCHAR(16) DEFAULT "tgz",
preserveSecs INTEGER DEFAULT 0,
INDEX creationTime_index (creationTime),
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS eventlog(
id VARCHAR(128) NOT NULL,
action VARCHAR(128) NOT NULL,
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
data TEXT, /* free flowing json based on action */
sourceJson TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
dataJson 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 +172,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 */
@@ -164,6 +188,7 @@ CREATE TABLE IF NOT EXISTS mail(
relayJson TEXT,
bannerJson TEXT,
dkimKeyJson MEDIUMTEXT,
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
FOREIGN KEY(domain) REFERENCES domains(domain),
@@ -189,16 +214,21 @@ CREATE TABLE IF NOT EXISTS mailboxes(
membersOnly BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
active BOOLEAN DEFAULT 1,
enablePop3 BOOLEAN DEFAULT 0,
FOREIGN KEY(domain) REFERENCES mail(domain),
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS subdomains(
CREATE TABLE IF NOT EXISTS locations(
appId VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
subdomain VARCHAR(128) NOT NULL,
type VARCHAR(128) NOT NULL, /* primary or redirect */
type VARCHAR(128) NOT NULL, /* primary, secondary, redirect, alias */
environmentVariable VARCHAR(128), /* only set for secondary */
certificateJson MEDIUMTEXT,
FOREIGN KEY(domain) REFERENCES domains(domain),
FOREIGN KEY(appId) REFERENCES apps(id),
@@ -207,23 +237,27 @@ CREATE TABLE IF NOT EXISTS subdomains(
CREATE TABLE IF NOT EXISTS tasks(
id int NOT NULL AUTO_INCREMENT,
type VARCHAR(32) NOT NULL,
argsJson TEXT,
percent INTEGER DEFAULT 0,
message TEXT,
errorJson TEXT,
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 +268,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 +279,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 +292,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 MEDIUMBLOB,
PRIMARY KEY(id));
CHARACTER SET utf8 COLLATE utf8_bin;

12055
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,77 +11,67 @@
"url": "https://git.cloudron.io/cloudron/box.git"
},
"dependencies": {
"@google-cloud/dns": "^2.1.0",
"@google-cloud/storage": "^5.8.0",
"@google-cloud/dns": "^2.2.4",
"@google-cloud/storage": "^5.19.2",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^3.2.0",
"aws-sdk": "^2.850.0",
"async": "^3.2.3",
"aws-sdk": "^2.1115.0",
"basic-auth": "^2.0.1",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.10.1",
"body-parser": "^1.20.0",
"cloudron-manifestformat": "^5.16.0",
"connect": "^3.7.0",
"connect-lastmile": "^2.0.0",
"connect-lastmile": "^2.1.1",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.5",
"cookie-session": "^1.4.0",
"cookie-parser": "^1.4.6",
"cookie-session": "^2.0.0",
"cron": "^1.8.2",
"db-migrate": "^0.11.12",
"db-migrate-mysql": "^2.1.2",
"debug": "^4.3.1",
"dockerode": "^3.2.1",
"db-migrate": "^0.11.13",
"db-migrate-mysql": "^2.2.0",
"debug": "^4.3.4",
"dockerode": "^3.3.1",
"ejs": "^3.1.6",
"ejs-cli": "^2.2.1",
"express": "^4.17.1",
"ipaddr.js": "^2.0.0",
"js-yaml": "^4.0.0",
"json": "^10.0.0",
"ejs-cli": "^2.2.3",
"express": "^4.17.3",
"ipaddr.js": "^2.0.1",
"js-yaml": "^4.1.0",
"json": "^11.0.0",
"jsonwebtoken": "^8.5.1",
"ldapjs": "^2.2.4",
"ldapjs": "^2.3.2",
"lodash": "^4.17.21",
"lodash.chunk": "^4.2.0",
"mime": "^2.5.2",
"moment": "^2.29.1",
"moment-timezone": "^0.5.33",
"moment": "^2.29.2",
"moment-timezone": "^0.5.34",
"morgan": "^1.10.0",
"multiparty": "^4.2.2",
"mustache-express": "^1.3.0",
"multiparty": "^4.2.3",
"mysql": "^2.18.1",
"nodemailer": "^6.4.18",
"nodemailer": "^6.7.3",
"nodemailer-smtp-transport": "^2.7.4",
"once": "^1.4.0",
"pretty-bytes": "^5.6.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.4.4",
"readdirp": "^3.5.0",
"request": "^2.88.2",
"rimraf": "^3.0.2",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^1.1.1",
"semver": "^7.3.4",
"showdown": "^1.9.1",
"qrcode": "^1.5.0",
"readdirp": "^3.6.0",
"safetydance": "^2.2.0",
"semver": "^7.3.7",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^6.1.0",
"supererror": "^0.7.2",
"superagent": "^7.1.1",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.2.0",
"tldjs": "^2.3.1",
"underscore": "^1.12.0",
"ua-parser-js": "^1.0.2",
"underscore": "^1.13.2",
"uuid": "^8.3.2",
"validator": "^13.5.2",
"ws": "^7.4.3",
"validator": "^13.7.0",
"ws": "^8.5.0",
"xml2js": "^0.4.23"
},
"devDependencies": {
"expect.js": "*",
"hock": "^1.4.1",
"js2xmlparser": "^4.0.1",
"mocha": "^8.3.0",
"js2xmlparser": "^4.0.2",
"mocha": "^9.2.2",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^13.0.7",
"node-sass": "^5.0.0",
"recursive-readdir": "^2.2.2"
"nock": "^13.2.4",
"node-sass": "^7.0.1",
"nyc": "^15.1.0"
},
"scripts": {
"test": "./runTests",

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/box 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,15 +34,18 @@ 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 --filter "label=isCloudronManaged" | xargs --no-run-if-empty docker rm -f
docker rm -f mysql-server
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
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 --gateway 172.18.0.1 cloudron --ipv6 --subnet=fd00:c107:d509::/64 || true
# create the same mysql server version to test with
OUT=`docker inspect mysql-server` || true
@@ -60,6 +63,12 @@ while ! mysqladmin ping -h"${MYSQL_IP}" --silent; do
sleep 1
done
echo "=> Ensure local base image"
docker pull cloudron/base:3.0.0@sha256:455c70428723e3a823198c57472785437eb6eab082e79b3ff04ea584faf46e92
echo "=> Create iptables blocklist"
sudo ipset create cloudron_blocklist hash:net || true
echo "=> Starting cloudron-syslog"
cloudron-syslog --logdir "${DATA_DIR}/platformdata/logs/" &
@@ -70,10 +79,15 @@ echo "=> Run database migrations"
cd "${source_dir}"
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
echo "=> Run tests with mocha"
TESTS=${DEFAULT_TESTS}
if [[ $# -gt 0 ]]; then
TESTS="$*"
fi
BOX_ENV=test ./node_modules/mocha/bin/_mocha --bail --no-timeouts --exit -R spec ${TESTS}
if [[ -z ${COVERAGE+x} ]]; then
echo "=> Run tests with mocha"
BOX_ENV=test ./node_modules/.bin/mocha --bail --no-timeouts --exit -R spec ${TESTS}
else
echo "=> Run tests with mocha and coverage"
BOX_ENV=test ./node_modules/.bin/nyc --reporter=html ./node_modules/.bin/mocha --no-timeouts --exit -R spec ${TESTS}
fi

View File

@@ -26,8 +26,8 @@ readonly GREEN='\033[32m'
readonly DONE='\033[m'
# verify the system has minimum requirements met
if [[ "${rootfs_type}" != "ext4" ]]; then
echo "Error: Cloudron requires '/' to be ext4" # see #364
if [[ "${rootfs_type}" != "ext4" && "${rootfs_type}" != "xfs" ]]; then
echo "Error: Cloudron requires '/' to be ext4 or xfs" # see #364
exit 1
fi
@@ -41,22 +41,35 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
exit 1
fi
if systemctl -q is-active box; then
if [[ "$(uname -m)" != "x86_64" ]]; then
echo "Error: Cloudron only supports amd64/x86_64"
exit 1
fi
if cvirt=$(systemd-detect-virt --container); then
echo "Error: Cloudron does not support ${cvirt}, only runs on bare metal or with full hardware virtualization"
exit 1
fi
# 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
initBaseImage="true"
provider="generic"
requestedVersion=""
installServerOrigin="https://api.cloudron.io"
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
consoleServerOrigin="https://console.cloudron.io"
sourceTarballUrl=""
rebootServer="true"
setupToken=""
setupToken="" # this is a OTP for securing an installation (https://forum.cloudron.io/topic/6389/add-password-for-initial-configuration)
appstoreSetupToken=""
redo="false"
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,generate-setup-token" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,provider:,version:,env:,skip-reboot,generate-setup-token,setup-token:,redo" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -68,17 +81,20 @@ while true; do
if [[ "$2" == "dev" ]]; then
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
consoleServerOrigin="https://console.dev.cloudron.io"
installServerOrigin="https://api.dev.cloudron.io"
elif [[ "$2" == "staging" ]]; then
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
consoleServerOrigin="https://console.staging.cloudron.io"
installServerOrigin="https://api.staging.cloudron.io"
elif [[ "$2" == "unstable" ]]; then
installServerOrigin="https://api.dev.cloudron.io"
fi
shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--redo) redo="true"; shift;;
--setup-token) appstoreSetupToken="$2"; shift 2;;
--generate-setup-token) setupToken="$(openssl rand -hex 10)"; shift;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
@@ -93,11 +109,18 @@ fi
# Only --help works with mismatched ubuntu
ubuntu_version=$(lsb_release -rs)
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubuntu_version}" != "20.04" ]]; then
echo "Cloudron requires Ubuntu 16.04, 18.04 or 20.04" > /dev/stderr
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubuntu_version}" != "20.04" && "${ubuntu_version}" != "22.04" ]]; then
echo "Cloudron requires Ubuntu 18.04, 20.04, 22.04" > /dev/stderr
exit 1
fi
if which nginx >/dev/null || which docker >/dev/null || which node > /dev/null; then
if [[ "${redo}" == "false" ]]; then
echo "Error: Some packages like nginx/docker/nodejs are already installed. Cloudron requires specific versions of these packages and will install them as part of it's installation. Please start with a fresh Ubuntu install and run this script again." > /dev/stderr
exit 1
fi
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
@@ -132,17 +155,15 @@ echo ""
echo " Join us at https://forum.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
exit 1
fi
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install --no-install-recommends curl python3 ubuntu-standard software-properties-common -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
exit 1
fi
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install --no-install-recommends curl python3 ubuntu-standard software-properties-common -y &>> "${LOG_FILE}"; then
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
exit 1
fi
echo "=> Checking version"
@@ -162,7 +183,7 @@ if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=
exit 1
fi
echo "=> Downloading version ${version} ..."
echo "=> Downloading Cloudron version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
if ! $curl -sL "${sourceTarballUrl}" | tar -zxf - -C "${box_src_tmp_dir}"; then
@@ -170,18 +191,16 @@ if ! $curl -sL "${sourceTarballUrl}" | tar -zxf - -C "${box_src_tmp_dir}"; then
exit 1
fi
if [[ "${initBaseImage}" == "true" ]]; then
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..."
# initializeBaseUbuntuImage.sh args (provider, infraversion path) are only to support installation of pre 5.3 Cloudrons
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "generic" "../src" &>> "${LOG_FILE}"; then
echo "Init script failed. See ${LOG_FILE} for details"
exit 1
fi
echo ""
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..."
init_ubuntu_script=$(test -f "${box_src_tmp_dir}/scripts/init-ubuntu.sh" && echo "${box_src_tmp_dir}/scripts/init-ubuntu.sh" || echo "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh")
if ! /bin/bash "${init_ubuntu_script}" &>> "${LOG_FILE}"; then
echo "Init script failed. See ${LOG_FILE} for details"
exit 1
fi
echo ""
# The provider flag is still used for marketplace images
echo "=> Installing version ${version} (this takes some time) ..."
echo "=> Installing Cloudron version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
echo "${provider}" > /etc/cloudron/PROVIDER
[[ ! -z "${setupToken}" ]] && echo "${setupToken}" > /etc/cloudron/SETUP_TOKEN
@@ -193,6 +212,20 @@ fi
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('api_server_origin', '${apiServerOrigin}');" 2>/dev/null
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web_server_origin', '${webServerOrigin}');" 2>/dev/null
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('console_server_origin', '${consoleServerOrigin}');" 2>/dev/null
if [[ -n "${appstoreSetupToken}" ]]; then
if ! setupResponse=$(curl -sX POST -H "Content-type: application/json" --data "{\"setupToken\": \"${appstoreSetupToken}\"}" "${apiServerOrigin}/api/v1/cloudron_setup_done"); then
echo "Could not complete setup. See ${LOG_FILE} for details"
exit 1
fi
cloudronId=$(echo "${setupResponse}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["cloudronId"])')
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('cloudron_id', '${cloudronId}');" 2>/dev/null
appstoreApiToken=$(echo "${setupResponse}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["cloudronToken"])')
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('appstore_api_token', '${appstoreApiToken}');" 2>/dev/null
fi
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
@@ -203,7 +236,7 @@ while true; do
sleep 10
done
if ! ip=$(curl -s --fail --connect-timeout 2 --max-time 2 https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
if ! ip=$(curl -s --fail --connect-timeout 2 --max-time 2 https://ipv4.api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
ip='<IP>'
fi
if [[ -z "${setupToken}" ]]; then

View File

@@ -8,14 +8,15 @@ set -eu -o pipefail
PASTEBIN="https://paste.cloudron.io"
OUT="/tmp/cloudron-support.log"
LINE="\n========================================================\n"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGWS+930b8QdzbchGljt3KSljH9wRhYvht8srrtQHdzg support@cloudron.io"
HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
This script collects diagnostic information to help debug server related issues.
Options:
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--reset-appstore-account Reset associated cloudron.io account
--help Show this message
"
# We require root
@@ -26,7 +27,7 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login,reset-appstore-account" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -37,12 +38,20 @@ while true; do
# fall through
;&
--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_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' AND username IS NOT NULL ORDER BY creationTime LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
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."
dashboard_domain=$(mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" 2>/dev/null)
mysql -NB -uroot -ppassword -e "INSERT INTO box.settings (name, value) VALUES ('ghosts_config', '{\"${admin_username}\":\"${admin_password}\"}') ON DUPLICATE KEY UPDATE name='ghosts_config', value='{\"${admin_username}\":\"${admin_password}\"}'" 2>/dev/null
echo "Login at https://${dashboard_domain} as ${admin_username} / ${admin_password} . This password may only be used once."
exit 0
;;
--reset-appstore-account)
echo -e "This will reset the Cloudron.io account associated with this Cloudron. Once reset, you can re-login with a different account in the Cloudron Dashboard. See https://docs.cloudron.io/appstore/#change-account for more information.\n"
read -e -p "Reset the Cloudron.io account? [y/N] " choice
[[ "$choice" != [Yy]* ]] && exit 1
mysql -uroot -ppassword -e "DELETE FROM box.settings WHERE name='cloudron_token';" 2>/dev/null
dashboard_domain=$(mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" 2>/dev/null)
echo "Account reset. Please re-login at https://${dashboard_domain}/#/appstore"
exit 0
;;
--) break;;
@@ -68,6 +77,31 @@ if [[ "`df --output="avail" /tmp | sed -n 2p`" -lt "5120" ]]; then
exit 1
fi
if [[ "${enableSSH}" == "true" ]]; then
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
ssh_user="cloudron-support"
keys_file="/home/cloudron-support/.ssh/authorized_keys"
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo "Key file: ${keys_file}" >> $OUT
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
touch "${keys_file}" # required for concat to work
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
chmod 600 "${keys_file}"
chown "${ssh_user}" "${keys_file}"
fi
echo "Done"
exit 0
fi
echo -n "Generating Cloudron Support stats..."
# clear file
@@ -110,40 +144,8 @@ iptables -L &>> $OUT
echo "Done"
if [[ "${enableSSH}" == "true" ]]; then
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if [[ -d /home/ubuntu ]]; then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
ssh_user="root"
keys_file="/root/.ssh/authorized_keys"
fi
echo -e $LINE"SSH"$LINE >> $OUT
echo "Username: ${ssh_user}" >> $OUT
echo "Port: ${ssh_port}" >> $OUT
echo "PermitRootLogin: ${permit_root_login}" >> $OUT
echo "Key file: ${keys_file}" >> $OUT
echo -n "Enabling ssh access for the Cloudron support team..."
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
touch "${keys_file}" # required for concat to work
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
chmod 600 "${keys_file}"
chown "${ssh_user}" "${keys_file}"
fi
echo "Done"
fi
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent --data-binary "@$OUT" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
echo ""

View File

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

198
scripts/init-ubuntu.sh Executable file
View File

@@ -0,0 +1,198 @@
#!/bin/bash
# This script is run on the base ubuntu. Put things here which are managed by ubuntu
set -euv -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly arg_infraversionpath="${SOURCE_DIR}/../src"
function die {
echo $1
exit 1
}
export DEBIAN_FRONTEND=noninteractive
readonly ubuntu_codename=$(lsb_release -cs)
readonly ubuntu_version=$(lsb_release -rs)
# hold grub since updating it breaks on some VPS providers. also, dist-upgrade will trigger it
apt-mark hold grub* >/dev/null
apt-get -o Dpkg::Options::="--force-confdef" update -y
apt-get -o Dpkg::Options::="--force-confdef" upgrade -y
apt-mark unhold grub* >/dev/null
echo "==> Installing required packages"
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
case "${ubuntu_version}" in
16.04)
gpg_package="gnupg"
mysql_package="mysql-server-5.7"
ntpd_package=""
python_package="python2.7"
nginx_package="" # we use custom package for TLS v1.3 support
;;
18.04)
gpg_package="gpg"
mysql_package="mysql-server-5.7"
ntpd_package=""
python_package="python2.7"
nginx_package="" # we use custom package for TLS v1.3 support
;;
20.04)
gpg_package="gpg"
mysql_package="mysql-server-8.0"
ntpd_package="systemd-timesyncd"
python_package="python3.8"
nginx_package="nginx-full"
;;
22.04)
gpg_package="gpg"
mysql_package="mysql-server-8.0"
ntpd_package="systemd-timesyncd"
python_package="python3.10"
nginx_package="nginx-full"
;;
esac
apt-get -y install --no-install-recommends \
acl \
apparmor \
build-essential \
cifs-utils \
cron \
curl \
debconf-utils \
dmsetup \
$gpg_package \
ipset \
iptables \
lib${python_package} \
linux-generic \
logrotate \
$mysql_package \
nfs-common \
$nginx_package \
$ntpd_package \
openssh-server \
pwgen \
resolvconf \
sshfs \
swaks \
tzdata \
unattended-upgrades \
unbound \
unzip \
xfsprogs
# on some providers like scaleway the sudo file is changed and we want to keep the old one
apt-get -o Dpkg::Options::="--force-confold" install -y --no-install-recommends sudo
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
apt-get install -y --no-install-recommends $python_package # Install python which is required for npm rebuild
# do not upgrade grub because it might prompt user and break this script
echo "==> Enable memory accounting"
apt-get -y --no-upgrade --no-install-recommends install grub2-common
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
update-grub
echo "==> Install collectd"
# without this, libnotify4 will install gnome-shell
apt-get install -y libnotify4 libcurl3-gnutls --no-install-recommends
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
if [[ "${ubuntu_version}" == "22.04" ]]; then
readonly launchpad="https://launchpad.net/ubuntu/+source/collectd/5.12.0-9/+build/23189375/+files"
cd /tmp && wget -q "${launchpad}/collectd_5.12.0-9_amd64.deb" "${launchpad}/collectd-utils_5.12.0-9_amd64.deb" "${launchpad}/collectd-core_5.12.0-9_amd64.deb" "${launchpad}/libcollectdclient1_5.12.0-9_amd64.deb"
cd /tmp && apt install -y --no-install-recommends ./libcollectdclient1_5.12.0-9_amd64.deb ./collectd-core_5.12.0-9_amd64.deb ./collectd_5.12.0-9_amd64.deb ./collectd-utils_5.12.0-9_amd64.deb && rm -f /tmp/collectd_*.deb
echo -e "\nLD_PRELOAD=/usr/lib/python3.10/config-3.10-x86_64-linux-gnu/libpython3.10.so" >> /etc/default/collectd
else
if ! apt-get install -y --no-install-recommends collectd collectd-utils; then
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
echo "Failed to install collectd, continuing anyway. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
fi
if [[ "${ubuntu_version}" == "20.04" ]]; then
echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
fi
fi
# some hosts like atlantic install ntp which conflicts with timedatectl. https://serverfault.com/questions/1024770/ubuntu-20-04-time-sync-problems-and-possibly-incorrect-status-information
echo "==> Configuring host"
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
if systemctl is-active ntp; then
systemctl stop ntp
apt purge -y ntp
fi
timedatectl set-ntp 1
# mysql follows the system timezone
timedatectl set-timezone UTC
echo "==> Adding sshd configuration warning"
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://docs.cloudron.io/security/#securing-ssh-access' -i /etc/ssh/sshd_config
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
echo "==> Disabling motd news"
if [[ -f "/etc/default/motd-news" ]]; then
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
fi
# If privacy extensions are not disabled on server, this breaks IPv6 detection
# https://bugs.launchpad.net/ubuntu/+source/procps/+bug/1068756
if [[ ! -f /etc/sysctl.d/99-cloudimg-ipv6.conf ]]; then
echo "==> Disable temporary address (IPv6)"
echo -e "# See https://bugs.launchpad.net/ubuntu/+source/procps/+bug/1068756\nnet.ipv6.conf.all.use_tempaddr = 0\nnet.ipv6.conf.default.use_tempaddr = 0\n\n" > /etc/sysctl.d/99-cloudimg-ipv6.conf
fi
# Disable exim4 (1blu.de)
systemctl stop exim4 || true
systemctl disable exim4 || true
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed)
systemctl stop bind9 || true
systemctl disable bind9 || true
# on ovh images dnsmasq seems to run by default
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
# on ssdnodes postfix seems to run by default
systemctl stop postfix || true
systemctl disable postfix || true
# on ubuntu 18.04 and 20.04, this is the default. this requires resolvconf for DNS to work further after the disable
systemctl stop systemd-resolved || true
systemctl disable systemd-resolved || true
# on vultr, ufw is enabled by default. we have our own firewall
ufw disable || true
# we need unbound to work as this is required for installer.sh to do any DNS requests
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: no" > /etc/unbound/unbound.conf.d/cloudron-network.conf
systemctl restart unbound
# Ubuntu 22 has private home directories by default (https://discourse.ubuntu.com/t/private-home-directories-for-ubuntu-21-04-onwards/)
sed -e 's/^HOME_MODE\([[:space:]]\+\).*$/HOME_MODE\10755/' -i /etc/login.defs
# create the yellowtent user. system user has different numeric range, no age and won't show in login/gdm UI
# the nologin will also disable su/login
if ! id yellowtent 2>/dev/null; then
useradd --system --comment "Cloudron Box" --create-home --shell /usr/sbin/nologin yellowtent
fi
# add support user (no password, sudo)
if ! id cloudron-support 2>/dev/null; then
useradd --system --comment "Cloudron Support (support@cloudron.io)" --create-home --no-user-group --shell /bin/bash cloudron-support
fi

View File

@@ -15,6 +15,48 @@ function log() {
echo -e "$(date +'%Y-%m-%dT%H:%M:%S')" "==> installer: $1"
}
apt_ready="no"
function prepare_apt_once() {
[[ "${apt_ready}" == "yes" ]] && return
log "Making sure apt is in a good state"
log "Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
# it's unclear what needs to be run first or whether both these command should be run. so keep trying both
for count in {1..3}; do
# alternative to apt-install -y --fix-missing ?
if ! dpkg --force-confold --configure -a; then
log "dpkg reconfigure failed (try $count)"
dpkg_configure="no"
else
dpkg_configure="yes"
fi
if ! apt update -y; then
log "apt update failed (try $count)"
apt_update="no"
else
apt_update="yes"
fi
[[ "${dpkg_configure}" == "yes" && "${apt_update}" == "yes" ]] && break
sleep 1
done
apt_ready="yes"
if [[ "${dpkg_configure}" == "yes" && "${apt_update}" == "yes" ]]; then
log "apt is ready"
else
log "apt is not ready but proceeding anyway"
fi
}
readonly user=yellowtent
readonly box_src_dir=/home/${user}/box
@@ -29,59 +71,60 @@ readonly is_update=$(systemctl is-active -q box && echo "yes" || echo "no")
log "Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION)"
log "updating docker"
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
readonly docker_version=20.10.14
if ! which docker 2>/dev/null || [[ $(docker version --format {{.Client.Version}}) != "${docker_version}" ]]; then
log "installing/updating docker"
# create systemd drop-in file already to make sure images are with correct driver
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 --experimental --ip6tables" > /etc/systemd/system/docker.service.d/cloudron.conf
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.4.3-1_amd64.deb" -o /tmp/containerd.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.5.11-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
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
log "Failed to fix packages. Retry"
sleep 1
done
# the latest docker might need newer packages
while ! apt update -y; do
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
log "Failed to install docker. Retry"
sleep 1
done
log "installing docker"
prepare_apt_once
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
fi
# we want atleast nginx 1.14 for TLS v1.3 support. Ubuntu 20/22 already has nginx 1.18
# Ubuntu 18 OpenSSL does not have TLS v1.3 support, so we use the upstream nginx packages
readonly nginx_version=$(nginx -v 2>&1)
if [[ "${nginx_version}" != *"1.18."* ]]; then
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
if [[ "${ubuntu_version}" == "20.04" ]]; then
if [[ "${nginx_version}" == *"Ubuntu"* ]]; then
log "switching nginx to ubuntu package"
prepare_apt_once
apt remove -y nginx
apt install -y nginx-full
fi
elif [[ "${ubuntu_version}" == "18.04" ]]; then
if [[ "${nginx_version}" != *"1.18."* ]]; then
log "installing/updating 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
prepare_apt_once
# 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
fi
log "updating node"
readonly node_version=14.15.4
if [[ "$(node --version)" != "v${node_version}" ]]; then
readonly node_version=16.14.2
if ! which node 2>/dev/null || [[ "$(node --version)" != "v${node_version}" ]]; then
log "installing/updating node ${node_version}"
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
rm -rf /usr/local/node-16.13.1
fi
# this is here (and not in updater.js) because rebuild requires the above node
# note that rebuild requires the above node
for try in `seq 1 10`; do
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
@@ -99,7 +142,7 @@ if [[ ${try} -eq 10 ]]; then
fi
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(' '));")
images=$(node -e "let 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(' '));")
log "\tPulling docker images: ${images}"
for image in ${images}; do
@@ -116,7 +159,7 @@ done
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"
CLOUDRON_SYSLOG_VERSION="1.1.0"
while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLOUDRON_SYSLOG_VERSION} ]]; do
rm -rf "${CLOUDRON_SYSLOG_DIR}"
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
@@ -125,10 +168,15 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
sleep 5
done
if ! id "${user}" 2>/dev/null; then
useradd "${user}" -m
log "creating cloudron-support user"
if ! id cloudron-support 2>/dev/null; then
useradd --system --comment "Cloudron Support (support@cloudron.io)" --create-home --no-user-group --shell /bin/bash cloudron-support
fi
log "locking the ${user} account"
usermod --shell /usr/sbin/nologin "${user}"
passwd --lock "${user}"
if [[ "${is_update}" == "yes" ]]; then
log "stop box service for update"
${box_src_dir}/setup/stop.sh

View File

@@ -14,9 +14,10 @@ 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/box"
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)"
@@ -36,11 +37,17 @@ systemctl enable apparmor
systemctl restart apparmor
usermod ${USER} -a -G docker
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
if ! grep -q ip6tables /etc/systemd/system/docker.service.d/cloudron.conf; then
log "Adding ip6tables flag to docker" # https://github.com/moby/moby/pull/41622
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2 --experimental --ip6tables" > /etc/systemd/system/docker.service.d/cloudron.conf
systemctl daemon-reload
systemctl restart docker
fi
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
mkdir -p "${MAIL_DATA_DIR}"
# keep these in sync with paths.js
log "Ensuring directories"
@@ -50,7 +57,8 @@ mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/redis"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner" \
"${PLATFORM_DATA_DIR}/addons/mail/dkim"
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
@@ -61,15 +69,10 @@ 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"
mkdir -p "${PLATFORM_DATA_DIR}/sshfs"
mkdir -p "${PLATFORM_DATA_DIR}/cifs"
# ensure backups folder exists and is writeable
mkdir -p /var/backups
@@ -85,23 +88,19 @@ sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
-i /lib/systemd/system/systemd-journald.service
# Give user access to system logs
usermod -a -G systemd-journal ${USER}
mkdir -p /var/log/journal # in some images, this directory is not created making system log to /run/systemd instead
chown root:systemd-journal /var/log/journal
usermod -a -G systemd-journal ${USER} # Give user access to system logs
if [[ ! -d /var/log/journal ]]; then # in some images, this directory is not created making system log to /run/systemd instead
mkdir -p /var/log/journal
chown root:systemd-journal /var/log/journal
chmod g+s /var/log/journal # sticky bit for group propagation
fi
systemctl daemon-reload
systemctl restart systemd-journald
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}
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
# If IP6 is not enabled, dns queries seem to fail on some hosts. -s returns false if file missing or 0 size
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
cp -f "${script_dir}/start/unbound.conf" /etc/unbound/unbound.conf.d/cloudron-network.conf
# update the root anchor after a out-of-disk-space situation (see #269)
unbound-anchor -a /var/lib/unbound/root.key
@@ -109,6 +108,7 @@ unbound-anchor -a /var/lib/unbound/root.key
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
@@ -116,7 +116,7 @@ systemctl enable box
systemctl enable cloudron-firewall
systemctl enable --now cloudron-disable-thp
# update firewall rules
# update firewall rules. this must be done after docker created it's rules
systemctl restart cloudron-firewall
# For logrotate
@@ -129,26 +129,28 @@ systemctl restart unbound
systemctl restart cloudron-syslog
log "Configuring sudoers"
rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
rm -f /etc/sudoers.d/${USER} /etc/sudoers.d/cloudron
cp "${script_dir}/start/sudoers" /etc/sudoers.d/cloudron
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"
if [[ "${ubuntu_version}" == "20.04" ]]; then
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
if ! grep -q LD_PRELOAD /etc/default/collectd; then
echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
fi
fi
systemctl restart collectd
log "Configuring sysctl"
# If privacy extensions are not disabled on server, this breaks IPv6 detection
# https://bugs.launchpad.net/ubuntu/+source/procps/+bug/1068756
if [[ ! -f /etc/sysctl.d/99-cloudimg-ipv6.conf ]]; then
echo "==> Disable temporary address (IPv6)"
echo -e "# See https://bugs.launchpad.net/ubuntu/+source/procps/+bug/1068756\nnet.ipv6.conf.all.use_tempaddr = 0\nnet.ipv6.conf.default.use_tempaddr = 0\n\n" > /etc/sysctl.d/99-cloudimg-ipv6.conf
sysctl -p
fi
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
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
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
@@ -172,7 +174,7 @@ fi
# worker_rlimit_nofile in nginx config can be max this number
mkdir -p /etc/systemd/system/nginx.service.d
if ! grep -q "^LimitNOFILE=" /etc/systemd/system/nginx.service.d/cloudron.conf; then
if ! grep -q "^LimitNOFILE=" /etc/systemd/system/nginx.service.d/cloudron.conf 2>/dev/null; then
echo -e "[Service]\nLimitNOFILE=16384\n" > /etc/systemd/system/nginx.service.d/cloudron.conf
fi
@@ -207,7 +209,8 @@ done
readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
if [[ "${ubuntu_version}" == "20.04" ]]; then
readonly mysqlVersion=$(mysql -NB -u root -p${mysql_root_password} -e 'SELECT VERSION()' 2>/dev/null)
if [[ "${mysqlVersion}" == "8.0."* ]]; then
# mysql 8 added a new caching_sha2_password scheme which mysqljs does not support
mysql -u root -p${mysql_root_password} -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${mysql_root_password}';"
fi
@@ -224,37 +227,18 @@ fi
rm -f /etc/cloudron/cloudron.conf
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
log "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
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" "${PLATFORM_DATA_DIR}/sshfs" "${PLATFORM_DATA_DIR}/cifs"
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
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}"
# do not chown the boxdata/mail directory entirely; dovecot gets upset
chown "${USER}:${USER}" "${MAIL_DATA_DIR}"
log "Starting Cloudron"
systemctl start box

View File

@@ -3,100 +3,154 @@
set -eu -o pipefail
echo "==> Setting up firewall"
iptables -t filter -N CLOUDRON || true
iptables -t filter -F CLOUDRON # empty any existing rules
has_ipv6=$(cat /proc/net/if_inet6 >/dev/null 2>&1 && echo "yes" || echo "no")
# wait for 120 seconds for xtables lock, checking every 1 second
readonly iptables="iptables --wait 120 --wait-interval 1"
readonly ip6tables="ip6tables --wait 120 --wait-interval 1"
function ipxtables() {
$iptables "$@"
[[ "${has_ipv6}" == "yes" ]] && $ip6tables "$@"
}
ipxtables -t filter -N CLOUDRON || true
ipxtables -t filter -F CLOUDRON # empty any existing rules
# first setup any user IP block lists
ipset create cloudron_blocklist hash:net || true
ipset create cloudron_blocklist6 hash:net family inet6 || true
/home/yellowtent/box/src/scripts/setblocklist.sh
iptables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist src -j DROP
$iptables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist src -j DROP
# the DOCKER-USER chain is not cleared on docker restart
if ! iptables -t filter -C DOCKER-USER -m set --match-set cloudron_blocklist src -j DROP; then
iptables -t filter -I DOCKER-USER 1 -m set --match-set cloudron_blocklist src -j DROP
if ! $iptables -t filter -C DOCKER-USER -m set --match-set cloudron_blocklist src -j DROP; then
$iptables -t filter -I DOCKER-USER 1 -m set --match-set cloudron_blocklist src -j DROP
fi
$ip6tables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist6 src -j DROP
# there is no DOCKER-USER chain in ip6tables, bug?
$ip6tables -D FORWARD -m set --match-set cloudron_blocklist6 src -j DROP || true
$ip6tables -I FORWARD 1 -m set --match-set cloudron_blocklist6 src -j DROP
# 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
ipxtables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
ipxtables -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. we used to use --dports but it has a 15 port limit (XT_MULTI_PORTS)
ports_json="/home/yellowtent/boxdata/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
IFS=',' arr=(${allowed_tcp_ports})
for p in "${arr[@]}"; do
iptables -A CLOUDRON -p tcp -m tcp --dport "${p}" -j ACCEPT
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
for p in $allowed_tcp_ports; do
ipxtables -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
IFS=',' arr=(${allowed_udp_ports})
for p in "${arr[@]}"; do
iptables -A CLOUDRON -p udp -m udp --dport "${p}" -j ACCEPT
if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_udp_ports.join(' '))" 2>/dev/null); then
for p in $allowed_udp_ports; do
ipxtables -A CLOUDRON -p udp -m udp --dport "${p}" -j ACCEPT
done
fi
# LDAP user directory allow list
ipset create cloudron_ldap_allowlist hash:net || true
ipset flush cloudron_ldap_allowlist
ipset create cloudron_ldap_allowlist6 hash:net family inet6 || true
ipset flush cloudron_ldap_allowlist6
ldap_allowlist_json="/home/yellowtent/platformdata/firewall/ldap_allowlist.txt"
# delete any existing redirect rule
$iptables -t nat -D PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004 2>/dev/null || true
$ip6tables -t nat -D PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004 >/dev/null || true
if [[ -f "${ldap_allowlist_json}" ]]; then
# without the -n block, any last line without a new line won't be read it!
while read -r line || [[ -n "$line" ]]; do
[[ -z "${line}" ]] && continue # ignore empty lines
[[ "$line" =~ ^#.*$ ]] && continue # ignore lines starting with #
if [[ "$line" == *":"* ]]; then
ipset add -! cloudron_ldap_allowlist6 "${line}" # the -! ignore duplicates
else
ipset add -! cloudron_ldap_allowlist "${line}" # the -! ignore duplicates
fi
done < "${ldap_allowlist_json}"
# ldap server we expose 3004 and also redirect from standard ldaps port 636
$iptables -t nat -I PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004
$iptables -t filter -A CLOUDRON -m set --match-set cloudron_ldap_allowlist src -p tcp --dport 3004 -j ACCEPT
$ip6tables -t nat -I PREROUTING -p tcp --dport 636 -j REDIRECT --to-ports 3004
$ip6tables -t filter -A CLOUDRON -m set --match-set cloudron_ldap_allowlist6 src -p tcp --dport 3004 -j ACCEPT
fi
# turn and stun service
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:51000 -j ACCEPT
ipxtables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
ipxtables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
ipxtables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:51000 -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
iptables -t filter -A CLOUDRON -s 172.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
iptables -t filter -A CLOUDRON -i lo -j ACCEPT # required for localhost connections (mysql)
# ICMPv6 is very fundamental to IPv6 connectivity unlike ICMPv4
$iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
$iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
$ip6tables -t filter -A CLOUDRON -p ipv6-icmp -j ACCEPT
ipxtables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
$iptables -t filter -A CLOUDRON -s 172.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
ipxtables -t filter -A CLOUDRON -i lo -j ACCEPT # required for localhost connections (mysql)
# log dropped incoming. keep this at the end of all the rules
iptables -t filter -A CLOUDRON -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
iptables -t filter -A CLOUDRON -j DROP
ipxtables -t filter -A CLOUDRON -m limit --limit 2/min -j LOG --log-prefix "Packet dropped: " --log-level 7
ipxtables -t filter -A CLOUDRON -j DROP
if ! iptables -t filter -C INPUT -j CLOUDRON 2>/dev/null; then
iptables -t filter -I INPUT -j CLOUDRON
fi
# prepend our chain to the filter table
$iptables -t filter -C INPUT -j CLOUDRON 2>/dev/null || $iptables -t filter -I INPUT -j CLOUDRON
$ip6tables -t filter -C INPUT -j CLOUDRON 2>/dev/null || $ip6tables -t filter -I INPUT -j CLOUDRON
# Setup rate limit chain (the recent info is at /proc/net/xt_recent)
iptables -t filter -N CLOUDRON_RATELIMIT || true
iptables -t filter -F CLOUDRON_RATELIMIT # empty any existing rules
ipxtables -t filter -N CLOUDRON_RATELIMIT || true
ipxtables -t filter -F CLOUDRON_RATELIMIT # empty any existing rules
# log dropped incoming. keep this at the end of all the rules
iptables -t filter -N CLOUDRON_RATELIMIT_LOG || true
iptables -t filter -F CLOUDRON_RATELIMIT_LOG # empty any existing rules
iptables -t filter -A CLOUDRON_RATELIMIT_LOG -m limit --limit 2/min -j LOG --log-prefix "IPTables RateLimit: " --log-level 7
iptables -t filter -A CLOUDRON_RATELIMIT_LOG -j DROP
ipxtables -t filter -N CLOUDRON_RATELIMIT_LOG || true
ipxtables -t filter -F CLOUDRON_RATELIMIT_LOG # empty any existing rules
ipxtables -t filter -A CLOUDRON_RATELIMIT_LOG -m limit --limit 2/min -j LOG --log-prefix "IPTables RateLimit: " --log-level 7
ipxtables -t filter -A CLOUDRON_RATELIMIT_LOG -j DROP
# http https
for port in 80 443; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
ipxtables -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
ipxtables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --set --name "public-${port}"
ipxtables -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
done
# ldaps
for port in 636 3004; do
ipxtables -A CLOUDRON_RATELIMIT -p tcp --syn --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
done
# docker translates (dnat) 25, 587, 993, 4190 in the PREROUTING step
for port in 2525 4190 9993; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
$iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
done
# msa, ldap, imap, sieve
for port in 2525 3002 4190 9993; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 500 -j CLOUDRON_RATELIMIT_LOG
# msa, ldap, imap, sieve, pop3
for port in 2525 3002 4190 9993 9995; do
$iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 500 -j CLOUDRON_RATELIMIT_LOG
done
# cloudron docker network: mysql postgresql redis mongodb
for port in 3306 5432 6379 27017; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
$iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
done
if ! iptables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null; then
iptables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
fi
# Add the rate limit chain to input chain
$iptables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null || $iptables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
$ip6tables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null || $ip6tables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
# 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"
ipxtables -D FORWARD -j CLOUDRON_RATELIMIT || true
ipxtables -I FORWARD 1 -j CLOUDRON_RATELIMIT

View File

@@ -4,13 +4,17 @@
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
if [[ -f /tmp/.cloudron-motd-cache ]]; then
ip=$(cat /tmp/.cloudron-motd-cache)
elif ! ip=$(curl --fail --connect-timeout 2 --max-time 2 -q https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
cache_file="/var/cache/cloudron-motd-cache"
if [[ -z "$(ls -A /home/yellowtent/platformdata/addons/mail/dkim)" ]]; then
if [[ ! -f "${cache_file}" ]]; then
curl --fail --connect-timeout 2 --max-time 2 -q https://ipv4.api.cloudron.io/api/v1/helper/public_ip --output "${cache_file}" || true
fi
if [[ -f "${cache_file}" ]]; then
ip=$(sed -n -e 's/.*"ip": "\(.*\)"/\1/p' /var/cache/cloudron-motd-cache)
else
ip='<IP>'
fi
echo "${ip}" > /tmp/.cloudron-motd-cache
if [[ ! -f /etc/cloudron/SETUP_TOKEN ]]; then
url="https://${ip}"

View File

@@ -13,7 +13,7 @@
##############################################################################
Hostname "localhost"
#FQDNLookup true
FQDNLookup false
#BaseDir "/var/lib/collectd"
#PluginDir "/usr/lib/collectd"
#TypesDB "/usr/share/collectd/types.db" "/etc/collectd/my_types.db"
@@ -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
@@ -230,7 +232,7 @@ LoadPlugin write_graphite
<Plugin write_graphite>
<Node "graphing">
Host "localhost"
Host "127.0.0.1"
Port "2003"
Protocol "tcp"
LogSendErrors true

View File

@@ -14,7 +14,7 @@ def read():
for d in disks:
device = d[0]
if 'devicemapper' in d[1] or not device.startswith('/dev/'): continue
instance = device[len('/dev/'):].replace('/', '_') # see #348
instance = device[len('/dev/'):].replace('/', '_').replace('.', '_') # see #348
try:
st = os.statvfs(d[1]) # handle disk removal
@@ -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
@@ -18,6 +18,12 @@ 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

@@ -19,15 +19,10 @@ http {
include mime.types;
default_type application/octet-stream;
# the collectd config depends on this log format
log_format combined2 '$remote_addr - [$time_local] '
'"$request" $status $body_bytes_sent $request_time '
'"$http_referer" "$host" "$http_user_agent"';
# required for long host names
server_names_hash_bucket_size 128;
access_log /var/log/nginx/access.log combined2;
access_log /var/log/nginx/access.log combined;
sendfile on;

View File

@@ -1,6 +1,9 @@
# sudo logging breaks journalctl output with very long urls (systemd bug)
Defaults !syslog
Defaults!/home/yellowtent/box/src/scripts/checkvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/checkvolume.sh
Defaults!/home/yellowtent/box/src/scripts/clearvolume.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/clearvolume.sh
@@ -13,9 +16,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
@@ -41,11 +41,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
@@ -59,3 +56,17 @@ 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/setldapallowlist.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setldapallowlist.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
Defaults!/home/yellowtent/box/src/scripts/remountmount.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remountmount.sh
cloudron-support ALL=(ALL) NOPASSWD: ALL

View File

@@ -13,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
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,7 +1,11 @@
# Unbound is used primarily for RBL queries (host 2.0.0.127.zen.spamhaus.org)
# We cannot use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
server:
port: 53
interface: 127.0.0.1
interface: 172.18.0.1
ip-freebind: yes
do-ip6: no
access-control: 127.0.0.1 allow
access-control: 172.18.0.1/16 allow
@@ -10,4 +14,3 @@ server:
# enable below for logging to journalctl -u unbound
# verbosity: 5
# log-queries: yes

View File

@@ -1,29 +1,26 @@
'use strict';
exports = module.exports = {
verifyToken: verifyToken
verifyToken
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('./boxerror.js'),
tokendb = require('./tokendb.js'),
safe = require('safetydance'),
tokens = require('./tokens.js'),
users = require('./users.js');
function verifyToken(accessToken, callback) {
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 user = await users.get(token.identifier);
if (!user) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not found');
if (!user.active) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not active');
if (!user.active) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
await safe(tokens.update(token.id, { lastUsedTime: new Date() })); // ignore any error
callback(null, user);
});
});
return user;
}

553
src/acme2.js Normal file
View File

@@ -0,0 +1,553 @@
'use strict';
exports = module.exports = {
getCertificate,
// testing
_name: 'acme',
_getChallengeSubdomain: getChallengeSubdomain
};
const assert = require('assert'),
blobs = require('./blobs.js'),
BoxError = require('./boxerror.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
dns = require('./dns.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 = 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) {
const buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
return urlBase64Encode(buf.toString('base64'));
}
function getModulus(pem) {
assert(Buffer.isBuffer(pem));
const stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
if (!stdout) return null;
const 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.ACME_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
if (!nonce) throw new BoxError(BoxError.ACME_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.ACME_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
debug(`updateContact: contact of user updated to ${this.email}`);
};
async function generateAccountKey() {
const 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}`);
return acmeAccountKey;
}
Acme2.prototype.ensureAccount = async function () {
const payload = {
termsOfServiceAgreed: true
};
debug('ensureAccount: registering user');
this.accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY);
if (!this.accountKeyPem) {
debug('ensureAccount: generating new account keys');
this.accountKeyPem = await generateAccountKey();
await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem);
}
let result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
if (result.status === 403 && result.body.type === 'urn:ietf:params:acme:error:unauthorized') {
debug(`ensureAccount: key was revoked. ${result.status} ${JSON.stringify(result.body)}. generating new account key`);
this.accountKeyPem = await generateAccountKey();
await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem);
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) throw new BoxError(BoxError.ACME_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
debug(`ensureAccount: 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.ACME_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.ACME_ERROR, 'invalid authorizations in order');
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.ACME_ERROR, 'invalid finalize in order');
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.ACME_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, debug }, 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.ACME_ERROR, `Bad response when waiting for order. 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.ACME_ERROR, `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.ACME_ERROR, `Unexpected status or invalid response when waiting for order: ${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.ACME_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, debug }, 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.ACME_ERROR, `Bad response code when waiting for challenge : ${result.status}`);
}
debug(`waitForChallenge: status is "${result.body.status}" "${JSON.stringify(result.body)}"`);
if (result.body.status === 'pending') throw new BoxError(BoxError.ACME_ERROR, 'Challenge is in pending state');
else if (result.body.status === 'valid') return;
else throw new BoxError(BoxError.ACME_ERROR, `Unexpected status when waiting for challenge: ${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.ACME_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.rm(tmpdir, { recursive: true, force: 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, debug }, async () => {
debug(`downloadCertificate: downloading certificate of ${hostname}`);
const result = await this.postAsGet(certUrl);
if (result.statusCode === 202) throw new BoxError(BoxError.ACME_ERROR, 'Retry downloading certificate');
if (result.statusCode !== 200) throw new BoxError(BoxError.ACME_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.ACME_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.ACME_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}`);
await dns.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ]);
await dns.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 });
return 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}`);
await dns.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ]);
};
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.ACME_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.ensureAccount();
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, debug }, async () => {
const response = await superagent.get(this.caDirectory).timeout(30000).ok(() => true);
if (response.status !== 200) throw new BoxError(BoxError.ACME_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.ACME_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 = dns.makeWildcard(vhost);
debug(`getCertificate: will get wildcard cert for ${vhost}`);
}
await this.loadDirectory();
await this.acmeFlow(vhost, domain, paths);
};
async function getCertificate(vhost, domain, paths, options) {
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');
await promiseRetry({ times: 3, interval: 0, debug }, async function () {
debug(`getCertificate: for vhost ${vhost} and domain ${domain}`);
const acme = new Acme2(options || { });
return await acme.getCertificate(vhost, domain, paths);
});
}

81
src/addonconfigs.js Normal file
View File

@@ -0,0 +1,81 @@
'use strict';
exports = module.exports = {
get,
set,
unset,
getByAppId,
getByName,
unsetByAppId,
getAppIdByValue,
};
const assert = require('assert'),
database = require('./database.js');
async function set(appId, addonId, env) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert(Array.isArray(env));
await unset(appId, addonId);
if (env.length === 0) return;
const query = 'INSERT INTO appAddonConfigs(appId, addonId, name, value) VALUES ';
const args = [ ], queryArgs = [ ];
for (let i = 0; i < env.length; i++) {
args.push(appId, addonId, env[i].name, env[i].value);
queryArgs.push('(?, ?, ?, ?)');
}
await database.query(query + queryArgs.join(','), args);
}
async function unset(appId, addonId) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
await database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ]);
}
async function unsetByAppId(appId) {
assert.strictEqual(typeof appId, 'string');
await database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ]);
}
async function get(appId, addonId) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
const results = await database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ]);
return results;
}
async function getByAppId(appId) {
assert.strictEqual(typeof appId, 'string');
const results = await database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ]);
return results;
}
async function getAppIdByValue(addonId, namePattern, value) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof namePattern, 'string');
assert.strictEqual(typeof value, 'string');
const results = await database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ]);
if (results.length === 0) return null;
return results[0].appId;
}
async function getByName(appId, addonId, namePattern) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof namePattern, 'string');
const results = await database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ]);
if (results.length === 0) return null;
return results[0].value;
}

View File

@@ -1,573 +0,0 @@
'use strict';
exports = module.exports = {
get,
add,
exists,
del,
update,
getAll,
getPortBindings,
delPortBinding,
setAddonConfig,
getAddonConfig,
getAddonConfigByAppId,
getAddonConfigByName,
unsetAddonConfig,
unsetAddonConfigByAppId,
getAppIdByAddonConfigValue,
getByIpAddress,
setHealth,
setTask,
getAppStoreIds,
// subdomain table types
SUBDOMAIN_TYPE_PRIMARY: 'primary',
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
SUBDOMAIN_TYPE_ALIAS: 'alias',
_clear: clear
};
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
safe = require('safetydance'),
util = require('util');
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'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(',');
const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
assert(result.manifestJson === null || typeof result.manifestJson === 'string');
result.manifest = safe.JSON.parse(result.manifestJson);
delete result.manifestJson;
assert(result.tagsJson === null || typeof result.tagsJson === 'string');
result.tags = safe.JSON.parse(result.tagsJson) || [];
delete result.tagsJson;
assert(result.reverseProxyConfigJson === null || typeof result.reverseProxyConfigJson === 'string');
result.reverseProxyConfig = safe.JSON.parse(result.reverseProxyConfigJson) || {};
delete result.reverseProxyConfigJson;
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
result.portBindings = { };
let hostPorts = result.hostPorts === null ? [ ] : result.hostPorts.split(',');
let environmentVariables = result.environmentVariables === null ? [ ] : result.environmentVariables.split(',');
let portTypes = result.portTypes === null ? [ ] : result.portTypes.split(',');
delete result.hostPorts;
delete result.environmentVariables;
delete result.portTypes;
for (let i = 0; i < environmentVariables.length; i++) {
result.portBindings[environmentVariables[i]] = { hostPort: parseInt(hostPorts[i], 10), type: portTypes[i] };
}
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
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.proxyAuth = !!result.proxyAuth;
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
result.debugMode = safe.JSON.parse(result.debugModeJson);
delete result.debugModeJson;
assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string');
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
delete result.servicesConfigJson;
let subdomains = JSON.parse(result.subdomains), domains = JSON.parse(result.domains), subdomainTypes = JSON.parse(result.subdomainTypes);
delete result.subdomains;
delete result.domains;
delete result.subdomainTypes;
result.alternateDomains = [];
result.aliasDomains = [];
for (let i = 0; i < subdomainTypes.length; i++) {
if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_PRIMARY) {
result.location = subdomains[i];
result.domain = domains[i];
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_REDIRECT) {
result.alternateDomains.push({ domain: domains[i], subdomain: subdomains[i] });
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_ALIAS) {
result.aliasDomains.push({ domain: domains[i], subdomain: subdomains[i] });
}
}
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
delete result.envNames;
delete result.envValues;
result.env = {};
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
if (envNames[i]) result.env[envNames[i]] = envValues[i];
}
let volumeIds = JSON.parse(result.volumeIds);
delete result.volumeIds;
let volumeReadOnlys = JSON.parse(result.volumeReadOnlys);
delete result.volumeReadOnlys;
result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty
result.error = safe.JSON.parse(result.errorJson);
delete result.errorJson;
result.taskId = result.taskId ? String(result.taskId) : null;
}
// each query simply join apps table with another table by id. we then join the full result together
const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id';
const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id';
const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(subdomains.subdomain) AS subdomains, JSON_ARRAYAGG(subdomains.domain) AS domains, JSON_ARRAYAGG(subdomains.type) AS subdomainTypes FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId GROUP BY apps.id';
const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id';
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, volumeIds, volumeReadOnlys FROM apps`
+ ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id`
+ ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id`
+ ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id`
+ ` LEFT JOIN (${MOUNTS_QUERY}) AS q4 on q4.id = apps.id`;
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} WHERE apps.id = ?`, [ id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
postProcess(result[0]);
callback(null, result[0]);
});
}
function getByIpAddress(ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} WHERE apps.containerIp = ?`, [ ip ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
postProcess(result[0]);
callback(null, result[0]);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query(`${APPS_QUERY} ORDER BY apps.id`, [ ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appStoreId, 'string');
assert(manifest && typeof manifest === 'object');
assert.strictEqual(typeof manifest.version, 'string');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof portBindings, 'object');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
portBindings = portBindings || { };
var manifestJson = JSON.stringify(manifest);
const accessRestriction = data.accessRestriction || null;
const accessRestrictionJson = JSON.stringify(accessRestriction);
const memoryLimit = data.memoryLimit || 0;
const cpuShares = data.cpuShares || 512;
const installationState = data.installationState;
const runState = data.runState;
const sso = 'sso' in data ? data.sso : null;
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
const env = data.env || {};
const label = data.label || null;
const tagsJson = data.tags ? JSON.stringify(data.tags) : null;
const mailboxName = data.mailboxName || null;
const mailboxDomain = data.mailboxDomain || null;
const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null;
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares, '
+ 'sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson) '
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit, cpuShares,
sso, debugModeJson, mailboxName, mailboxDomain, label, tagsJson, reverseProxyConfigJson ]
});
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, domain, location, exports.SUBDOMAIN_TYPE_PRIMARY ]
});
Object.keys(portBindings).forEach(function (env) {
queries.push({
query: 'INSERT INTO appPortBindings (environmentVariable, hostPort, type, appId) VALUES (?, ?, ?, ?)',
args: [ env, portBindings[env].hostPort, portBindings[env].type, id ]
});
});
Object.keys(env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, env[name] ]
});
});
if (data.alternateDomains) {
data.alternateDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]
});
});
}
if (data.aliasDomains) {
data.aliasDomains.forEach(function (d) {
queries.push({
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]
});
});
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such domain'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function exists(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT 1 FROM apps WHERE id=?', [ id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result.length !== 0);
});
}
function getPortBindings(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + PORT_BINDINGS_FIELDS + ' FROM appPortBindings WHERE appId = ?', [ id ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
var portBindings = { };
for (var i = 0; i < results.length; i++) {
portBindings[results[i].environmentVariable] = { hostPort: results[i].hostPort, type: results[i].type };
}
callback(null, portBindings);
});
}
function delPortBinding(hostPort, type, callback) {
assert.strictEqual(typeof hostPort, 'number');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
var queries = [
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] },
{ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[5].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
database.query.bind(null, 'DELETE FROM subdomains'),
database.query.bind(null, 'DELETE FROM appPortBindings'),
database.query.bind(null, 'DELETE FROM appAddonConfigs'),
database.query.bind(null, 'DELETE FROM appEnvVars'),
database.query.bind(null, 'DELETE FROM apps')
], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null);
});
}
function update(id, app, callback) {
updateWithConstraints(id, app, '', callback);
}
function updateWithConstraints(id, app, constraints, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof constraints, 'string');
assert.strictEqual(typeof callback, 'function');
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
assert(!('aliasDomains' in app) || Array.isArray(app.aliasDomains));
assert(!('tags' in app) || Array.isArray(app.tags));
assert(!('env' in app) || typeof app.env === 'object');
var queries = [ ];
if ('portBindings' in app) {
var portBindings = app.portBindings || { };
// replace entries by app id
queries.push({ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] });
Object.keys(portBindings).forEach(function (env) {
var values = [ portBindings[env].hostPort, portBindings[env].type, env, id ];
queries.push({ query: 'INSERT INTO appPortBindings (hostPort, type, environmentVariable, appId) VALUES(?, ?, ?, ?)', args: values });
});
}
if ('env' in app) {
queries.push({ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] });
Object.keys(app.env).forEach(function (name) {
queries.push({
query: 'INSERT INTO appEnvVars (appId, name, value) VALUES (?, ?, ?)',
args: [ id, name, app.env[name] ]
});
});
}
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]});
if ('alternateDomains' in app) {
app.alternateDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
});
}
if ('aliasDomains' in app) {
app.aliasDomains.forEach(function (d) {
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]});
});
}
}
if ('mounts' in app) {
queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]});
app.mounts.forEach(function (m) {
queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]});
});
}
var fields = [ ], values = [ ];
for (var 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]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') {
fields.push(p + ' = ?');
values.push(app[p]);
}
}
if (values.length !== 0) {
values.push(id);
queries.push({ query: 'UPDATE apps SET ' + fields.join(', ') + ' WHERE id = ? ' + constraints, args: values });
}
database.transaction(queries, function (error, results) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[results.length - 1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
return callback(null);
});
}
function setHealth(appId, health, healthTime, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof health, 'string');
assert(util.isDate(healthTime));
assert.strictEqual(typeof callback, 'function');
var values = { health, healthTime };
updateWithConstraints(appId, values, '', callback);
}
function setTask(appId, values, options, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof values, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
if (options.requiredState === null) {
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
} else {
updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`, callback);
}
}
function getAppStoreIds(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT id, appStoreId FROM apps', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function setAddonConfig(appId, addonId, env, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert(util.isArray(env));
assert.strictEqual(typeof callback, 'function');
unsetAddonConfig(appId, addonId, function (error) {
if (error) return callback(error);
if (env.length === 0) return callback(null);
var query = 'INSERT INTO appAddonConfigs(appId, addonId, name, value) VALUES ';
var args = [ ], queryArgs = [ ];
for (var i = 0; i < env.length; i++) {
args.push(appId, addonId, env[i].name, env[i].value);
queryArgs.push('(?, ?, ?, ?)');
}
database.query(query + queryArgs.join(','), args, function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null);
});
});
}
function unsetAddonConfig(appId, addonId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function unsetAddonConfigByAppId(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function getAddonConfig(appId, addonId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function getAddonConfigByAppId(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof namePattern, 'string');
assert.strictEqual(typeof value, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null, results[0].appId);
});
}
function getAddonConfigByName(appId, addonId, namePattern, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof addonId, 'string');
assert.strictEqual(typeof namePattern, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null, results[0].value);
});
}

View File

@@ -1,11 +1,10 @@
'use strict';
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
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'),
@@ -16,17 +15,15 @@ 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 = 20 * 60 * 1000; // 20 minutes
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // 60 minutes
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 setHealth(app, health, callback) {
async function setHealth(app, health) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof health, 'string');
assert.strictEqual(typeof callback, 'function');
// app starts out with null health
// if it became healthy, we update immediately. this is required for ui to say "running" etc
@@ -41,160 +38,144 @@ function setHealth(app, health, callback) {
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 });
if (!app.debugMode) await eventlog.add(eventlog.ACTION_APP_UP, AuditSource.HEALTH_MONITOR, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
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 });
if (!app.debugMode) await eventlog.add(eventlog.ACTION_APP_DOWN, AuditSource.HEALTH_MONITOR, { app: app });
}
} else {
debug(`setHealth: ${app.id} (${app.fqdn}) waiting for ${(UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000} to update health`);
return callback(null);
return;
}
appdb.setHealth(app.id, health, healthTime, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null); // app uninstalled?
if (error) return callback(error);
const [error] = await safe(apps.setHealth(app.id, health, healthTime));
if (error && error.reason === BoxError.NOT_FOUND) return; // app uninstalled?
if (error) throw error;
app.health = health;
app.healthTime = healthTime;
callback(null);
});
app.health = health;
app.healthTime = healthTime;
}
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
async function checkAppHealth(app, options) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options, 'object');
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
return callback(null);
}
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) return;
const manifest = app.manifest;
docker.inspect(app.containerId, function (error, data) {
if (error || !data || !data.State) return setHealth(app, apps.HEALTH_ERROR, callback);
if (data.State.Running !== true) return setHealth(app, apps.HEALTH_DEAD, callback);
const [error, data] = await safe(docker.inspect(app.containerId));
if (error || !data || !data.State) return await setHealth(app, apps.HEALTH_ERROR);
if (data.State.Running !== true) return await setHealth(app, apps.HEALTH_DEAD);
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return await setHealth(app, apps.HEALTH_HEALTHY);
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.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)
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
}
});
});
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
const [healthCheckError, response] = await safe(superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
.redirects(0)
.ok(() => true)
.timeout(options.timeout * 1000));
if (healthCheckError) {
await setHealth(app, apps.HEALTH_UNHEALTHY);
} else if (response.status > 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
await setHealth(app, apps.HEALTH_UNHEALTHY);
} else {
await setHealth(app, apps.HEALTH_HEALTHY);
}
}
function getContainerInfo(containerId, callback) {
docker.inspect(containerId, function (error, result) {
if (error) return callback(error);
async function getContainerInfo(containerId) {
const result = await docker.inspect(containerId);
const appId = safe.query(result, 'Config.Labels.appId', null);
const appId = safe.query(result, 'Config.Labels.appId', null);
if (appId) return { app: await apps.get(appId) }; // don't get by container id as this can be an exec container
if (!appId) return callback(null, null /* app */, { name: result.Name }); // addon
apps.get(appId, callback); // don't get by container id as this can be an exec container
});
if (result.Name.startsWith('/redis-')) {
return { app: await apps.get(result.Name.slice('/redis-'.length)), addonName: 'redis' };
} else {
return { addonName: result.Name.slice(1) }; // addon . Name has a '/' in the beginning for some reason
}
}
/*
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) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
async function processDockerEvents(options) {
assert.strictEqual(typeof options, 'object');
const since = ((new Date().getTime() / 1000) - intervalSecs).toFixed(0);
const since = ((new Date().getTime() / 1000) - options.intervalSecs).toFixed(0);
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return callback(error);
const stream = await docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) });
stream.setEncoding('utf8');
stream.on('data', async function (data) { // this is actually ldjson, we only process the first line for now
const event = safe.JSON.parse(data);
if (!event) return;
const containerId = String(event.id);
stream.setEncoding('utf8');
stream.on('data', function (data) {
const event = JSON.parse(data);
const containerId = String(event.id);
const [error, info] = await safe(getContainerInfo(containerId));
const program = error ? containerId : (info.addonName || info.app.fqdn);
const now = Date.now();
getContainerInfo(containerId, function (error, app, addon) {
const program = error ? containerId : (app ? app.fqdn : addon.name);
const now = Date.now();
const notifyUser = !(app && app.debugMode) && ((now - gLastOomMailTime) > OOM_EVENT_LIMIT);
// do not send mails for dev apps
const notifyUser = !(info.app && info.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 });
if (notifyUser) {
await eventlog.add(eventlog.ACTION_APP_OOM, AuditSource.HEALTH_MONITOR, { event, containerId, addonName: info?.addonName || null, app: info?.app || null });
gLastOomMailTime = now;
}
});
});
stream.on('error', function (error) {
debug('Error reading docker events', error);
callback();
});
stream.on('end', callback);
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
gLastOomMailTime = now;
}
});
stream.on('error', function (error) {
debug('Error reading docker events', error);
});
stream.on('end', function () {
// debug('Event stream ended');
});
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), options.timeout); // https://github.com/apocas/dockerode/issues/179
}
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
async function processApp(options) {
assert.strictEqual(typeof options, 'object');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
const allApps = await apps.list();
async.each(allApps, checkAppHealth, function (error) {
const alive = allApps
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
const healthChecks = allApps.map((app) => checkAppHealth(app, options)); // start healthcheck in parallel
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.` + (error ? ` ${error.reason}` : ''));
await Promise.allSettled(healthChecks); // wait for all promises to finish
callback(null);
});
});
const alive = allApps
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.`);
}
function run(intervalSecs, callback) {
async function run(intervalSecs) {
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)
], function (error) {
if (error) debug(`run: could not check app health. ${error.message}`);
callback();
});
await processApp({ timeout: (intervalSecs - 3) * 1000 });
await processDockerEvents({ intervalSecs, timeout: 3000 });
}

88
src/apppasswords.js Normal file
View File

@@ -0,0 +1,88 @@
'use strict';
exports = module.exports = {
get,
add,
list,
del,
removePrivateFields
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
crypto = require('crypto'),
database = require('./database.js'),
hat = require('./hat.js'),
safe = require('safetydance'),
uuid = require('uuid'),
_ = require('underscore');
const APP_PASSWORD_FIELDS = [ 'id', 'name', 'userId', 'identifier', 'hashedPassword', 'creationTime' ].join(',');
function validateAppPasswordName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 char');
if (name.length >= 200) return new BoxError(BoxError.BAD_FIELD, 'name too long');
return null;
}
function removePrivateFields(appPassword) {
return _.pick(appPassword, 'id', 'name', 'userId', 'identifier', 'creationTime');
}
async function get(id) {
assert.strictEqual(typeof id, 'string');
const result = await database.query('SELECT ' + APP_PASSWORD_FIELDS + ' FROM appPasswords WHERE id = ?', [ id ]);
if (result.length === 0) return null;
return result[0];
}
async function add(userId, identifier, name) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof name, 'string');
let error = validateAppPasswordName(name);
if (error) throw error;
if (identifier.length < 1) throw new BoxError(BoxError.BAD_FIELD, 'identifier must be atleast 1 char');
const password = hat(16 * 4);
const hashedPassword = crypto.createHash('sha256').update(password).digest('base64');
const appPassword = {
id: 'uid-' + uuid.v4(),
name,
userId,
identifier,
password,
hashedPassword
};
const query = 'INSERT INTO appPasswords (id, userId, identifier, name, hashedPassword) VALUES (?, ?, ?, ?, ?)';
const args = [ appPassword.id, appPassword.userId, appPassword.identifier, appPassword.name, appPassword.hashedPassword ];
[error] = await safe(database.query(query, args));
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('appPasswords_name_userId_identifier') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'name/app combination already exists');
if (error && error.code === 'ER_NO_REFERENCED_ROW_2' && error.sqlMessage.indexOf('userId')) throw new BoxError(BoxError.NOT_FOUND, 'user not found');
if (error) throw error;
return { id: appPassword.id, password: appPassword.password };
}
async function list(userId) {
assert.strictEqual(typeof userId, 'string');
return await database.query('SELECT ' + APP_PASSWORD_FIELDS + ' FROM appPasswords WHERE userId = ?', [ userId ]);
}
async function del(id) {
assert.strictEqual(typeof id, 'string');
const result = await database.query('DELETE FROM appPasswords WHERE id = ?', [ id ]);
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'password not found');
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,27 +7,27 @@ exports = module.exports = {
getApp,
getAppVersion,
trackBeginSetup,
trackFinishedSetup,
registerWithLoginCredentials,
updateCloudron,
purchaseApp,
unpurchaseApp,
getUserToken,
getWebToken,
getSubscription,
isFreePlan,
getAppUpdate,
getBoxUpdate,
createTicket
createTicket,
// exported for tests
_unregister: unregister
};
var apps = require('./apps.js'),
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:appstore'),
@@ -37,6 +37,7 @@ var apps = require('./apps.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
superagent = require('superagent'),
support = require('./support.js'),
util = require('util');
@@ -52,7 +53,7 @@ let gFeatures = {
privateDockerRegistry: false,
branding: false,
support: false,
directoryConfig: false,
profileConfig: false,
mailboxMaxCount: 5,
emailPremium: false
};
@@ -76,99 +77,66 @@ function isAppAllowed(appstoreId, listingConfig) {
return true;
}
function getCloudronToken(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCloudronToken(function (error, token) {
if (error) return callback(error);
if (!token) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Missing token'));
callback(null, token);
});
}
function login(email, password, totpToken, callback) {
async function login(email, password, totpToken) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof totpToken, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
email: email,
password: password,
totpToken: totpToken
};
const [error, response] = await safe(superagent.post(`${settings.apiServerOrigin()}/api/v1/login`)
.send({ email, password, totpToken })
.timeout(30 * 1000)
.ok(() => true));
const url = settings.apiServerOrigin() + '/api/v1/login';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Login error. status code: ${response.status}`);
if (!response.body.accessToken) throw new BoxError(BoxError.EXTERNAL_ERROR, `Login error. invalid response: ${response.text}`);
callback(null, result.body); // { userId, accessToken }
});
return response.body; // { userId, accessToken }
}
function registerUser(email, password, callback) {
async function registerUser(email, password) {
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
var data = {
email: email,
password: password,
};
const [error, response] = await safe(superagent.post(`${settings.apiServerOrigin()}/api/v1/register_user`)
.send({ email, password, utmSource: 'cloudron-dashboard' })
.timeout(30 * 1000)
.ok(() => true));
const url = settings.apiServerOrigin() + '/api/v1/register_user';
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
callback(null);
});
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 409) throw new BoxError(BoxError.ALREADY_EXISTS, 'Registration error: account already exists');
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Registration error. invalid response: ${response.status}`);
}
function getUserToken(callback) {
assert.strictEqual(typeof callback, 'function');
async function getWebToken() {
if (settings.isDemo()) throw new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode');
if (settings.isDemo()) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
const token = await settings.getAppstoreWebToken();
if (!token) throw new BoxError(BoxError.NOT_FOUND); // user will have to re-login with password somehow
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/user_token`;
superagent.post(url).send({}).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `getUserToken status code: ${result.status}`));
callback(null, result.body.accessToken);
});
});
return token;
}
function getSubscription(callback) {
assert.strictEqual(typeof callback, 'function');
async function getSubscription() {
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const [error, response] = await safe(superagent.get(`${settings.apiServerOrigin()}/api/v1/subscription`)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
const url = settings.apiServerOrigin() + '/api/v1/subscription';
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR));
if (result.statusCode === 502) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 502) throw new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`);
// update the features cache
gFeatures = result.body.features;
safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8');
// update the features cache
gFeatures = response.body.features;
safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8');
callback(null, result.body);
});
});
return response.body;
}
function isFreePlan(subscription) {
@@ -176,226 +144,209 @@ function isFreePlan(subscription) {
}
// See app.js install it will create a db record first but remove it again if appstore purchase fails
function purchaseApp(data, callback) {
async function purchaseApp(data) {
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId, appId }
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof data.appId, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
const [error, response] = await safe(superagent.post(`${settings.apiServerOrigin()}/api/v1/cloudronapps`)
.send(data)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); // appstoreId does not exist
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 402) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
// 200 if already purchased, 201 is newly purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
callback(null);
});
});
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND); // appstoreId does not exist
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 402) throw new BoxError(BoxError.LICENSE_ERROR, response.body.message);
// 200 if already purchased, 201 is newly purchased
if (response.status !== 201 && response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', response.status, response.body));
}
function unpurchaseApp(appId, data, callback) {
async function unpurchaseApp(appId, data) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId }
assert(data.appstoreId || data.manifestId);
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null); // was never purchased
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 !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
let [error, response] = await safe(superagent.get(url)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
superagent.del(url).send(data).query({ accessToken: token }).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 !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 404) return; // was never purchased
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `App unpurchase failed to get app. status:${response.status}`);
callback(null);
});
});
});
[error, response] = await safe(superagent.del(url)
.send(data)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, `App unpurchase failed. status:${response.status}`);
}
function getBoxUpdate(options, callback) {
async function getBoxUpdate(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
const query = {
accessToken: token,
boxVersion: constants.VERSION,
automatic: options.automatic
};
const query = {
accessToken: token,
boxVersion: constants.VERSION,
automatic: options.automatic
};
const [error, response] = await safe(superagent.get(`${settings.apiServerOrigin()}/api/v1/boxupdate`)
.query(query)
.timeout(30 * 1000)
.ok(() => true));
superagent.get(url).query(query).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
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 === 204) return callback(null, null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 204) return; // no update
if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
var updateInfo = result.body;
const updateInfo = response.body;
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Update version invalid or is a downgrade: %s %s', result.statusCode, result.text)));
}
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
callback(null, updateInfo);
});
});
}
function getAppUpdate(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
const query = {
accessToken: token,
boxVersion: constants.VERSION,
appId: app.appStoreId,
appVersion: app.manifest.version,
automatic: options.automatic
};
superagent.get(url).query(query).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 === 204) return callback(null); // no update
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
const updateInfo = result.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
updateInfo.unstable = !!updateInfo.unstable;
// { id, creationDate, manifest, unstable }
callback(null, updateInfo);
});
});
}
function registerCloudron(data, callback) {
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`;
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${result.statusCode} ${error.message}`));
// cloudronId, token, licenseKey
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
if (!result.body.cloudronToken) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token'));
if (!result.body.licenseKey) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no license'));
async.series([
settings.setCloudronId.bind(null, result.body.cloudronId),
settings.setCloudronToken.bind(null, result.body.cloudronToken),
settings.setLicenseKey.bind(null, result.body.licenseKey),
], function (error) {
if (error) return callback(error);
debug(`registerCloudron: Cloudron registered with id ${result.body.cloudronId}`);
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;
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_begin`;
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}`);
});
}
// This works without a Cloudron token as this Cloudron was not yet registered
function trackFinishedSetup(domain) {
assert.strictEqual(typeof domain, 'string');
const url = `${settings.apiServerOrigin()}/api/v1/helper/setup_finished`;
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}`);
});
}
function registerWithLoginCredentials(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
function maybeSignup(done) {
if (!options.signup) return done();
registerUser(options.email, options.password, done);
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Update version invalid or is a downgrade: %s %s', response.status, response.text));
}
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
if (!updateInfo.version || typeof updateInfo.version !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', response.status, response.text));
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', response.status, response.text));
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', response.status, response.text));
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', response.status, response.text));
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', response.status, response.text));
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', response.status, response.text));
maybeSignup(function (error) {
if (error) return callback(error);
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);
});
});
});
return updateInfo;
}
function createTicket(info, auditSource, callback) {
async function getAppUpdate(app, options) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
const query = {
accessToken: token,
boxVersion: constants.VERSION,
appId: app.appStoreId,
appVersion: app.manifest.version,
automatic: options.automatic
};
const [error, response] = await safe(superagent.get(`${settings.apiServerOrigin()}/api/v1/appupdate`)
.query(query)
.timeout(30 * 1000)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 204) return; // no update
if (response.status !== 200 || !response.body) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
const updateInfo = response.body;
// for the appstore, x.y.z is the same as x.y.z-0 but in semver, x.y.z > x.y.z-0
const curAppVersion = semver.prerelease(app.manifest.version) ? app.manifest.version : `${app.manifest.version}-0`;
// do some sanity checks
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', response.status, response.text));
}
updateInfo.unstable = !!updateInfo.unstable;
// { id, creationDate, manifest, unstable }
return updateInfo;
}
async function registerCloudron(data) {
assert.strictEqual(typeof data, 'object');
const { domain, accessToken, version, existingApps } = data;
const [error, response] = await safe(superagent.post(`${settings.apiServerOrigin()}/api/v1/register_cloudron`)
.send({ domain, accessToken, version, existingApps })
.timeout(30 * 1000)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${response.statusCode} ${error.message}`);
// cloudronId, token
if (!response.body.cloudronId) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id');
if (!response.body.cloudronToken) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token');
await settings.setCloudronId(response.body.cloudronId);
await settings.setAppstoreApiToken(response.body.cloudronToken);
await settings.setAppstoreWebToken(accessToken);
debug(`registerCloudron: Cloudron registered with id ${response.body.cloudronId}`);
}
async function updateCloudron(data) {
assert.strictEqual(typeof data, 'object');
const { domain } = data;
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
const query = {
accessToken: token
};
const [error, response] = await safe(superagent.post(`${settings.apiServerOrigin()}/api/v1/update_cloudron`)
.query(query)
.send({ domain })
.timeout(30 * 1000)
.ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
debug(`updateCloudron: Cloudron updated with data ${JSON.stringify(data)}`);
}
async function registerWithLoginCredentials(options) {
assert.strictEqual(typeof options, 'object');
if (options.signup) await registerUser(options.email, options.password);
const result = await login(options.email, options.password, options.totpToken || '');
await registerCloudron({ domain: settings.dashboardDomain(), accessToken: result.accessToken, version: constants.VERSION });
for (const app of await apps.list()) {
await purchaseApp({ appId: app.id, appstoreId: app.appStoreId, manifestId: app.manifest.id || 'customapp' });
}
}
async function unregister() {
await settings.setCloudronId('');
await settings.setAppstoreApiToken('');
await settings.setAppstoreWebToken('');
}
async function createTicket(info, auditSource) {
assert.strictEqual(typeof info, 'object');
assert.strictEqual(typeof info.email, 'string');
assert.strictEqual(typeof info.displayName, 'string');
@@ -403,128 +354,96 @@ function createTicket(info, auditSource, callback) {
assert.strictEqual(typeof info.subject, 'string');
assert.strictEqual(typeof info.description, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
function collectAppInfoIfNeeded(callback) {
if (!info.appId) return callback();
apps.get(info.appId, callback);
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
if (info.enableSshSupport) {
await safe(support.enableRemoteSupport(true, auditSource));
info.ipv4 = await sysinfo.getServerIPv4();
}
function enableSshIfNeeded(callback) {
if (!info.enableSshSupport) return callback();
info.app = info.appId ? await apps.get(info.appId) : null;
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
support.enableRemoteSupport(true, auditSource, function (error) {
// ensure we can at least get the ticket through
if (error) debug('Unable to enable SSH support.', error);
const request = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true);
callback();
});
// either send as JSON through body or as multipart, depending on attachments
if (info.app) {
request.field('infoJSON', JSON.stringify(info));
const logPaths = await apps.getLogPaths(info.app);
for (const logPath of logPaths) {
const logs = safe.child_process.execSync(`tail --lines=1000 ${logPath}`);
if (logs) request.attach(path.basename(logPath), logs, path.basename(logPath));
}
} else {
request.send(info);
}
getCloudronToken(function (error, token) {
if (error) return callback(error);
const [error, response] = await safe(request);
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
enableSshIfNeeded(function (error) {
if (error) return callback(error);
await eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
collectAppInfoIfNeeded(function (error, app) {
if (error) return callback(error);
if (app) info.app = app;
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
var req = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
.query({ accessToken: token })
.timeout(30 * 1000);
// either send as JSON through body or as multipart, depending on attachments
if (info.app) {
req.field('infoJSON', JSON.stringify(info));
apps.getLocalLogfilePaths(info.app).forEach(function (filePath) {
var logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`);
if (logs) req.attach(path.basename(filePath), logs, path.basename(filePath));
});
} else {
req.send(info);
}
req.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
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 !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
callback(null, { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
});
});
});
});
return { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` };
}
function getApps(callback) {
assert.strictEqual(typeof callback, 'function');
async function getApps() {
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
getCloudronToken(function (error, token) {
if (error) return callback(error);
const unstable = await settings.getUnstableAppsConfig();
settings.getUnstableAppsConfig(function (error, unstable) {
if (error) return callback(error);
const [error, response] = await safe(superagent.get(`${settings.apiServerOrigin()}/api/v1/apps`)
.query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable })
.timeout(30 * 1000)
.ok(() => true));
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || 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('App listing failed. %s %j', result.status, result.body)));
if (!result.body.apps) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 403 || response.status === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', response.status, response.body));
if (!response.body.apps) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', response.status, response.text));
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return callback(error);
const filteredApps = result.body.apps.filter(app => isAppAllowed(app.id, listingConfig));
callback(null, filteredApps);
});
});
});
});
const listingConfig = await settings.getAppstoreListingConfig();
const filteredApps = response.body.apps.filter(app => isAppAllowed(app.id, listingConfig));
return filteredApps;
}
function getAppVersion(appId, version, callback) {
async function getAppVersion(appId, version) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return callback(error);
const listingConfig = await settings.getAppstoreListingConfig();
if (!isAppAllowed(appId, listingConfig)) return callback(new BoxError(BoxError.FEATURE_DISABLED));
if (!isAppAllowed(appId, listingConfig)) throw new BoxError(BoxError.FEATURE_DISABLED);
getCloudronToken(function (error, token) {
if (error) return callback(error);
const token = await settings.getAppstoreApiToken();
if (!token) throw new BoxError(BoxError.LICENSE_ERROR, 'Missing token');
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
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('App fetch failed. %s %j', result.status, result.body)));
const [error, response] = await safe(superagent.get(url)
.query({ accessToken: token })
.timeout(30 * 1000)
.ok(() => true));
callback(null, result.body);
});
});
});
if (error) throw new BoxError(BoxError.NETWORK_ERROR, error.message);
if (response.status === 403 || response.statusCode === 401) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (response.status === 404) throw new BoxError(BoxError.NOT_FOUND);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', response.status, response.body));
return response.body;
}
function getApp(appId, callback) {
async function getApp(appId) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
getAppVersion(appId, 'latest', callback);
return await getAppVersion(appId, 'latest');
}

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ exports = module.exports = {
scheduleTask
};
let assert = require('assert'),
const assert = require('assert'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:apptaskmanager'),
fs = require('fs'),
@@ -13,7 +13,6 @@ let assert = require('assert'),
path = require('path'),
paths = require('./paths.js'),
scheduler = require('./scheduler.js'),
services = require('./services.js'),
tasks = require('./tasks.js');
let gActiveTasks = { }; // indexed by app id
@@ -21,12 +20,11 @@ let gPendingTasks = [ ];
let gInitialized = false;
const TASK_CONCURRENCY = 3;
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
function waitText(lockOperation) {
if (lockOperation === locker.OP_BOX_UPDATE) return 'Waiting for Cloudron to finish updating. See the Settings view';
if (lockOperation === locker.OP_PLATFORM_START) return 'Waiting for Cloudron to initialize';
if (lockOperation === locker.OP_FULL_BACKUP) return 'Wait for Cloudron to finish backup. See the Backups view';
if (lockOperation === locker.OP_FULL_BACKUP) return 'Waiting for Cloudron to finish backup. See the Backups view';
return ''; // cannot happen
}
@@ -37,31 +35,31 @@ function initializeSync() {
}
// callback is called when task is finished
function scheduleTask(appId, taskId, options, callback) {
function scheduleTask(appId, taskId, options, onFinished) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof onFinished, 'function');
if (!gInitialized) initializeSync();
if (appId in gActiveTasks) {
return callback(new BoxError(BoxError.CONFLICT, `Task for %s is already active: ${appId}`));
return onFinished(new BoxError(BoxError.CONFLICT, `Task for %s is already active: ${appId}`));
}
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, options, callback });
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' });
gPendingTasks.push({ appId, taskId, options, onFinished });
return;
}
var lockError = locker.recursiveLock(locker.OP_APPTASK);
const lockError = locker.recursiveLock(locker.OP_APPTASK);
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, options, callback });
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) });
gPendingTasks.push({ appId, taskId, options, onFinished });
return;
}
@@ -74,13 +72,11 @@ function scheduleTask(appId, taskId, options, callback) {
scheduler.suspendJobs(appId);
tasks.startTask(taskId, Object.assign(options, { logFile }), function (error, result) {
callback(error, result);
onFinished(error, result);
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);
});
}
@@ -91,6 +87,5 @@ function startNextTask() {
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
const t = gPendingTasks.shift();
scheduleTask(t.appId, t.taskId, t.options, t.callback);
scheduleTask(t.appId, t.taskId, t.options, t.onFinished);
}

View File

@@ -1,16 +1,24 @@
'use strict';
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' },
class AuditSource {
constructor(username, userId, ip) {
this.username = username;
this.userId = userId || null;
this.ip = ip || null;
}
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 };
static fromRequest(req) {
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
return new AuditSource(req.user?.username, req.user?.id, ip);
}
}
// these can be static variables but see https://stackoverflow.com/questions/60046847/eslint-does-not-allow-static-class-properties#comment122122927_60464446
AuditSource.CRON = new AuditSource('cron');
AuditSource.HEALTH_MONITOR = new AuditSource('healthmonitor');
AuditSource.EXTERNAL_LDAP_TASK = new AuditSource('externalldap');
AuditSource.EXTERNAL_LDAP_AUTO_CREATE = new AuditSource('externalldap');
AuditSource.APPTASK = new AuditSource('apptask');
AuditSource.PLATFORM = new AuditSource('platform');
exports = module.exports = AuditSource;

View File

@@ -19,7 +19,13 @@
<username>%EMAILADDRESS%</username>
<addThisServer>true</addThisServer>
</outgoingServer>
<incomingServer type="pop3">
<hostname><%= mailFqdn %></hostname>
<port>995</port>
<socketType>SSL</socketType>
<username>%EMAILADDRESS%</username>
<authentication>password-cleartext</authentication>
</incomingServer>
<documentation url="http://cloudron.io/email/#autodiscover">
<descr lang="en">Cloudron Email</descr>
</documentation>

303
src/backupcleaner.js Normal file
View File

@@ -0,0 +1,303 @@
'use strict';
const BoxError = require('./boxerror.js');
exports = module.exports = {
run,
_applyBackupRetentionPolicy: applyBackupRetentionPolicy
};
const apps = require('./apps.js'),
assert = require('assert'),
backupFormat = require('./backupformat.js'),
backups = require('./backups.js'),
constants = require('./constants.js'),
debug = require('debug')('box:backupcleaner'),
moment = require('moment'),
mounts = require('./mounts.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
storage = require('./storage.js'),
_ = require('underscore');
function applyBackupRetentionPolicy(allBackups, policy, referencedBackupIds) {
assert(Array.isArray(allBackups));
assert.strictEqual(typeof policy, 'object');
assert(Array.isArray(referencedBackupIds));
const now = new Date();
for (const backup of allBackups) {
if (backup.state === backups.BACKUP_STATE_ERROR) {
backup.discardReason = 'error';
} else if (backup.state === backups.BACKUP_STATE_CREATING) {
if ((now - backup.creationTime) < 48*60*60*1000) backup.keepReason = 'creating';
else backup.discardReason = 'creating-too-long';
} else if (referencedBackupIds.includes(backup.id)) {
backup.keepReason = 'reference';
} else if ((backup.preserveSecs === -1) || ((now - backup.creationTime) < (backup.preserveSecs * 1000))) {
backup.keepReason = 'preserveSecs';
} else if ((now - backup.creationTime < policy.keepWithinSecs * 1000) || policy.keepWithinSecs < 0) {
backup.keepReason = 'keepWithinSecs';
}
}
const KEEP_FORMATS = {
keepDaily: 'Y-M-D',
keepWeekly: 'Y-W',
keepMonthly: 'Y-M',
keepYearly: 'Y'
};
for (const format of [ 'keepDaily', 'keepWeekly', 'keepMonthly', 'keepYearly' ]) {
if (!(format in policy)) continue;
const n = policy[format]; // we want to keep "n" backups of format
if (!n) continue; // disabled rule
let lastPeriod = null, keptSoFar = 0;
for (const backup of allBackups) {
if (backup.discardReason) continue; // already discarded for some reason
if (backup.keepReason && backup.keepReason !== 'reference') continue; // kept for some other reason
const period = moment(backup.creationTime).format(KEEP_FORMATS[format]);
if (period === lastPeriod) continue; // already kept for this period
lastPeriod = period;
backup.keepReason = backup.keepReason ? `${backup.keepReason}+${format}` : format;
if (++keptSoFar === n) break;
}
}
if (policy.keepLatest) {
let latestNormalBackup = allBackups.find(b => b.state === backups.BACKUP_STATE_NORMAL);
if (latestNormalBackup && !latestNormalBackup.keepReason) latestNormalBackup.keepReason = 'latest';
}
for (const backup of allBackups) {
debug(`applyBackupRetentionPolicy: ${backup.id} ${backup.type} ${backup.keepReason || backup.discardReason || 'unprocessed'}`);
}
}
async function removeBackup(backupConfig, backup, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backup, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const backupFilePath = backupFormat.api(backup.format).getBackupFilePath(backupConfig, backup.remotePath);
let removeError;
if (backup.format ==='tgz') {
progressCallback({ message: `${backup.remotePath}: Removing ${backupFilePath}`});
[removeError] = await safe(storage.api(backupConfig.provider).remove(backupConfig, backupFilePath));
} else {
progressCallback({ message: `${backup.remotePath}: Removing directory ${backupFilePath}`});
[removeError] = await safe(storage.api(backupConfig.provider).removeDir(backupConfig, backupFilePath, progressCallback));
}
if (removeError) {
debug('removeBackup: error removing backup %j : %s', backup, removeError.message);
return;
}
// prune empty directory if possible
const [pruneError] = await safe(storage.api(backupConfig.provider).remove(backupConfig, path.dirname(backupFilePath)));
if (pruneError) debug('removeBackup: unable to prune backup directory %s : %s', path.dirname(backupFilePath), pruneError.message);
const [delError] = await safe(backups.del(backup.id));
if (delError) debug(`removeBackup: error removing ${backup.id} from database`, delError);
else debug(`removeBackup: removed ${backup.remotePath}`);
}
async function cleanupAppBackups(backupConfig, referencedBackupIds, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert(Array.isArray(referencedBackupIds));
assert.strictEqual(typeof progressCallback, 'function');
const removedAppBackupPaths = [];
const allApps = await apps.list();
const allAppIds = allApps.map(a => a.id);
const appBackups = await backups.getByTypePaged(backups.BACKUP_TYPE_APP, 1, 1000);
// collate the backups by app id. note that the app could already have been uninstalled
let appBackupsById = {};
for (const appBackup of appBackups) {
if (!appBackupsById[appBackup.identifier]) appBackupsById[appBackup.identifier] = [];
appBackupsById[appBackup.identifier].push(appBackup);
}
// apply backup policy per app. keep latest backup only for existing apps
let appBackupsToRemove = [];
for (const appId of Object.keys(appBackupsById)) {
applyBackupRetentionPolicy(appBackupsById[appId], _.extend({ keepLatest: allAppIds.includes(appId) }, backupConfig.retentionPolicy), referencedBackupIds);
appBackupsToRemove = appBackupsToRemove.concat(appBackupsById[appId].filter(b => !b.keepReason));
}
for (const appBackup of appBackupsToRemove) {
await progressCallback({ message: `Removing app backup (${appBackup.identifier}): ${appBackup.id}`});
removedAppBackupPaths.push(appBackup.remotePath);
await removeBackup(backupConfig, appBackup, progressCallback); // never errors
}
debug('cleanupAppBackups: done');
return removedAppBackupPaths;
}
async function cleanupMailBackups(backupConfig, referencedBackupIds, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert(Array.isArray(referencedBackupIds));
assert.strictEqual(typeof progressCallback, 'function');
const removedMailBackupPaths = [];
const mailBackups = await backups.getByTypePaged(backups.BACKUP_TYPE_MAIL, 1, 1000);
applyBackupRetentionPolicy(mailBackups, _.extend({ keepLatest: true }, backupConfig.retentionPolicy), referencedBackupIds);
for (const mailBackup of mailBackups) {
if (mailBackup.keepReason) continue;
await progressCallback({ message: `Removing mail backup ${mailBackup.remotePath}`});
removedMailBackupPaths.push(mailBackup.remotePath);
await removeBackup(backupConfig, mailBackup, progressCallback); // never errors
}
debug('cleanupMailBackups: done');
return removedMailBackupPaths;
}
async function cleanupBoxBackups(backupConfig, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
let referencedBackupIds = [], removedBoxBackupPaths = [];
const boxBackups = await backups.getByTypePaged(backups.BACKUP_TYPE_BOX, 1, 1000);
applyBackupRetentionPolicy(boxBackups, _.extend({ keepLatest: true }, backupConfig.retentionPolicy), [] /* references */);
for (const boxBackup of boxBackups) {
if (boxBackup.keepReason) {
referencedBackupIds = referencedBackupIds.concat(boxBackup.dependsOn);
continue;
}
await progressCallback({ message: `Removing box backup ${boxBackup.remotePath}`});
removedBoxBackupPaths.push(boxBackup.remotePath);
await removeBackup(backupConfig, boxBackup, progressCallback);
}
debug('cleanupBoxBackups: done');
return { removedBoxBackupPaths, referencedBackupIds };
}
// cleans up the database by checking if backup existsing in the remote
async function cleanupMissingBackups(backupConfig, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const perPage = 1000;
const missingBackupPaths = [];
if (constants.TEST) return missingBackupPaths;
let page = 1, result = [];
do {
result = await backups.list(page, perPage);
for (const backup of result) {
let backupFilePath = backupFormat.api(backup.format).getBackupFilePath(backupConfig, backup.remotePath);
if (backup.format === 'rsync') backupFilePath = backupFilePath + '/'; // add trailing slash to indicate directory
const [existsError, exists] = await safe(storage.api(backupConfig.provider).exists(backupConfig, backupFilePath));
if (existsError || exists) continue;
await progressCallback({ message: `Removing missing backup ${backup.remotePath}`});
const [delError] = await safe(backups.del(backup.id));
if (delError) debug(`cleanupMissingBackups: error removing ${backup.id} from database`, delError);
missingBackupPaths.push(backup.remotePath);
}
++ page;
} while (result.length === perPage);
debug('cleanupMissingBackups: done');
return missingBackupPaths;
}
// removes the snapshots of apps that have been uninstalled
async function cleanupSnapshots(backupConfig) {
assert.strictEqual(typeof backupConfig, 'object');
const contents = safe.fs.readFileSync(paths.SNAPSHOT_INFO_FILE, 'utf8');
const info = safe.JSON.parse(contents);
if (!info) return;
delete info.box;
const progressCallback = (progress) => { debug(`cleanupSnapshots: ${progress.message}`); };
for (const appId of Object.keys(info)) {
const app = await apps.get(appId);
if (app) continue; // app is still installed
if (info[appId].format ==='tgz') {
await safe(storage.api(backupConfig.provider).remove(backupConfig, backupFormat.api(info[appId].format).getBackupFilePath(backupConfig, `snapshot/app_${appId}`)), { debug });
} else {
await safe(storage.api(backupConfig.provider).removeDir(backupConfig, backupFormat.api(info[appId].format).getBackupFilePath(backupConfig, `snapshot/app_${appId}`), progressCallback), { debug });
}
safe.fs.unlinkSync(path.join(paths.BACKUP_INFO_DIR, `${appId}.sync.cache`));
safe.fs.unlinkSync(path.join(paths.BACKUP_INFO_DIR, `${appId}.sync.cache.new`));
await safe(backups.setSnapshotInfo(appId, null /* info */), { debug });
debug(`cleanupSnapshots: cleaned up snapshot of app ${appId}`);
}
debug('cleanupSnapshots: done');
}
async function run(progressCallback) {
assert.strictEqual(typeof progressCallback, 'function');
const backupConfig = await settings.getBackupConfig();
if (mounts.isManagedProvider(backupConfig.provider) || backupConfig.provider === 'mountpoint') {
const hostPath = mounts.isManagedProvider(backupConfig.provider) ? paths.MANAGED_BACKUP_MOUNT_DIR : backupConfig.mountPoint;
const status = await mounts.getStatus(backupConfig.provider, hostPath); // { state, message }
debug(`clean: mount point status is ${JSON.stringify(status)}`);
if (status.state !== 'active') throw new BoxError(BoxError.MOUNT_ERROR, `Backup endpoint is not mounted: ${status.message}`);
}
if (backupConfig.retentionPolicy.keepWithinSecs < 0) {
debug('cleanup: keeping all backups');
return {};
}
await progressCallback({ percent: 10, message: 'Cleaning box backups' });
const { removedBoxBackupPaths, referencedBackupIds } = await cleanupBoxBackups(backupConfig, progressCallback); // references is app or mail backup ids
await progressCallback({ percent: 20, message: 'Cleaning mail backups' });
const removedMailBackupPaths = await cleanupMailBackups(backupConfig, referencedBackupIds, progressCallback);
await progressCallback({ percent: 40, message: 'Cleaning app backups' });
const removedAppBackupPaths = await cleanupAppBackups(backupConfig, referencedBackupIds, progressCallback);
await progressCallback({ percent: 70, message: 'Cleaning missing backups' });
const missingBackupPaths = await cleanupMissingBackups(backupConfig, progressCallback);
await progressCallback({ percent: 90, message: 'Cleaning snapshots' });
await cleanupSnapshots(backupConfig);
return { removedBoxBackupPaths, removedMailBackupPaths, removedAppBackupPaths, missingBackupPaths };
}

View File

@@ -1,177 +0,0 @@
'use strict';
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
safe = require('safetydance'),
util = require('util');
var BACKUPS_FIELDS = [ 'id', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
exports = module.exports = {
add,
getByTypePaged,
getByIdentifierPaged,
getByIdentifierAndStatePaged,
get,
del,
update,
list,
_clear: clear
};
function postProcess(result) {
assert.strictEqual(typeof result, 'object');
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
result.manifest = result.manifestJson ? safe.JSON.parse(result.manifestJson) : null;
delete result.manifestJson;
}
function getByIdentifierAndStatePaged(identifier, state, page, perPage, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof state, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ identifier, state, (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 getByTypePaged(type, page, perPage, callback) {
assert.strictEqual(typeof type, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, (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 getByIdentifierPaged(identifier, page, perPage, callback) {
assert.strictEqual(typeof identifier, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE identifier = ? ORDER BY creationTime DESC LIMIT ?,?',
[ identifier, (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 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');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE id = ? ORDER BY creationTime DESC',
[ id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Backup not found'));
postProcess(result[0]);
callback(null, result[0]);
});
}
function add(id, data, callback) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof id, 'string');
assert(data.encryptionVersion === null || typeof data.encryptionVersion === 'number');
assert.strictEqual(typeof data.packageVersion, 'string');
assert.strictEqual(typeof data.type, 'string');
assert.strictEqual(typeof data.identifier, 'string');
assert.strictEqual(typeof data.state, 'string');
assert(util.isArray(data.dependsOn));
assert.strictEqual(typeof data.manifest, 'object');
assert.strictEqual(typeof data.format, 'string');
assert.strictEqual(typeof callback, 'function');
var creationTime = data.creationTime || new Date(); // allow tests to set the time
var manifestJson = JSON.stringify(data.manifest);
database.query('INSERT INTO backups (id, identifier, encryptionVersion, packageVersion, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ id, data.identifier, data.encryptionVersion, data.packageVersion, data.type, creationTime, data.state, data.dependsOn.join(','), manifestJson, data.format ],
function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function update(id, backup, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof backup, 'object');
assert.strictEqual(typeof callback, 'function');
var fields = [ ], values = [ ];
for (var p in backup) {
fields.push(p + ' = ?');
values.push(backup[p]);
}
values.push(id);
database.query('UPDATE backups SET ' + fields.join(', ') + ' WHERE id = ?', values, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'Backup not found'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE backups', [], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}

12
src/backupformat.js Normal file
View File

@@ -0,0 +1,12 @@
'use strict';
exports = module.exports = {
api
};
function api(format) {
switch (format) {
case 'tgz': return require('./backupformat/tgz.js');
case 'rsync': return require('./backupformat/rsync.js');
}
}

245
src/backupformat/rsync.js Normal file
View File

@@ -0,0 +1,245 @@
'use strict';
exports = module.exports = {
getBackupFilePath,
download,
upload,
_saveFsMetadata: saveFsMetadata,
_restoreFsMetadata: restoreFsMetadata
};
const assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
DataLayout = require('../datalayout.js'),
debug = require('debug')('box:backupformat/rsync'),
fs = require('fs'),
hush = require('../hush.js'),
once = require('../once.js'),
path = require('path'),
safe = require('safetydance'),
storage = require('../storage.js'),
syncer = require('../syncer.js'),
util = require('util');
function getBackupFilePath(backupConfig, remotePath) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
const rootPath = storage.api(backupConfig.provider).getRootPath(backupConfig);
return path.join(rootPath, remotePath);
}
function sync(backupConfig, remotePath, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
// the number here has to take into account the s3.upload partSize (which is 10MB). So 20=200MB
const concurrency = backupConfig.syncConcurrency || (backupConfig.provider === 's3' ? 20 : 10);
const removeDir = util.callbackify(storage.api(backupConfig.provider).removeDir);
const remove = util.callbackify(storage.api(backupConfig.provider).remove);
syncer.sync(dataLayout, function processTask(task, iteratorCallback) {
debug('sync: processing task: %j', task);
// the empty task.path is special to signify the directory
const destPath = task.path && backupConfig.encryption ? hush.encryptFilePath(task.path, backupConfig.encryption) : task.path;
const backupFilePath = path.join(getBackupFilePath(backupConfig, remotePath), destPath);
if (task.operation === 'removedir') {
debug(`Removing directory ${backupFilePath}`);
return removeDir(backupConfig, backupFilePath, progressCallback, iteratorCallback);
} else if (task.operation === 'remove') {
debug(`Removing ${backupFilePath}`);
return remove(backupConfig, backupFilePath, iteratorCallback);
}
let retryCount = 0;
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after read stream error
++retryCount;
if (task.operation === 'add') {
progressCallback({ message: `Adding ${task.path}` + (retryCount > 1 ? ` (Try ${retryCount})` : '') });
debug(`Adding ${task.path} position ${task.position} try ${retryCount}`);
const stream = hush.createReadStream(dataLayout.toLocalPath('./' + task.path), backupConfig.encryption);
stream.on('error', (error) => retryCallback(error.message.includes('ENOENT') ? null : error)); // ignore error if file disappears
stream.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: `Uploading ${task.path}` }); // 0M@0MBps looks wrong
progressCallback({ message: `Uploading ${task.path}: ${transferred}M@${speed}MBps` }); // 0M@0MBps looks wrong
});
// only create the destination path when we have confirmation that the source is available. otherwise, we end up with
// files owned as 'root' and the cp later will fail
stream.on('open', function () {
storage.api(backupConfig.provider).upload(backupConfig, backupFilePath, stream, function (error) {
debug(error ? `Error uploading ${task.path} try ${retryCount}: ${error.message}` : `Uploaded ${task.path}`);
retryCallback(error);
});
});
}
}, iteratorCallback);
}, concurrency, function (error) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
callback();
});
}
// this is not part of 'snapshotting' because we need root access to traverse
async function saveFsMetadata(dataLayout, metadataFile) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof metadataFile, 'string');
// contains paths prefixed with './'
const metadata = {
emptyDirs: [],
execFiles: [],
symlinks: []
};
// we assume small number of files. spawnSync will raise a ENOBUFS error after maxBuffer
for (let lp of dataLayout.localPaths()) {
const emptyDirs = safe.child_process.execSync(`find ${lp} -type d -empty`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 30 });
if (emptyDirs === null) throw 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)));
const execFiles = safe.child_process.execSync(`find ${lp} -type f -executable`, { encoding: 'utf8', maxBuffer: 1024 * 1024 * 30 });
if (execFiles === null) throw 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) throw 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))) throw new BoxError(BoxError.FS_ERROR, `Error writing fs metadata: ${safe.error.message}`);
}
async function restoreFsMetadata(dataLayout, metadataFile) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof metadataFile, 'string');
debug(`Recreating empty directories in ${dataLayout.toString()}`);
const metadataJson = safe.fs.readFileSync(metadataFile, 'utf8');
if (metadataJson === null) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Error loading fsmetadata.json:' + safe.error.message);
const metadata = safe.JSON.parse(metadataJson);
if (metadata === null) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Error parsing fsmetadata.json:' + safe.error.message);
for (const emptyDir of metadata.emptyDirs) {
const [mkdirError] = await safe(fs.promises.mkdir(dataLayout.toLocalPath(emptyDir), { recursive: true }));
if (mkdirError) throw new BoxError(BoxError.FS_ERROR, `unable to create path: ${mkdirError.message}`);
}
for (const execFile of metadata.execFiles) {
const [chmodError] = await safe(fs.promises.chmod(dataLayout.toLocalPath(execFile), parseInt('0755', 8)));
if (chmodError) throw new BoxError(BoxError.FS_ERROR, `unable to chmod: ${chmodError.message}`);
}
for (const symlink of (metadata.symlinks || [])) {
if (!symlink.target) continue;
// the path may not exist if we had a directory full of symlinks
const [mkdirError] = await safe(fs.promises.mkdir(path.dirname(dataLayout.toLocalPath(symlink.path)), { recursive: true }));
if (mkdirError) throw new BoxError(BoxError.FS_ERROR, `unable to symlink (mkdir): ${mkdirError.message}`);
const [symlinkError] = await safe(fs.promises.symlink(symlink.target, dataLayout.toLocalPath(symlink.path), 'file'));
if (symlinkError) throw new BoxError(BoxError.FS_ERROR, `unable to symlink: ${symlinkError.message}`);
}
}
function downloadDir(backupConfig, backupFilePath, dataLayout, progressCallback, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupFilePath, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`downloadDir: ${backupFilePath} to ${dataLayout.toString()}`);
function downloadFile(entry, done) {
let relativePath = path.relative(backupFilePath, entry.fullPath);
if (backupConfig.encryption) {
const { error, result } = hush.decryptFilePath(relativePath, backupConfig.encryption);
if (error) return done(new BoxError(BoxError.CRYPTO_ERROR, 'Unable to decrypt file'));
relativePath = result;
}
const destFilePath = dataLayout.toLocalPath('./' + relativePath);
fs.mkdir(path.dirname(destFilePath), { recursive: true }, function (error) {
if (error) return done(new BoxError(BoxError.FS_ERROR, error.message));
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
storage.api(backupConfig.provider).download(backupConfig, entry.fullPath, function (error, sourceStream) {
if (error) {
progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} errored: ${error.message}` });
return retryCallback(error);
}
let destStream = hush.createWriteStream(destFilePath, backupConfig.encryption);
// protect against multiple errors. must destroy the write stream so that a previous retry does not write
let closeAndRetry = once((error) => {
if (error) progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} errored: ${error.message}` });
else progressCallback({ message: `Download ${entry.fullPath} to ${destFilePath} finished` });
sourceStream.destroy();
destStream.destroy();
retryCallback(error);
});
destStream.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: `Downloading ${entry.fullPath}` }); // 0M@0MBps looks wrong
progressCallback({ message: `Downloading ${entry.fullPath}: ${transferred}M@${speed}MBps` });
});
destStream.on('error', closeAndRetry);
sourceStream.on('error', closeAndRetry);
progressCallback({ message: `Downloading ${entry.fullPath} to ${destFilePath}` });
sourceStream.pipe(destStream, { end: true }).on('done', closeAndRetry);
});
}, done);
});
}
storage.api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, iteratorDone) {
// https://www.digitalocean.com/community/questions/rate-limiting-on-spaces?answer=40441
const concurrency = backupConfig.downloadConcurrency || (backupConfig.provider === 's3' ? 30 : 10);
async.eachLimit(entries, concurrency, downloadFile, iteratorDone);
}, callback);
}
async function download(backupConfig, remotePath, dataLayout, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
debug(`download: Downloading ${remotePath} to ${dataLayout.toString()}`);
const backupFilePath = getBackupFilePath(backupConfig, remotePath);
const downloadDirAsync = util.promisify(downloadDir);
await downloadDirAsync(backupConfig, backupFilePath, dataLayout, progressCallback);
await restoreFsMetadata(dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`);
}
async function upload(backupConfig, remotePath, dataLayout, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof dataLayout, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const syncAsync = util.promisify(sync);
await saveFsMetadata(dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`);
await syncAsync(backupConfig, remotePath, dataLayout, progressCallback);
}

195
src/backupformat/tgz.js Normal file
View File

@@ -0,0 +1,195 @@
'use strict';
exports = module.exports = {
getBackupFilePath,
download,
upload
};
const assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
DataLayout = require('../datalayout.js'),
debug = require('debug')('box:backupformat/tgz'),
{ DecryptStream, EncryptStream } = require('../hush.js'),
once = require('../once.js'),
path = require('path'),
progressStream = require('progress-stream'),
storage = require('../storage.js'),
tar = require('tar-fs'),
zlib = require('zlib');
function getBackupFilePath(backupConfig, remotePath) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
const rootPath = storage.api(backupConfig.provider).getRootPath(backupConfig);
const fileType = backupConfig.encryption ? '.tar.gz.enc' : '.tar.gz';
return path.join(rootPath, remotePath + fileType);
}
function tarPack(dataLayout, encryption) {
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof encryption, 'object');
const pack = tar.pack('/', {
dereference: false, // pack the symlink and not what it points to
entries: dataLayout.localPaths(),
ignoreStatError: (path, err) => {
debug(`tarPack: error stat'ing ${path} - ${err.code}`);
return err.code === 'ENOENT'; // ignore if file or dir got removed (probably some temporary file)
},
map: function(header) {
header.name = dataLayout.toRemotePath(header.name);
// the tar pax format allows us to encode filenames > 100 and size > 8GB (see #640)
// https://www.systutorials.com/docs/linux/man/5-star/
if (header.size > 8589934590 || header.name > 99) header.pax = { size: header.size };
return header;
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
});
const gzip = zlib.createGzip({});
const ps = progressStream({ time: 10000 }); // emit 'progress' every 10 seconds
pack.on('error', function (error) {
debug('tarPack: tar stream error.', error);
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
gzip.on('error', function (error) {
debug('tarPack: gzip stream error.', error);
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
if (encryption) {
const encryptStream = new EncryptStream(encryption);
encryptStream.on('error', function (error) {
debug('tarPack: encrypt stream error.', error);
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
pack.pipe(gzip).pipe(encryptStream).pipe(ps);
} else {
pack.pipe(gzip).pipe(ps);
}
return ps;
}
function tarExtract(inStream, dataLayout, encryption) {
assert.strictEqual(typeof inStream, 'object');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof encryption, 'object');
const gunzip = zlib.createGunzip({});
const ps = progressStream({ time: 10000 }); // display a progress every 10 seconds
const extract = tar.extract('/', {
map: function (header) {
header.name = dataLayout.toLocalPath(header.name);
return header;
},
dmode: 500 // ensure directory is writable
});
const emitError = once((error) => {
inStream.destroy();
ps.emit('error', error);
});
inStream.on('error', function (error) {
debug('tarExtract: input stream error.', error);
emitError(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
gunzip.on('error', function (error) {
debug('tarExtract: gunzip stream error.', error);
emitError(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
extract.on('error', function (error) {
debug('tarExtract: extract stream error.', error);
emitError(new BoxError(BoxError.EXTERNAL_ERROR, error.message));
});
extract.on('finish', function () {
debug('tarExtract: done.');
// we use a separate event because ps is a through2 stream which emits 'finish' event indicating end of inStream and not extract
ps.emit('done');
});
if (encryption) {
const decrypt = new DecryptStream(encryption);
decrypt.on('error', function (error) {
debug('tarExtract: decrypt stream error.', error);
emitError(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to decrypt: ${error.message}`));
});
inStream.pipe(ps).pipe(decrypt).pipe(gunzip).pipe(extract);
} else {
inStream.pipe(ps).pipe(gunzip).pipe(extract);
}
return ps;
}
async function download(backupConfig, remotePath, dataLayout, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
debug(`download: Downloading ${remotePath} to ${dataLayout.toString()}`);
const backupFilePath = getBackupFilePath(backupConfig, remotePath);
return new Promise((resolve, reject) => {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
progressCallback({ message: `Downloading backup ${remotePath}` });
storage.api(backupConfig.provider).download(backupConfig, backupFilePath, function (error, sourceStream) {
if (error) return retryCallback(error);
const ps = tarExtract(sourceStream, dataLayout, backupConfig.encryption);
ps.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Downloading backup' }); // 0M@0MBps looks wrong
progressCallback({ message: `Downloading ${transferred}M@${speed}MBps` });
});
ps.on('error', retryCallback);
ps.on('done', retryCallback);
});
}, (error) => {
if (error) return reject(error);
resolve();
});
});
}
async function upload(backupConfig, remotePath, dataLayout, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof dataLayout, 'object');
assert.strictEqual(typeof progressCallback, 'function');
return new Promise((resolve, reject) => {
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error
const tarStream = tarPack(dataLayout, backupConfig.encryption);
tarStream.on('progress', function (progress) {
const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024);
if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0MBps looks wrong
progressCallback({ message: `Uploading backup ${transferred}M@${speed}MBps` });
});
tarStream.on('error', retryCallback); // already returns BoxError
storage.api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, remotePath), tarStream, retryCallback);
}, (error) => {
if (error) return reject(error);
resolve();
});
});
}

File diff suppressed because it is too large Load Diff

492
src/backuptask.js Normal file
View File

@@ -0,0 +1,492 @@
'use strict';
exports = module.exports = {
fullBackup,
restore,
backupApp,
downloadApp,
backupMail,
downloadMail,
upload,
};
const apps = require('./apps.js'),
assert = require('assert'),
backupFormat = require('./backupformat.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
DataLayout = require('./datalayout.js'),
database = require('./database.js'),
debug = require('debug')('box:backuptask'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
services = require('./services.js'),
settings = require('./settings.js'),
shell = require('./shell.js'),
storage = require('./storage.js');
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
function canBackupApp(app) {
// only backup apps that are installed or specific pending states
// stopped apps cannot be backed up because addons might be down (redis)
if (app.runState === apps.RSTATE_STOPPED) return false;
// we used to check the health here but that doesn't work for stopped apps. it's better to just fail
// and inform the user if the backup fails and the app addons have not been setup yet.
return app.installationState === apps.ISTATE_INSTALLED ||
app.installationState === apps.ISTATE_PENDING_CONFIGURE ||
app.installationState === apps.ISTATE_PENDING_BACKUP || // called from apptask
app.installationState === apps.ISTATE_PENDING_UPDATE; // called from apptask
}
// this function is called via backupupload (since it needs root to traverse app's directory)
async function upload(remotePath, format, dataLayoutString, progressCallback) {
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataLayoutString, 'string');
assert.strictEqual(typeof progressCallback, 'function');
debug(`upload: path ${remotePath} format ${format} dataLayout ${dataLayoutString}`);
const dataLayout = DataLayout.fromString(dataLayoutString);
const backupConfig = await settings.getBackupConfig();
await safe(storage.api(backupConfig.provider).checkPreconditions(backupConfig, dataLayout));
await backupFormat.api(format).upload(backupConfig, remotePath, dataLayout, progressCallback);
}
async function download(backupConfig, remotePath, format, dataLayout, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof format, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
debug(`download: Downloading ${remotePath} of format ${format} to ${dataLayout.toString()}`);
await backupFormat.api(format).download(backupConfig, remotePath, dataLayout, progressCallback);
}
async function restore(backupConfig, remotePath, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof progressCallback, 'function');
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
if (!boxDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving boxdata: ${safe.error.message}`);
const dataLayout = new DataLayout(boxDataDir, []);
await download(backupConfig, remotePath, backupConfig.format, dataLayout, progressCallback);
debug('restore: download completed, importing database');
await database.importFromFile(`${dataLayout.localRoot()}/box.mysqldump`);
debug('restore: database imported');
await settings.initCache();
}
async function downloadApp(app, restoreConfig, progressCallback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof restoreConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
if (!appDataDir) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
const dataLayout = new DataLayout(appDataDir, app.storageVolumeId ? [{ localDir: await apps.getStorageDir(app), remoteDir: 'data' }] : []);
const startTime = new Date();
const backupConfig = restoreConfig.backupConfig || await settings.getBackupConfig();
await download(backupConfig, restoreConfig.remotePath, restoreConfig.backupFormat, dataLayout, progressCallback);
debug('downloadApp: time: %s', (new Date() - startTime)/1000);
}
async function runBackupUpload(uploadConfig, progressCallback) {
assert.strictEqual(typeof uploadConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const { remotePath, backupConfig, dataLayout, progressTag } = uploadConfig;
assert.strictEqual(typeof remotePath, 'string');
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressTag, 'string');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
// https://stackoverflow.com/questions/48387040/node-js-recommended-max-old-space-size
const envCopy = Object.assign({}, process.env);
if (backupConfig.memoryLimit && backupConfig.memoryLimit >= 2*1024*1024*1024) {
const heapSize = Math.min((backupConfig.memoryLimit/1024/1024) - 256, 8192);
debug(`runBackupUpload: adjusting heap size to ${heapSize}M`);
envCopy.NODE_OPTIONS = `--max-old-space-size=${heapSize}`;
}
let result = ''; // the script communicates error result as a string
function onMessage(progress) { // this is { message } or { result }
if ('message' in progress) return progressCallback({ message: `${progress.message} (${progressTag})` });
debug(`runBackupUpload: result - ${JSON.stringify(progress)}`);
result = progress.result;
}
const [error] = await safe(shell.promises.sudo(`backup-${remotePath}`, [ BACKUP_UPLOAD_CMD, remotePath, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, ipc: true, onMessage }));
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
throw new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed');
} else if (error && error.code === 50) { // exited with error
throw new BoxError(BoxError.EXTERNAL_ERROR, result);
}
}
async function snapshotBox(progressCallback) {
assert.strictEqual(typeof progressCallback, 'function');
progressCallback({ message: 'Snapshotting box' });
const startTime = new Date();
await database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`);
debug(`snapshotBox: took ${(new Date() - startTime)/1000} seconds`);
}
async function uploadBoxSnapshot(backupConfig, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
await snapshotBox(progressCallback);
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
if (!boxDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving boxdata: ${safe.error.message}`);
const uploadConfig = {
remotePath: 'snapshot/box',
backupConfig,
dataLayout: new DataLayout(boxDataDir, []),
progressTag: 'box'
};
progressCallback({ message: 'Uploading box snapshot' });
const startTime = new Date();
await runBackupUpload(uploadConfig, progressCallback);
debug(`uploadBoxSnapshot: took ${(new Date() - startTime)/1000} seconds`);
await backups.setSnapshotInfo('box', { timestamp: new Date().toISOString(), format: backupConfig.format });
}
async function copy(backupConfig, srcRemotePath, destRemotePath, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof srcRemotePath, 'string');
assert.strictEqual(typeof destRemotePath, 'string');
assert.strictEqual(typeof progressCallback, 'function');
const { provider, format } = backupConfig;
const oldFilePath = backupFormat.api(format).getBackupFilePath(backupConfig, srcRemotePath);
const newFilePath = backupFormat.api(format).getBackupFilePath(backupConfig, destRemotePath);
const startTime = new Date();
await safe(storage.api(provider).copy(backupConfig, oldFilePath, newFilePath, progressCallback));
debug(`copy: copied successfully to ${destRemotePath}. Took ${(new Date() - startTime)/1000} seconds`);
}
async function rotateBoxBackup(backupConfig, tag, options, dependsOn, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert(Array.isArray(dependsOn));
assert.strictEqual(typeof progressCallback, 'function');
const remotePath = `${tag}/box_v${constants.VERSION}`;
const format = backupConfig.format;
debug(`rotateBoxBackup: rotating to id ${remotePath}`);
const data = {
remotePath,
encryptionVersion: backupConfig.encryption ? 2 : null,
packageVersion: constants.VERSION,
type: backups.BACKUP_TYPE_BOX,
state: backups.BACKUP_STATE_CREATING,
identifier: backups.BACKUP_IDENTIFIER_BOX,
dependsOn,
manifest: null,
format,
preserveSecs: options.preserveSecs || 0
};
const id = await backups.add(data);
const [error] = await safe(copy(backupConfig, 'snapshot/box', remotePath, progressCallback));
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
await backups.setState(id, state);
if (error) throw error;
return id;
}
async function backupBox(dependsOn, tag, options, progressCallback) {
assert(Array.isArray(dependsOn));
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const backupConfig = await settings.getBackupConfig();
await uploadBoxSnapshot(backupConfig, progressCallback);
return await rotateBoxBackup(backupConfig, tag, options, dependsOn, progressCallback);
}
async function rotateAppBackup(backupConfig, app, tag, options, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const snapshotInfo = backups.getSnapshotInfo(app.id);
const manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
const remotePath = `${tag}/app_${app.fqdn}_v${manifest.version}`;
const format = backupConfig.format;
debug(`rotateAppBackup: rotating ${app.fqdn} to path ${remotePath}`);
const data = {
remotePath,
encryptionVersion: backupConfig.encryption ? 2 : null,
packageVersion: manifest.version,
type: backups.BACKUP_TYPE_APP,
state: backups.BACKUP_STATE_CREATING,
identifier: app.id,
dependsOn: [],
manifest,
format,
preserveSecs: options.preserveSecs || 0
};
const id = await backups.add(data);
const [error] = await safe(copy(backupConfig, `snapshot/app_${app.id}`, remotePath, progressCallback));
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
await backups.setState(id, state);
if (error) throw error;
return id;
}
async function backupApp(app, options, progressCallback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
if (options.snapshotOnly) return await snapshotApp(app, progressCallback);
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
debug(`backupApp: backing up ${app.fqdn} with tag ${tag}`);
return await backupAppWithTag(app, tag, options, progressCallback);
}
async function snapshotApp(app, progressCallback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const startTime = new Date();
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
await apps.backupConfig(app);
await services.backupAddons(app, app.manifest.addons);
debug(`snapshotApp: ${app.fqdn} took ${(new Date() - startTime)/1000} seconds`);
}
async function uploadAppSnapshot(backupConfig, app, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
await snapshotApp(app, progressCallback);
const remotePath = `snapshot/app_${app.id}`;
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
if (!appDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving appsdata: ${safe.error.message}`);
const dataLayout = new DataLayout(appDataDir, app.storageVolumeId ? [{ localDir: await apps.getStorageDir(app), remoteDir: 'data' }] : []);
progressCallback({ message: `Uploading app snapshot ${app.fqdn}`});
const uploadConfig = {
remotePath,
backupConfig,
dataLayout,
progressTag: app.fqdn
};
const startTime = new Date();
await runBackupUpload(uploadConfig, progressCallback);
debug(`uploadAppSnapshot: ${app.fqdn} uploaded to ${remotePath}. ${(new Date() - startTime)/1000} seconds`);
await backups.setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: app.manifest, format: backupConfig.format });
}
async function backupAppWithTag(app, tag, options, progressCallback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
if (!canBackupApp(app)) { // if we cannot backup, reuse it's most recent backup
const results = await backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1);
if (results.length === 0) return null; // no backup to re-use
return results[0].id;
}
const backupConfig = await settings.getBackupConfig();
await uploadAppSnapshot(backupConfig, app, progressCallback);
return await rotateAppBackup(backupConfig, app, tag, options, progressCallback);
}
async function uploadMailSnapshot(backupConfig, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const mailDataDir = safe.fs.realpathSync(paths.MAIL_DATA_DIR);
if (!mailDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving maildata: ${safe.error.message}`);
const uploadConfig = {
remotePath: 'snapshot/mail',
backupConfig,
dataLayout: new DataLayout(mailDataDir, []),
progressTag: 'mail'
};
progressCallback({ message: 'Uploading mail snapshot' });
const startTime = new Date();
await runBackupUpload(uploadConfig, progressCallback);
debug(`uploadMailSnapshot: took ${(new Date() - startTime)/1000} seconds`);
await backups.setSnapshotInfo('mail', { timestamp: new Date().toISOString(), format: backupConfig.format });
}
async function rotateMailBackup(backupConfig, tag, options, progressCallback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const remotePath = `${tag}/mail_v${constants.VERSION}`;
const format = backupConfig.format;
debug(`rotateMailBackup: rotating to ${remotePath}`);
const data = {
remotePath,
encryptionVersion: backupConfig.encryption ? 2 : null,
packageVersion: constants.VERSION,
type: backups.BACKUP_TYPE_MAIL,
state: backups.BACKUP_STATE_CREATING,
identifier: backups.BACKUP_IDENTIFIER_MAIL,
dependsOn: [],
manifest: null,
format,
preserveSecs: options.preserveSecs || 0
};
const id = await backups.add(data);
const [error] = await safe(copy(backupConfig, 'snapshot/mail', remotePath, progressCallback));
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
await backups.setState(id, state);
if (error) throw error;
return id;
}
async function backupMailWithTag(tag, options, progressCallback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
debug(`backupMailWithTag: backing up mail with tag ${tag}`);
const backupConfig = await settings.getBackupConfig();
await uploadMailSnapshot(backupConfig, progressCallback);
return await rotateMailBackup(backupConfig, tag, options, progressCallback);
}
async function backupMail(options, progressCallback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
debug(`backupMail: backing up mail with tag ${tag}`);
return await backupMailWithTag(tag, options, progressCallback);
}
async function downloadMail(restoreConfig, progressCallback) {
assert.strictEqual(typeof restoreConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const mailDataDir = safe.fs.realpathSync(paths.MAIL_DATA_DIR);
if (!mailDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving maildata: ${safe.error.message}`);
const dataLayout = new DataLayout(mailDataDir, []);
const startTime = new Date();
await download(restoreConfig.backupConfig, restoreConfig.remotePath, restoreConfig.backupFormat, dataLayout, progressCallback);
debug('downloadMail: time: %s', (new Date() - startTime)/1000);
}
// this function is called from external process. calling process is expected to have a lock
async function fullBackup(options, progressCallback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,''); // unique tag under which all apps/mail/box backs up
const allApps = await apps.list();
let percent = 1;
let step = 100/(allApps.length+3);
const appBackupIds = [];
for (let i = 0; i < allApps.length; i++) {
const app = allApps[i];
progressCallback({ percent: percent, message: `Backing up ${app.fqdn} (${i+1}/${allApps.length})` });
percent += step;
if (!app.enableBackup) {
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length}) since automatic backup disabled`);
continue; // nothing to backup
}
const startTime = new Date();
const appBackupId = await backupAppWithTag(app, tag, options, (progress) => progressCallback({ percent, message: progress.message }));
debug(`fullBackup: app ${app.fqdn} backup finished. Took ${(new Date() - startTime)/1000} seconds`);
if (appBackupId) appBackupIds.push(appBackupId); // backupId can be null if in BAD_STATE and never backed up
}
progressCallback({ percent, message: 'Backing up mail' });
percent += step;
const mailBackupId = await backupMailWithTag(tag, options, (progress) => progressCallback({ percent, message: progress.message }));
progressCallback({ percent, message: 'Backing up system data' });
percent += step;
const dependsOn = appBackupIds.concat(mailBackupId);
const backupId = await backupBox(dependsOn, tag, options, (progress) => progressCallback({ percent, message: progress.message }));
return backupId;
}

64
src/blobs.js Normal file
View File

@@ -0,0 +1,64 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
get,
getString,
set,
setString,
del,
ACME_ACCOUNT_KEY: 'acme_account_key',
ADDON_TURN_SECRET: 'addon_turn_secret',
SFTP_PUBLIC_KEY: 'sftp_public_key',
SFTP_PRIVATE_KEY: 'sftp_private_key',
PROXY_AUTH_TOKEN_SECRET: 'proxy_auth_token_secret',
CERT_PREFIX: 'cert',
_clear: clear
};
const assert = require('assert'),
database = require('./database.js');
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 getString(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.toString('utf8');
}
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 setString(id, value) {
assert.strictEqual(typeof id, 'string');
assert(value === null || typeof value === 'string');
await database.query('INSERT INTO blobs (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value=VALUES(value)', [ id, Buffer.from(value) ]);
}
async function del(id) {
await database.query('DELETE FROM blobs WHERE id=?', [ id ]);
}
async function clear() {
await database.query('DELETE FROM blobs');
}

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