Compare commits

..

573 Commits

Author SHA1 Message Date
Girish Ramakrishnan 5447aa7c80 missed this one 2020-09-15 14:46:47 -07:00
Girish Ramakrishnan 933918ea27 Fix docs url 2020-09-15 14:46:22 -07:00
Girish Ramakrishnan cbbcdc5df1 regenerate nginx configs
Users are seeing SSL_ERROR_RX_UNEXPECTED_NEW_SESSION_TICKET. Probably related
some of the app configs had ssl_session_tickets off and some didn't . It seems nginx
has some issue if they are inconsistent (see also https://github.com/nginx-proxy/nginx-proxy/issues/580#issuecomment-249587149).

https://forum.cloudron.io/topic/3157/ssl-error-after-upgrading-to-5-6-0-on-ubuntu-16
2020-09-15 08:26:41 -07:00
Girish Ramakrishnan 4dfa7b132d ignore duplicates 2020-09-14 13:53:58 -07:00
Girish Ramakrishnan fb5bfaa2bd read does not read last line without a newline 2020-09-14 13:52:10 -07:00
Girish Ramakrishnan 20e206fa43 migrate blocklist to a txt file
this allows easy copy/pasting of existing deny lists which contain
comments and blank lines
2020-09-14 12:10:29 -07:00
Girish Ramakrishnan 467fa59023 Fix timeout issue when adding lots of network ranges 2020-09-14 09:56:35 -07:00
Girish Ramakrishnan 166c06c628 log the partSize 2020-09-10 00:09:54 -07:00
Girish Ramakrishnan 5ff3c8961c mail: log denial of max mail size 2020-09-09 22:48:43 -07:00
Girish Ramakrishnan 08f33f0e78 Add mail location audit log 2020-09-09 22:31:50 -07:00
Girish Ramakrishnan 0c5a637203 Fix progress indicator when mail location is being changed 2020-09-09 21:49:44 -07:00
Girish Ramakrishnan e3b4fdb6b1 better logs of the scheduler 2020-09-09 20:09:16 -07:00
Girish Ramakrishnan e730a6e282 log: do not show app update message for no updates 2020-09-09 19:26:45 -07:00
Girish Ramakrishnan 722808a0e4 firewall: make sure blocklist is first in the forward chain 2020-09-09 17:47:20 -07:00
Girish Ramakrishnan eae33161c1 Forgot the CLOUDRON_ prefix 2020-09-08 19:33:59 -07:00
Girish Ramakrishnan f14df141f7 Add MAIL_SERVER_HOST
This points to the mail fqdn
2020-09-08 19:33:23 -07:00
Girish Ramakrishnan f7a4330cd1 Add CLOUDRON_LDAP_HOST
We have MYSQL_HOST, POSTGRESQL_HOST etc. Just this LDAP has _SERVER
2020-09-08 19:32:21 -07:00
Johannes Zellner 23474c9752 Only disable motd-news if file exists 2020-09-04 10:49:07 +02:00
Girish Ramakrishnan fc08f9823e s3: copy parts in parallel 2020-09-03 14:31:56 -07:00
Girish Ramakrishnan 639bddb4b7 Do not use app.manifest.title since it may not be set for custom apps 2020-09-03 13:49:38 -07:00
Girish Ramakrishnan f87b32fc7b do not allow setting blocklist in demo mode 2020-09-02 23:04:48 -07:00
Girish Ramakrishnan 468ad6d578 Add some new backup regions 2020-09-02 19:39:58 -07:00
Girish Ramakrishnan 8b5c7d3d87 make http redirect to https://final-destination 2020-09-02 18:56:22 -07:00
Girish Ramakrishnan e791084793 bump timeout to 24 hours 2020-09-02 18:19:25 -07:00
Girish Ramakrishnan 316a1ae2c5 only scale back containers on infra change 2020-09-02 18:13:08 -07:00
Girish Ramakrishnan 71beca68dc Fix nginx reload race 2020-09-02 18:02:22 -07:00
Johannes Zellner aae79db27a Mention that we use task types also in the dashboard 2020-09-02 17:06:25 +02:00
Girish Ramakrishnan 6f188da2a6 Do not call onActivated when not activated
regression caused by ba29889f54
2020-09-01 15:35:43 -07:00
Girish Ramakrishnan 9ae4ce82a7 scheduler: stash the containerId in the state
the container id will change when the app is re-configured.
in the future, maybe it's better to do this like sftp.rebuild()
2020-09-01 12:56:06 -07:00
Girish Ramakrishnan 5adfa722d4 Add some debug information 2020-09-01 12:35:31 -07:00
Girish Ramakrishnan c26dda7cc9 require owner for network blocklist 2020-08-31 22:53:22 -07:00
Girish Ramakrishnan b7440ee516 Do IP based check first before accepting port based checks 2020-08-31 21:55:45 -07:00
Girish Ramakrishnan e4b06b16a9 firewall: implement blocklist 2020-08-31 21:46:07 -07:00
Girish Ramakrishnan 491af5bd9a stop apps before updating the databases because postgres will "lock" them preventing import 2020-08-31 17:53:29 -07:00
Girish Ramakrishnan 9b67ab9713 typo 2020-08-31 08:58:38 -07:00
Girish Ramakrishnan f0a62600af No need to accept them here since the ports are managed by docker 2020-08-31 08:58:02 -07:00
Girish Ramakrishnan dd5dfd98b7 ensure box update backups are also preserved for 3 weeks 2020-08-30 21:38:13 -07:00
Girish Ramakrishnan d5ec38c4db do not restrict postgresql db memory
see also 3ea6610923
2020-08-30 21:37:57 -07:00
Girish Ramakrishnan f945463dbe postgresql: enable uuid-ossp extension 2020-08-26 19:29:41 -07:00
Girish Ramakrishnan cf9439fb3b systemd 237 ignores --nice value in systemd-run 2020-08-26 17:30:47 -07:00
Girish Ramakrishnan 6901847c49 Update mail container for banner changes
fixes #341
2020-08-24 14:30:39 -07:00
Girish Ramakrishnan c54c25c35e fix task signature 2020-08-24 12:57:48 -07:00
Girish Ramakrishnan 5728bce6bc Fix typos 2020-08-24 10:28:53 -07:00
Girish Ramakrishnan d752403ed6 mail: add API to get/set banner
part of #341
2020-08-24 08:56:13 -07:00
Girish Ramakrishnan a48c08bd23 Fix async loop 2020-08-23 18:21:00 -07:00
Girish Ramakrishnan e46bbe8546 Add missing changes 2020-08-22 16:43:00 -07:00
Girish Ramakrishnan f5c8f18980 spamassassin: custom configs and wl/bl 2020-08-22 15:57:26 -07:00
Johannes Zellner 2d2270a337 Ensure stderr and exceptions also go to logfile
Bring back supererror for stacktraces when no Error object is throwing
2020-08-21 10:40:32 +02:00
Johannes Zellner d315c53ff8 Only rebuild sftp is something has changed 2020-08-21 09:24:06 +02:00
Girish Ramakrishnan d36b06acf7 Fix mail location route 2020-08-20 23:12:43 -07:00
Girish Ramakrishnan 2299af1dba Add route to set max email size 2020-08-20 22:18:27 -07:00
Girish Ramakrishnan e25ccc5e9a Double the timeout for upload now that chunks can have custom sizes 2020-08-20 16:50:58 -07:00
Girish Ramakrishnan 3ea6610923 do not restrict memory on startup of database addons
this helps the import case where we need all the memory we can get.
we scale the memory down once platform is ready in any case.
2020-08-20 11:16:35 -07:00
Girish Ramakrishnan 2d50f10fd6 Fix some typos 2020-08-19 23:14:05 -07:00
Girish Ramakrishnan 81d0637483 Allow box auto update pattern to be configurable
We just use the current app auto update pattern as the default.
There is now only one pattern for box and app updates.

Fixes #727
2020-08-19 22:09:41 -07:00
Girish Ramakrishnan 6c4df5abf0 unify update check into a single job 2020-08-19 21:43:12 -07:00
Girish Ramakrishnan 2eb0b5eedd remove unused parse-links module 2020-08-19 15:53:12 -07:00
Girish Ramakrishnan 0e00492f54 backups: make part size configurable 2020-08-19 14:39:20 -07:00
Girish Ramakrishnan b84a62eb5d Add to changes 2020-08-19 13:35:42 -07:00
Johannes Zellner c41ed95afe Remove wrong assert 2020-08-19 19:22:10 +02:00
Johannes Zellner fe07013383 Ensure only one sftp rebuild is in progress 2020-08-19 19:13:34 +02:00
Johannes Zellner 4f9cb9a8a1 sftp.rebuild does not need options anymore 2020-08-19 19:08:12 +02:00
Johannes Zellner ec5129d25b Rebuild sftp addon after an apptask 2020-08-19 18:23:44 +02:00
Johannes Zellner 6a781c62ec Improve task progress values
0: not yet handled
1: queued
2: started
100: finished
2020-08-19 16:58:53 +02:00
Girish Ramakrishnan c01ee83cd7 add note on why we delete 2020-08-18 23:53:14 -07:00
Girish Ramakrishnan cc591e399d scheduler: make the container run in same networking space to prevent further churn
idea comes from https://github.com/moby/moby/pull/9402#issuecomment-67259655
and https://github.com/moby/moby/pull/9402#issuecomment-67224239

see also:
https://github.com/moby/moby/issues/9098
https://github.com/moby/moby/pull/9167
https://github.com/moby/moby/issues/12899#issuecomment-97816048 (exec mem leak)
https://github.com/moby/moby/pull/38704

part of #732
2020-08-18 23:44:53 -07:00
Girish Ramakrishnan 7462c703f3 typo 2020-08-18 21:40:10 -07:00
Girish Ramakrishnan 879a6b4202 do not error if container already exists 2020-08-18 21:15:54 -07:00
Girish Ramakrishnan 0ae8dc1040 scheduler: reduce container churn
When we have a lot of app, docker has a tough time keeping up with
the container churn.

The reason why we don't use docker exec is that there is no way
to delete or manage exec containers.

Fixes #732
2020-08-18 20:26:19 -07:00
Girish Ramakrishnan 242548b36a If swap file exists, do nothing
this gives users more control on how to allocate swap
2020-08-18 12:57:51 -07:00
Girish Ramakrishnan 252aedda25 remove verbose logs 2020-08-18 12:46:55 -07:00
Girish Ramakrishnan 3507269321 Allow mail server name to be configurable
Fixes #721
2020-08-17 21:49:59 -07:00
Girish Ramakrishnan 9a5dce33db Be explicit about mailserver routes 2020-08-17 16:26:04 -07:00
Girish Ramakrishnan c4101a62ed rename function to setupDnsAndCert
this way, we can reuse this logic for the mail domain as well
2020-08-17 16:18:48 -07:00
Girish Ramakrishnan f52037f305 Remove cloudron.setupDashboard 2020-08-17 16:18:19 -07:00
Girish Ramakrishnan 03bd67c4e7 coding style 2020-08-17 16:18:12 -07:00
Girish Ramakrishnan 1eef239392 setting dashboard domain now only updates dashboard domain (and not mail)
part of #721
2020-08-17 16:09:20 -07:00
Girish Ramakrishnan d1e14ed691 rename function to setupDashboarDnsAndCert 2020-08-17 15:42:15 -07:00
Girish Ramakrishnan 60a787ce3d If db name exists, re-use it (for repair mode) 2020-08-17 12:04:02 -07:00
Girish Ramakrishnan f96bc6d5f4 keep mongodb database names short 2020-08-17 10:28:49 -07:00
Girish Ramakrishnan 5d439d9e79 Revert "Update mongodb to 4.2.8"
This reverts commit 9d2284add7.

We started updating because some users hit this error

MongoError: namespace name generated from index name "f6d689d0-0098-4ee5-b3ed-a812a75d9ae8.rocketchat_livechat_inquiry.$queueOrder_1_estimatedWaitingTimeQueue_1_estimatedServiceTimeAt_1" is too long (127 byte max)

MongoDB 4.4 bumps up the indices length but the real issue is that database
name that cloudron generates is big enough to make the whole thing exceed.
We will make a fix to make those db names shorter.
2020-08-17 09:44:06 -07:00
Girish Ramakrishnan 1453178693 settings.setAdmin -> setAdminLocation 2020-08-15 19:24:32 -07:00
Girish Ramakrishnan 510121bf54 remove support for hyphentated domains
this has not been used for a long time
2020-08-15 18:50:07 -07:00
Girish Ramakrishnan 2d607b394c Fix the exporting style 2020-08-15 18:19:01 -07:00
Girish Ramakrishnan bd12b0e441 These fields are now in the subdomains table 2020-08-15 17:25:51 -07:00
Girish Ramakrishnan 738b4e60fa notification: we do not retry update/backup every 4 hours anymore 2020-08-15 10:07:05 -07:00
Girish Ramakrishnan 1ae2f55c04 Remove verbose debug 2020-08-15 09:12:52 -07:00
Girish Ramakrishnan 2ebdf9673d Add VAAPI caps for transcoding 2020-08-14 18:48:53 -07:00
Girish Ramakrishnan 0427d790e5 Explain the command more clearly 2020-08-14 10:27:23 -07:00
Girish Ramakrishnan 90add7cf47 Add changes 2020-08-14 09:39:50 -07:00
Girish Ramakrishnan 26b1f8dfdb Do not automatically update to unstable release
fixes #726
2020-08-13 14:26:42 -07:00
Girish Ramakrishnan ba29889f54 remove IP nginx configuration that redirects to dashboard after activation
fixes #728
2020-08-13 14:10:17 -07:00
Girish Ramakrishnan 9d2284add7 Update mongodb to 4.2.8
Fixes #725
2020-08-13 11:32:48 -07:00
Girish Ramakrishnan dd44edde0a only clear backup cache if specific fields changed 2020-08-11 14:01:29 -07:00
Girish Ramakrishnan 885e90e810 add a todo 2020-08-11 12:57:37 -07:00
Girish Ramakrishnan 9cdf5dd0f3 backups: time the rotation and total as well 2020-08-11 10:28:11 -07:00
Girish Ramakrishnan df6e3eb1e6 Add deleteConcurrency setting 2020-08-11 09:14:09 -07:00
Girish Ramakrishnan 05026771e1 add memoryLimit, copyConcurrency, downloadConcurrency to backup config 2020-08-10 22:12:01 -07:00
Girish Ramakrishnan 7039108438 pass memory limit as argument to starttask.sh 2020-08-10 21:53:07 -07:00
Girish Ramakrishnan 02ee13cfb2 return empty array when listing 2020-08-10 21:32:54 -07:00
Girish Ramakrishnan 096e244252 Fix typo that causes aliases in lists to bounce
https://forum.cloudron.io/topic/2890/bug-with-mailing-lists-that-point-to-aliases
2020-08-10 17:49:27 -07:00
Girish Ramakrishnan bf5b7294a0 Add missing debugs 2020-08-10 14:54:37 -07:00
Girish Ramakrishnan a5da266643 groups: when listing, return members as well 2020-08-10 13:50:18 -07:00
Girish Ramakrishnan cf7bb49e15 More missing 5.5 changes 2020-08-10 10:16:09 -07:00
Girish Ramakrishnan 208b732bda yet more 5.5 changes 2020-08-10 10:07:50 -07:00
Girish Ramakrishnan c73d93b8bd more 5.5 changes 2020-08-10 10:05:47 -07:00
Girish Ramakrishnan 98a96eae2b Update mongodb
part of #725
2020-08-10 09:36:56 -07:00
Girish Ramakrishnan 2f9fe30c9d sftp: only mount data dirs that exist
when restoring, the platform starts first and the sftp container
goes and creates app data dirs with root permission. this prevents
the app restore logic from downloading the backup since it expects
yellowtent perm
2020-08-09 12:10:20 -07:00
Girish Ramakrishnan aeee8afc02 export database: fix async logic 2020-08-09 11:14:11 -07:00
Girish Ramakrishnan e85f0a4f52 Rename to box-task
this way we can do systemctl stop box*
2020-08-09 11:14:11 -07:00
Johannes Zellner da98649667 Ensure group listAllWitMembers also returns an ordered list 2020-08-09 11:34:53 +02:00
Girish Ramakrishnan 5ac08cc06b sftp: fix home directory path 2020-08-08 18:16:35 -07:00
Girish Ramakrishnan da72597dd3 Fix start/stop task scripts for ubuntu 16 2020-08-08 11:10:02 -07:00
Girish Ramakrishnan 1f1c94de70 Fix certificate ordering logic
* app certs set by user are always preferred
* If fallback, choose fallback certs. ignore others
* If LE, try to pick LE certs. Otherwise, provider fallback.

Fixes #724
2020-08-07 23:02:24 -07:00
Girish Ramakrishnan 60b3fceea6 reset-failed state of tasks during startup 2020-08-07 22:41:09 -07:00
Girish Ramakrishnan 5073809486 More 5.5.0 changes 2020-08-07 22:20:20 -07:00
Girish Ramakrishnan debd779cfd new public gpg key that doesn't expire
gpg --export admin@cloudron.io > releases.gpg
2020-08-07 22:17:30 -07:00
Girish Ramakrishnan 6b9454100e certs: remove caas backend 2020-08-07 17:58:27 -07:00
Girish Ramakrishnan 779ad24542 domains: remove caas backend, it is unused 2020-08-07 17:57:48 -07:00
Girish Ramakrishnan b94dbf5fa3 remove restricted fallback cert
this feature was never used. iirc, it was for managed hosting
2020-08-07 17:57:25 -07:00
Girish Ramakrishnan 45c49c9757 route53: verifyDnsConfig lists zones using old API
It should be using the listHostedZonesByName API but it was using the old
API (which has a 100 zone limitation) because it was using old credentials.
2020-08-07 09:54:02 -07:00
Girish Ramakrishnan 91288c96b1 s3: set queue size to 3
fixes #691
2020-08-07 00:28:00 -07:00
Girish Ramakrishnan f8e22a0730 Fix tests 2020-08-07 00:21:15 -07:00
Girish Ramakrishnan 114b45882a Set memory limit to 400M for tasks 2020-08-07 00:21:15 -07:00
Girish Ramakrishnan b1b6f70118 Kill all tasks on shutdown and startup
BindsTo will kill all the tasks when systemctl stop box is executed.
But when restarted, it keeps the tasks running. Because of this behavior,
we kill the tasks on startup and stop of the box code.
2020-08-06 23:47:40 -07:00
Girish Ramakrishnan 648d42dfe4 Empty debug prints as undefined for some reason 2020-08-06 23:23:56 -07:00
Girish Ramakrishnan 99f989c384 run apptask and backup task with a nice
A child process inherits whatever nice value is held by the parent at the time that it is forked
2020-08-06 16:46:39 -07:00
Girish Ramakrishnan 2112c7d096 sudo: remove the nice support 2020-08-06 16:44:35 -07:00
Girish Ramakrishnan ac63d00c93 run tasks as separate cgroup via systemd
this allows us to adjust the nice value and memory settings per task

part of #691
2020-08-06 16:43:14 -07:00
Girish Ramakrishnan e04871f79f pass log file as argument to task worker
initially, i thought i can hardcode the log file into taskworker.js
depending on the task type but for apptask, it's not easy to get the
appId from the taskId unless we introspect task arguments as well.
it's easier for now to pass it as an argument.
2020-08-05 00:46:34 -07:00
Girish Ramakrishnan 182c162dc4 hardcode logging of box code to box.log 2020-08-04 13:30:18 -07:00
Johannes Zellner 822b38cc89 Fallback to NOOP callback if not supplied 2020-08-04 14:32:01 +02:00
Girish Ramakrishnan d564003c87 backup cleaner: referenced backups must be counted as part of period
otherwise, we end up in a state where box backups keeps referencing
app backups and app backup cleanup is only performed on the remaining
app backups.
2020-08-03 21:22:27 -07:00
Girish Ramakrishnan 1b307632ab Use debug instead of console.* everywhere
No need to patch up console.* anymore

also removes supererror
2020-08-02 12:04:55 -07:00
Girish Ramakrishnan aa747cea85 update postgresl for pg_stat_statements,plpgsql extensions (loomio) 2020-08-02 11:36:42 -07:00
Girish Ramakrishnan f4a322478d cloudron.target is not needed 2020-08-01 20:00:20 -07:00
Girish Ramakrishnan d2882433a5 run backup uploader with a nice of 15
the gzip takes a lot of cpu processing and hogs the CPU. With a nice
level, we give other things higher priority.

An alternate idea that was explored was to use cpulimit. This is to
send SIGSTOP and SIGCONT periodically but this will not make use of the
CPU if it's idle (unlike nice).

Another idea is to use cgroups, but it's not clear how to use it with
the dynamic setup we have.

part of #691
2020-07-31 18:23:36 -07:00
Girish Ramakrishnan a94b175805 Add timing information for backups 2020-07-31 12:59:15 -07:00
Girish Ramakrishnan 37d81da806 do system checks once a day 2020-07-31 11:20:17 -07:00
Girish Ramakrishnan d089444441 db upgrade: stop containers only after exporting
we cannot export if the containers were nuked in the platform logic.
for this reason, move the removal near the place where they get started.
2020-07-30 15:28:53 -07:00
Girish Ramakrishnan b0d65a1bae rename startApps to markApps 2020-07-30 15:28:50 -07:00
Girish Ramakrishnan 16288cf277 better debug 2020-07-30 11:42:03 -07:00
Girish Ramakrishnan 7ddbabf781 Make the error message clearer 2020-07-30 11:29:43 -07:00
Girish Ramakrishnan fe35f4497b Fix two typos 2020-07-30 10:58:24 -07:00
Girish Ramakrishnan 625463f6ab export the database before upgrade
it's possible that
a) backups are completely disabled
b) skip backup option is selected when upgrading

in the above cases, the dump file is not generated and thus any addon
upgrade will fail. to fix, we dump the db fresh for database upgrades.
2020-07-30 10:23:08 -07:00
Johannes Zellner ff632b6816 Add more external ldap tests 2020-07-30 15:22:03 +02:00
Johannes Zellner fbc666f178 Make externalldap sync more robust 2020-07-30 15:08:01 +02:00
Girish Ramakrishnan d89bbdd50c Update to PostgreSQL 11 2020-07-29 21:54:05 -07:00
Girish Ramakrishnan 96f9aa39b2 add note on why we check for app updates separately 2020-07-29 20:27:06 -07:00
Girish Ramakrishnan 7330814d0f More 5.5 changes 2020-07-29 16:11:09 -07:00
Johannes Zellner 312efdcd94 Fix debug message 2020-07-29 20:38:46 +02:00
Girish Ramakrishnan 5db78ae359 Fix more usages of backup.intervalSecs 2020-07-29 11:25:59 -07:00
Girish Ramakrishnan 97967e60e8 remove yahoo from smtp test list 2020-07-29 11:25:59 -07:00
Johannes Zellner 9106b5d182 Avoid using extra /data dir for filemanager 2020-07-29 20:14:14 +02:00
Johannes Zellner 74bdb6cb9d Only mount app data volumes if localstorage is used 2020-07-29 19:58:41 +02:00
Johannes Zellner 0a44d426fa Explicitly mount all apps into the sftp container 2020-07-29 19:47:37 +02:00
Johannes Zellner e1718c4e8d If app.dataDir is set, first unmount from sftp before deleting on uninstall 2020-07-29 17:54:32 +02:00
Girish Ramakrishnan f511a610b5 backups: take a pattern instead of interval secs
part of #699
2020-07-28 21:54:56 -07:00
Girish Ramakrishnan 4d5715188d Increase invite link expiry to a week 2020-07-28 14:19:19 -07:00
Johannes Zellner 2ea21be5bd Add basic backup check route tests 2020-07-28 17:23:21 +02:00
Johannes Zellner 5bb0419699 Add backup check route
Part of #719
2020-07-28 17:18:50 +02:00
Johannes Zellner a8131eed71 Run initial backup configuration check only after activation
Part of #719
2020-07-28 17:12:38 +02:00
Girish Ramakrishnan ed09c06ba4 Add option to remove mailbox data
Fixes #720
2020-07-27 22:55:09 -07:00
Girish Ramakrishnan 3c59a0ff31 make it clear it is exported for testing 2020-07-27 22:07:25 -07:00
Girish Ramakrishnan a6d24b3e48 postgresql: add btree_gist,postgres_fdw extensions for gitlab 2020-07-24 22:30:45 -07:00
Girish Ramakrishnan 060135eecb Next release is 5.5 2020-07-24 09:33:53 -07:00
Johannes Zellner ef296c24fe Mount data custom app data location specifically into sftp addon
Fixes #722
2020-07-24 15:43:26 +02:00
Girish Ramakrishnan 707aaf25ec Add note on underscore in usernames 2020-07-23 16:29:54 -07:00
Girish Ramakrishnan 7edeb0c358 nginx displays version in stderr 2020-07-22 17:57:55 -07:00
Girish Ramakrishnan e516af14b2 typo 2020-07-22 17:53:04 -07:00
Girish Ramakrishnan 4086f2671d Disable ldap/directory config/2fa in demo mode 2020-07-22 16:18:22 -07:00
Girish Ramakrishnan 23c4550430 Update postgresql addon to have citext extension for loomio 2020-07-22 08:29:44 -07:00
Johannes Zellner 31d25cd6be Add 5.4.1 changes 2020-07-19 21:11:05 +02:00
Johannes Zellner 07b3c7a245 Use sftp addon with fixed symlinks 2020-07-18 19:27:02 +02:00
Girish Ramakrishnan a00b7281a7 Fixup changelog 2020-07-17 10:43:22 -07:00
Girish Ramakrishnan ddeee0c970 Add note that links expire in 24 hours 2020-07-16 15:17:51 -07:00
Johannes Zellner 8aad71efd0 Add more feature flags 2020-07-16 18:14:25 +02:00
Johannes Zellner 2028f6b984 Do not reassign ubunt_codename in base image init 2020-07-16 16:42:15 +02:00
Girish Ramakrishnan bff4999d27 mail: add mailbox count route 2020-07-15 15:48:30 -07:00
Johannes Zellner d429015f83 Add more 3.4.0 changes 2020-07-15 14:57:06 +02:00
Johannes Zellner e2628e2d43 Use latest filemanager addon
Fixes dot- and json-files
2020-07-14 17:16:41 +02:00
Girish Ramakrishnan 05dcbee7e3 backups: add b2 provider
part of #508
2020-07-13 14:52:35 -07:00
Johannes Zellner a81919262e Use addon with chown functionality 2020-07-13 18:48:42 +02:00
Girish Ramakrishnan b14b5f141b Hide nginx version 2020-07-13 09:27:57 -07:00
Girish Ramakrishnan 1259d11173 Add back provider field into getStatus 2020-07-13 08:46:05 -07:00
Johannes Zellner 0a7b132be8 Remove or increase timeouts for filemanager 2020-07-13 17:05:22 +02:00
Girish Ramakrishnan ed9210eede Add mandatory 2FA flag
part of #716
2020-07-10 10:25:04 -07:00
Girish Ramakrishnan 9ee6aa54c6 avatar is not part of the profile lock
this is because avatar is not exposed via LDAP anyways. it's purely
a personal dashboard thing.
2020-07-10 09:43:42 -07:00
Girish Ramakrishnan 7cfc455cd3 make tests pass again
also disable column statistics on ubuntu 20
2020-07-10 09:33:35 -07:00
Johannes Zellner a481ceac8c Allow larger file uploads for filemanager 2020-07-10 18:23:55 +02:00
Girish Ramakrishnan 8c7eff4e24 user: add routes to set/clear avatar 2020-07-10 07:23:38 -07:00
Girish Ramakrishnan c6c584ff74 user: move avatar handling into model code 2020-07-10 07:01:15 -07:00
Johannes Zellner ba50eb121d Use new sftp addon 2020-07-10 14:13:16 +02:00
Johannes Zellner aa8ebbd7ea Add filemanager proxy routes 2020-07-10 14:10:52 +02:00
Girish Ramakrishnan 64bc9c6dbe disable profile view for all users to avoid confusion 2020-07-09 21:54:09 -07:00
Girish Ramakrishnan bba9963b7c Add directoryConfig feature flag
Fixes #704
2020-07-09 21:51:22 -07:00
Girish Ramakrishnan 6ea2aa4a54 return profileLocked in config route
part of #704
2020-07-09 17:28:44 -07:00
Girish Ramakrishnan 3c3f81365b add route to get/set directory config
part of #704
2020-07-09 17:12:07 -07:00
Girish Ramakrishnan 3adeed381b setup account based on directory config
part of #704
2020-07-09 16:39:34 -07:00
Girish Ramakrishnan 0f5b7278b8 add directory config setting
part of #704
2020-07-09 16:02:58 -07:00
Girish Ramakrishnan f94ff49fb9 users: replace modifiedAt with ts 2020-07-09 16:02:49 -07:00
Girish Ramakrishnan d512a9c30d rename function 2020-07-09 16:02:43 -07:00
Girish Ramakrishnan 0c5113ed5b email is never used in account setup 2020-07-09 15:37:35 -07:00
Girish Ramakrishnan 2469f4cdff rename function to sendPasswordResetByIdentifier 2020-07-09 15:37:35 -07:00
Girish Ramakrishnan 9c53bfb7fb Do not show LDAP logs, it spams a lot 2020-07-07 11:16:47 -07:00
Girish Ramakrishnan 8b8144588d list must search members 2020-07-05 11:44:46 -07:00
Girish Ramakrishnan 77553da4c1 mail: add search param for mailbox and mailing list api 2020-07-05 11:23:53 -07:00
Girish Ramakrishnan cbcf943691 mail: parameterize the query 2020-07-05 10:48:08 -07:00
Girish Ramakrishnan 725a19e5b5 mail: Add pagination to lists API
Fixes #712
2020-07-05 10:48:04 -07:00
Girish Ramakrishnan f9115f902a Do not send alive status
we used to do this for managed hosting to track scaling but we don't
need this info anymore
2020-07-03 19:13:27 -07:00
Girish Ramakrishnan e4faf26d74 5.3.4 changes
(cherry picked from commit 77785097c1)
2020-07-03 14:23:20 -07:00
Girish Ramakrishnan 1c96fbb533 Fixes for tests 2020-07-03 13:47:56 -07:00
Girish Ramakrishnan 3dc163c33d database: rework connection logic 2020-07-03 13:14:00 -07:00
Girish Ramakrishnan edae94cf2e Bump max_connection for postgres addon to 200 2020-07-02 15:47:26 -07:00
Girish Ramakrishnan d1ff8e9d6b Fix crash when mysql crashes 2020-07-02 15:10:05 -07:00
Girish Ramakrishnan 70743bd285 database: Fix event emitter warning
the connection object gets reused after release. this means that we keep
attaching the 'error' event and not unlistening.

--trace-warnings can be added to box.service to get the stack trace
2020-07-02 12:00:56 -07:00
Johannes Zellner 493f1505f0 Check also for mountpoint on filesystem with external disk 2020-07-02 19:08:27 +02:00
Girish Ramakrishnan 007e3b5eef Add changes 2020-07-01 14:29:40 -07:00
Johannes Zellner d9bf6c0933 also support uniqueMember property next to member for ldap groups 2020-07-01 17:08:17 +02:00
Johannes Zellner 324344d118 Reusue the single correct ldap.createClient call also in auth 2020-07-01 14:59:26 +02:00
Johannes Zellner 5cb71e9443 No need to return externalLdapConfig in getClient() 2020-07-01 14:52:11 +02:00
Johannes Zellner cca19f00c5 Fallback to mailPrimaryAddress in ldap sync 2020-07-01 14:34:41 +02:00
Girish Ramakrishnan 6648f41f3d nginx: [warn] the "ssl" directive is deprecated, use the "listen ... ssl" directive 2020-06-30 16:00:52 -07:00
Girish Ramakrishnan c1e6b47fd6 Fix sogo aliases
Fixes cloudron/sogo#18
2020-06-30 14:29:50 -07:00
Girish Ramakrishnan 0f103ccce1 Add ping capability (for statping) 2020-06-30 07:40:17 -07:00
Girish Ramakrishnan bc6e652293 5.3.3 changes 2020-06-29 19:52:08 -07:00
Girish Ramakrishnan 85b4f2dbdd print sudo command to check failures 2020-06-29 14:03:34 -07:00
Girish Ramakrishnan d47b83a63b Package lock mystery 2020-06-29 14:03:15 -07:00
Girish Ramakrishnan b2e9fa7e0d aschema: dd servicesConfigJson 2020-06-26 15:48:39 -07:00
Girish Ramakrishnan a9fb444622 Use nginx 1.18 for security fixes 2020-06-26 14:57:53 -07:00
Girish Ramakrishnan 33ba22a021 Put this in 5.3.2 itself 2020-06-26 10:41:32 -07:00
Girish Ramakrishnan 57de0282cd remove provider from trackBeginSetup 2020-06-26 09:55:39 -07:00
Girish Ramakrishnan 8568fd26d8 Fix failing test 2020-06-26 09:48:10 -07:00
Girish Ramakrishnan 84f41e08cf Add mlock capability to manifest (for vault app) 2020-06-26 09:27:35 -07:00
Johannes Zellner a96da20536 TODO is done for filesystem backend moutnpoint check 2020-06-26 17:57:26 +02:00
Johannes Zellner 5199a9342e Add missing ldap client error handling 2020-06-26 17:55:42 +02:00
Girish Ramakrishnan 893ecec0fa redis: Set maxmemory and maxmemory-policy 2020-06-26 08:54:47 -07:00
Girish Ramakrishnan e3da6419f5 Add 5.3.2 changes 2020-06-26 08:48:01 -07:00
Girish Ramakrishnan 0750d2ba50 More changes 2020-06-25 16:48:11 -07:00
Girish Ramakrishnan f1fcb65fbe Do not install sshfs. user will install it if they want
we don't use sshfs anywhere in our code ourselves
2020-06-25 12:21:49 -07:00
Girish Ramakrishnan 215aa65d5a Fix provider usage
* do not send to appstore anymore
* do not set in getStatus/getConfig
* provider is not needed when registering cloudron
2020-06-25 11:20:05 -07:00
Girish Ramakrishnan 85f67c13da remove unused registerWithLicense 2020-06-25 11:11:52 -07:00
Girish Ramakrishnan 6dcc478aeb add to changes 2020-06-25 09:20:37 -07:00
Johannes Zellner 3f2496db6f Support self-signed certs for external ldap/ad 2020-06-25 17:45:59 +02:00
Johannes Zellner 612f79f9e0 Copy over changes for 5.3.1 2020-06-25 14:22:44 +02:00
Johannes Zellner 90fb1cd735 We also need enableBackup property for app listing api 2020-06-25 12:31:00 +02:00
Girish Ramakrishnan 7c24d9c6c6 Give graphite more memory 2020-06-22 09:55:01 -07:00
Johannes Zellner 60f1b2356a Also make nfs storage provider same as cifs and sshfs 2020-06-22 15:51:05 +02:00
Johannes Zellner 0b8f21508f Add more changes 2020-06-22 12:04:34 +02:00
Johannes Zellner ae128c0fa4 If no appstore account is setup restrict features to free plan 2020-06-22 12:02:10 +02:00
Girish Ramakrishnan 1b4ec9ecf9 Update changes 2020-06-18 10:25:45 -07:00
Girish Ramakrishnan b0ce0b61d6 logging: fix crash when router errors 2020-06-18 09:27:09 -07:00
Girish Ramakrishnan e1ffdaddfa Fix timeout issues in postgresql and mysql addon 2020-06-17 16:39:30 -07:00
Johannes Zellner af8344f482 remove unused requires 2020-06-16 14:37:06 +02:00
Johannes Zellner 7dc2596b3b Ensure we support pre 5.3 Cloudron installation 2020-06-16 14:10:14 +02:00
Johannes Zellner 0109956fc2 do not rely on some argument passed through for infraversion base path 2020-06-16 14:09:55 +02:00
Johannes Zellner 945fe3f3ec Do not spam install logs with nodejs tarball contents 2020-06-16 13:58:23 +02:00
Johannes Zellner 9c868135f3 app sso flag is not restricted now 2020-06-16 13:09:06 +02:00
Girish Ramakrishnan 5be288023b update mail container to record separator and spam folder 2020-06-15 13:50:46 -07:00
Girish Ramakrishnan a03f97186c Make mail auth case insensitive 2020-06-15 09:58:55 -07:00
Johannes Zellner 0aab891980 Support nginx logs 2020-06-15 17:30:16 +02:00
Johannes Zellner 5268d3f57d Fix test for systems without swap 2020-06-15 16:06:54 +02:00
Girish Ramakrishnan 129cbb5beb backups: fix cleanup
The various changes are:
* Latest backup is always kept for box and app backups
* If the latest backup is part of the policy, it is not counted twice
* Latest backup comes into action only when all backups are outside the retention policy
* For uninstalled apps, latest backup is not preserved
* This way the latest backup of apps that are not referenced in box backup is preserved.
  (for example, for stopped apps)

fixes #692
2020-06-14 22:06:00 -07:00
Girish Ramakrishnan 2601d2945d Fix backup tests 2020-06-14 14:01:01 -07:00
Girish Ramakrishnan e3829eb24b typo 2020-06-14 14:00:29 -07:00
Girish Ramakrishnan f6cb1a0863 backups: query using identifier instead of type
this allows us to move the enums into backups.js instead of backupdb.js
2020-06-14 12:27:41 -07:00
Girish Ramakrishnan 4f964101a0 add identifier to backups table 2020-06-14 11:39:44 -07:00
Girish Ramakrishnan f6dcba025f auditSource is not used in the worker 2020-06-14 09:09:41 -07:00
Johannes Zellner d6ec65d456 Do not remove alternateDomains to allow apps view filter to work 2020-06-14 13:39:15 +02:00
Girish Ramakrishnan 65d8074a07 Fix failing backup test 2020-06-12 12:58:11 -07:00
Girish Ramakrishnan e3af61ca4a Fix failing test 2020-06-12 12:52:32 -07:00
Girish Ramakrishnan a58f1268f0 mail: Add Auto-Submitted header to NDRs 2020-06-11 19:48:37 -07:00
Girish Ramakrishnan 41eacc4bc5 postgresql: Add unaccent extension 2020-06-11 09:53:53 -07:00
Girish Ramakrishnan aabb9dee13 Fix transaction rollback logic 2020-06-11 09:50:49 -07:00
Girish Ramakrishnan c855d75f35 remove mkdirp use
node 10.12 has { recursive: true }
2020-06-11 08:27:48 -07:00
Girish Ramakrishnan 8f5cdcf439 backups: some logs for debugging 2020-06-10 23:00:23 -07:00
Girish Ramakrishnan 984559427e update manifest format to 5.3.0 2020-06-09 11:35:54 -07:00
Johannes Zellner 89494ced41 Check for sshfs and cifs backup backends, if they are mounted 2020-06-08 17:46:52 +02:00
Johannes Zellner ef764c2393 Merge sshfs.js into filesystem.js 2020-06-08 17:08:26 +02:00
Johannes Zellner 8624e2260d add storage api to make preflight checks
Currently there is only disk space checking but sshfs and cifs need
mount point checking as well
2020-06-08 16:25:05 +02:00
Johannes Zellner aa011f4add add ldap group tests and fixes for the found issues 2020-06-07 13:49:01 +02:00
Girish Ramakrishnan 3df61c9ab8 do not automatically update unstable updates
part of #698
2020-06-05 16:26:23 -07:00
Girish Ramakrishnan a4516776d6 make canAutoupdateApp take updateInfo object
part of #698
2020-06-05 16:06:37 -07:00
Girish Ramakrishnan 54d0ade997 curl uses -s and not -q 2020-06-05 13:50:40 -07:00
Johannes Zellner 3557fcd129 Add sshfs quirks to shared code in filesytstem.js 2020-06-05 13:45:25 +02:00
Johannes Zellner 330b4a613c Retrieve the backupPath from the storage provider itself 2020-06-05 13:27:18 +02:00
Johannes Zellner 7ba3412aae Add some sshfs config tests 2020-06-05 12:43:09 +02:00
Johannes Zellner 6f60495d4d Initial version of sshfs storage backend 2020-06-05 11:39:51 +02:00
Johannes Zellner 0b2eb8fb9e Sync users into groups
This does not yet remove users from groups

Part of #685
2020-06-05 11:28:57 +02:00
Johannes Zellner 48af17e052 Groups are lowercase on Cloudron 2020-06-05 10:13:19 +02:00
Johannes Zellner b7b1055530 Avoid the pyramid 2020-06-05 09:26:52 +02:00
Johannes Zellner e7029c0afd Remove unsused and poorly named groups.getGroups() API 2020-06-05 09:24:00 +02:00
Johannes Zellner cba3674ac0 Stop ldap syncing if we hit some internal error 2020-06-05 09:03:30 +02:00
Girish Ramakrishnan 865a549885 say connected 2020-06-04 11:27:11 -07:00
Girish Ramakrishnan 50dcf827a5 remove console.error use in many places
the backtraces just flood the logs

apphealthtask: remove console.error
remove spurious console.dir
cleanup scheduler error logging
2020-06-04 11:21:56 -07:00
Girish Ramakrishnan f5fb582f83 log status and message in morgan
connect lastmile does not forward final handler to express anymore.
otherwise, express logs using console.error()
https://github.com/expressjs/express/issues/2263
2020-06-04 09:17:58 -07:00
Girish Ramakrishnan dbba502f83 remove message from debug 2020-06-04 09:17:58 -07:00
Girish Ramakrishnan aae49f16a2 database: do no reconnect in query 2020-06-04 09:17:58 -07:00
Girish Ramakrishnan 45d5f8c74d make rollback return an error
fixes #690
2020-06-04 09:17:58 -07:00
Girish Ramakrishnan 6cfd64e536 database: do not crash if connection errors
Part of #690
2020-06-04 09:17:58 -07:00
Girish Ramakrishnan c5cc404b3e do not retry here
Part of #690
2020-06-04 09:17:58 -07:00
Johannes Zellner 42cbcc6ce3 groups.create() now needs source argument 2020-06-04 14:20:05 +02:00
Johannes Zellner 812bdcd462 Fix groups test by ensuring we order by name 2020-06-04 14:03:06 +02:00
Johannes Zellner f275409ee8 Fix cloudron api tests 2020-06-04 13:55:47 +02:00
Johannes Zellner 8994ac3727 Fix backup retention tests 2020-06-04 13:43:25 +02:00
Johannes Zellner 7c5ff5e4d5 Create user groups for ldap groups 2020-06-04 13:26:13 +02:00
Johannes Zellner c5e84d5469 Add source property to userGroups 2020-06-04 13:25:55 +02:00
Johannes Zellner c143450dc6 WIP 2020-06-04 12:59:27 +02:00
Johannes Zellner 07b95c2c4b Add groups.getByName() 2020-06-04 12:48:35 +02:00
Johannes Zellner c30734f7f3 Show in the logs if group sync is disabled 2020-06-04 12:40:28 +02:00
Johannes Zellner 91f506c17b Explicitly enable/disable ldap group sync 2020-06-04 12:28:31 +02:00
Girish Ramakrishnan 7a17695ad5 Retry in 10 seconds to not make things worse
Part of #690
2020-06-03 16:05:48 -07:00
Girish Ramakrishnan f5076c87d4 add to changes 2020-06-03 13:52:53 -07:00
Girish Ramakrishnan a47d6e1f3a cloudron-setup: --provider is dead
Long live --provider

Part of #693
2020-06-03 13:47:30 -07:00
Girish Ramakrishnan f6ff1abb00 cloudron-setup: remove --license arg. unused 2020-06-03 13:16:39 -07:00
Johannes Zellner 386aaf6470 Initial code to fetch LDAP groups during sync 2020-06-03 22:12:38 +02:00
Johannes Zellner 2b3c4cf0ff avatar blob now comes in only via branding api calls 2020-06-02 15:13:50 +02:00
Girish Ramakrishnan b602e921d0 better error message if domains exists 2020-06-01 16:11:02 -07:00
Girish Ramakrishnan 2fc3cdc2a2 remove superfluous debug 2020-06-01 09:40:56 -07:00
Girish Ramakrishnan e2cadbfc30 Fix uniqueness constraint in app passwords table
Fixes #688
2020-05-30 13:25:29 -07:00
Girish Ramakrishnan 3ffa935da7 Revert "part focal support"
This reverts commit 7d36533524.

not ready yet
2020-05-30 10:58:28 -07:00
Girish Ramakrishnan 5f539e331a 5.3.0 changes 2020-05-30 09:45:24 -07:00
Girish Ramakrishnan 356d0fabda Add note that pattern must match dashboard code 2020-05-30 09:44:33 -07:00
Girish Ramakrishnan 122ec75cb6 Fix links 2020-05-29 19:10:42 -07:00
Girish Ramakrishnan a3a48e1a49 poll for updates a bit more often 2020-05-29 13:39:16 -07:00
Girish Ramakrishnan 4ede765e1f typo: memoryLimit -> memory 2020-05-29 13:29:01 -07:00
Girish Ramakrishnan 4fa181b346 re-use the latest backup id for non-backupable apps
for stopped apps, as an example
2020-05-28 14:16:38 -07:00
Johannes Zellner 4f76d91ae9 Add backup_config settings API tests 2020-05-28 21:42:25 +02:00
Girish Ramakrishnan 20d1759fa5 Run update checker on stopped apps, we just don't update them 2020-05-28 12:41:51 -07:00
Girish Ramakrishnan 433e783ede do not allow backup, import, update in stopped state 2020-05-28 12:41:51 -07:00
Johannes Zellner 47f47d916d Fixup tests 2020-05-28 21:05:21 +02:00
Johannes Zellner b31ac7d1fd Revert backup policy fallback and check in rest api
Check is now in proper location at backups.testConfig()
2020-05-28 20:44:44 +02:00
Johannes Zellner ea47fb7305 Properly check for backup policy in testConfig() 2020-05-28 20:44:44 +02:00
Girish Ramakrishnan 82170f8f1b Fix failing test 2020-05-28 11:04:39 -07:00
Girish Ramakrishnan acb2655f58 rename variable (it ensures backup and may not actually backup) 2020-05-28 11:03:49 -07:00
Girish Ramakrishnan b1464517e6 centralize all the cron patterns in one place 2020-05-28 11:01:46 -07:00
Girish Ramakrishnan 151e6351f6 add couple of 5.2 changes 2020-05-28 09:37:57 -07:00
Johannes Zellner 154f768281 Forgot .length 2020-05-28 16:44:45 +02:00
Johannes Zellner 90c857e8fc Further validate retentionPolicy api input 2020-05-28 16:27:07 +02:00
Johannes Zellner 7a3efa2631 Ensure we get a proper retention policy for backups 2020-05-28 16:26:21 +02:00
Girish Ramakrishnan 38cc767f27 move up the backup cron to not overlap auto-updates 2020-05-27 23:04:04 -07:00
Girish Ramakrishnan e1a718c78f remove redundant call to canBackupApp 2020-05-27 22:48:48 -07:00
Girish Ramakrishnan 32a4450e5e 5.2.4 changes
(cherry picked from commit 2dc7342f09)
2020-05-27 22:35:30 -07:00
Girish Ramakrishnan fca3f606d2 Do not backup stopped apps 2020-05-27 21:04:01 -07:00
Girish Ramakrishnan 4a0a934a76 start using vhost style for accessing s3 style storage
if bucket name has a '.', accept self-signed

fixes #680
2020-05-27 17:50:37 -07:00
Girish Ramakrishnan f7c406bec9 s3: bucket name cannot contain _ or capitals or .
we can make it more elaborate, but not sure if it's needed

https://blogs.easydynamics.com/2016/10/24/aws-s3-bucket-name-validation-regex/
2020-05-27 17:01:42 -07:00
Girish Ramakrishnan f4807a6354 update many node modules 2020-05-27 16:52:22 -07:00
Girish Ramakrishnan 0960008b7b 5.2.4 changes
(cherry picked from commit 4267f5ea0a)
2020-05-26 17:07:03 -07:00
Girish Ramakrishnan 04a1aa38b4 Add CIFS as storage provider
part of #686
2020-05-26 15:31:45 -07:00
Girish Ramakrishnan f84622efa1 fs: add create/unlink tests 2020-05-26 15:31:41 -07:00
Girish Ramakrishnan f6c4614275 Do not restart stopped apps
(cherry picked from commit 2e76b8bed9)
2020-05-26 07:54:35 -07:00
Girish Ramakrishnan 7d36533524 part focal support
part of #684
2020-05-25 19:49:15 -07:00
Girish Ramakrishnan 5cd3df4869 better nginx config for higher loads 2020-05-25 15:25:00 -07:00
Girish Ramakrishnan b0480f48f3 Add changes 2020-05-24 20:12:19 -07:00
Girish Ramakrishnan 2e820c343a remove meaningless debug 2020-05-24 20:11:03 -07:00
Girish Ramakrishnan ce927a2247 Set dmode in tar extract 2020-05-24 20:08:17 -07:00
Girish Ramakrishnan ae810d59e9 mail: fix crash in audit logs 2020-05-24 18:50:10 -07:00
Girish Ramakrishnan 1438ee52a1 import: fix crash because encryption is unset 2020-05-24 18:42:04 -07:00
Girish Ramakrishnan de4b3e55fa Use apps.getAll so that app.fqdn is valid 2020-05-24 18:21:35 -07:00
Girish Ramakrishnan d2cd78c5cb more debug() removal 2020-05-24 12:30:48 -07:00
Girish Ramakrishnan d000719fa2 app health monitor is too verbose 2020-05-24 11:43:17 -07:00
Girish Ramakrishnan efea4ed615 more debug() removal 2020-05-24 11:35:31 -07:00
Girish Ramakrishnan 67a931c4b8 Remove verbose logs 2020-05-24 11:33:53 -07:00
Girish Ramakrishnan bdcc5c0629 Mbps -> MBps
Fixes #682
2020-05-23 13:31:23 -07:00
Girish Ramakrishnan d113cfc0ba add comment on how often du value is stored 2020-05-22 20:06:45 -07:00
Girish Ramakrishnan 4a3ab50878 5.2.1 changes 2020-05-22 18:49:28 -07:00
Girish Ramakrishnan b39261c8cf remove extra $ 2020-05-22 16:56:01 -07:00
Girish Ramakrishnan 7efb57c8da restart apps on addon container change
when the IP changes on addon container re-create, the apps don't
detect this (maybe there is some large DNS cache timeout in docker)
2020-05-22 16:45:03 -07:00
Girish Ramakrishnan 90c24cf356 add cleanup policy test 2020-05-21 14:30:21 -07:00
Girish Ramakrishnan 54abada561 backups: add progressCallback to cleanup funcs 2020-05-21 13:46:16 -07:00
Girish Ramakrishnan f1922660be add a new line 2020-05-21 10:57:57 -07:00
Girish Ramakrishnan 795e3c57da Add a header for encrypted backup files
this is required to identify old backups and new backups for decryption
2020-05-20 22:44:26 -07:00
Girish Ramakrishnan 3f201464a5 Fix bug where SRS translation was done on the main domain instead of mailing list domain 2020-05-20 21:55:48 -07:00
Girish Ramakrishnan 8ac0be6bb5 Update postgresql for schema ownership fix 2020-05-20 16:44:32 -07:00
Johannes Zellner 130805e7bd Add changes 2020-05-19 14:59:28 +02:00
Girish Ramakrishnan b8c7357fea redis: if container inactive, return stopped status 2020-05-18 14:43:23 -07:00
Girish Ramakrishnan 819f8e338f stop app now stops it's services as well 2020-05-18 14:33:07 -07:00
Girish Ramakrishnan 9569e46ff8 use docker.restart instead of start/stop since it is atomic 2020-05-18 13:35:42 -07:00
Girish Ramakrishnan b7baab2d0f restore: set encryption to null 2020-05-18 09:07:18 -07:00
Girish Ramakrishnan e2d284797d set HOME explicity when calling migrate script 2020-05-17 21:50:50 -07:00
Girish Ramakrishnan a3ac343fe2 installer: print from and to versions 2020-05-17 21:34:39 -07:00
Girish Ramakrishnan dadde96e41 remove login events from addons
more often then not this just spams the eventlog
2020-05-15 21:40:34 -07:00
Girish Ramakrishnan 99475c51e8 fix encryption of 0-length files 2020-05-15 16:05:12 -07:00
Girish Ramakrishnan cc9b4e26b5 use done event to signal write success (just like in extract) 2020-05-15 15:24:12 -07:00
Girish Ramakrishnan 32f232d3c0 destroy input stream on error 2020-05-15 15:21:24 -07:00
Girish Ramakrishnan 235047ad0b bind to source stream error event immediately
download() is async and the source stream error is missed
2020-05-15 14:54:05 -07:00
Girish Ramakrishnan 228f75de0b better error messages 2020-05-15 14:35:19 -07:00
Girish Ramakrishnan 2f89e7e2b4 drop NET_RAW since this allows packet sniffing
this however breaks ping
2020-05-15 12:47:36 -07:00
Girish Ramakrishnan 437f39deb3 More changes 2020-05-15 09:16:24 -07:00
Girish Ramakrishnan 59582f16c4 skip validation in the route 2020-05-14 21:45:13 -07:00
Girish Ramakrishnan af9e3e38ce apply backup retention policy
part of #441
2020-05-14 21:31:24 -07:00
Girish Ramakrishnan d992702b87 rename to keepWithinSecs
part of #441
2020-05-14 16:45:28 -07:00
Girish Ramakrishnan 6a9fe1128f move retentionSecs inside retentionPolicy
part of #441
2020-05-14 16:33:29 -07:00
Johannes Zellner 573da29a4d Once upon a time where settings worked 2020-05-14 23:35:03 +02:00
Johannes Zellner 00cff1a728 Mention that SECRET_PLACEHOLDER is also used in dashboard client.js 2020-05-14 23:04:08 +02:00
Johannes Zellner 9bdeff0a39 Always use constants.SECRET_PLACEHOLDER 2020-05-14 23:02:02 +02:00
Girish Ramakrishnan a1f263c048 stash the backup password in filesystem for safety
we will add a release note asking the user to nuke it
2020-05-14 12:59:37 -07:00
Girish Ramakrishnan 346eac389c bind ui is hidden for this release 2020-05-14 11:57:12 -07:00
Johannes Zellner f52c16b209 Ensure encryption property on backup config always exists 2020-05-14 20:22:10 +02:00
Girish Ramakrishnan 4faf880aa4 Fix crash with unencrypted backups 2020-05-14 11:18:41 -07:00
Girish Ramakrishnan f417a49b34 Add encryptionVersion to backups
this will identify the old style backups and warn user that a restore
doesn't work anymore
2020-05-13 22:37:02 -07:00
Girish Ramakrishnan 66fd713d12 rename version to packageVersion 2020-05-13 21:55:50 -07:00
Girish Ramakrishnan 2e7630f97e remove stale logs 2020-05-13 19:23:04 -07:00
Girish Ramakrishnan 3f10524532 cleanup cache file to start encrypted rsync backups afresh 2020-05-13 16:35:13 -07:00
Johannes Zellner 51f9826918 Strip quotes for TXT records on name.com
The docs and support claim quotes are needed, but the actual API usage
shows otherwise. We do this to not break users, but ideally name.com
gives a correct and clear answer
2020-05-14 01:03:10 +02:00
Girish Ramakrishnan f5bb76333b do hmac validation on filename iv as well
also, pass encryption object instead of config
2020-05-13 10:11:07 -07:00
Girish Ramakrishnan 4947faa5ca update mail container 2020-05-12 23:19:31 -07:00
Girish Ramakrishnan 101dc3a93c s3: do not retry when testing config 2020-05-12 22:45:01 -07:00
Girish Ramakrishnan bd3ee0fa24 add changes 2020-05-12 22:00:05 -07:00
Girish Ramakrishnan 2c52668a74 remove format validation in provider config 2020-05-12 22:00:01 -07:00
Girish Ramakrishnan 03edd8c96b remove max_old_space_size
we have limited understanding of this option
2020-05-12 20:14:35 -07:00
Girish Ramakrishnan 37dfa41e01 Add hmac to the file data
https://stackoverflow.com/questions/10279403/confused-how-to-use-aes-and-hmac
https://en.wikipedia.org/wiki/Padding_oracle_attack

part of #579
2020-05-12 19:59:06 -07:00
Girish Ramakrishnan ea8a3d798e create encryption keys from password during app import & restore 2020-05-12 15:53:18 -07:00
Girish Ramakrishnan 1df94fd84d backups: generate keys from password
this also removes storage of password from db

part of #579
2020-05-12 15:14:51 -07:00
Girish Ramakrishnan 5af957dc9c add changes
part of #579
2020-05-12 10:56:07 -07:00
Girish Ramakrishnan 21073c627e rename backup key to password
Fixes #579
2020-05-12 10:55:10 -07:00
Girish Ramakrishnan 66cdba9c1a remove chat link in readme 2020-05-12 10:21:21 -07:00
Girish Ramakrishnan 56d3b38ce6 read/write iv in the encrypted files
part of #579
2020-05-11 22:35:25 -07:00
Girish Ramakrishnan 15d0275045 key must atleast be 8 chars
part of #579
2020-05-11 16:11:41 -07:00
Girish Ramakrishnan 991c1a0137 check if manifest property is present in network response 2020-05-11 14:52:55 -07:00
Girish Ramakrishnan 7d549dbbd5 logrotate: add some comments 2020-05-11 14:38:50 -07:00
Johannes Zellner e27c5583bb Apps without dockerImage cannot be auto-updated 2020-05-11 23:20:17 +02:00
Girish Ramakrishnan 650c49637f logrotate: Add turn service logs 2020-05-11 13:14:52 -07:00
Girish Ramakrishnan eb5dcf1c3e typo 2020-05-11 11:58:14 -07:00
Girish Ramakrishnan ed2b61b709 Add to changes 2020-05-10 15:35:06 -07:00
Girish Ramakrishnan 41466a3018 No need to poll every hour for updates! 2020-05-06 18:58:35 -07:00
Girish Ramakrishnan 2e130ef99d Add automatic flag for update checks
The appstore can then known if a user clicked the check for updates
button manually or if it was done by the automatic updater.

We will fix appstore so that updates are always provided for manual checks.
automatic updates will follow our roll out plan.

We do have one issue that the automatic update checker will reset the manual
updates when it runs, but this is OK.
2020-05-06 18:57:59 -07:00
Girish Ramakrishnan a96fb39a82 mail relay: fix delivery event log 2020-05-05 20:34:45 -07:00
Girish Ramakrishnan c9923c8d4b spam: large emails were not scanned 2020-05-05 15:23:27 -07:00
Girish Ramakrishnan 74b0ff338b Disallow cloudtorrent in demo mode 2020-05-04 14:56:10 -07:00
Girish Ramakrishnan dcaccc2d7a add redis status
part of #671
2020-05-03 19:46:07 -07:00
Johannes Zellner d60714e4e6 Use webmaster@ instead of support@ as LetsEncrypt fallback 2020-05-03 11:02:18 +02:00
Girish Ramakrishnan d513d5d887 appstore: Better error messages 2020-05-02 18:30:44 -07:00
Girish Ramakrishnan 386566fd4b Fcf: ix crash when no email provide with global key 2020-05-02 18:06:21 -07:00
Girish Ramakrishnan 3357ca76fe specify the invalid bind name in error message 2020-05-02 11:07:58 -07:00
Girish Ramakrishnan a183ce13ee put the status code in the error message 2020-04-30 09:24:22 -07:00
Girish Ramakrishnan e9d0ed8e1e Add binds support to containers 2020-04-29 22:51:46 -07:00
Girish Ramakrishnan 66f66fd14f docker: clean up volume API 2020-04-29 21:28:49 -07:00
Girish Ramakrishnan b49d30b477 Add OVH Object Storage backend 2020-04-29 12:47:57 -07:00
Girish Ramakrishnan 73d83ec57e Ensure stopped apps are getting backed up 2020-04-29 12:05:01 -07:00
Girish Ramakrishnan efb39fb24b refactor for addon/service/container consistency
addon - app manifest thing. part of app lifecycle
services - implementation of addon (may have containers assoc)
2020-04-28 15:32:02 -07:00
Girish Ramakrishnan 73623f2e92 add serviceConfig to appdb
part of #671
2020-04-28 15:31:58 -07:00
Girish Ramakrishnan fbcc4cfa50 Rename KNOWN_ADDONS to ADDONS 2020-04-27 22:59:35 -07:00
Girish Ramakrishnan 474a3548e0 Rename KNOWN_SERVICES to SERVICES 2020-04-27 22:59:11 -07:00
Girish Ramakrishnan 2cdf68379b Revert "add volume support"
This reverts commit b8bb69f730.

Revert this for now, we will try a simpler non-object volume first
2020-04-27 22:55:43 -07:00
Girish Ramakrishnan cc8509f8eb More 5.2 changes 2020-04-26 22:28:43 -07:00
Girish Ramakrishnan a520c1b1cb Update all docker images to use base image 2.0.0 2020-04-26 17:09:31 -07:00
Girish Ramakrishnan 75fc2cbcfb Update base image 2020-04-25 10:37:08 -07:00
Girish Ramakrishnan b8bb69f730 add volume support
part of #668, #569
2020-04-24 22:09:07 -07:00
Girish Ramakrishnan b46d3e74d6 Fix crash in cloudflare error handling 2020-04-23 12:07:54 -07:00
Girish Ramakrishnan 77a1613107 test: fix alias routes 2020-04-22 18:16:33 -07:00
Girish Ramakrishnan 62fab7b09f mail: allow alternate mx 2020-04-22 17:36:34 -07:00
Johannes Zellner 5d87352b28 backupId cannot be null during restore 2020-04-21 16:00:19 +02:00
Girish Ramakrishnan ff60f5a381 move aliases route under mailbox
since aliases can now span domains

fixes #577
2020-04-20 19:17:55 -07:00
Girish Ramakrishnan 7f666d9369 mail: implement aliases across domains
part of #577
2020-04-20 15:19:48 -07:00
Girish Ramakrishnan 442f16dbd0 more changes 2020-04-18 22:56:38 -07:00
Girish Ramakrishnan 2dcab77ed1 Fix issue where app with oauth addon will not backup or uninstall 2020-04-18 10:08:20 -07:00
Girish Ramakrishnan 13be04a169 Deny non-member email immediately 2020-04-18 02:51:31 -07:00
Girish Ramakrishnan e3767c3a54 remove obsolete isadmin flag 2020-04-18 02:32:17 -07:00
Girish Ramakrishnan ce957c8dd5 update mail container 2020-04-18 02:31:59 -07:00
Girish Ramakrishnan 0606b2994c add membersOnly flag to a mailing list 2020-04-17 17:44:14 -07:00
Girish Ramakrishnan 33acccbaaa only check the p key for dkim
this less-strict DKIM check allows users to set a stronger DKIM key
2020-04-17 12:45:21 -07:00
Girish Ramakrishnan 1e097abe86 Add note on dkim key length 2020-04-17 10:29:14 -07:00
Girish Ramakrishnan e51705c41d acme: request ECC certs 2020-04-17 10:22:01 -07:00
Girish Ramakrishnan 7eafa661fe check .well-known presence upstream
this is required for apps like nextcloud which have caldav/cardav
routes
2020-04-15 16:56:41 -07:00
Girish Ramakrishnan 2fe323e587 remove bogus internal route 2020-04-14 23:11:44 -07:00
Girish Ramakrishnan 4e608d04dc 5.1.4 changes 2020-04-11 18:45:39 -07:00
Girish Ramakrishnan 531d314e25 Show error message if gpg failed 2020-04-11 17:11:55 -07:00
Girish Ramakrishnan 1ab23d2902 fix indexOf value comparison 2020-04-11 14:21:05 -07:00
Girish Ramakrishnan b3496e1354 Add ECDHE-RSA-AES128-SHA256 to cipher list
one of our users had the site reverse proxied. it broke after the
5.1 cipher change and they nailed it down to using this cipher.

https://security.stackexchange.com/questions/72926/is-tls-ecdhe-rsa-with-aes-128-cbc-sha256-a-safe-cipher-suite-to-use
says this is safe

The following prints the cipher suite:

    log_format combined2 '$remote_addr - [$time_local] '
        '$ssl_protocol/$ssl_cipher '
        '"$request" $status $body_bytes_sent $request_time '
        '"$http_referer" "$host" "$http_user_agent"';
2020-04-10 09:49:06 -07:00
Girish Ramakrishnan 2efa0aaca4 serve custom well-known documents via nginx 2020-04-09 00:15:56 -07:00
Girish Ramakrishnan ef9aeb0772 Bump default version for tests 2020-04-08 14:24:58 -07:00
Girish Ramakrishnan 924a0136eb 5.1.3 changes 2020-04-08 13:52:53 -07:00
Girish Ramakrishnan c382fc375e Set the resetTokenCreationTime in invitation links 2020-04-08 13:11:24 -07:00
Girish Ramakrishnan 2544acddfa Fix crash with misconfigured reverse proxy
https://forum.cloudron.io/topic/2288/mastodon-terminal-not-starting
2020-04-08 09:43:43 -07:00
Johannes Zellner 58072892d6 Add 5.1.2 changes 2020-04-08 11:52:32 +02:00
Johannes Zellner 85a897c78c Remove console.log debug leftover 2020-04-08 11:48:12 +02:00
Girish Ramakrishnan 6adf5772d8 update turn config to prevent internal access
https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
2020-04-07 15:37:31 -07:00
Girish Ramakrishnan f98e3b1960 more 5.1.1 changes 2020-04-03 10:41:37 -07:00
Johannes Zellner 671a967e35 Add 5.1.1 changes 2020-04-03 13:33:03 +02:00
Johannes Zellner 950ef0074f Add libcurl3-gnutls as explicit dependency 2020-04-03 09:45:03 +02:00
Girish Ramakrishnan 5515324fd4 coturn -> turn in docker repo name 2020-04-02 19:51:14 -07:00
Girish Ramakrishnan e72622ed4f Fix crash during auto-update 2020-04-02 19:47:29 -07:00
Girish Ramakrishnan e821733a58 add note on exposed ports 2020-04-02 18:09:26 -07:00
Girish Ramakrishnan a03c0e4475 mail: disable hostname validation 2020-04-02 15:00:11 -07:00
Girish Ramakrishnan 3203821546 typo 2020-04-02 12:29:20 -07:00
Girish Ramakrishnan 16f3cee5c5 install custom nginx only on xenial
https://nginx.org/en/linux_packages.html#Ubuntu
http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/
2020-04-02 11:54:22 -07:00
Johannes Zellner 57afb46cbd Ensure nginx installation will not overwrite our conf files 2020-04-02 16:57:55 +02:00
Johannes Zellner 91dde5147a add-apt-repository does not call apt-get update 2020-04-02 13:54:39 +02:00
Johannes Zellner d0692f7379 Ensure we have latest nginx 2020-04-02 12:37:02 +02:00
Girish Ramakrishnan e360658c6e More changes 2020-04-01 17:00:01 -07:00
Girish Ramakrishnan e7dc77e6de bump mail container for mailbox size fix 2020-04-01 16:31:07 -07:00
Girish Ramakrishnan e240a8b58f add comment on the struct 2020-04-01 16:26:16 -07:00
Girish Ramakrishnan 38d4f2c27b Add note on what df output is 2020-04-01 15:59:48 -07:00
Girish Ramakrishnan 552e2a036c Use block size instead of apparent size in du
https://stackoverflow.com/questions/5694741/why-is-the-output-of-du-often-so-different-from-du-b

df uses superblock info to get consumed blocks/disk size. du with -b
prints actual file size instead of the disk space used by the files.
2020-04-01 15:24:53 -07:00
Johannes Zellner 2d4b978032 It will be 5.1.0 2020-04-01 22:30:50 +02:00
Johannes Zellner 36e00f0c84 We will release a 5.0.7 patch release first 2020-04-01 22:26:23 +02:00
Johannes Zellner ef64b2b945 Use coturn addon tag 1.0.0 2020-04-01 21:50:21 +02:00
Johannes Zellner f6cd33ae24 Set turn secret for apps 2020-04-01 21:50:09 +02:00
Girish Ramakrishnan dd109f149f mail: fix eventlog db perms 2020-04-01 12:24:54 -07:00
Girish Ramakrishnan 5b62d63463 clear mailbox on update and restore
part of #669
2020-03-31 17:51:27 -07:00
Girish Ramakrishnan 3fec599c0c remove mail domain add/remove API
merge this as a transaction into domains API

fixes #669
2020-03-31 14:48:19 -07:00
Girish Ramakrishnan e30ea9f143 make mailbox domain nullable
for apps that do not use sendmail/recvmail addon, these are now null.
otherwise, there is no way to edit the mailbox in the UI

part of #669
2020-03-31 11:26:19 -07:00
Johannes Zellner 7cb0c31c59 Also restart turn server on dashboard domain change 2020-03-31 14:52:09 +02:00
Johannes Zellner b00a7e3cbb Update turn addon 2020-03-31 10:55:41 +02:00
Johannes Zellner e63446ffa2 Support persistent turn secret 2020-03-31 09:28:57 +02:00
Girish Ramakrishnan 580da19bc2 Less strict dmarc validation
fixes #666
2020-03-30 19:32:25 -07:00
Girish Ramakrishnan 936f456cec make reset tokens only valid for a day
fixes #563

mysql timestamps cannot be null. it will become current timestamp when
set as null
2020-03-30 17:13:31 -07:00
Girish Ramakrishnan 5d6a02f73c mysql: create the my.cnf in run time dir 2020-03-30 16:32:54 -07:00
Girish Ramakrishnan b345195ea9 add missing fields in users table 2020-03-30 16:32:28 -07:00
Girish Ramakrishnan 3e6b66751c typoe in assert 2020-03-30 15:17:34 -07:00
Johannes Zellner f78571e46d Support reserved port ranges 2020-03-30 10:01:52 +02:00
Johannes Zellner f52000958c Update manifest format to 5.1.1 2020-03-30 08:43:28 +02:00
Johannes Zellner 5ac9c6ce02 add turn,stun ports to RESERVED ones
We still need to protect the TURN port range
2020-03-30 08:30:06 +02:00
Johannes Zellner 1110a67483 Add turn addon setup and teardown calls 2020-03-30 08:24:52 +02:00
Girish Ramakrishnan 57bb1280f8 better error message 2020-03-29 20:12:59 -07:00
Girish Ramakrishnan 25c000599f Fix assert (appStoreId is optional) 2020-03-29 19:12:07 -07:00
Girish Ramakrishnan 86f45e2769 Fix failing test 2020-03-29 18:55:44 -07:00
Girish Ramakrishnan 7110240e73 Only a Cloudron owner can install/update/exec apps with the docker addon
this should have been part of f1975d8f2b
2020-03-29 18:52:37 -07:00
Girish Ramakrishnan 1da37b66d8 use resource pattern in apps routes
this makes it easy to implement access control in route handlers
2020-03-29 17:11:10 -07:00
Girish Ramakrishnan f1975d8f2b only owner can install/repair/update/exec docker addon apps 2020-03-29 16:24:04 -07:00
Girish Ramakrishnan f407ce734a restrict the app to bind mount under /app/data only
rest have to be volumes
2020-03-29 13:57:45 -07:00
Girish Ramakrishnan f813cfa8db Listen only on the docker interface 2020-03-29 13:11:16 -07:00
Girish Ramakrishnan d5880cb953 TODO block is obsolete 2020-03-29 13:10:19 -07:00
Girish Ramakrishnan 95da9744c1 Prefix env vars with CLOUDRON_ 2020-03-29 09:35:34 -07:00
Girish Ramakrishnan 85c3e45cde remove oauth addon code 2020-03-29 09:35:34 -07:00
Johannes Zellner 520a396ded Use turn server with certificates 2020-03-29 09:32:48 +02:00
Johannes Zellner 13ad611c96 Remove ssh related settings from the turn container config 2020-03-29 09:32:48 +02:00
Girish Ramakrishnan 85f58d9681 more changes 2020-03-28 23:10:17 -07:00
Johannes Zellner c1de62acef Update coturn 2020-03-29 07:30:42 +02:00
Johannes Zellner 7e47e36773 Fix portrange notation in firewall service 2020-03-29 07:25:36 +02:00
Johannes Zellner 00b6217cab Fix turn tls port 2020-03-29 07:09:17 +02:00
Girish Ramakrishnan acc2b5a1a3 remove unused param 2020-03-28 22:05:43 -07:00
Girish Ramakrishnan b06feaa36b more changes 2020-03-28 17:48:55 -07:00
Johannes Zellner 89cf8a455a Allow turn and stun service ports 2020-03-28 23:33:44 +01:00
Johannes Zellner 710046a94f Add coturn addon service 2020-03-28 22:46:32 +01:00
Johannes Zellner b366b0fa6a Stop container with isCloudronManged labels instead of by network 2020-03-28 22:46:32 +01:00
Girish Ramakrishnan f9e7a8207a cloudron-support: make it --owner-login 2020-03-27 18:58:12 -07:00
Johannes Zellner 6178bf3d4b Update sftp addon 2020-03-27 14:54:35 +01:00
Girish Ramakrishnan f3b979f112 More 5.0.6 changelog 2020-03-26 21:56:18 -07:00
Girish Ramakrishnan 9faae96d61 make app password work with sftp 2020-03-26 21:50:25 -07:00
Girish Ramakrishnan 2135fe5dd0 5.0.6 changelog
(cherry picked from commit 3c1a1f1b81)
2020-03-26 19:32:58 -07:00
Girish Ramakrishnan 007a8d248d make eventlog routes owner only 2020-03-26 18:54:16 -07:00
Girish Ramakrishnan 58d4a3455b email: add type filter to eventlog 2020-03-25 22:05:49 -07:00
Girish Ramakrishnan 8e3c14f245 5.0.5 changes
(cherry picked from commit cc6ddf50b1)
2020-03-25 08:13:38 -07:00
Girish Ramakrishnan 91af2495a6 Make key validation work for ecc certs 2020-03-24 21:20:21 -07:00
Girish Ramakrishnan 7d7df5247b Update cipher suite based on ssl-config recommendation
ssl_prefer_server_ciphers off is the recommendation since the cpihers
are deprecated

https://serverfault.com/questions/997614/setting-ssl-prefer-server-ciphers-directive-in-nginx-config
2020-03-24 19:24:58 -07:00
Girish Ramakrishnan f99450d264 Enable TLSv1.3 and remove TLSv1 and 1.1
IE10 does not have 1.2, so maybe we can risk it

As per Android documentaion TLS 1.2 is fully supported after API level 20/Android 5(Lolipop)

https://discussions.qualys.com/thread/17020-tls-12-support-for-android-devices
https://www.ryandesignstudio.com/what-is-tls/
2020-03-24 14:37:08 -07:00
Girish Ramakrishnan d3eeb5f48a mail: disable host and proto mismatch 2020-03-24 11:50:52 -07:00
Girish Ramakrishnan 1e8a02f91a Make token expiry a year
we now have a UI to invalid all tokens easily, so this should be OK.
2020-03-23 21:51:13 -07:00
Girish Ramakrishnan 97c3bd8b8e mail: incoming mail from dynamic hostnames was rejected 2020-03-23 21:50:36 -07:00
Girish Ramakrishnan 09ce27d74b bump default token expiry to a month 2020-03-21 18:46:38 -07:00
Girish Ramakrishnan 2447e91a9f mail: throttle denied events 2020-03-20 14:04:16 -07:00
Girish Ramakrishnan e6d881b75d Use owner email for LE certs
https://forum.cloudron.io/topic/2244/email-contact-on-let-s-encrypt-ssl-tls-certificates-uses-password-recovery-email-rather-than-primary-email-address
2020-03-20 13:39:58 -07:00
Girish Ramakrishnan 36f963dce8 remove unncessary debug in routes 2020-03-19 17:05:31 -07:00
Girish Ramakrishnan 1b15d28212 eventlog: add start/stop/restart logs 2020-03-19 17:02:55 -07:00
Girish Ramakrishnan 4e0c15e102 use short form syntax 2020-03-19 16:48:31 -07:00
Girish Ramakrishnan c9e40f59de bump the timeout for really slow disks 2020-03-19 13:33:53 -07:00
Girish Ramakrishnan 38cf31885c Make backup configure owner only 2020-03-18 17:23:23 -07:00
Girish Ramakrishnan 4420470242 comcast does not allow port 25 check anymore 2020-03-17 13:55:35 -07:00
Girish Ramakrishnan 9b05786615 appstore: add whitelist/blacklist 2020-03-15 17:20:48 -07:00
Girish Ramakrishnan 725b2c81ee custom.yml is obsolete 2020-03-15 16:50:42 -07:00
Girish Ramakrishnan 661965f2e0 Add branding tests 2020-03-15 16:38:15 -07:00
Girish Ramakrishnan 7e0ef60305 Fix incorrect role comparison 2020-03-15 16:19:22 -07:00
Girish Ramakrishnan 2ac0fe21c6 ghost file depends on base dir 2020-03-15 11:41:39 -07:00
Girish Ramakrishnan b997f2329d make branding route for owner only 2020-03-15 11:39:02 -07:00
Girish Ramakrishnan 23ee758ac9 do not check for updates for stopped apps 2020-03-15 09:48:08 -07:00
Girish Ramakrishnan 9ea12e71f0 linode: dns backend
the dns is very slow - https://github.com/certbot/certbot/pull/6320
takes a good 15 minutes at minimum to propagate

https://certbot-dns-linode.readthedocs.io/en/stable/
https://www.linode.com/community/questions/17296/linode-dns-propagation-time
2020-03-13 11:44:43 -07:00
Girish Ramakrishnan d3594c2dd6 change ownership of ghost file for good measure 2020-03-12 10:30:51 -07:00
Girish Ramakrishnan 6ee4b0da27 Move out ghost file to platformdata
Since /tmp is world writable this might cause privilege escalation

https://forum.cloudron.io/topic/2222/impersonate-user-privilege-escalation
2020-03-12 10:24:21 -07:00
Girish Ramakrishnan 3e66feb514 mail: add mailbox acl 2020-03-10 22:12:15 -07:00
Girish Ramakrishnan cd91a5ef64 5.0.3 changes 2020-03-10 17:18:21 -07:00
Girish Ramakrishnan cf89609633 mail: acl was enabled by mistake 2020-03-10 17:15:23 -07:00
173 changed files with 8186 additions and 5266 deletions
+270
View File
@@ -1821,3 +1821,273 @@
* restore: carefully replace backup config
* spam: per mailbox bayes db and training
[5.0.3]
* Show backup disk usage in graphs
* Add per-user app passwords
* Make app not responding page customizable
* Make footer customizable
* Add UI to import backups
* Display timestamps in browser timezone in the UI
* Mail eventlog and usage
* Add user roles - owner, admin, user manager and user
* Setup logrotate configs for collectd since upstream does not set it up
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
* linode: add object storage backend
* restore: carefully replace backup config
* spam: per mailbox bayes db and training
[5.0.4]
* Fix potential previlige escalation because of ghost file
* linode: dns backend
* make branding routes owner only
* add branding API
* Add app start/stop/restart events
* Use the primary email for LE account
* make mail eventlog more descriptive
[5.0.5]
* Fix bug where incoming mail from dynamic hostnames was rejected
* Increase token expiry
* Fix bug in tag UI where tag removal did not work
[5.0.6]
* Make mail eventlog only visible to owners
* Make app password work with sftp
[5.1.0]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
[5.1.1]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
* Fix collectd installation
* graphs: sort disk contents by usage
* backups: show apps that are not automatically backed up in backup view
[5.1.2]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs
* Fix collectd installation
* graphs: sort disk contents by usage
* backups: show apps that are not automatically backed up in backup view
* turn: deny local address peers https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
[5.1.3]
* Fix crash with misconfigured reverse proxy
* Fix issue where invitation links are not working anymore
[5.1.4]
* Add support for custom .well-known documents to be served
* Add ECDHE-RSA-AES128-SHA256 to cipher list
* Fix GPG signature verification
[5.1.5]
* Check for .well-known routes upstream as fallback. This broke nextcloud's caldav/carddav
[5.2.0]
* acme: request ECC certs
* less-strict DKIM check to allow users to set a stronger DKIM key
* Add members only flag to mailing list
* oauth: add backward compat layer for backup and uninstall
* fix bug in disk usage sorting
* mail: aliases can be across domains
* mail: allow an external MX to be set
* Add UI to download backup config as JSON (and import it)
* Ensure stopped apps are getting backed up
* Add OVH Object Storage backend
* Add per-app redis status and configuration to Services
* spam: large emails were not scanned
* mail relay: fix delivery event log
* manual update check always gets the latest updates
* graphs: fix issue where large number of apps would crash the box code (query param limit exceeded)
* backups: fix various security issues in encypted backups (thanks @mehdi)
* graphs: add app graphs
* older encrypted backups cannot be used in this version
* Add backup listing UI
* stopping an app will stop dependent services
* Add new wasabi s3 storage region us-east-2
* mail: Fix bug where SRS translation was done on the main domain instead of mailing list domain
* backups: add retention policy
* Drop `NET_RAW` caps from container preventing sniffing of network traffic
[5.2.1]
* Fix app disk graphs
* restart apps on addon container change
[5.2.2]
* regression: import UI
* Mbps -> MBps
* Remove verbose logs
* Set dmode in tar extract
* mail: fix crash in audit logs
* import: fix crash because encryption is unset
* create redis with the correct label
[5.2.3]
* Do not restart stopped apps
[5.2.4]
* mail: enable/disable incoming mail was showing an error
* Do not trigger backup of stopped apps. Instead, we will just retain it's existing backups
based on retention policy
* remove broken disk graphs
* fix OVH backups
[5.3.0]
* better nginx config for higher loads
* backups: add CIFS storage provider
* backups: add SSHFS storage provider
* backups: add NFS storage provider
* s3: use vhost style
* Fix crash when redis config was set
* Update schedule was unselected in the UI
* cloudron-setup: --provider is now optional
* show warning for unstable updates
* add forumUrl to app manifest
* postgresql: add unaccent extension for peertube
* mail: Add Auto-Submitted header to NDRs
* backups: ensure that the latest backup of installed apps is always preserved
* add nginx logs
* mail: make authentication case insensitive
* Fix timeout issues in postgresql and mysql addon
* Do not count stopped apps for memory use
* LDAP group synchronization
[5.3.1]
* better nginx config for higher loads
* backups: add CIFS storage provider
* backups: add SSHFS storage provider
* backups: add NFS storage provider
* s3: use vhost style
* Fix crash when redis config was set
* Update schedule was unselected in the UI
* cloudron-setup: --provider is now optional
* show warning for unstable updates
* add forumUrl to app manifest
* postgresql: add unaccent extension for peertube
* mail: Add Auto-Submitted header to NDRs
* backups: ensure that the latest backup of installed apps is always preserved
* add nginx logs
* mail: make authentication case insensitive
* Fix timeout issues in postgresql and mysql addon
* Do not count stopped apps for memory use
* LDAP group synchronization
[5.3.2]
* Do not install sshfs package
* 'provider' is not required anymore in various API calls
* redis: Set maxmemory and maxmemory-policy
* Add mlock capability to manifest (for vault app)
[5.3.3]
* Fix issue where some postinstall messages where causing angular to infinite loop
[5.3.4]
* Fix issue in database error handling
[5.4.0]
* Update nginx to 1.18 for various security fixes
* Add ping capability (for statping app)
* Fix bug where aliases were displayed incorrectly in SOGo
* Add univention as LDAP provider
* Bump max_connection for postgres addon to 200
* mail: Add pagination to mailing list API
* Allow admin to lock email and display name of users
* Allow admin to ensure all users have 2FA setup
* ami: fix regression where we didn't send provider as part of get status call
* nginx: hide version
* backups: add b2 provider
* Add filemanager webinterface
* Add darkmode
* Add note that password reset and invite links expire in 24 hours
[5.4.1]
* Update nginx to 1.18 for various security fixes
* Add ping capability (for statping app)
* Fix bug where aliases were displayed incorrectly in SOGo
* Add univention as LDAP provider
* Bump max_connection for postgres addon to 200
* mail: Add pagination to mailing list API
* Allow admin to lock email and display name of users
* Allow admin to ensure all users have 2FA setup
* ami: fix regression where we didn't send provider as part of get status call
* nginx: hide version
* backups: add b2 provider
* Add filemanager webinterface
* Add darkmode
* Add note that password reset and invite links expire in 24 hours
[5.5.0]
* postgresql: update to PostgreSQL 11
* postgresql: add citext extension to whitelist for loomio
* postgresql: add btree_gist,postgres_fdw,pg_stat_statements,plpgsql extensions for gitlab
* SFTP/Filebrowser: fix access of external data directories
* Fix contrast issues in dark mode
* Add option to delete mailbox data when mailbox is delete
* Allow days/hours of backups and updates to be configurable
* backup cleaner: fix issue where referenced backups where not counted against time periods
* route53: fix issue where verification failed if user had more than 100 zones
* rework task workers to run them in a separate cgroup
* backups: now much faster thanks to reworking of task worker
* When custom fallback cert is set, make sure it's used over LE certs
* mongodb: update to MongoDB 4.0.19
* List groups ordered by name
* Invite links are now valid for a week
* Update release GPG key
* Add pre-defined variables ($CLOUDRON_APPID) for better post install messages
* filemanager: show folder first
[5.6.0]
* Remove IP nginx configuration that redirects to dashboard after activation
* dashboard: looks for search string in app title as well
* Add vaapi caps for transcoding
* Fix issue where the long mongodb database names where causing app indices of rocket.chat to overflow (> 127)
* Do not resize swap if swap file exists. This means that users can now control how swap is allocated on their own.
* SFTP: fix issue where parallel rebuilds would cause an error
* backups: make part size configurable
* mail: set max email size
* mail: allow mail server location to be set
* spamassassin: custom configs and wl/bl
* Do not automatically update to unstable release
* scheduler: reduce container churn
* mail: add API to set banner
* Fix bug where systemd 237 ignores --nice value in systemd-run
* postgresql: enable uuid-ossp extension
* firewall: add blocklist
* HTTP URLs now redirect directly to the HTTPS of the final domain
* linode: Add singapore region
* ovh: add sydney region
* s3: makes multi-part copies in parallel
[5.6.1]
* Blocklists are now stored in a text file instead of json
* regenerate nginx configs
+5 -15
View File
@@ -29,9 +29,9 @@ anyone to effortlessly host web applications on their server on their own terms.
* Trivially migrate to another server keeping your apps and data (for example, switch your
infrastructure provider or move to a bigger server).
* Comprehensive [REST API](https://cloudron.io/documentation/developer/api/).
* Comprehensive [REST API](https://docs.cloudron.io/api/).
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
* [CLI](https://docs.cloudron.io/custom-apps/cli/) to configure apps.
* Alerts, audit logs, graphs, dns management ... and much more
@@ -41,25 +41,15 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
## Installing
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
[Install script](https://docs.cloudron.io/installation/) - [Pricing](https://cloudron.io/pricing.html)
**Note:** This repo is a small part of what gets installed on your server - there is
the dashboard, database addons, graph container, base image etc. Cloudron also relies
on external services such as the App Store for apps to be installed. As such, don't
clone this repo and npm install and expect something to work.
## Documentation
## Support
* [Documentation](https://cloudron.io/documentation/)
## Related repos
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
the containers in the Cloudron.
## Community
* [Chat](https://chat.cloudron.io)
* [Documentation](https://docs.cloudron.io/)
* [Forum](https://forum.cloudron.io/)
* [Support](mailto:support@cloudron.io)
+17 -9
View File
@@ -4,8 +4,7 @@ set -euv -o pipefail
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly arg_provider="${1:-generic}"
readonly arg_infraversionpath="${SOURCE_DIR}/${2:-}"
readonly arg_infraversionpath="${SOURCE_DIR}/../src"
function die {
echo $1
@@ -14,6 +13,9 @@ function die {
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
@@ -27,8 +29,6 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
ubuntu_version=$(lsb_release -rs)
ubuntu_codename=$(lsb_release -cs)
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
apt-get -y install \
acl \
@@ -39,12 +39,12 @@ apt-get -y install \
debconf-utils \
dmsetup \
$gpg_package \
ipset \
iptables \
libpython2.7 \
linux-generic \
logrotate \
mysql-server-5.7 \
nginx-full \
openssh-server \
pwgen \
resolvconf \
@@ -54,6 +54,12 @@ apt-get -y install \
unbound \
xfsprogs
echo "==> installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${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 sudo
@@ -63,7 +69,7 @@ cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upg
echo "==> Installing node.js"
mkdir -p /usr/local/node-10.18.1
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.18.1
curl -sL https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-10.18.1
ln -sf /usr/local/node-10.18.1/bin/node /usr/bin/node
ln -sf /usr/local/node-10.18.1/bin/npm /usr/bin/npm
apt-get install -y python # Install python which is required for npm rebuild
@@ -111,7 +117,7 @@ for image in ${images}; do
done
echo "==> Install collectd"
if ! apt-get install -y collectd collectd-utils; then
if ! apt-get install -y 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
@@ -124,11 +130,13 @@ timedatectl set-ntp 1
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://cloudron.io/documentation/security/#securing-ssh-access' -i /etc/ssh/sshd_config
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"
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/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 and conflicts with unbound)
systemctl stop bind9 || true
+43 -39
View File
@@ -2,57 +2,61 @@
'use strict';
// prefix all output with a timestamp
// debug() already prefixes and uses process.stderr NOT console.*
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
var orig = console[log];
console[log] = function () {
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
};
});
require('supererror')({ splatchError: true });
let async = require('async'),
constants = require('./src/constants.js'),
dockerProxy = require('./src/dockerproxy.js'),
fs = require('fs'),
ldap = require('./src/ldap.js'),
paths = require('./src/paths.js'),
server = require('./src/server.js');
console.log();
console.log('==========================================');
console.log(` Cloudron ${constants.VERSION} `);
console.log('==========================================');
console.log();
const NOOP_CALLBACK = function () { };
function setupLogging(callback) {
if (process.env.BOX_ENV === 'test') return callback();
var logfileStream = fs.createWriteStream(paths.BOX_LOG_FILE, { flags:'a' });
process.stdout.write = process.stderr.write = logfileStream.write.bind(logfileStream);
callback();
}
async.series([
setupLogging,
server.start,
ldap.start,
dockerProxy.start
], function (error) {
if (error) {
console.error('Error starting server', error);
console.log('Error starting server', error);
process.exit(1);
}
console.log('Cloudron is up and running');
});
var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
console.log('Received SIGINT. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
console.log('Received SIGTERM. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
// require those here so that logging handler is already setup
require('supererror');
const debug = require('debug')('box:box');
process.on('SIGINT', function () {
debug('Received SIGINT. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
debug('Received SIGTERM. Shutting down.');
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
dockerProxy.stop(NOOP_CALLBACK);
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);
});
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
});
@@ -12,8 +12,6 @@ exports.up = function(db, callback) {
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
if (error) return done(error);
console.dir(results);
async.eachSeries(results, function (r, next) {
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
}, done);
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN resetTokenCreationTime', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,28 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY mailboxDomain VARCHAR(128)', [], function (error) { // make it nullable
if (error) console.error(error);
// clear mailboxName/Domain for apps that do not use mail addons
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
var manifest = JSON.parse(app.manifestJson);
if (manifest.addons['sendmail'] || manifest.addons['recvmail']) return iteratorDone();
db.runSql('UPDATE apps SET mailboxName=?, mailboxDomain=? WHERE id=?', [ null, null, app.id ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mailboxes ADD COLUMN membersOnly BOOLEAN DEFAULT 0', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN membersOnly', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,28 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN aliasDomain VARCHAR(128)'),
function setAliasDomain(done) {
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
if (!mailbox.aliasTarget) return iteratorDone();
db.runSql('UPDATE mailboxes SET aliasDomain=? WHERE name=? AND domain=?', [ mailbox.domain, mailbox.name, mailbox.domain ], iteratorDone);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD CONSTRAINT mailboxes_aliasDomain_constraint FOREIGN KEY(aliasDomain) REFERENCES mail(domain)'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasTarget aliasName VARCHAR(128)')
], callback);
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP FOREIGN KEY mailboxes_aliasDomain_constraint'),
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN aliasDomain'),
db.runSql.bind(db, 'ALTER TABLE mailboxes CHANGE aliasName aliasTarget VARCHAR(128)')
], callback);
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN servicesConfigJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN servicesConfigJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN bindsJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,35 @@
'use strict';
const backups = require('../src/backups.js'),
fs = require('fs');
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
if (backupConfig.key) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.key);
backups.cleanupCacheFilesSync();
fs.writeFileSync('/home/yellowtent/platformdata/BACKUP_PASSWORD',
'This file contains your Cloudron backup password.\nBefore Cloudron v5.2, this was saved in the database.' +
'From Cloudron 5.2, this password is not required anymore. We generate strong keys based off this password and use those keys to encrypt the backups.\n' +
'This means that the password is only required at decryption/restore time.\n\n' +
'This file can be safely removed and only exists for the off-chance that you do not remember your backup password.\n\n' +
`Password: ${backupConfig.key}\n`,
'utf8');
} else {
backupConfig.encryption = null;
}
delete backupConfig.key;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE version packageVersion VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE packageVersion version VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,24 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN encryptionVersion INTEGER', function (error) {
if (error) return callback(error);
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
if (!backupConfig.encryption) return callback(null);
// mark old encrypted backups as v1
db.runSql('UPDATE backups SET encryptionVersion=1', callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN encryptionVersion', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,18 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
backupConfig.retentionPolicy = { keepWithinSecs: backupConfig.retentionSecs };
delete backupConfig.retentionSecs;
// mark old encrypted backups as v1
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,18 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
if (backupConfig.provider !== 'minio' && backupConfig.provider !== 's3-v4-compat') return callback();
backupConfig.s3ForcePathStyle = true; // usually minio is self-hosted. s3 v4 compat, we don't know
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,17 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
// http://stackoverflow.com/questions/386294/what-is-the-maximum-length-of-a-valid-email-address
async.series([
db.runSql.bind(db, 'ALTER TABLE appPasswords DROP INDEX name'),
db.runSql.bind(db, 'ALTER TABLE appPasswords ADD CONSTRAINT appPasswords_name_userId_identifier UNIQUE (name, userId, identifier)'),
], callback);
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,17 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE userGroups ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
if (error) return callback(error);
callback();
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE userGroups DROP COLUMN source', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,38 @@
'use strict';
const async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN identifier VARCHAR(128)', function (error) {
if (error) return callback(error);
db.all('SELECT * FROM backups', function (error, backups) {
if (error) return callback(error);
async.eachSeries(backups, function (backup, next) {
let identifier = 'unknown';
if (backup.type === 'box') {
identifier = 'box';
} else {
const match = backup.id.match(/app_(.+?)_.+/);
if (match) identifier = match[1];
}
db.runSql('UPDATE backups SET identifier=? WHERE id=?', [ identifier, backup.id ], next);
}, function (error) {
if (error) return callback(error);
db.runSql('ALTER TABLE backups MODIFY COLUMN identifier VARCHAR(128) NOT NULL', callback);
});
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN identifier', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,16 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
db.runSql('ALTER TABLE users DROP COLUMN modifiedAt', callback);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN ts', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,29 @@
'use strict';
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
if (error || results.length === 0) return callback(error);
var backupConfig = JSON.parse(results[0].value);
if (backupConfig.intervalSecs === 6 * 60 * 60) { // every 6 hours
backupConfig.schedulePattern = '00 00 5,11,17,23 * * *';
} else if (backupConfig.intervalSecs === 12 * 60 * 60) { // every 12 hours
backupConfig.schedulePattern = '00 00 5,17 * * *';
} else if (backupConfig.intervalSecs === 24 * 60 * 60) { // every day
backupConfig.schedulePattern = '00 00 23 * * *';
} else if (backupConfig.intervalSecs === 3 * 24 * 60 * 60) { // every 3 days (based on day)
backupConfig.schedulePattern = '00 00 23 * * 1,3,5';
} else if (backupConfig.intervalSecs === 7 * 24 * 60 * 60) { // every week (saturday)
backupConfig.schedulePattern = '00 00 23 * * 6';
} else { // default to everyday
backupConfig.schedulePattern = '00 00 23 * * *';
}
delete backupConfig.intervalSecs;
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,23 @@
'use strict';
const async = require('async');
exports.up = function(db, callback) {
db.all('SELECT value FROM settings WHERE name="admin_domain"', function (error, results) {
if (error || results.length === 0) return callback(error);
const adminDomain = results[0].value;
async.series([
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_domain', adminDomain ]),
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_fqdn', `my.${adminDomain}` ])
], callback);
});
};
exports.down = function(db, callback) {
async.series([
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_domain"'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_fqdn"'),
], callback);
};
@@ -0,0 +1,22 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
db.runSql('SELECT * FROM settings WHERE name=?', ['app_autoupdate_pattern'], function (error, results) {
if (error || results.length === 0) return callback(error); // will use defaults from box code
var updatePattern = results[0].value; // use app auto update patter for the box as well
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'DELETE FROM settings WHERE name=? OR name=?', ['app_autoupdate_pattern', 'box_autoupdate_pattern']),
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['autoupdate_pattern', updatePattern]),
db.runSql.bind(db, 'COMMIT')
], callback);
});
};
exports.down = function(db, callback) {
callback();
};
@@ -0,0 +1,15 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE mail ADD COLUMN bannerJson TEXT', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE mail DROP COLUMN bannerJson', function (error) {
if (error) console.error(error);
callback(error);
});
};
@@ -0,0 +1,27 @@
'use strict';
const OLD_FIREWALL_CONFIG_JSON = '/home/yellowtent/boxdata/firewall-config.json';
const PORTS_FILE = '/home/yellowtent/boxdata/firewall/ports.json';
const BLOCKLIST_FILE = '/home/yellowtent/boxdata/firewall/blocklist.txt';
const fs = require('fs');
exports.up = function (db, callback) {
if (!fs.existsSync(OLD_FIREWALL_CONFIG_JSON)) return callback();
try {
const dataJson = fs.readFileSync(OLD_FIREWALL_CONFIG_JSON, 'utf8');
const data = JSON.parse(dataJson);
fs.writeFileSync(BLOCKLIST_FILE, data.blocklist.join('\n') + '\n', 'utf8');
fs.writeFileSync(PORTS_FILE, JSON.stringify({ allowed_tcp_ports: data.allowed_tcp_ports }, null, 4), 'utf8');
fs.unlinkSync(OLD_FIREWALL_CONFIG_JSON);
} catch (error) {
console.log('Error migrating old firewall config', error);
}
callback();
};
exports.down = function (db, callback) {
callback();
};
+19 -9
View File
@@ -21,19 +21,23 @@ CREATE TABLE IF NOT EXISTS users(
password VARCHAR(1024) NOT NULL,
salt VARCHAR(512) NOT NULL,
createdAt VARCHAR(512) NOT NULL,
modifiedAt VARCHAR(512) NOT NULL,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
displayName VARCHAR(512) DEFAULT "",
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
role VARCHAR(32),
resetToken VARCHAR(128) DEFAULT "",
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT 1,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS userGroups(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(254) NOT NULL UNIQUE,
source VARCHAR(128) DEFAULT "",
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS groupMembers(
@@ -62,8 +66,6 @@ CREATE TABLE IF NOT EXISTS apps(
containerId VARCHAR(128),
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL,
domain VARCHAR(128) NOT NULL,
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
@@ -76,13 +78,15 @@ CREATE TABLE IF NOT EXISTS apps(
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
mailboxDomain VARCHAR(128) NOT NULL, // mailbox domain of this apps
mailboxName VARCHAR(128), // mailbox of this app
mailboxDomain VARCHAR(128), // mailbox domain of this apps
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
taskId INTEGER, // current task
errorJson TEXT,
bindsJson TEXT, // bind mounts
servicesConfigJson TEXT, // app services configuration
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
FOREIGN KEY(taskId) REFERENCES tasks(id),
@@ -117,8 +121,10 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
CREATE TABLE IF NOT EXISTS backups(
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
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 */
state VARCHAR(16) NOT NULL,
manifestJson TEXT, /* to validate if the app can be installed in this version of box */
@@ -155,6 +161,7 @@ CREATE TABLE IF NOT EXISTS mail(
mailFromValidation BOOLEAN DEFAULT 1,
catchAllJson TEXT,
relayJson TEXT,
bannerJson TEXT,
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
@@ -174,12 +181,15 @@ CREATE TABLE IF NOT EXISTS mailboxes(
name VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
ownerId VARCHAR(128) NOT NULL, /* user id */
aliasTarget VARCHAR(128), /* the target name type is an alias */
aliasName VARCHAR(128), /* the target name type is an alias */
aliasDomain VARCHAR(128), /* the target domain */
membersJson TEXT, /* members of a group. fully qualified */
membersOnly BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
FOREIGN KEY(domain) REFERENCES mail(domain),
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
UNIQUE (name, domain));
CREATE TABLE IF NOT EXISTS subdomains(
@@ -211,7 +221,7 @@ CREATE TABLE IF NOT EXISTS notifications(
message TEXT,
acknowledged BOOLEAN DEFAULT false,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY appPasswords_name_appId_identifier (name, userId, identifier),
PRIMARY KEY (id)
);
@@ -223,7 +233,7 @@ CREATE TABLE IF NOT EXISTS appPasswords(
hashedPassword VARCHAR(1024) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(userId) REFERENCES users(id),
UNIQUE (name, userId),
PRIMARY KEY (id)
);
+374 -308
View File
File diff suppressed because it is too large Load Diff
+22 -21
View File
@@ -18,67 +18,68 @@
"@google-cloud/storage": "^2.5.0",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^2.6.3",
"aws-sdk": "^2.610.0",
"aws-sdk": "^2.685.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^4.0.0",
"cloudron-manifestformat": "^5.6.0",
"connect": "^3.7.0",
"connect-lastmile": "^1.2.2",
"connect-lastmile": "^2.0.0",
"connect-timeout": "^1.9.0",
"cookie-session": "^1.4.0",
"cron": "^1.8.2",
"db-migrate": "^0.11.6",
"db-migrate": "^0.11.11",
"db-migrate-mysql": "^1.1.10",
"debug": "^4.1.1",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"ejs-cli": "^2.1.1",
"ejs-cli": "^2.2.0",
"express": "^4.17.1",
"js-yaml": "^3.13.1",
"ipaddr.js": "^2.0.0",
"js-yaml": "^3.14.0",
"json": "^9.0.6",
"ldapjs": "^1.0.2",
"lodash": "^4.17.15",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.4",
"moment-timezone": "^0.5.27",
"morgan": "^1.9.1",
"mime": "^2.4.6",
"moment": "^2.26.0",
"moment-timezone": "^0.5.31",
"morgan": "^1.10.0",
"multiparty": "^4.2.1",
"mysql": "^2.18.1",
"nodemailer": "^6.4.2",
"nodemailer": "^6.4.6",
"nodemailer-smtp-transport": "^2.7.4",
"once": "^1.4.0",
"parse-links": "^0.1.0",
"pretty-bytes": "^5.3.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.4.4",
"readdirp": "^3.3.0",
"request": "^2.88.0",
"readdirp": "^3.4.0",
"request": "^2.88.2",
"rimraf": "^2.6.3",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^1.0.0",
"safetydance": "^1.1.1",
"semver": "^6.1.1",
"showdown": "^1.9.1",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^5.2.1",
"superagent": "^5.2.2",
"supererror": "^0.7.2",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.1.0",
"tar-stream": "^2.1.2",
"tldjs": "^2.3.1",
"underscore": "^1.9.2",
"underscore": "^1.10.2",
"uuid": "^3.4.0",
"validator": "^11.0.0",
"ws": "^7.2.1",
"ws": "^7.3.0",
"xml2js": "^0.4.23"
},
"devDependencies": {
"expect.js": "*",
"hock": "^1.3.3",
"js2xmlparser": "^4.0.0",
"hock": "^1.4.1",
"js2xmlparser": "^4.0.1",
"mocha": "^6.1.4",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^10.0.6",
"node-sass": "^4.12.0",
"node-sass": "^4.14.1",
"recursive-readdir": "^2.2.2"
},
"scripts": {
+8 -60
View File
@@ -41,21 +41,19 @@ if systemctl -q is-active box; then
fi
initBaseImage="true"
# provisioning data
provider=""
provider="generic"
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
sourceTarballUrl=""
rebootServer="true"
license=""
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,license:" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
case "$1" in
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
--help) echo "See https://docs.cloudron.io/installation/ on how to install Cloudron"; exit 0;;
--provider) provider="$2"; shift 2;;
--version) requestedVersion="$2"; shift 2;;
--env)
@@ -67,7 +65,6 @@ while true; do
webServerOrigin="https://staging.cloudron.io"
fi
shift 2;;
--license) license="$2"; shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--) break;;
@@ -91,48 +88,6 @@ fi
# Can only write after we have confirmed script has root access
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
if [[ -z "${provider}" ]]; then
echo "--provider is required ($AVAILABLE_PROVIDERS)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "azure-image" && \
"${provider}" != "caas" && \
"${provider}" != "cloudscale" && \
"${provider}" != "contabo" && \
"${provider}" != "digitalocean" && \
"${provider}" != "digitalocean-mp" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "interox" && \
"${provider}" != "interox-image" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "linode-oneclick" && \
"${provider}" != "linode-stackscript" && \
"${provider}" != "netcup" && \
"${provider}" != "netcup-image" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "skysilk" && \
"${provider}" != "skysilk-image" && \
"${provider}" != "time4vps" && \
"${provider}" != "time4vps-image" && \
"${provider}" != "upcloud" && \
"${provider}" != "upcloud-image" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
exit 1
fi
echo ""
echo "##############################################"
echo " Cloudron Setup (${requestedVersion:-latest})"
@@ -151,12 +106,6 @@ if [[ "${initBaseImage}" == "true" ]]; then
exit 1
fi
echo "=> Ensure required apt sources"
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
exit 1
fi
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories. See ${LOG_FILE}"
@@ -196,20 +145,19 @@ fi
if [[ "${initBaseImage}" == "true" ]]; then
echo -n "=> Installing base dependencies and downloading docker images (this takes some time) ..."
if ! /bin/bash "${box_src_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${provider}" "../src" &>> "${LOG_FILE}"; then
# 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 ""
fi
# NOTE: this install script only supports 4.2 and above
# The provider flag is still used for marketplace images
echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
echo "${provider}" > /etc/cloudron/PROVIDER
[[ -n "${license}" ]] && echo -n "$license" > /etc/cloudron/LICENSE
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
@@ -221,13 +169,13 @@ mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
if status=$($curl -s -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
break # we are up and running
fi
sleep 10
done
if ! 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
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
ip='<IP>'
fi
echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
+12 -7
View File
@@ -13,7 +13,7 @@ HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--admin-login Login as administrator
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
@@ -26,7 +26,7 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -34,10 +34,15 @@ while true; do
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--admin-login)
# fall through
;&
--owner-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > /tmp/cloudron_ghost.json
echo "Login as ${admin_username} / ${admin_password} . Remove /tmp/cloudron_ghost.json when done."
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} . Remove ${ghost_file} when done."
exit 0
;;
--) break;;
@@ -52,7 +57,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
echo ""
df -h
echo ""
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
echo "To recover from a full disk, follow the guide at https://docs.cloudron.io/troubleshooting/#recovery-after-disk-full"
exit 1
fi
@@ -89,7 +94,7 @@ echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
du -hcsL /var/backups/* &>> $OUT || true
echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
systemctl status --lines=100 box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
echo -e $LINE"Box logs"$LINE >> $OUT
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
@@ -107,7 +112,7 @@ if [[ "${enableSSH}" == "true" ]]; then
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
# support.js uses similar logic
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
if [[ -d /home/ubuntu ]]; then
ssh_user="ubuntu"
keys_file="/home/ubuntu/.ssh/authorized_keys"
else
+26 -11
View File
@@ -11,9 +11,8 @@ if [[ ${EUID} -ne 0 ]]; then
exit 1
fi
readonly USER=yellowtent
readonly BOX_SRC_DIR=/home/${USER}/box
readonly BASE_DATA_DIR=/home/${USER}
readonly user=yellowtent
readonly box_src_dir=/home/${user}/box
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -24,6 +23,8 @@ readonly ubuntu_codename=$(lsb_release -cs)
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
echo "==> installer: Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION) <=="
echo "==> installer: updating docker"
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
@@ -56,6 +57,20 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
fi
readonly nginx_version=$(nginx -v 2>&1)
if [[ "${nginx_version}" != *"1.18."* ]]; then
echo "==> installer: installing nginx 1.18"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-1~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
rm /tmp/nginx.deb
fi
if ! which ipset; then
echo "==> installer: installing ipset"
apt install -y ipset
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.18.1" ]]; then
mkdir -p /usr/local/node-10.18.1
@@ -109,22 +124,22 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
sleep 5
done
if ! id "${USER}" 2>/dev/null; then
useradd "${USER}" -m
if ! id "${user}" 2>/dev/null; then
useradd "${user}" -m
fi
if [[ "${is_update}" == "yes" ]]; then
echo "==> installer: stop cloudron.target service for update"
${BOX_SRC_DIR}/setup/stop.sh
echo "==> installer: stop box service for update"
${box_src_dir}/setup/stop.sh
fi
# ensure we are not inside the source directory, which we will remove now
cd /root
echo "==> installer: switching the box code"
rm -rf "${BOX_SRC_DIR}"
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
chown -R "${USER}:${USER}" "${BOX_SRC_DIR}"
rm -rf "${box_src_dir}"
mv "${box_src_tmp_dir}" "${box_src_dir}"
chown -R "${user}:${user}" "${box_src_dir}"
echo "==> installer: calling box setup script"
"${BOX_SRC_DIR}/setup/start.sh"
"${box_src_dir}/setup/start.sh"
+29 -5
View File
@@ -20,6 +20,11 @@ readonly ubuntu_version=$(lsb_release -rs)
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
# this needs to match the cloudron/base:2.0.0 gid
if ! getent group media; then
addgroup --gid 500 --system media
fi
echo "==> Configuring docker"
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
systemctl enable apparmor
@@ -39,7 +44,7 @@ mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/redis"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner"
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
@@ -52,10 +57,12 @@ mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
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
# ensure backups folder exists and is writeable
mkdir -p /var/backups
@@ -79,6 +86,9 @@ 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}
echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
@@ -91,11 +101,13 @@ unbound-anchor -a /var/lib/unbound/root.key
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
systemctl disable cloudron.target || true
rm -f /etc/systemd/system/cloudron.target
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
systemctl daemon-reload
systemctl enable unbound
systemctl enable cloudron-syslog
systemctl enable cloudron.target
systemctl enable box
systemctl enable cloudron-firewall
# update firewall rules
@@ -144,8 +156,15 @@ cp "${script_dir}/start/nginx/mime.types" "${PLATFORM_DATA_DIR}/nginx/mime.types
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
# default nginx service file does not restart on crash
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
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
echo -e "[Service]\nLimitNOFILE=16384\n" > /etc/systemd/system/nginx.service.d/cloudron.conf
fi
systemctl daemon-reload
systemctl start nginx
# restart mysql to make sure it has latest config
@@ -170,9 +189,11 @@ readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
# set HOME explicity, because it's not set when the installer calls it. this is done because
# paths.js uses this env var and some of the migrate code requires box code
echo "==> Migrating data"
cd "${BOX_SRC_DIR}"
if ! BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
if ! HOME=${HOME_DIR} BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
echo "DB migration failed"
exit 1
fi
@@ -191,6 +212,9 @@ fi
echo "==> Cleaning up stale redis directories"
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
echo "==> Cleaning up old logs"
rm -f /home/yellowtent/platformdata/logs/*/*.log.* || true
echo "==> Changing ownership"
# 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
@@ -206,7 +230,7 @@ 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
echo "==> Starting Cloudron"
systemctl start cloudron.target
systemctl start box
sleep 2 # give systemd sometime to start the processes
+23 -5
View File
@@ -6,11 +6,29 @@ echo "==> Setting up firewall"
iptables -t filter -N CLOUDRON || true
iptables -t filter -F CLOUDRON # empty any existing rules
# NOTE: keep these in sync with src/apps.js validatePortBindings
# allow ssh, http, https, ping, dns
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
# ssh is allowed alternately on port 202
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
# first setup any user IP block lists
ipset create cloudron_blocklist hash:net || true
/home/yellowtent/box/src/scripts/setblocklist.sh
iptables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist src -j DROP
if ! iptables -t filter -C FORWARD -m set --match-set cloudron_blocklist src -j DROP; then
iptables -t filter -A FORWARD -m set --match-set cloudron_blocklist src -j DROP
fi
# allow related and establisted connections
iptables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443 -j ACCEPT # 202 is the alternate ssh port
# whitelist any user 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
[[ -n "${allowed_tcp_ports}" ]] && iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports "${allowed_tcp_ports}" -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
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
+3 -3
View File
@@ -14,8 +14,8 @@ if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://${ip} on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
printf "Cloudron overview - https://docs.cloudron.io/ \n"
printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n"
else
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
printf "\t\t\t-----------------------\n"
@@ -23,7 +23,7 @@ else
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
printf "are automatically installed on this server every night.\n"
printf "\n"
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
printf "Read more at https://docs.cloudron.io/security/#os-updates\n"
fi
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
+5
View File
@@ -4,6 +4,11 @@ set -eu -o pipefail
readonly APPS_SWAP_FILE="/apps.swap"
if [[ -f "${APPS_SWAP_FILE}" ]]; then
echo "Swap file already exists at /apps.swap . Skipping"
exit
fi
# all sizes are in mb
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly swap_size=$((${physical_memory} > 4096 ? 4096 : ${physical_memory})) # min(RAM, 4GB) if you change this, fix enoughResourcesAvailable() in client.js
+4 -1
View File
@@ -3,10 +3,12 @@ import collectd,os,subprocess,sys,re,time
# https://www.programcreek.com/python/example/106897/collectd.register_read
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
def du(pathinfo):
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
# -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'])
if pathinfo['exclude'] != '':
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
@@ -26,6 +28,7 @@ def parseSize(size):
def dockerSize():
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
# https://godoc.org/github.com/docker/go-units#HumanSize is used. so it's 1000 (KB) and not 1024 (KiB)
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
try:
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
-40
View File
@@ -1,40 +0,0 @@
# add customizations here
# after making changes run "sudo systemctl restart box"
# appstore:
# blacklist:
# - io.wekan.cloudronapp
# - io.cloudron.openvpn
# whitelist:
# org.wordpress.cloudronapp: {}
# chat.rocket.cloudronapp: {}
# com.nextcloud.cloudronapp: {}
#
# backups:
# configurable: true
#
# domains:
# dynamicDns: true
# changeDashboardDomain: true
#
# subscription:
# configurable: true
#
# support:
# email: support@cloudron.io
# remoteSupport: true
#
# ticketFormBody: |
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
# * [Forum](https://forum.cloudron.io/)
#
# submitTickets: true
#
# alerts:
# email: support@cloudron.io
# notifyCloudronAdmins: false
#
# footer:
# body: '&copy; 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
+2
View File
@@ -10,6 +10,7 @@
/home/yellowtent/platformdata/logs/redis-*/*.log
/home/yellowtent/platformdata/logs/crash/*.log
/home/yellowtent/platformdata/logs/collectd/*.log
/home/yellowtent/platformdata/logs/turn/*.log
/home/yellowtent/platformdata/logs/updater/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1
@@ -17,6 +18,7 @@
missingok
# we never compress so we can simply tail the files
nocompress
# this truncates the original log file and not the rotated one
copytruncate
}
+9 -2
View File
@@ -1,11 +1,18 @@
user www-data;
worker_processes 1;
# detect based on available CPU cores
worker_processes auto;
# this is 4096 by default. See /proc/<PID>/limits and /etc/security/limits.conf
# usually twice the worker_connections (one for uptsream, one for downstream)
# see also LimitNOFILE=16384 in systemd drop-in
worker_rlimit_nofile 8192;
pid /run/nginx.pid;
events {
worker_connections 1024;
# a single worker has these many simultaneous connections max
worker_connections 4096;
}
http {
+12
View File
@@ -50,3 +50,15 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
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/rmmailbox.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmailbox.sh
Defaults!/home/yellowtent/box/src/scripts/starttask.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/starttask.sh
Defaults!/home/yellowtent/box/src/scripts/stoptask.sh env_keep="HOME BOX_ENV"
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
+5 -4
View File
@@ -1,20 +1,21 @@
[Unit]
Description=Cloudron Admin
OnFailure=crashnotifier@%n.service
StopWhenUnneeded=true
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
BindsTo=systemd-journald.service
After=mysql.service nginx.service
; As cloudron-resize-fs is a one-shot, the Wants= automatically ensures that the service *finishes*
Wants=cloudron-resize-fs.service
[Install]
WantedBy=multi-user.target
[Service]
Type=idle
WorkingDirectory=/home/yellowtent/box
Restart=always
; Systemd does not append logs when logging to files, we spawn a shell first and exec to replace it after setting up the pipes
ExecStart=/bin/sh -c 'echo "Logging to /home/yellowtent/platformdata/logs/box.log"; exec /usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js >> /home/yellowtent/platformdata/logs/box.log 2>&1'
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
ExecStart=/home/yellowtent/box/box.js
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box:*,connect-lastmile,-box:ldap" "BOX_ENV=cloudron" "NODE_ENV=production"
; kill apptask processes as well
KillMode=control-group
; Do not kill this process on OOM. Children inherit this score. Do not set it to -1000 so that MemoryMax can keep working
-10
View File
@@ -1,10 +0,0 @@
[Unit]
Description=Cloudron Smartserver
Documentation=https://cloudron.io/documentation.html
StopWhenUnneeded=true
Requires=box.service
After=box.service
# AllowIsolate=yes
[Install]
WantedBy=multi-user.target
+1 -1
View File
@@ -4,4 +4,4 @@ set -eu -o pipefail
echo "Stopping cloudron"
systemctl stop cloudron.target
systemctl stop box
+512 -212
View File
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -41,7 +41,7 @@ var assert = require('assert'),
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson',
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
@@ -94,6 +94,14 @@ function postProcess(result) {
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;
assert(result.bindsJson === null || typeof result.bindsJson === 'string');
result.binds = safe.JSON.parse(result.bindsJson) || {};
delete result.bindsJson;
result.alternateDomains = result.alternateDomains || [];
result.alternateDomains.forEach(function (d) {
delete d.appId;
@@ -427,7 +435,7 @@ function updateWithConstraints(id, app, constraints, callback) {
var fields = [ ], values = [ ];
for (var p in app) {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig') {
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
fields.push(`${p}Json = ?`);
values.push(JSON.stringify(app[p]));
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
+8 -22
View File
@@ -73,22 +73,14 @@ function checkAppHealth(app, callback) {
assert.strictEqual(typeof callback, 'function');
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
return callback(null);
}
const manifest = app.manifest;
docker.inspect(app.containerId, function (error, data) {
if (error || !data || !data.State) {
debugApp(app, 'Error inspecting container');
return setHealth(app, apps.HEALTH_ERROR, callback);
}
if (data.State.Running !== true) {
debugApp(app, 'exited');
return setHealth(app, apps.HEALTH_DEAD, callback);
}
if (error || !data || !data.State) return setHealth(app, apps.HEALTH_ERROR, callback);
if (data.State.Running !== true) return setHealth(app, apps.HEALTH_DEAD, callback);
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
@@ -103,10 +95,8 @@ function checkAppHealth(app, callback) {
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
debugApp(app, 'not alive (network error): %s', error.message);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
debugApp(app, 'not alive : %s', error || res.status);
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
@@ -180,18 +170,14 @@ function processDockerEvents(intervalSecs, callback) {
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
apps.getAll(function (error, result) {
apps.getAll(function (error, allApps) {
if (error) return callback(error);
async.each(result, checkAppHealth, function (error) {
if (error) console.error(error);
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 alive = result
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
.map(a => a.fqdn)
.join(', ');
debug('apps alive: [%s]', alive);
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.` + (error ? ` ${error.reason}` : ''));
callback(null);
});
@@ -206,7 +192,7 @@ function run(intervalSecs, callback) {
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs)
], function (error) {
if (error) debug(error);
if (error) debug(`run: could not check app health. ${error.message}`);
callback();
});
+891 -837
View File
File diff suppressed because it is too large Load Diff
+42 -156
View File
@@ -11,7 +11,6 @@ exports = module.exports = {
trackFinishedSetup: trackFinishedSetup,
registerWithLoginCredentials: registerWithLoginCredentials,
registerWithLicense: registerWithLicense,
purchaseApp: purchaseApp,
unpurchaseApp: unpurchaseApp,
@@ -20,8 +19,6 @@ exports = module.exports = {
getSubscription: getSubscription,
isFreePlan: isFreePlan,
sendAliveStatus: sendAliveStatus,
getAppUpdate: getAppUpdate,
getBoxUpdate: getBoxUpdate,
@@ -34,32 +31,26 @@ var apps = require('./apps.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
mail = require('./mail.js'),
os = require('os'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
superagent = require('superagent'),
users = require('./users.js'),
util = require('util');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js
let gFeatures = {
userMaxCount: null,
externalLdap: true,
eventLog: true,
privateDockerRegistry: true,
branding: true,
userManager: true,
multiAdmin: true,
support: true
userMaxCount: 5,
domainMaxCount: 1,
externalLdap: false,
privateDockerRegistry: false,
branding: false,
support: false,
directoryConfig: false,
mailboxMaxCount: 5,
emailPremium: false
};
// attempt to load feature cache in case appstore would be down
@@ -127,7 +118,7 @@ function registerUser(email, password, callback) {
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));
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);
@@ -235,112 +226,8 @@ function unpurchaseApp(appId, data, callback) {
});
}
function sendAliveStatus(callback) {
callback = callback || NOOP_CALLBACK;
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
async.series([
function (callback) {
settings.getAll(function (error, result) {
if (error) return callback(error);
allSettings = result;
callback();
});
},
function (callback) {
domains.getAll(function (error, result) {
if (error) return callback(error);
allDomains = result;
callback();
});
},
function (callback) {
mail.getDomains(function (error, result) {
if (error) return callback(error);
mailDomains = result;
callback();
});
},
function (callback) {
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
if (error) return callback(error);
loginEvents = result;
callback();
});
},
function (callback) {
users.count(function (error, result) {
if (error) return callback(error);
userCount = result;
callback();
});
},
function (callback) {
groups.count(function (error, result) {
if (error) return callback(error);
groupCount = result;
callback();
});
}
], function (error) {
if (error) return callback(error);
var backendSettings = {
backupConfig: {
provider: allSettings[settings.BACKUP_CONFIG_KEY].provider,
hardlinks: !allSettings[settings.BACKUP_CONFIG_KEY].noHardlinks
},
domainConfig: {
count: allDomains.length,
domains: Array.from(new Set(allDomains.map(function (d) { return { domain: d.domain, provider: d.provider }; })))
},
mailConfig: {
outboundCount: mailDomains.length,
inboundCount: mailDomains.filter(function (d) { return d.enabled; }).length,
catchAllCount: mailDomains.filter(function (d) { return d.catchAll.length !== 0; }).length,
relayProviders: Array.from(new Set(mailDomains.map(function (d) { return d.relay.provider; })))
},
userCount: userCount,
groupCount: groupCount,
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
timeZone: allSettings[settings.TIME_ZONE_KEY],
sysinfoProvider: allSettings[settings.SYSINFO_CONFIG_KEY].provider
};
var data = {
version: constants.VERSION,
adminFqdn: settings.adminFqdn(),
provider: settings.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/alive`;
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));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
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('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
});
});
}
function getBoxUpdate(callback) {
function getBoxUpdate(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
@@ -348,7 +235,13 @@ function getBoxUpdate(callback) {
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
const query = {
accessToken: token,
boxVersion: constants.VERSION,
automatic: options.automatic
};
superagent.get(url).query(query).timeout(10 * 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));
@@ -374,16 +267,24 @@ function getBoxUpdate(callback) {
});
}
function getAppUpdate(app, callback) {
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({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
superagent.get(url).query(query).timeout(10 * 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));
@@ -401,7 +302,9 @@ function getAppUpdate(app, callback) {
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
}
// { id, creationDate, manifest }
updateInfo.unstable = !!updateInfo.unstable;
// { id, creationDate, manifest, unstable }
callback(null, updateInfo);
});
});
@@ -415,7 +318,7 @@ function registerCloudron(data, callback) {
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: ${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'));
@@ -438,18 +341,16 @@ function registerCloudron(data, callback) {
// This works without a Cloudron token as this Cloudron was not yet registered
let gBeginSetupAlreadyTracked = false;
function trackBeginSetup(provider) {
assert.strictEqual(typeof provider, 'string');
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({ provider }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
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}`);
});
}
@@ -460,23 +361,8 @@ function trackFinishedSetup(domain) {
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 console.error(error.message);
if (result.statusCode !== 200) return console.error(error.message);
});
}
function registerWithLicense(license, domain, callback) {
assert.strictEqual(typeof license, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT));
const provider = settings.provider();
const version = constants.VERSION;
registerCloudron({ license, domain, provider, version }, callback);
if (error && !error.response) return debug(`trackFinishedSetup: ${error.message}`);
if (result.statusCode !== 200) return debug(`trackFinishedSetup: ${result.statusCode} ${error.message}`);
});
}
@@ -491,7 +377,7 @@ function registerWithLoginCredentials(options, callback) {
}
getCloudronToken(function (error, token) {
if (token) return callback(new BoxError(BoxError.CONFLICT));
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
maybeSignup(function (error) {
if (error) return callback(error);
@@ -499,7 +385,7 @@ function registerWithLoginCredentials(options, callback) {
login(options.email, options.password, options.totpToken || '', function (error, result) {
if (error) return callback(error);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, provider: settings.provider(), version: constants.VERSION, purpose: options.purpose || '' }, callback);
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken, version: constants.VERSION, purpose: options.purpose || '' }, callback);
});
});
});
@@ -524,7 +410,7 @@ function createTicket(info, auditSource, callback) {
if (error) return callback(error);
collectAppInfoIfNeeded(function (error, result) {
if (error) console.error('Unable to get app info', error);
if (error) return callback(error);
if (result) info.app = result;
let url = settings.apiServerOrigin() + '/api/v1/ticket';
+25 -16
View File
@@ -17,8 +17,6 @@ exports = module.exports = {
_waitForDnsPropagation: waitForDnsPropagation
};
require('supererror')({ splatchError: true });
var addons = require('./addons.js'),
appdb = require('./appdb.js'),
apps = require('./apps.js'),
@@ -37,7 +35,6 @@ var addons = require('./addons.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
manifestFormat = require('cloudron-manifestformat'),
mkdirp = require('mkdirp'),
net = require('net'),
os = require('os'),
path = require('path'),
@@ -176,7 +173,7 @@ function createAppDir(app, callback) {
assert.strictEqual(typeof callback, 'function');
const appDir = path.join(paths.APPS_DATA_DIR, app.id);
mkdirp(appDir, function (error) {
fs.mkdir(appDir, { recursive: true }, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating directory: ${error.message}`, { appDir }));
callback(null);
@@ -431,12 +428,12 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`));
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 }, function (error) {
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: app.location, domain: app.domain }));
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 }, function (error) {
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: domain.subdomain, domain: domain.domain }));
iteratorCallback();
@@ -741,7 +738,8 @@ function migrateDataDir(app, args, progressCallback, callback) {
debugApp(app, 'error migrating data dir : %s', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
callback();
});
}
@@ -786,7 +784,8 @@ function configure(app, args, progressCallback, callback) {
debugApp(app, 'error reconfiguring : %s', error);
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
}
callback(null);
callback();
});
}
@@ -853,7 +852,7 @@ function update(app, args, progressCallback, callback) {
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) console.error('Portbinding does not exist in database.');
if (error && error.reason === BoxError.NOT_FOUND) debug('update: portbinding does not exist in database', error);
else if (error) return next(error);
// also delete from app object for further processing (the db is updated in the next step)
@@ -869,10 +868,10 @@ function update(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
downloadIcon.bind(null, app),
progressCallback.bind(null, { percent: 70, message: 'Updating addons' }),
progressCallback.bind(null, { percent: 60, message: 'Updating addons' }),
addons.setupAddons.bind(null, app, updateConfig.manifest.addons),
progressCallback.bind(null, { percent: 80, message: 'Creating container' }),
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
createContainer.bind(null, app),
startApp.bind(null, app),
@@ -899,7 +898,10 @@ function start(app, args, progressCallback, callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
progressCallback.bind(null, { percent: 20, message: 'Starting container' }),
progressCallback.bind(null, { percent: 10, message: 'Starting app services' }),
addons.startAppServices.bind(null, app),
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
docker.startContainer.bind(null, app.id),
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
@@ -927,6 +929,9 @@ function stop(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 20, message: 'Stopping container' }),
docker.stopContainers.bind(null, app.id),
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
addons.stopAppServices.bind(null, app),
progressCallback.bind(null, { percent: 100, message: 'Done' }),
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
], function seriesDone(error) {
@@ -973,16 +978,18 @@ function uninstall(app, args, progressCallback, callback) {
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),
addons.teardownAddons.bind(null, app, app.manifest.addons),
progressCallback.bind(null, { percent: 40, message: 'Deleting app data directory' }),
progressCallback.bind(null, { percent: 40, message: 'Cleanup file manager' }),
progressCallback.bind(null, { percent: 50, message: 'Deleting app data directory' }),
deleteAppDir.bind(null, app, { removeDirectory: true }),
progressCallback.bind(null, { percent: 50, message: 'Deleting image' }),
progressCallback.bind(null, { percent: 60, message: 'Deleting image' }),
docker.deleteImage.bind(null, app.manifest),
progressCallback.bind(null, { percent: 60, message: 'Unregistering domains' }),
progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }),
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)),
progressCallback.bind(null, { percent: 70, message: 'Cleanup icon' }),
progressCallback.bind(null, { percent: 80, message: 'Cleanup icon' }),
removeIcon.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
@@ -1038,6 +1045,8 @@ function run(appId, args, progressCallback, callback) {
return stop(app, args, progressCallback, callback);
case apps.ISTATE_PENDING_RESTART:
return restart(app, args, progressCallback, callback);
case apps.ISTATE_INSTALLED: // can only happen when we have a bug in our code while testing/development
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback);
default:
debugApp(app, 'apptask launched with invalid command');
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
+8 -1
View File
@@ -12,6 +12,7 @@ let assert = require('assert'),
safe = require('safetydance'),
path = require('path'),
paths = require('./paths.js'),
sftp = require('./sftp.js'),
tasks = require('./tasks.js');
let gActiveTasks = { }; // indexed by app id
@@ -68,11 +69,17 @@ function scheduleTask(appId, taskId, callback) {
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */ }, function (error, result) {
// TODO: set memory limit for app backup task
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */, nice: 15 }, function (error, result) {
callback(error, result);
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
// post app task hooks
sftp.rebuild(function (error) {
if (error) console.error('Unable to rebuild sftp:', error);
});
});
}
+25 -30
View File
@@ -6,27 +6,20 @@ var assert = require('assert'),
safe = require('safetydance'),
util = require('util');
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ];
var BACKUPS_FIELDS = [ 'id', 'identifier', 'creationTime', 'packageVersion', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs', 'encryptionVersion' ];
exports = module.exports = {
add: add,
add,
getByTypeAndStatePaged: getByTypeAndStatePaged,
getByTypePaged: getByTypePaged,
getByTypePaged,
getByIdentifierPaged,
getByIdentifierAndStatePaged,
get: get,
del: del,
update: update,
getByAppIdPaged: getByAppIdPaged,
get,
del,
update,
_clear: clear,
BACKUP_TYPE_APP: 'app',
BACKUP_TYPE_BOX: 'box',
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
BACKUP_STATE_CREATING: 'creating',
BACKUP_STATE_ERROR: 'error'
_clear: clear
};
function postProcess(result) {
@@ -38,15 +31,15 @@ function postProcess(result) {
delete result.manifestJson;
}
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
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 type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
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); });
@@ -56,7 +49,7 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
}
function getByTypePaged(type, page, perPage, callback) {
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
assert.strictEqual(typeof type, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
@@ -71,15 +64,14 @@ function getByTypePaged(type, page, perPage, callback) {
});
}
function getByAppIdPaged(page, perPage, appId, callback) {
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 appId, 'string');
assert.strictEqual(typeof callback, 'function');
// box versions (0.93.x and below) used to use appbackup_ prefix
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
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); });
@@ -106,8 +98,11 @@ function get(id, callback) {
function add(id, data, callback) {
assert(data && typeof data === 'object');
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof data.version, 'string');
assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX);
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');
@@ -116,8 +111,8 @@ function add(id, data, callback) {
var creationTime = data.creationTime || new Date(); // allow tests to set the time
var manifestJson = JSON.stringify(data.manifest);
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
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));
+478 -255
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -50,6 +50,7 @@ BoxError.FS_ERROR = 'FileSystem Error';
BoxError.INACTIVE = 'Inactive';
BoxError.INTERNAL_ERROR = 'Internal Error';
BoxError.INVALID_CREDENTIALS = 'Invalid Credentials';
BoxError.IPTABLES_ERROR = 'IPTables Error';
BoxError.LICENSE_ERROR = 'License Error';
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
BoxError.MAIL_ERROR = 'Mail Error';
@@ -92,6 +93,7 @@ BoxError.toHttpError = function (error) {
case BoxError.MAIL_ERROR:
case BoxError.DOCKER_ERROR:
case BoxError.ADDONS_ERROR:
case BoxError.IPTABLES_ERROR:
return new HttpError(424, error);
case BoxError.DATABASE_ERROR:
case BoxError.INTERNAL_ERROR:
+2 -2
View File
@@ -332,7 +332,7 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
// in some old releases, csr file was corrupt. so always regenerate it
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
} else {
var key = safe.child_process.execSync('openssl genrsa 4096');
var key = safe.child_process.execSync('openssl ecparam -genkey -name secp384r1'); // openssl ecparam -list_curves
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
@@ -452,7 +452,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(error);
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
if (error) return callback(error);
callback(null, challenge);
-22
View File
@@ -1,22 +0,0 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'caas'
};
var assert = require('assert'),
debug = require('debug')('box:cert/caas.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
-22
View File
@@ -1,22 +0,0 @@
'use strict';
exports = module.exports = {
getCertificate: getCertificate,
// testing
_name: 'fallback'
};
var assert = require('assert'),
debug = require('debug')('box:cert/fallback.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('getCertificate: using fallback certificate', hostname);
return callback(null, '', '');
}
-24
View File
@@ -1,24 +0,0 @@
'use strict';
// -------------------------------------------
// This file just describes the interface
//
// New backends can start from here
// -------------------------------------------
exports = module.exports = {
getCertificate: getCertificate
};
var assert = require('assert'),
BoxError = require('../boxerror.js');
function getCertificate(hostname, domain, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
return callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'getCertificate is not implemented'));
}
+104 -62
View File
@@ -1,27 +1,28 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
getConfig: getConfig,
getLogs: getLogs,
initialize,
uninitialize,
getConfig,
getLogs,
reboot: reboot,
isRebootRequired: isRebootRequired,
reboot,
isRebootRequired,
onActivated: onActivated,
onActivated,
prepareDashboardDomain: prepareDashboardDomain,
setDashboardDomain: setDashboardDomain,
setDashboardAndMailDomain: setDashboardAndMailDomain,
renewCerts: renewCerts,
setupDnsAndCert,
setupDashboard: setupDashboard,
prepareDashboardDomain,
setDashboardDomain,
updateDashboardDomain,
renewCerts,
runSystemChecks: runSystemChecks,
runSystemChecks
};
var apps = require('./apps.js'),
var addons = require('./addons.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
@@ -45,6 +46,7 @@ var apps = require('./apps.js'),
shell = require('./shell.js'),
spawn = require('child_process').spawn,
split = require('split'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
users = require('./users.js');
@@ -65,7 +67,7 @@ function uninitialize(callback) {
async.series([
cron.stopJobs,
platform.stop
platform.stopAllTasks
], callback);
}
@@ -76,8 +78,15 @@ function onActivated(callback) {
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
async.series([
reverseProxy.removeDefaultConfig, // remove IP based nginx config once user is created
platform.start,
cron.startJobs
cron.startJobs,
function checkBackupConfiguration(callback) {
backups.checkConfiguration(function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
});
}
], callback);
}
@@ -102,24 +111,49 @@ function notifyUpdate(callback) {
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
// configure nginx to be reachable by IP
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
const tasks = [
// stop all the systemd tasks
platform.stopAllTasks,
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
settings.getBackupConfig(function (error, backupConfig) {
if (error) return console.error('Failed to read backup config.', error);
backups.configureCollectd(backupConfig, NOOP_CALLBACK);
});
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
function (callback) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(error);
// always generate webadmin config since we have no versioning mechanism for the ejs
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
backups.configureCollectd(backupConfig, callback);
});
},
// check activation state and start the platform
users.isActivated(function (error, activated) {
if (error) return debug(error);
if (!activated) return debug('initialize: not activated yet'); // not activated
// always generate webadmin config since we have no versioning mechanism for the ejs
function (callback) {
if (!settings.adminDomain()) return callback();
onActivated(NOOP_CALLBACK);
reverseProxy.writeAdminConfig(settings.adminDomain(), callback);
},
// check activation state and start the platform
function (callback) {
users.isActivated(function (error, activated) {
if (error) return callback(error);
// configure nginx to be reachable by IP when not activated. for the moment, the IP based redirect exists even after domain is setup
// just in case user forgot or some network error happenned in the middle (then browser refresh takes you to activation page)
// we remove the config as a simple security measure to not expose IP <-> domain
if (!activated) {
debug('runStartupTasks: not activated. generating IP based redirection config');
return reverseProxy.writeDefaultConfig(callback);
}
onActivated(callback);
});
}
];
// we used to run tasks in parallel but simultaneous nginx reloads was causing issues
async.series(async.reflectAll(tasks), function (error, results) {
results.forEach((result, idx) => {
if (result.error) debug(`Startup task at index ${idx} failed: ${result.error.message}`);
});
});
}
@@ -138,17 +172,18 @@ function getConfig(callback) {
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
isDemo: settings.isDemo(),
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
features: appstore.getFeatures()
features: appstore.getFeatures(),
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
});
});
}
function reboot(callback) {
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) {
if (error) console.error('Failed to clear reboot notification.', error);
if (error) debug('reboot: failed to clear reboot notification.', error);
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
});
@@ -166,24 +201,11 @@ function runSystemChecks(callback) {
assert.strictEqual(typeof callback, 'function');
async.parallel([
checkBackupConfiguration,
checkMailStatus,
checkRebootRequired
], callback);
}
function checkBackupConfiguration(callback) {
assert.strictEqual(typeof callback, 'function');
debug('checking backup configuration');
backups.checkConfiguration(function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
});
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -274,7 +296,7 @@ function prepareDashboardDomain(domain, auditSource, callback) {
const conflict = result.filter(app => app.fqdn === fqdn);
if (conflict.length) return callback(new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.ADMIN_LOCATION, domain, auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
@@ -301,7 +323,7 @@ function setDashboardDomain(domain, auditSource, callback) {
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
settings.setAdmin(domain, fqdn, function (error) {
settings.setAdminLocation(domain, fqdn, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
@@ -313,35 +335,24 @@ function setDashboardDomain(domain, auditSource, callback) {
}
// call this only post activation because it will restart mail server
function setDashboardAndMailDomain(domain, auditSource, callback) {
function updateDashboardDomain(domain, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`setDashboardAndMailDomain: ${domain}`);
debug(`updateDashboardDomain: ${domain}`);
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
setDashboardDomain(domain, auditSource, function (error) {
if (error) return callback(error);
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
addons.restartService('turn', NOOP_CALLBACK); // to update the realm variable
callback(null);
});
}
function setupDashboard(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
async.series([
domains.prepareDashboardDomain.bind(null, settings.adminDomain(), auditSource, progressCallback),
setDashboardDomain.bind(null, settings.adminDomain(), auditSource)
], callback);
}
function renewCerts(options, auditSource, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
@@ -355,3 +366,34 @@ function renewCerts(options, auditSource, callback) {
callback(null, taskId);
});
}
function setupDnsAndCert(subdomain, domain, auditSource, progressCallback, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = domains.fqdn(subdomain, domainObject);
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.series([
(done) => { progressCallback({ message: `Updating DNS of ${adminFqdn}` }); done(); },
domains.upsertDnsRecords.bind(null, subdomain, domain, 'A', [ ip ]),
(done) => { progressCallback({ message: `Waiting for DNS of ${adminFqdn}` }); done(); },
domains.waitForDnsRecord.bind(null, subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ message: `Getting certificate of ${adminFqdn}` }); done(); },
reverseProxy.ensureCertificate.bind(null, domains.fqdn(subdomain, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
callback(null);
});
});
});
}
+4 -5
View File
@@ -32,17 +32,16 @@ exports = module.exports = {
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
DEFAULT_TOKEN_EXPIRATION: 7 * 24 * 60 * 60 * 1000, // 1 week
DEFAULT_TOKEN_EXPIRATION: 365 * 24 * 60 * 60 * 1000, // 1 year
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
DEMO_USERNAME: 'cloudron',
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
AUTOUPDATE_PATTERN_NEVER: 'never',
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8), // also used in dashboard client.js
CLOUDRON: CLOUDRON,
TEST: TEST,
@@ -51,6 +50,6 @@ exports = module.exports = {
FOOTER: '&copy; 2020 &nbsp; [Cloudron](https://cloudron.io) &nbsp; &nbsp; &nbsp; [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '4.2.0-test'
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '5.1.1-test'
};
+36 -69
View File
@@ -1,16 +1,23 @@
'use strict';
// IMPORTANT: These patterns are together because they spin tasks which acquire a lock
// If the patterns overlap all the time, then the task may not ever get a chance to run!
// If you change this change dashboard patterns in settings.html
const DEFAULT_CLEANUP_BACKUPS_PATTERN = '00 30 1,3,5,23 * * *',
DEFAULT_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *';
exports = module.exports = {
startJobs: startJobs,
startJobs,
stopJobs: stopJobs,
stopJobs,
handleSettingsChanged: handleSettingsChanged
handleSettingsChanged,
DEFAULT_AUTOUPDATE_PATTERN,
};
var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
@@ -29,12 +36,9 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
updateChecker = require('./updatechecker.js');
var gJobs = {
alive: null, // send periodic stats
appAutoUpdater: null,
boxAutoUpdater: null,
appUpdateChecker: null,
autoUpdater: null,
backup: null,
boxUpdateChecker: null,
updateChecker: null,
systemChecks: null,
diskSpaceChecker: null,
certificateRenew: null,
@@ -60,15 +64,9 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function startJobs(callback) {
assert.strictEqual(typeof callback, 'function');
const randomMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // every hour on a random minute
onTick: appstore.sendAliveStatus,
start: true
});
const randomTick = Math.floor(60*Math.random());
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
start: true
});
@@ -79,15 +77,10 @@ function startJobs(callback) {
start: true
});
gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
start: true
});
gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
// this is run separately from the update itself so that the user can disable automatic updates but can still get a notification
gJobs.updateCheckerJob = new CronJob({
cronTime: `${randomTick} ${randomTick} 1,5,9,13,17,21,23 * * *`,
onTick: () => updateChecker.checkForUpdates({ automatic: true }, NOOP_CALLBACK),
start: true
});
@@ -98,7 +91,7 @@ function startJobs(callback) {
});
gJobs.cleanupBackups = new CronJob({
cronTime: '00 45 1,3,5,23 * * *', // every 6 hours. try not to overlap with ensureBackup job
cronTime: DEFAULT_CLEANUP_BACKUPS_PATTERN,
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true
});
@@ -138,8 +131,7 @@ function startJobs(callback) {
const tz = allSettings[settings.TIME_ZONE_KEY];
backupConfigChanged(allSettings[settings.BACKUP_CONFIG_KEY], tz);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY], tz);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY], tz);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY], tz);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
@@ -154,8 +146,7 @@ function handleSettingsChanged(key, value) {
switch (key) {
case settings.TIME_ZONE_KEY:
case settings.BACKUP_CONFIG_KEY:
case settings.APP_AUTOUPDATE_PATTERN_KEY:
case settings.BOX_AUTOUPDATE_PATTERN_KEY:
case settings.AUTOUPDATE_PATTERN_KEY:
case settings.DYNAMIC_DNS_KEY:
debug('handleSettingsChanged: recreating all jobs');
async.series([
@@ -172,71 +163,47 @@ function backupConfigChanged(value, tz) {
assert.strictEqual(typeof value, 'object');
assert.strictEqual(typeof tz, 'string');
debug(`backupConfigChanged: interval ${value.intervalSecs} (${tz})`);
debug(`backupConfigChanged: schedule ${value.schedulePattern} (${tz})`);
if (gJobs.backup) gJobs.backup.stop();
let pattern;
if (value.intervalSecs <= 6 * 60 * 60) {
pattern = '00 00 1,7,13,19 * * *'; // no option but to backup in the middle of the day
} else {
pattern = '00 00 1,3,5,23 * * *'; // avoid middle of the day backups
}
gJobs.backup = new CronJob({
cronTime: pattern,
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK),
cronTime: value.schedulePattern,
onTick: backups.startBackupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true,
timeZone: tz
});
}
function boxAutoupdatePatternChanged(pattern, tz) {
function autoupdatePatternChanged(pattern, tz) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof tz, 'string');
debug(`boxAutoupdatePatternChanged: pattern - ${pattern} (${tz})`);
debug(`autoupdatePatternChanged: pattern - ${pattern} (${tz})`);
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
if (gJobs.autoUpdater) gJobs.autoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.boxAutoUpdater = new CronJob({
gJobs.autoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
const updateInfo = updateChecker.getUpdateInfo();
// do box before app updates. for the off chance that the box logic fixes some app update logic issue
if (updateInfo.box && !updateInfo.box.unstable) {
debug('Starting box autoupdate to %j', updateInfo.box);
updater.updateToLatest({ skipBackup: false }, auditSource.CRON, NOOP_CALLBACK);
} else {
debug('No box auto updates available');
return;
}
},
start: true,
timeZone: tz
});
}
function appAutoupdatePatternChanged(pattern, tz) {
assert.strictEqual(typeof pattern, 'string');
assert.strictEqual(typeof tz, 'string');
debug(`appAutoupdatePatternChanged: pattern ${pattern} (${tz})`);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
if (pattern === constants.AUTOUPDATE_PATTERN_NEVER) return;
gJobs.appAutoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.apps) {
if (updateInfo.apps && Object.keys(updateInfo.apps).length > 0) {
debug('Starting app update to %j', updateInfo.apps);
apps.autoupdateApps(updateInfo.apps, auditSource.CRON, NOOP_CALLBACK);
} else {
debug('No app auto updates available');
}
},
start: true,
timeZone: tz
});
+38 -97
View File
@@ -17,12 +17,12 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
constants = require('./constants.js'),
debug = require('debug')('box:database'),
mysql = require('mysql'),
once = require('once'),
util = require('util');
var gConnectionPool = null,
gDefaultConnection = null;
var gConnectionPool = null;
const gDatabase = {
hostname: '127.0.0.1',
@@ -42,59 +42,37 @@ function initialize(callback) {
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
}
// https://github.com/mysqljs/mysql#pool-options
gConnectionPool = mysql.createPool({
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
connectionLimit: 5,
host: gDatabase.hostname,
user: gDatabase.username,
password: gDatabase.password,
port: gDatabase.port,
database: gDatabase.name,
multipleStatements: false,
waitForConnections: true, // getConnection() will wait until a connection is avaiable
ssl: false,
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
});
gConnectionPool.on('connection', function (connection) {
// connection objects are re-used. so we have to attach to the event here (once) to prevent crash
// note the pool also has an 'acquire' event but that is called whenever we do a getConnection()
connection.on('error', (error) => debug(`Connection ${connection.threadId} error: ${error.message} ${error.code}`));
connection.query('USE ' + gDatabase.name);
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
});
reconnect(callback);
callback(null);
}
function uninitialize(callback) {
if (gConnectionPool) {
gConnectionPool.end(callback);
gConnectionPool = null;
} else {
callback(null);
}
}
if (!gConnectionPool) return callback(null);
function reconnect(callback) {
callback = callback ? once(callback) : function () {};
gConnectionPool.getConnection(function (error, connection) {
if (error) {
console.error('Unable to reestablish connection to database. Try again in a bit.', error.message);
return setTimeout(reconnect.bind(null, callback), 1000);
}
connection.on('error', function (error) {
// by design, we catch all normal errors by providing callbacks.
// this function should be invoked only when we have no callbacks pending and we have a fatal error
assert(error.fatal, 'Non-fatal error on connection object');
console.error('Unhandled mysql connection error.', error);
// This is most likely an issue an can cause double callbacks from reconnect()
setTimeout(reconnect.bind(null, callback), 1000);
});
gDefaultConnection = connection;
callback(null);
});
gConnectionPool.end(callback);
gConnectionPool = null;
}
function clear(callback) {
@@ -107,80 +85,43 @@ function clear(callback) {
child_process.exec(cmd, callback);
}
function beginTransaction(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No database connection pool.'));
gConnectionPool.getConnection(function (error, connection) {
if (error) {
console.error('Unable to get connection to database. Try again in a bit.', error.message);
return setTimeout(beginTransaction.bind(null, callback), 1000);
}
connection.beginTransaction(function (error) {
if (error) return callback(error);
return callback(null, connection);
});
});
}
function rollback(connection, callback) {
assert.strictEqual(typeof callback, 'function');
connection.rollback(function (error) {
if (error) console.error(error); // can this happen?
connection.release();
callback(null);
});
}
// FIXME: if commit fails, is it supposed to return an error ?
function commit(connection, callback) {
assert.strictEqual(typeof callback, 'function');
connection.commit(function (error) {
if (error) return rollback(connection, callback);
connection.release();
return callback(null);
});
}
function query() {
var args = Array.prototype.slice.call(arguments);
var callback = args[args.length - 1];
const args = Array.prototype.slice.call(arguments);
const callback = args[args.length - 1];
assert.strictEqual(typeof callback, 'function');
if (gDefaultConnection === null) return callback(new BoxError(BoxError.DATABASE_ERROR, 'No connection to database'));
if (constants.TEST && !gConnectionPool) return callback(new BoxError(BoxError.DATABASE_ERROR, 'database.js not initialized'));
args[args.length -1 ] = function (error, result) {
if (error && error.fatal) {
gDefaultConnection = null;
setTimeout(reconnect, 1000);
}
callback(error, result);
};
gDefaultConnection.query.apply(gDefaultConnection, args);
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
}
function transaction(queries, callback) {
assert(util.isArray(queries));
assert.strictEqual(typeof callback, 'function');
beginTransaction(function (error, conn) {
callback = once(callback);
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback(error);
async.mapSeries(queries, function iterator(query, done) {
conn.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return rollback(conn, callback.bind(null, error));
const releaseConnection = (error) => { connection.release(); callback(error); };
commit(conn, callback.bind(null, null, results));
connection.beginTransaction(function (error) {
if (error) return releaseConnection(error);
async.mapSeries(queries, function iterator(query, done) {
connection.query(query.query, query.args, done);
}, function seriesDone(error, results) {
if (error) return connection.rollback(() => releaseConnection(error));
connection.commit(function (error) {
if (error) return connection.rollback(() => releaseConnection(error));
connection.release();
callback(null, results);
});
});
});
});
}
@@ -203,7 +144,7 @@ function exportToFile(file, callback) {
// latest mysqldump enables column stats by default which is not present in MySQL 5.7 server
// this option must not be set in production cloudrons which still use the old mysqldump
const disableColStats = (constants.TEST && process.env.DESKTOP_SESSION !== 'ubuntu') ? '--column-statistics=0' : '';
const disableColStats = (constants.TEST && require('fs').readFileSync('/etc/lsb-release', 'utf-8').includes('20.04')) ? '--column-statistics=0' : '';
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
-176
View File
@@ -1,176 +0,0 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/caas'),
domains = require('../domains.js'),
settings = require('../settings.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
function formatError(response) {
return util.format('Caas DNS error [%s] %j', response.statusCode, response.body);
}
function getFqdn(location, domain) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
return (location === '') ? domain : location + '-' + domain;
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
// do not return the 'key'. in caas, this is private
delete domainObject.fallbackCertificate.key;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
let fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('add: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
values: values
};
superagent
.post(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token })
.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 === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
superagent
.get(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
.query({ token: dnsConfig.token, type: type })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null, result.body.values);
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
var data = {
type: type,
values: values
};
superagent
.del(settings.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
.query({ token: dnsConfig.token })
.send(data)
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
return callback(null);
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token,
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
};
const location = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
}
+12 -6
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -25,12 +26,12 @@ var assert = require('assert'),
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function translateRequestError(result, callback) {
@@ -39,9 +40,14 @@ function translateRequestError(result, callback) {
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
if (result.statusCode === 422) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
let error = result.body.errors[0];
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
if (result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) {
let message = 'Unknown error';
if (typeof result.body.error === 'string') {
message = `message: ${result.body.error} statusCode: ${result.statusCode}`;
} else if (Array.isArray(result.body.errors) && result.body.errors.length > 0) {
let error = result.body.errors[0];
message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
}
return callback(new BoxError(BoxError.ACCESS_DENIED, message));
}
@@ -284,7 +290,7 @@ function verifyDnsConfig(domainObject, callback) {
if (dnsConfig.tokenType !== 'GlobalApiKey' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
if (dnsConfig.tokenType === 'GlobalApiKey') {
if ('email' in dnsConfig && typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
if (typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
}
const ip = '127.0.0.1';
+3 -2
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -28,12 +29,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getInternal(dnsConfig, zoneName, name, type, callback) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -26,12 +27,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function upsert(domainObject, location, type, values, callback) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -21,12 +22,12 @@ var assert = require('assert'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER;
domainObject.config.credentials.private_key = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.credentials.private_key === domains.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
}
function getDnsCredentials(dnsConfig) {
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -32,12 +33,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER;
domainObject.config.apiSecret = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.apiSecret === domains.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
if (newConfig.apiSecret === constants.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
}
function upsert(domainObject, location, type, values, callback) {
+2 -2
View File
@@ -21,13 +21,13 @@ var assert = require('assert'),
util = require('util');
function removePrivateFields(domainObject) {
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
// in-place removal of tokens and api keys with constants.SECRET_PLACEHOLDER
return domainObject;
}
// eslint-disable-next-line no-unused-vars
function injectPrivateFields(newConfig, currentConfig) {
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
// in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER
}
function upsert(domainObject, location, type, values, callback) {
+310
View File
@@ -0,0 +1,310 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
let async = require('async'),
assert = require('assert'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/linode'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
const LINODE_ENDPOINT = 'https://api.linode.com/v4';
function formatError(response) {
return util.format('Linode DNS error [%s] %j', response.statusCode, response.body);
}
function removePrivateFields(domainObject) {
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getZoneId(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
// returns 100 at a time
superagent.get(`${LINODE_ENDPOINT}/domains`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
if (!Array.isArray(result.body.data)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
const zone = result.body.data.find(d => d.domain === zoneName);
if (!zone || !zone.id) return callback(new BoxError(BoxError.NOT_FOUND, 'Zone not found'));
debug(`getZoneId: zone id of ${zoneName} is ${zone.id}`);
callback(null, zone.id);
});
}
function getZoneRecords(dnsConfig, zoneName, name, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`);
getZoneId(dnsConfig, zoneName, function (error, zoneId) {
if (error) return callback(error);
let page = 0, more = false;
let records = [];
async.doWhilst(function (iteratorDone) {
const url = `${LINODE_ENDPOINT}/domains/${zoneId}/records?page=${++page}`;
superagent.get(url)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
records = records.concat(result.body.data.filter(function (record) {
return (record.type === type && record.name === name);
}));
more = result.body.page !== result.body.pages;
iteratorDone();
});
}, function () { return more; }, function (error) {
debug('getZoneRecords:', error, JSON.stringify(records));
if (error) return callback(error);
callback(null, { zoneId, records });
});
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { records }) {
if (error) return callback(error);
var tmp = records.map(function (record) { return record.target; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
let i = 0, recordIds = []; // used to track available records to update instead of create
async.eachSeries(values, function (value, iteratorCallback) {
let data = {
type: type,
ttl_sec: 300 // lowest
};
if (type === 'MX') {
data.priority = parseInt(value.split(' ')[0], 10);
data.target = value.split(' ')[1];
} else if (type === 'TXT') {
data.target = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes
} else {
data.target = value;
}
if (i >= records.length) {
data.name = name; // only set for new records
superagent.post(`${LINODE_ENDPOINT}/domains/${zoneId}/records`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(result.body.id);
return iteratorCallback(null);
});
} else {
superagent.put(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${records[i].id}`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(result.body.id);
return iteratorCallback(null);
});
}
}, function (error) {
if (error) return callback(error);
debug('upsert: completed with recordIds:%j', recordIds);
callback();
});
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
if (records.length === 0) return callback(null);
var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); });
debug('del: %j', tmp);
if (tmp.length === 0) return callback(null);
// FIXME we only handle the first one currently
superagent.del(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${tmp[0].id}`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
return callback(null);
});
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.linode.com') === -1) {
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}
+3 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecheap'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -25,12 +26,12 @@ var assert = require('assert'),
const ENDPOINT = 'https://api.namecheap.com/xml.response';
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getQuery(dnsConfig, callback) {
+11 -2
View File
@@ -12,6 +12,7 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -27,12 +28,12 @@ function formatError(response) {
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
domainObject.config.token = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
@@ -54,6 +55,10 @@ function addRecord(dnsConfig, zoneName, name, type, values, callback) {
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else if (type === 'TXT') {
// we have to strip the quoting for some odd reason for name.com! If you change that also change updateRecord
let tmp = values[0];
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
} else {
data.answer = values[0];
}
@@ -91,6 +96,10 @@ function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callbac
if (type === 'MX') {
data.priority = parseInt(values[0].split(' ')[0], 10);
data.answer = values[0].split(' ')[1];
} else if (type === 'TXT') {
// we have to strip the quoting for some odd reason for name.com! If you change that also change addRecord
let tmp = values[0];
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
} else {
data.answer = values[0];
}
+6 -4
View File
@@ -13,6 +13,7 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/route53'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
@@ -21,12 +22,12 @@ var assert = require('assert'),
_ = require('underscore');
function removePrivateFields(domainObject) {
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER;
domainObject.config.secretAccessKey = constants.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.secretAccessKey === domains.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
}
function getDnsCredentials(dnsConfig) {
@@ -280,13 +281,14 @@ function verifyDnsConfig(domainObject, callback) {
}
const location = 'cloudrontestdns';
const newDomainObject = Object.assign({ }, domainObject, { config: credentials });
upsert(domainObject, location, 'A', [ ip ], function (error) {
upsert(newDomainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) {
del(newDomainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
+63 -63
View File
@@ -6,8 +6,6 @@ exports = module.exports = {
injectPrivateFields: injectPrivateFields,
removePrivateFields: removePrivateFields,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
ping: ping,
info: info,
@@ -19,7 +17,6 @@ exports = module.exports = {
stopContainerByName: stopContainer,
stopContainers: stopContainers,
deleteContainer: deleteContainer,
deleteContainerByName: deleteContainer,
deleteImage: deleteImage,
deleteContainers: deleteContainers,
createSubcontainer: createSubcontainer,
@@ -55,12 +52,6 @@ const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
function debugApp(app) {
assert(typeof app === 'object');
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
}
function testRegistryConfig(auth, callback) {
assert.strictEqual(typeof auth, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -73,13 +64,13 @@ function testRegistryConfig(auth, callback) {
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.password === exports.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
}
function removePrivateFields(registryConfig) {
assert.strictEqual(typeof registryConfig, 'object');
if (registryConfig.password) registryConfig.password = exports.SECRET_PLACEHOLDER;
if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
return registryConfig;
}
@@ -188,6 +179,19 @@ function downloadImage(manifest, callback) {
}, callback);
}
function getBindsSync(app) {
assert.strictEqual(typeof app, 'object');
let binds = [];
for (let name of Object.keys(app.binds)) {
const bind = app.binds[name];
binds.push(`${bind.hostPath}:/media/${name}:${bind.readOnly ? 'ro' : 'rw'}`);
}
return binds;
}
function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof name, 'string');
@@ -195,12 +199,11 @@ function createSubcontainer(app, name, cmd, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
let isAppContainer = !cmd; // non app-containers are like scheduler
var manifest = app.manifest;
var exposedPorts = {}, dockerPortBindings = { };
var domain = app.fqdn;
const hostname = isAppContainer ? app.id : name;
const envPrefix = manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
@@ -252,15 +255,9 @@ function createSubcontainer(app, name, cmd, options, callback) {
addons.getEnvironment(app, function (error, addonEnv) {
if (error) return callback(error);
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode
var containerOptions = {
let containerOptions = {
name: name, // for referencing containers
Tty: isAppContainer,
Hostname: hostname,
Image: app.manifest.dockerImage,
Cmd: (isAppContainer && app.debugMode && app.debugMode.cmd) ? app.debugMode.cmd : cmd,
Env: stdEnv.concat(addonEnv).concat(portEnv).concat(appEnv),
@@ -277,6 +274,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
HostConfig: {
Mounts: addons.getMountsSync(app, app.manifest.addons),
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
LogConfig: {
Type: 'syslog',
Config: {
@@ -296,32 +294,52 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
CpuShares: app.cpuShares,
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
NetworkMode: 'cloudron', // user defined bridge network
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
},
NetworkingConfig: {
EndpointsConfig: {
cloudron: {
Aliases: [ name ] // this allows sub-containers reach app containers by name
}
}
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
CapAdd: [],
CapDrop: []
}
};
// do no set hostname of containers to location as it might conflict with addons names. for example, an app installed in mail
// location may not reach mail container anymore by DNS. We cannot set hostname to fqdn either as that sets up the dns
// name to look up the internal docker ip. this makes curl from within container fail
// Note that Hostname has no effect on DNS. We have to use the --net-alias for dns.
// Hostname cannot be set with container NetworkMode. Subcontainers run is the network space of the app container
// This is done to prevent lots of up/down events and iptables locking
if (isAppContainer) {
containerOptions.Hostname = app.id;
containerOptions.HostConfig.NetworkMode = 'cloudron'; // user defined bridge network
containerOptions.HostConfig.Dns = ['172.18.0.1']; // use internal dns
containerOptions.HostConfig.DnsSearch = ['.']; // use internal dns
containerOptions.NetworkingConfig = {
EndpointsConfig: {
cloudron: {
Aliases: [ name ] // adds hostname entry with container name
}
}
};
} else {
containerOptions.HostConfig.NetworkMode = `container:${app.containerId}`;
}
var capabilities = manifest.capabilities || [];
if (capabilities.includes('net_admin')) {
containerOptions.HostConfig.CapAdd = [
'NET_ADMIN'
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
if (capabilities.includes('vaapi')) {
containerOptions.HostConfig.Devices = [
{ PathOnHost: '/dev/dri', PathInContainer: '/dev/dri', CgroupPermissions: 'rwm' }
];
}
containerOptions = _.extend(containerOptions, options);
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
gConnection.createContainer(containerOptions, function (error, container) {
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, container);
@@ -338,7 +356,6 @@ function startContainer(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
debug('Starting container %s', containerId);
container.start(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
@@ -354,7 +371,6 @@ function restartContainer(containerId, callback) {
assert.strictEqual(typeof callback, 'function');
var container = gConnection.getContainer(containerId);
debug('Restarting container %s', containerId);
container.restart(function (error) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
@@ -375,7 +391,6 @@ function stopContainer(containerId, callback) {
}
var container = gConnection.getContainer(containerId);
debug('Stopping container %s', containerId);
var options = {
t: 10 // wait for 10 seconds before killing it
@@ -384,24 +399,18 @@ function stopContainer(containerId, callback) {
container.stop(options, function (error) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
debug('Waiting for container ' + containerId);
container.wait(function (error, data) {
container.wait(function (error/*, data */) {
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message));
debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : '');
return callback(null);
});
});
}
function deleteContainer(containerId, callback) {
function deleteContainer(containerId, callback) { // id can also be name
assert(!containerId || typeof containerId === 'string');
assert.strictEqual(typeof callback, 'function');
debug('deleting container %s', containerId);
if (containerId === null) return callback(null);
var container = gConnection.getContainer(containerId);
@@ -428,8 +437,6 @@ function deleteContainers(appId, options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('deleting containers of %s', appId);
let labels = [ 'appId=' + appId ];
if (options.managedOnly) labels.push('isCloudronManaged=true');
@@ -446,8 +453,6 @@ function stopContainers(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Stopping containers of %s', appId);
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
@@ -514,7 +519,7 @@ function inspect(containerId, callback) {
var container = gConnection.getContainer(containerId);
container.inspect(function (error, result) {
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to find container ${containerId}`));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
callback(null, result);
@@ -573,10 +578,10 @@ function memoryUsage(containerId, callback) {
});
}
function createVolume(app, name, volumeDataDir, callback) {
assert.strictEqual(typeof app, 'object');
function createVolume(name, volumeDataDir, labels, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof volumeDataDir, 'string');
assert.strictEqual(typeof labels, 'object');
assert.strictEqual(typeof callback, 'function');
const volumeOptions = {
@@ -587,10 +592,7 @@ function createVolume(app, name, volumeDataDir, callback) {
device: volumeDataDir,
o: 'bind'
},
Labels: {
'fqdn': app.fqdn,
'appId': app.id
},
Labels: labels
};
// requires sudo because the path can be outside appsdata
@@ -605,8 +607,7 @@ function createVolume(app, name, volumeDataDir, callback) {
});
}
function clearVolume(app, name, options, callback) {
assert.strictEqual(typeof app, 'object');
function clearVolume(name, options, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -626,14 +627,13 @@ function clearVolume(app, name, options, callback) {
}
// this only removes the volume and not the data
function removeVolume(app, name, callback) {
assert.strictEqual(typeof app, 'object');
function removeVolume(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
let volume = gConnection.getVolume(name);
volume.remove(function (error) {
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume of ${app.id} ${error.message}`));
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`));
callback();
});
+16 -17
View File
@@ -23,11 +23,6 @@ var apps = require('./apps.js'),
var gHttpServer = null;
function authorizeApp(req, res, next) {
// TODO add here some authorization
// - block apps not using the docker addon
// - block calls regarding platform containers
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (constants.TEST) {
req.app = { id: 'testappid' };
@@ -60,10 +55,12 @@ function attachDockerRequest(req, res, next) {
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
res.write(' ');
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
dockerResponse.on('error', function (error) { debug('dockerResponse error:', error); });
dockerResponse.pipe(res, { end: true });
});
req.dockerRequest.on('error', () => {}); // abort() throws
next();
}
@@ -74,22 +71,21 @@ function containersCreate(req, res, next) {
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data');
debug('Original volume binds:', req.body.HostConfig.Binds);
debug('Original bind mounts:', req.body.HostConfig.Binds);
let binds = [];
for (let bind of (req.body.HostConfig.Binds || [])) {
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
else binds.push(`${dockerDataDir}/${bind}`);
if (!bind.startsWith('/app/data/')) {
req.dockerRequest.abort();
return next(new HttpError(400, 'Binds must be under /app/data/'));
}
binds.push(bind.replace(new RegExp('^/app/data/'), appDataDir + '/'));
}
// cleanup the paths from potential double slashes
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
debug('Rewritten volume binds:', binds);
debug('Rewritten bind mounts:', binds);
safe.set(req.body, 'HostConfig.Binds', binds);
let plainBody = JSON.stringify(req.body);
@@ -117,6 +113,9 @@ function start(callback) {
assert(gHttpServer === null, 'Already started');
let json = middleware.json({ strict: true });
// we protect container create as the app/admin can otherwise mount random paths (like the ghost file)
// protected other paths is done by preventing install/exec access of apps using docker addon
let router = new express.Router();
router.post('/:version/containers/create', containersCreate);
@@ -137,7 +136,7 @@ function start(callback) {
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '172.18.0.1', callback);
// Overwrite the default 2min request timeout. This is required for large builds for example
gHttpServer.setTimeout(60 * 60 * 1000);
+21 -10
View File
@@ -51,17 +51,22 @@ function getAll(callback) {
});
}
function add(name, domain, callback) {
function add(name, data, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof domain.zoneName, 'string');
assert.strictEqual(typeof domain.provider, 'string');
assert.strictEqual(typeof domain.config, 'object');
assert.strictEqual(typeof domain.tlsConfig, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
let queries = [
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig) ] },
{ query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] },
];
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'Domain already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
@@ -100,7 +105,12 @@ function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
let queries = [
{ query: 'DELETE FROM mail WHERE domain = ?', args: [ domain ] },
{ query: 'DELETE FROM domains WHERE domain = ?', args: [ domain ] },
];
database.transaction(queries, function (error, results) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') {
if (error.message.indexOf('apps_mailDomain_constraint') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by an app or the mailbox of an app. Check the domains of apps and the Email section of each app.'));
if (error.message.indexOf('subdomains') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more app(s).'));
@@ -108,8 +118,9 @@ function del(domain, callback) {
return callback(new BoxError(BoxError.CONFLICT, error.message));
}
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
if (results[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
callback(null);
});
+21 -63
View File
@@ -26,20 +26,16 @@ module.exports = exports = {
parentDomain: parentDomain,
checkDnsRecords: checkDnsRecords,
prepareDashboardDomain: prepareDashboardDomain,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
checkDnsRecords: checkDnsRecords
};
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
@@ -48,18 +44,20 @@ var assert = require('assert'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
assert.strictEqual(typeof provider, 'string');
switch (provider) {
case 'caas': return require('./dns/caas.js');
case 'cloudflare': return require('./dns/cloudflare.js');
case 'route53': return require('./dns/route53.js');
case 'gcdns': return require('./dns/gcdns.js');
case 'digitalocean': return require('./dns/digitalocean.js');
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'linode': return require('./dns/linode.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'noop': return require('./dns/noop.js');
@@ -91,14 +89,12 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`));
if (error) return callback(error);
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
callback(null, result);
});
}
function fqdn(location, domainObject) {
return location + (location ? (domainObject.config.hyphenatedSubdomains ? '-' : '.') : '') + domainObject.domain;
return location + (location ? '.' : '') + domainObject.domain;
}
// Hostname validation comes from RFC 1123 (section 2.1)
@@ -132,10 +128,6 @@ function validateHostname(location, domainObject) {
if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' });
}
if (domainObject.config.hyphenatedSubdomains) {
if (location.indexOf('.') !== -1) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot contain a dot', { field: 'location' });
}
return null;
}
@@ -147,10 +139,9 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
case 'letsencrypt-prod':
case 'letsencrypt-staging':
case 'fallback':
case 'caas':
break;
default:
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
}
if (tlsConfig.wildcard) {
@@ -170,7 +161,7 @@ function add(domain, data, auditSource, callback) {
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
let { zoneName, provider, config, fallbackCertificate, tlsConfig, dkimSelector } = data;
if (!tld.isValid(domain)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
if (domain.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
@@ -193,10 +184,12 @@ function add(domain, data, auditSource, callback) {
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (!dkimSelector) dkimSelector = 'cloudron-' + settings.adminDomain().replace(/\./g, '');
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector }, function (error) {
if (error) return callback(error);
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
@@ -204,6 +197,8 @@ function add(domain, data, auditSource, callback) {
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
mail.onDomainAdded(domain, NOOP_CALLBACK);
callback();
});
});
@@ -307,12 +302,15 @@ function del(domain, auditSource, callback) {
assert.strictEqual(typeof callback, 'function');
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain'));
if (domain === settings.mailDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain'));
domaindb.del(domain, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
mail.onDomainRemoved(domain, NOOP_CALLBACK);
return callback(null);
});
}
@@ -333,19 +331,7 @@ function getName(domain, location, type) {
if (location === '') return part;
if (!domain.config.hyphenatedSubdomains) return part ? `${location}.${part}` : location;
// hyphenatedSubdomains
if (type !== 'TXT') return `${location}-${part}`;
if (location.startsWith('_acme-challenge.')) {
return `${location}-${part}`;
} else if (location === '_acme-challenge') {
const up = part.replace(/^[^.]*\.?/, ''); // this gets the domain one level up
return up ? `${location}.${up}` : location;
} else {
return `${location}.${part}`;
}
return part ? `${location}.${part}` : location;
}
function getDnsRecords(location, domain, type, callback) {
@@ -436,6 +422,9 @@ function waitForDnsRecord(location, domain, type, value, options, callback) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
// linode DNS takes ~15mins
if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000;
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
});
}
@@ -450,8 +439,7 @@ function removePrivateFields(domain) {
function removeRestrictedFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
// always ensure config object
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
result.config = {}; // always ensure config object
return result;
}
@@ -463,33 +451,3 @@ function makeWildcard(hostname) {
parts[0] = '*';
return parts.join('.');
}
function prepareDashboardDomain(domain, auditSource, progressCallback, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
if (error) return callback(error);
const adminFqdn = fqdn(constants.ADMIN_LOCATION, domainObject);
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.series([
(done) => { progressCallback({ percent: 10, message: `Updating DNS of ${adminFqdn}` }); done(); },
upsertDnsRecords.bind(null, constants.ADMIN_LOCATION, domain, 'A', [ ip ]),
(done) => { progressCallback({ percent: 40, message: `Waiting for DNS of ${adminFqdn}` }); done(); },
waitForDnsRecord.bind(null, constants.ADMIN_LOCATION, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ percent: 70, message: `Getting certificate of ${adminFqdn}` }); done(); },
reverseProxy.ensureCertificate.bind(null, fqdn(constants.ADMIN_LOCATION, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
callback(null);
});
});
});
}
+5 -1
View File
@@ -7,7 +7,7 @@ exports = module.exports = {
getByCreationTime: getByCreationTime,
cleanup: cleanup,
// keep in sync with webadmin index.js filter and CLI tool
// keep in sync with webadmin index.js filter
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CLONE: 'app.clone',
ACTION_APP_CONFIGURE: 'app.configure',
@@ -21,6 +21,9 @@ exports = module.exports = {
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_START: 'app.start',
ACTION_APP_STOP: 'app.stop',
ACTION_APP_RESTART: 'app.restart',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
@@ -36,6 +39,7 @@ exports = module.exports = {
ACTION_DOMAIN_UPDATE: 'domain.update',
ACTION_DOMAIN_REMOVE: 'domain.remove',
ACTION_MAIL_LOCATION: 'mail.location',
ACTION_MAIL_ENABLED: 'mail.enabled',
ACTION_MAIL_DISABLED: 'mail.disabled',
ACTION_MAIL_MAILBOX_ADD: 'mail.box.add',
+332 -66
View File
@@ -20,7 +20,9 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:externalldap'),
groups = require('./groups.js'),
ldap = require('ldapjs'),
once = require('once'),
settings = require('./settings.js'),
tasks = require('./tasks.js'),
users = require('./users.js');
@@ -40,14 +42,14 @@ function translateUser(ldapConfig, ldapUser) {
return {
username: ldapUser[ldapConfig.usernameField],
email: ldapUser.mail,
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
};
}
function validUserRequirements(user) {
if (!user.username || !user.email || !user.displayName) {
debug(`[LDAP user empty username/email/displayName] username=${user.username} email=${user.email} displayName=${user.displayName}`);
debug(`[Invalid LDAP user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
return false;
} else {
return true;
@@ -55,40 +57,95 @@ function validUserRequirements(user) {
}
// performs service bind if required
function getClient(externalLdapConfig, callback) {
function getClient(externalLdapConfig, doBindAuth, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof doBindAuth, 'boolean');
assert.strictEqual(typeof callback, 'function');
// ensure we only callback once since we also have to listen to client.error events
callback = once(callback);
// basic validation to not crash
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
var config = {
url: externalLdapConfig.url,
tlsOptions: {
rejectUnauthorized: externalLdapConfig.acceptSelfSignedCerts ? false : true
}
};
var client;
try {
client = ldap.createClient({ url: externalLdapConfig.url });
client = ldap.createClient(config);
} catch (e) {
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
}
if (!externalLdapConfig.bindDn) return callback(null, client);
// ensure we don't just crash
client.on('error', function (error) {
callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
});
// skip bind auth if none exist or if not wanted
if (!externalLdapConfig.bindDn || !doBindAuth) return callback(null, client);
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, client, externalLdapConfig);
callback(null, client);
});
}
function ldapGetByDN(externalLdapConfig, dn, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof dn, 'string');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
paged: true,
scope: 'sub' // We may have to make this configurable
};
debug(`Get object at ${dn}`);
// basic validation to not crash
try { ldap.parseDN(dn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid DN')); }
client.search(dn, searchOptions, function (error, result) {
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
let ldapObjects = [];
result.on('searchEntry', entry => ldapObjects.push(entry.object));
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
result.on('end', function (result) {
client.unbind();
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
if (ldapObjects.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
callback(null, ldapObjects[0]);
});
});
});
}
// TODO support search by email
function ldapSearch(externalLdapConfig, options, callback) {
function ldapUserSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, function (error, client) {
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
@@ -124,6 +181,48 @@ function ldapSearch(externalLdapConfig, options, callback) {
});
}
function ldapGroupSearch(externalLdapConfig, options, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
let searchOptions = {
paged: true,
scope: 'sub' // We may have to make this configurable
};
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter);
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
let extraFilter = ldap.parseFilter(options.filter);
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
}
debug(`Listing groups at ${externalLdapConfig.groupBaseDn} with filter ${searchOptions.filter.toString()}`);
client.search(externalLdapConfig.groupBaseDn, searchOptions, function (error, result) {
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
let ldapGroups = [];
result.on('searchEntry', entry => ldapGroups.push(entry.object));
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
result.on('end', function (result) {
client.unbind();
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
callback(null, ldapGroups);
});
});
});
}
function testConfig(config, callback) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -141,7 +240,20 @@ function testConfig(config, callback) {
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty'));
try { ldap.parseFilter(config.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
getClient(config, function (error, client) {
if ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean'));
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean'));
if (config.syncGroups) {
if (!config.groupBaseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty'));
try { ldap.parseDN(config.groupBaseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupBaseDn')); }
if (!config.groupFilter) return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
try { ldap.parseFilter(config.groupFilter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupFilter')); }
if (!config.groupnameField || typeof config.groupnameField !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
}
getClient(config, true, function (error, client) {
if (error) return callback(error);
var opts = {
@@ -167,7 +279,7 @@ function search(identifier, callback) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
if (error) return callback(error);
// translate ldap properties to ours
@@ -188,7 +300,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) {
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
if (!externalLdapConfig.autoCreate) return callback(new BoxError(BoxError.BAD_STATE, 'auto create not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
if (error) return callback(error);
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
@@ -198,7 +310,7 @@ function createAndVerifyUserIfNotExist(identifier, password, callback) {
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) {
if (error) {
console.error('Failed to auto create user', user.username, error);
debug(`createAndVerifyUserIfNotExist: Failed to auto create user ${user.username}`, error);
return callback(new BoxError(BoxError.INTERNAL_ERROR));
}
@@ -220,17 +332,20 @@ function verifyPassword(user, password, callback) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
if (error) return callback(error);
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
let client = ldap.createClient({ url: externalLdapConfig.url });
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
getClient(externalLdapConfig, false, function (error, client) {
if (error) return callback(error);
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
client.bind(ldapUsers[0].dn, password, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
});
});
});
});
@@ -255,6 +370,197 @@ function startSyncer(callback) {
});
}
function syncUsers(externalLdapConfig, progressCallback, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
ldapUserSearch(externalLdapConfig, {}, function (error, ldapUsers) {
if (error) return callback(error);
debug(`Found ${ldapUsers.length} users`);
let percent = 10;
let step = 30/(ldapUsers.length+1); // ensure no divide by 0
// we ignore all errors here and just log them for now
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
user = translateUser(externalLdapConfig, user);
if (!validUserRequirements(user)) return iteratorCallback();
percent += step;
progressCallback({ percent, message: `Syncing... ${user.username}` });
users.getByUsername(user.username, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
if (!result) {
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) debug('syncUsers: Failed to create user', user, error.message);
iteratorCallback();
});
} else if (result.source !== 'ldap') {
debug(`[conflicting user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
iteratorCallback();
} else if (result.email !== user.email || result.displayName !== user.displayName) {
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) debug('Failed to update user', user, error);
iteratorCallback();
});
} else {
// user known and up-to-date
debug(`[up-to-date user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
iteratorCallback();
}
});
}, callback);
});
}
function syncGroups(externalLdapConfig, progressCallback, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!externalLdapConfig.syncGroups) {
debug('Group sync is disabled');
progressCallback({ percent: 70, message: 'Skipping group sync...' });
return callback(null, []);
}
ldapGroupSearch(externalLdapConfig, {}, function (error, ldapGroups) {
if (error) return callback(error);
debug(`Found ${ldapGroups.length} groups`);
let percent = 40;
let step = 30/(ldapGroups.length+1); // ensure no divide by 0
// we ignore all non internal errors here and just log them for now
async.eachSeries(ldapGroups, function (ldapGroup, iteratorCallback) {
var groupName = ldapGroup[externalLdapConfig.groupnameField];
if (!groupName) return iteratorCallback();
// some servers return empty array for unknown properties :-/
if (typeof groupName !== 'string') return iteratorCallback();
// groups are lowercase
groupName = groupName.toLowerCase();
percent += step;
progressCallback({ percent, message: `Syncing... ${groupName}` });
groups.getByName(groupName, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
if (!result) {
debug(`[adding group] groupname=${groupName}`);
groups.create(groupName, 'ldap', function (error) {
if (error) debug('syncGroups: Failed to create group', groupName, error);
iteratorCallback();
});
} else {
debug(`[up-to-date group] groupname=${groupName}`);
iteratorCallback();
}
});
}, function (error) {
if (error) return callback(error);
debug('sync: ldap sync is done', error);
callback(error);
});
});
}
function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!externalLdapConfig.syncGroups) {
debug('Group users sync is disabled');
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
return callback(null, []);
}
groups.getAll(function (error, result) {
if (error) return callback(error);
var ldapGroups = result.filter(function (g) { return g.source === 'ldap'; });
debug(`Found ${ldapGroups.length} groups to sync users`);
async.eachSeries(ldapGroups, function (group, iteratorCallback) {
debug(`Sync users for group ${group.name}`);
ldapGroupSearch(externalLdapConfig, {}, function (error, result) {
if (error) return callback(error);
if (!result || result.length === 0) {
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
return callback();
}
// since our group names are lowercase we cannot use potentially case matching ldap filters
let found = result.find(function (r) {
if (!r[externalLdapConfig.groupnameField]) return false;
return r[externalLdapConfig.groupnameField].toLowerCase() === group.name;
});
if (!found) {
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
return callback();
}
var ldapGroupMembers = found.member || found.uniqueMember || [];
// if only one entry is in the group ldap returns a string, not an array!
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
async.eachSeries(ldapGroupMembers, function (memberDn, iteratorCallback) {
ldapGetByDN(externalLdapConfig, memberDn, function (error, result) {
if (error) {
debug(`Failed to get ${memberDn}:`, error);
return iteratorCallback();
}
debug(`Found member object at ${memberDn} adding to group ${group.name}`);
const username = result[externalLdapConfig.usernameField];
if (!username) return iteratorCallback();
users.getByUsername(username, function (error, result) {
if (error) {
debug(`syncGroupUsers: Failed to get user by username ${username}`, error);
return iteratorCallback();
}
groups.addMember(group.id, result.id, function (error) {
if (error && error.reason !== BoxError.ALREADY_EXISTS) debug('syncGroupUsers: Failed to add member', error);
iteratorCallback();
});
});
});
}, function (error) {
if (error) debug('syncGroupUsers: ', error);
iteratorCallback();
});
});
}, callback);
});
}
function sync(progressCallback, callback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
@@ -265,58 +571,18 @@ function sync(progressCallback, callback) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
ldapSearch(externalLdapConfig, {}, function (error, ldapUsers) {
async.series([
syncUsers.bind(null, externalLdapConfig, progressCallback),
syncGroups.bind(null, externalLdapConfig, progressCallback),
syncGroupUsers.bind(null, externalLdapConfig, progressCallback)
], function (error) {
if (error) return callback(error);
debug(`Found ${ldapUsers.length} users`);
let percent = 10;
let step = 90/(ldapUsers.length+1); // ensure no divide by 0
progressCallback({ percent: 100, message: 'Done' });
// we ignore all errors here and just log them for now
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
user = translateUser(externalLdapConfig, user);
debug('sync: ldap sync is done', error);
if (!validUserRequirements(user)) return iteratorCallback();
percent += step;
progressCallback({ percent, message: `Syncing... ${user.username}` });
users.getByUsername(user.username, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) {
debug(`Could not find user with username ${user.username}: ${error.message}`);
return iteratorCallback();
}
if (error) {
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) console.error('Failed to create user', user, error);
iteratorCallback();
});
} else if (result.source !== 'ldap') {
debug(`[conflicting user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
iteratorCallback();
} else if (result.email !== user.email || result.displayName !== user.displayName) {
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
if (error) debug('Failed to update user', user, error);
iteratorCallback();
});
} else {
// user known and up-to-date
debug(`[up-to-date user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
iteratorCallback();
}
});
}, function (error) {
debug('sync: ldap sync is done', error);
callback(error);
});
callback(error);
});
});
}
+7 -2
View File
@@ -5,6 +5,7 @@ exports = module.exports = {
};
var assert = require('assert'),
async = require('async'),
infra = require('./infra_version.js'),
paths = require('./paths.js'),
shell = require('./shell.js');
@@ -26,7 +27,7 @@ function startGraphite(existingInfra, callback) {
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=graphite \
-m 75m \
-m 150m \
--memory-swap 150m \
--dns 172.18.0.1 \
--dns-search=. \
@@ -37,5 +38,9 @@ function startGraphite(existingInfra, callback) {
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startGraphite', cmd, callback);
async.series([
shell.exec.bind(null, 'stopGraphite', 'docker stop graphite || true'),
shell.exec.bind(null, 'removeGraphite', 'docker rm -f graphite || true'),
shell.exec.bind(null, 'startGraphite', cmd)
], callback);
}
+19 -20
View File
@@ -2,6 +2,7 @@
exports = module.exports = {
get: get,
getByName: getByName,
getWithMembers: getWithMembers,
getAll: getAll,
getAllWithMembers: getAllWithMembers,
@@ -19,8 +20,6 @@ exports = module.exports = {
getMembership: getMembership,
setMembership: setMembership,
getGroups: getGroups,
_clear: clear
};
@@ -28,7 +27,7 @@ var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
var GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(',');
function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
@@ -42,6 +41,18 @@ function get(groupId, callback) {
});
}
function getByName(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE name = ?', [ name ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
callback(null, result[0]);
});
}
function getWithMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -63,7 +74,7 @@ function getWithMembers(groupId, callback) {
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups ORDER BY name', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
@@ -73,9 +84,8 @@ function getAll(callback) {
function getAllWithMembers(callback) {
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' GROUP BY userGroups.id', function (error, results) {
' GROUP BY userGroups.id ORDER BY name', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
@@ -83,12 +93,13 @@ function getAllWithMembers(callback) {
});
}
function add(id, name, callback) {
function add(id, name, source, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof source, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
database.query('INSERT INTO userGroups (id, name, source) VALUES (?, ?, ?)', [ id, name, source ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -260,15 +271,3 @@ function isMember(groupId, userId, callback) {
callback(null, result.length !== 0);
});
}
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
+26 -15
View File
@@ -4,6 +4,7 @@ exports = module.exports = {
create: create,
remove: remove,
get: get,
getByName: getByName,
update: update,
getWithMembers: getWithMembers,
getAll: getAll,
@@ -15,8 +16,6 @@ exports = module.exports = {
removeMember: removeMember,
isMember: isMember,
getGroups: getGroups,
setMembership: setMembership,
getMembership: getMembership,
@@ -45,8 +44,17 @@ function validateGroupname(name) {
return null;
}
function create(name, callback) {
function validateGroupSource(source) {
assert.strictEqual(typeof source, 'string');
if (source !== '' && source !== 'ldap') return new BoxError(BoxError.BAD_FIELD, 'source must be "" or "ldap"', { field: source });
return null;
}
function create(name, source, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof source, 'string');
assert.strictEqual(typeof callback, 'function');
// we store names in lowercase
@@ -55,8 +63,11 @@ function create(name, callback) {
var error = validateGroupname(name);
if (error) return callback(error);
error = validateGroupSource(source);
if (error) return callback(error);
var id = 'gid-' + uuid.v4();
groupdb.add(id, name, function (error) {
groupdb.add(id, name, source, function (error) {
if (error) return callback(error);
callback(null, { id: id, name: name });
@@ -85,6 +96,17 @@ function get(id, callback) {
});
}
function getByName(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getByName(name, function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
}
function getWithMembers(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -217,17 +239,6 @@ function update(groupId, data, callback) {
});
}
function getGroups(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getGroups(userId, function (error, results) {
if (error) return callback(error);
callback(null, results);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
+10 -9
View File
@@ -6,21 +6,22 @@
exports = module.exports = {
// a version change recreates all containers with latest docker config
'version': '48.17.0',
'version': '48.17.1',
'baseImages': [
{ repo: 'cloudron/base', tag: 'cloudron/base:1.0.0@sha256:147a648a068a2e746644746bbfb42eb7a50d682437cead3c67c933c546357617' }
{ repo: 'cloudron/base', tag: 'cloudron/base:2.0.0@sha256:f9fea80513aa7c92fe2e7bf3978b54c8ac5222f47a9a32a7f8833edf0eb5a4f4' }
],
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.1.0@sha256:eee0dfd3829d563f2063084bc0d7c8802c4bdd6e233159c6226a17ff7a9a3503' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.6.2@sha256:e7847f626bac91698dd7d779048d9b1ee4e1764d1399f2d0553da4882c272166' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.1.0@sha256:e1dd22aa6eef5beb7339834b200a8bb787ffc2264ce11139857a054108fefb4f' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.3.1@sha256:c1145d43c8a912fe6f5a5629a4052454a4aa6f23391c1efbffeec9d12d72a256' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:3.1.0@sha256:261c38d332a20cd4160930d7395fd342496159e94c522d92fde8163c680adc98' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:3.0.0@sha256:59e50b1f55e433ffdf6d678f8c658812b4119f631db8325572a52ee40d3bc562' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.3.0@sha256:0e31ec817e235b1814c04af97b1e7cf0053384aca2569570ce92bef0d95e94d2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.10.0@sha256:3aff92bfc85d6ca3cc6fc381c8a89625d2af95cc55ed2db692ef4e483e600372' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.3.0@sha256:b7bc1ca4f4d0603a01369a689129aa273a938ce195fe43d00d42f4f2d5212f50' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:2.0.2@sha256:cbd604eaa970c99ba5c4c2e7984929668e05de824172f880e8c576b2fb7c976d' }
}
};
+5 -21
View File
@@ -16,21 +16,15 @@ const NOOP_CALLBACK = function () { };
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
function ignoreError(func) {
return function (callback) {
func(function (error) {
if (error) console.error('Ignored error:', error);
function cleanupTokens(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
callback();
});
};
}
callback = callback || NOOP_CALLBACK;
function cleanupExpiredTokens(callback) {
assert.strictEqual(typeof callback, 'function');
debug('Cleaning up expired tokens');
tokendb.delExpired(function (error, result) {
if (error) return callback(error);
if (error) return debug('cleanupTokens: error removing expired tokens', error);
debug('Cleaned up %s expired tokens.', result);
@@ -38,16 +32,6 @@ function cleanupExpiredTokens(callback) {
});
}
function cleanupTokens(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
debug('Cleaning up expired tokens');
async.series([
ignoreError(cleanupExpiredTokens)
], callback);
}
function cleanupTmpVolume(containerInfo, callback) {
assert.strictEqual(typeof containerInfo, 'object');
assert.strictEqual(typeof callback, 'function');
+16 -16
View File
@@ -154,7 +154,6 @@ function userSearch(req, res, next) {
givenName: firstName,
username: user.username,
samaccountname: user.username, // to support ActiveDirectory clients
isadmin: users.compareRoles(user.role, users.ROLE_ADMIN) >= 0,
memberof: groups
}
};
@@ -347,7 +346,7 @@ function mailboxSearch(req, res, next) {
if (error) return callback(error);
aliases.forEach(function (a, idx) {
obj.attributes['mail' + idx] = `${a}@${mailbox.domain}`;
obj.attributes['mail' + idx] = `${a.name}@${a.domain}`;
});
// ensure all filter values are also lowercase
@@ -392,7 +391,7 @@ function mailAliasSearch(req, res, next) {
objectclass: ['nisMailAlias'],
objectcategory: 'nisMailAlias',
cn: `${alias.name}@${alias.domain}`,
rfc822MailMember: `${alias.aliasTarget}@${alias.domain}`
rfc822MailMember: `${alias.aliasName}@${alias.aliasDomain}`
}
};
@@ -418,7 +417,7 @@ function mailingListSearch(req, res, next) {
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
const name = parts[0], domain = parts[1];
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers) {
mail.resolveList(parts[0], parts[1], function (error, resolvedMembers, list) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.toString()));
@@ -431,6 +430,7 @@ function mailingListSearch(req, res, next) {
objectcategory: 'mailGroup',
cn: `${name}@${domain}`, // fully qualified
mail: `${name}@${domain}`,
membersOnly: list.membersOnly, // ldapjs only supports strings and string array. so this is not a bool!
mgrpRFC822MailMember: resolvedMembers // fully qualified
}
};
@@ -534,13 +534,16 @@ function authenticateSftp(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// actual user bind
users.verifyWithUsername(parts[0], req.credentials, users.AP_SFTP, function (error) {
apps.getByFqdn(parts[1], function (error, app) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
debug('sftp auth: success');
users.verifyWithUsername(parts[0], req.credentials, app.id, function (error) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
res.end();
debug('sftp auth: success');
res.end();
});
});
}
@@ -574,7 +577,7 @@ function userSearchSftp(req, res, next) {
var obj = {
dn: ldap.parseDN(`cn=${username}@${appFqdn},ou=sftp,dc=cloudron`).toString(),
attributes: {
homeDirectory: path.join('/app/data', app.id, 'data'),
homeDirectory: path.join('/app/data', app.id),
objectclass: ['user'],
objectcategory: 'person',
cn: user.id,
@@ -615,10 +618,7 @@ function authenticateMailAddon(req, res, next) {
// note: with sendmail addon, apps can send mail without a mailbox (unlike users)
appdb.getAppIdByAddonConfigValue(addonId, namePattern, req.credentials || '', function (error, appId) {
if (error && error.reason !== BoxError.NOT_FOUND) return next(new ldap.OperationsError(error.message));
if (appId) { // matched app password
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: email }, { appId: appId, addonId: addonId });
return res.end();
}
if (appId) return res.end();
mailboxdb.getMailbox(parts[0], parts[1], function (error, mailbox) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
@@ -645,14 +645,14 @@ function start(callback) {
debug: NOOP,
info: debug,
warn: debug,
error: console.error,
fatal: console.error
error: debug,
fatal: debug
};
gServer = ldap.createServer({ log: logger });
gServer.on('error', function (error) {
console.error('LDAP:', error);
debug('start: server error ', error);
});
gServer.search('ou=users,dc=cloudron', authenticateApp, userSearch);
+2
View File
@@ -8,6 +8,7 @@
maxsize 1M
missingok
delaycompress
# this truncates the original log file and not the rotated one
copytruncate
}
@@ -18,6 +19,7 @@
missingok
# we never compress so we can simply tail the files
nocompress
# this truncates the original log file and not the rotated one
copytruncate
}
+195 -111
View File
@@ -1,59 +1,65 @@
'use strict';
exports = module.exports = {
getStatus: getStatus,
checkConfiguration: checkConfiguration,
getStatus,
checkConfiguration,
getDomains: getDomains,
getLocation,
setLocation, // triggers the change task
changeLocation, // does the actual changing
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
clearDomains: clearDomains,
getDomains,
removePrivateFields: removePrivateFields,
getDomain,
clearDomains,
setDnsRecords: setDnsRecords,
onMailFqdnChanged: onMailFqdnChanged,
onDomainAdded,
onDomainRemoved,
validateName: validateName,
removePrivateFields,
setMailFromValidation: setMailFromValidation,
setCatchAllAddress: setCatchAllAddress,
setMailRelay: setMailRelay,
setMailEnabled: setMailEnabled,
setDnsRecords,
validateName,
setMailFromValidation,
setCatchAllAddress,
setMailRelay,
setMailEnabled,
setBanner,
startMail: restartMail,
restartMail: restartMail,
handleCertChanged: handleCertChanged,
getMailAuth: getMailAuth,
restartMail,
handleCertChanged,
getMailAuth,
sendTestMail: sendTestMail,
sendTestMail,
listMailboxes: listMailboxes,
removeMailboxes: removeMailboxes,
getMailbox: getMailbox,
addMailbox: addMailbox,
updateMailboxOwner: updateMailboxOwner,
removeMailbox: removeMailbox,
getMailboxCount,
listMailboxes,
getMailbox,
addMailbox,
updateMailboxOwner,
removeMailbox,
listAliases: listAliases,
getAliases: getAliases,
setAliases: setAliases,
getAliases,
setAliases,
getLists: getLists,
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList,
resolveList: resolveList,
getLists,
getList,
addList,
updateList,
removeList,
resolveList,
_removeMailboxes: removeMailboxes,
_readDkimPublicKeySync: readDkimPublicKeySync
};
var assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
cloudron = require('./cloudron.js'),
constants = require('./constants.js'),
debug = require('debug')('box:mail'),
dns = require('./native-dns.js'),
@@ -75,12 +81,14 @@ var assert = require('assert'),
shell = require('./shell.js'),
smtpTransport = require('nodemailer-smtp-transport'),
sysinfo = require('./sysinfo.js'),
tasks = require('./tasks.js'),
users = require('./users.js'),
validator = require('validator'),
_ = require('underscore');
const DNS_OPTIONS = { timeout: 5000 };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
const REMOVE_MAILBOX = path.join(__dirname, 'scripts/rmmailbox.sh');
function validateName(name) {
assert.strictEqual(typeof name, 'string');
@@ -100,8 +108,6 @@ function checkOutboundPort25(callback) {
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.comcast.net',
'smtp.1und1.de',
]);
@@ -208,7 +214,8 @@ function checkDkim(mailDomain, callback) {
if (txtRecords.length !== 0) {
dkim.value = txtRecords[0].join('');
dkim.status = (dkim.value === dkim.expected);
const actual = txtToDict(dkim.value);
dkim.status = actual.p === dkimKey;
}
callback(null, dkim);
@@ -237,14 +244,14 @@ function checkSpf(domain, mailFqdn, callback) {
let txtRecord = txtRecords[i].join(''); // https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
if (txtRecord.indexOf('v=spf1 ') !== 0) continue; // not SPF
spf.value = txtRecord;
spf.status = spf.value.indexOf(' a:' + settings.adminFqdn()) !== -1;
spf.status = spf.value.indexOf(' a:' + settings.mailFqdn()) !== -1;
break;
}
if (spf.status) {
spf.expected = spf.value;
} else if (i !== txtRecords.length) {
spf.expected = 'v=spf1 a:' + settings.adminFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
spf.expected = 'v=spf1 a:' + settings.mailFqdn() + ' ' + spf.value.slice('v=spf1 '.length);
}
callback(null, spf);
@@ -269,7 +276,7 @@ function checkMx(domain, mailFqdn, callback) {
if (error) return callback(error, mx);
if (mxRecords.length === 0) return callback(null, mx);
mx.status = mxRecords.length == 1 && mxRecords[0].exchange === mailFqdn;
mx.status = mxRecords.some(mx => mx.exchange === mailFqdn); // this lets use change priority and/or setup backup MX
mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange + '.'; }).join(' ');
if (mx.status) return callback(null, mx); // MX record is "my."
@@ -313,9 +320,8 @@ function checkDmarc(domain, callback) {
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
// allow extra fields in dmarc like rua
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
const actual = txtToDict(dmarc.value);
dmarc.status = actual.v === 'DMARC1'; // see box#666
}
callback(null, dmarc);
@@ -544,7 +550,7 @@ function checkConfiguration(callback) {
markdownMessage += '\n\n';
});
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://cloudron.io/documentation/troubleshooting/#mail-dns) for more information.\n';
if (markdownMessage) markdownMessage += 'Email Status is checked every 30 minutes.\n See the [troubleshooting docs](https://docs.cloudron.io/troubleshooting/#mail-dns) for more information.\n';
callback(null, markdownMessage); // empty message means all status checks succeeded
});
@@ -576,15 +582,18 @@ function createMailConfig(mailFqdn, mailDomain, callback) {
}
// create sections for per-domain configuration
mailDomains.forEach(function (domain) {
async.eachSeries(mailDomains, function (domain, iteratorDone) {
const catchAll = domain.catchAll.map(function (c) { return `${c}@${domain.domain}`; }).join(',');
const mailFromValidation = domain.mailFromValidation;
if (!safe.fs.appendFileSync(path.join(paths.ADDON_CONFIG_DIR, 'mail/mail.ini'),
`[${domain.domain}]\ncatch_all=${catchAll}\nmail_from_validation=${mailFromValidation}\n\n`, 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.text`, domain.banner.text || '')) return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create text banner file:' + safe.error.message));
if (!safe.fs.writeFileSync(`${paths.ADDON_CONFIG_DIR}/mail/banner/${domain.domain}.html`, domain.banner.html || '')) return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create html banner file:' + safe.error.message));
const relay = domain.relay;
const enableRelay = relay.provider !== 'cloudron-smtp' && relay.provider !== 'noop',
@@ -594,15 +603,19 @@ function createMailConfig(mailFqdn, mailDomain, callback) {
username = relay.username || '',
password = relay.password || '';
if (!enableRelay) return;
if (!enableRelay) return iteratorDone();
if (!safe.fs.appendFileSync(paths.ADDON_CONFIG_DIR + '/mail/smtp_forward.ini',
`[${domain.domain}]\nenable_outbound=true\nhost=${host}\nport=${port}\nenable_tls=true\nauth_type=${authType}\nauth_user=${username}\nauth_pass=${password}\n\n`, 'utf8')) {
return callback(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
return iteratorDone(new BoxError(BoxError.FS_ERROR, 'Could not create mail var file:' + safe.error.message));
}
});
callback(null, mailInDomains.length !== 0 /* allowInbound */);
iteratorDone();
}, function (error) {
if (error) return callback(error);
callback(null, mailInDomains.length !== 0 /* allowInbound */);
});
});
}
@@ -630,7 +643,10 @@ function configureMail(mailFqdn, mailDomain, callback) {
if (!safe.child_process.execSync(`cp ${bundle.certFilePath} ${mailCertFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create cert file:' + safe.error.message));
if (!safe.child_process.execSync(`cp ${bundle.keyFilePath} ${mailKeyFilePath}`)) return callback(new BoxError(BoxError.FS_ERROR, 'Could not create key file:' + safe.error.message));
shell.exec('startMail', 'docker rm -f mail || true', function (error) {
async.series([
shell.exec.bind(null, 'stopMail', 'docker stop mail || true'),
shell.exec.bind(null, 'removeMail', 'docker rm -f mail || true'),
], function (error) {
if (error) return callback(error);
createMailConfig(mailFqdn, mailDomain, function (error, allowInbound) {
@@ -799,6 +815,7 @@ function ensureDkimKeySync(mailDomain) {
return new BoxError(BoxError.FS_ERROR, safe.error);
}
// https://www.unlocktheinbox.com/dkim-key-length-statistics/ and https://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-authentication-dkim-easy.html for key size
if (!safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM')) return new BoxError(BoxError.OPENSSL_ERROR, safe.error);
@@ -887,56 +904,91 @@ function setDnsRecords(domain, callback) {
upsertDnsRecords(domain, settings.mailFqdn(), callback);
}
function onMailFqdnChanged(callback) {
function getLocation(callback) {
assert.strictEqual(typeof callback, 'function');
const mailFqdn = settings.mailFqdn(),
mailDomain = settings.adminDomain();
const domain = settings.mailDomain(), fqdn = settings.mailFqdn();
const subdomain = fqdn.substr(0, fqdn.length - domain.length - 1);
domains.getAll(function (error, allDomains) {
callback(null, { domain, subdomain });
}
function changeLocation(auditSource, progressCallback, callback) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
const fqdn = settings.mailFqdn(), domain = settings.mailDomain();
const subdomain = fqdn.substr(0, fqdn.length - domain.length - 1);
let progress = 20;
progressCallback({ percent: progress, message: `Setting up DNS of certs of mail server ${fqdn}` });
cloudron.setupDnsAndCert(subdomain, domain, auditSource, progressCallback, function (error) {
if (error) return callback(error);
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
upsertDnsRecords(domainObject.domain, mailFqdn, iteratorDone);
}, function (error) {
domains.getAll(function (error, allDomains) {
if (error) return callback(error);
configureMail(mailFqdn, mailDomain, callback);
async.eachOfSeries(allDomains, function (domainObject, idx, iteratorDone) {
progressCallback({ percent: progress, message: `Updating DNS of ${domainObject.domain}` });
progress += Math.round(70/allDomains.length);
upsertDnsRecords(domainObject.domain, fqdn, iteratorDone);
}, function (error) {
if (error) return callback(error);
progressCallback({ percent: 90, message: 'Restarting mail server' });
restartMailIfActivated(callback);
});
});
});
}
function addDomain(domain, callback) {
function setLocation(subdomain, domain, auditSource, callback) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
const fqdn = domains.fqdn(subdomain, domainObject);
callback();
settings.setMailLocation(domain, fqdn, function (error) {
if (error) return callback(error);
tasks.add(tasks.TASK_CHANGE_MAIL_LOCATION, [ auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_MAIL_LOCATION, auditSource, { subdomain, domain, taskId });
callback(null, taskId);
});
});
});
}
function removeDomain(domain, callback) {
function onDomainAdded(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT));
if (!settings.mailFqdn()) return callback(); // mail domain is not set yet (when provisioning)
maildb.del(domain, function (error) {
if (error) return callback(error);
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], callback);
}
restartMail(NOOP_CALLBACK);
function onDomainRemoved(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
callback();
});
restartMail(callback);
}
function clearDomains(callback) {
@@ -951,7 +1003,7 @@ function clearDomains(callback) {
// remove all fields that should never be sent out via REST API
function removePrivateFields(domain) {
let result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay');
let result = _.pick(domain, 'domain', 'enabled', 'mailFromValidation', 'catchAll', 'relay', 'banner');
if (result.relay.provider !== 'cloudron-smtp') {
if (result.relay.username === result.relay.password) result.relay.username = constants.SECRET_PLACEHOLDER;
result.relay.password = constants.SECRET_PLACEHOLDER;
@@ -973,6 +1025,20 @@ function setMailFromValidation(domain, enabled, callback) {
});
}
function setBanner(domain, banner, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof banner, 'object');
assert.strictEqual(typeof callback, 'function');
maildb.update(domain, { banner }, function (error) {
if (error) return callback(error);
restartMail(NOOP_CALLBACK);
callback(null);
});
}
function setCatchAllAddress(domain, addresses, callback) {
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(addresses));
@@ -1048,13 +1114,25 @@ function sendTestMail(domain, to, callback) {
});
}
function listMailboxes(domain, page, perPage, callback) {
function listMailboxes(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.listMailboxes(domain, page, perPage, function (error, result) {
mailboxdb.listMailboxes(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function getMailboxCount(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getMailboxCount(domain, function (error, result) {
if (error) return callback(error);
callback(null, result);
@@ -1127,31 +1205,25 @@ function updateMailboxOwner(name, domain, userId, auditSource, callback) {
});
}
function removeMailbox(name, domain, auditSource, callback) {
function removeMailbox(name, domain, options, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
mailboxdb.del(name, domain, function (error) {
if (error) return callback(error);
const deleteMailFunc = options.deleteMails ? shell.sudo.bind(null, 'removeMailbox', [ REMOVE_MAILBOX, `${name}@${domain}` ], {}) : (next) => next();
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
deleteMailFunc(function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`));
callback(null);
});
}
mailboxdb.del(name, domain, function (error) {
if (error) return callback(error);
function listAliases(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_REMOVE, auditSource, { name, domain });
mailboxdb.listAliases(domain, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
callback();
});
});
}
@@ -1178,12 +1250,15 @@ function setAliases(name, domain, aliases, callback) {
assert.strictEqual(typeof callback, 'function');
for (var i = 0; i < aliases.length; i++) {
aliases[i] = aliases[i].toLowerCase();
let name = aliases[i].name.toLowerCase();
let domain = aliases[i].domain.toLowerCase();
var error = validateName(aliases[i]);
let error = validateName(name);
if (error) return callback(error);
}
if (!validator.isEmail(`${name}@${domain}`)) return callback(new BoxError(BoxError.BAD_FIELD, `Invalid email: ${name}@${domain}`));
aliases[i] = { name, domain };
}
mailboxdb.setAliasesForName(name, domain, aliases, function (error) {
if (error) return callback(error);
@@ -1191,11 +1266,14 @@ function setAliases(name, domain, aliases, callback) {
});
}
function getLists(domain, callback) {
function getLists(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
mailboxdb.getLists(domain, function (error, result) {
mailboxdb.getLists(domain, search, page, perPage, function (error, result) {
if (error) return callback(error);
callback(null, result);
@@ -1214,10 +1292,11 @@ function getList(name, domain, callback) {
});
}
function addList(name, domain, members, auditSource, callback) {
function addList(name, domain, members, membersOnly, auditSource, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof name, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1230,19 +1309,20 @@ function addList(name, domain, members, auditSource, callback) {
if (!validator.isEmail(members[i])) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid mail member: ' + members[i]));
}
mailboxdb.addList(name, domain, members, function (error) {
mailboxdb.addList(name, domain, members, membersOnly, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members });
eventlog.add(eventlog.ACTION_MAIL_LIST_ADD, auditSource, { name, domain, members, membersOnly });
callback();
});
}
function updateList(name, domain, members, auditSource, callback) {
function updateList(name, domain, members, membersOnly, auditSource, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1258,10 +1338,10 @@ function updateList(name, domain, members, auditSource, callback) {
getList(name, domain, function (error, result) {
if (error) return callback(error);
mailboxdb.updateList(name, domain, members, function (error) {
mailboxdb.updateList(name, domain, members, membersOnly, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members });
eventlog.add(eventlog.ACTION_MAIL_MAILBOX_UPDATE, auditSource, { name, domain, oldMembers: result.members, members, membersOnly });
callback(null);
});
@@ -1283,6 +1363,7 @@ function removeList(name, domain, auditSource, callback) {
});
}
// resolves the members of a list. i.e the lists and aliases
function resolveList(listName, listDomain, callback) {
assert.strictEqual(typeof listName, 'string');
assert.strictEqual(typeof listDomain, 'string');
@@ -1313,18 +1394,21 @@ function resolveList(listName, listDomain, callback) {
visited.push(member);
mailboxdb.get(memberName, memberDomain, function (error, entry) {
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); }
if (error && error.reason == BoxError.NOT_FOUND) { result.push(member); return iteratorCallback(); } // let it bounce
if (error) return iteratorCallback(error);
if (entry.type === mailboxdb.TYPE_MAILBOX) { result.push(member); return iteratorCallback(); }
// no need to resolve alias because we only allow one level and within same domain
if (entry.type === mailboxdb.TYPE_ALIAS) { result.push(`${entry.aliasTarget}@${entry.domain}`); return iteratorCallback(); }
if (entry.type === mailboxdb.TYPE_MAILBOX) { // concrete mailbox
result.push(member);
} else if (entry.type === mailboxdb.TYPE_ALIAS) { // resolve aliases
toResolve = toResolve.concat(`${entry.aliasName}@${entry.aliasDomain}`);
} else { // resolve list members
toResolve = toResolve.concat(entry.members);
}
toResolve = toResolve.concat(entry.members);
iteratorCallback();
});
}, function (error) {
callback(error, result);
callback(error, result, list);
});
});
});
+2 -2
View File
@@ -7,8 +7,8 @@ The application '<%= title %>' installed at <%= appFqdn %> is not responding.
This is most likely a problem in the application.
To resolve this, you can try the following:
* Restart the app by opening the app's web terminal - https://cloudron.io/documentation/apps/#web-terminal
* Restore the app to the latest backup - https://cloudron.io/documentation/backups/#restoring-an-app
* Restart the app by opening the app's web terminal - https://docs.cloudron.io/apps/#web-terminal
* Restore the app to the latest backup - https://docs.cloudron.io/backups/#restoring-an-app
* Contact us via <%= supportEmail %> or https://forum.cloudron.io
+1 -1
View File
@@ -2,7 +2,7 @@
Dear <%= cloudronName %> Admin,
Cloudron failed to create a complete backup. Please see https://cloudron.io/documentation/troubleshooting/#backups
Cloudron failed to create a complete backup. Please see https://docs.cloudron.io/troubleshooting/#backups
for troubleshooting.
Logs for this failure are available at <%= logUrl %>
@@ -8,7 +8,7 @@ The Cloudron will attempt to renew the certificate every 12 hours
until the certificate expires (at which point it will switch to
using the fallback certificate).
See https://cloudron.io/documentation/troubleshooting/#certificates to
See https://docs.cloudron.io/troubleshooting/#certificates to
double check if your server is configured correctly to obtain certificates
via Let's Encrypt.
+2 -2
View File
@@ -6,8 +6,8 @@ Dear <%= cloudronName %> Admin,
If this message appears repeatedly, give the app more memory.
* To increase an app's memory limit - https://cloudron.io/documentation/apps/#increasing-the-memory-limit-of-an-app
* To increase a service's memory limit - https://cloudron.io/documentation/troubleshooting/#services
* To increase an app's memory limit - https://docs.cloudron.io/apps/#memory-limit
* To increase a service's memory limit - https://docs.cloudron.io/troubleshooting/#services
Out of memory event:
+5 -1
View File
@@ -8,7 +8,7 @@ be reset. If you did not request this reset, please ignore this message.
To reset your password, please visit the following page:
<%- resetLink %>
Please note that the password reset link will expire in 24 hours.
Powered by https://cloudron.io
@@ -29,6 +29,10 @@ Powered by https://cloudron.io
<a href="<%= resetLink %>">Click to reset your password</a>
</p>
<br/>
Please note that the password reset link will expire in 24 hours.
<br/>
<br/>
+4
View File
@@ -11,6 +11,7 @@ Follow the link to get started.
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
Please note that the invite link will expire in 7 days.
Powered by https://cloudron.io
@@ -36,6 +37,9 @@ Powered by https://cloudron.io
You are receiving this email because you were invited by <%= invitor.email %>.
<% } %>
<br/>
Please note that the invite link will expire in 7 days.
<br/>
Powered by <a href="https://cloudron.io">Cloudron</a>
+74 -64
View File
@@ -1,32 +1,32 @@
'use strict';
exports = module.exports = {
addMailbox: addMailbox,
addList: addList,
addMailbox,
addList,
updateMailboxOwner: updateMailboxOwner,
updateList: updateList,
del: del,
updateMailboxOwner,
updateList,
del,
listAliases: listAliases,
listMailboxes: listMailboxes,
getLists: getLists,
getMailboxCount,
listMailboxes,
getLists,
listAllMailboxes: listAllMailboxes,
listAllMailboxes,
get: get,
getMailbox: getMailbox,
getList: getList,
getAlias: getAlias,
get,
getMailbox,
getList,
getAlias,
getAliasesForName: getAliasesForName,
setAliasesForName: setAliasesForName,
getAliasesForName,
setAliasesForName,
getByOwnerId: getByOwnerId,
delByOwnerId: delByOwnerId,
delByDomain: delByDomain,
getByOwnerId,
delByOwnerId,
delByDomain,
updateName: updateName,
updateName,
_clear: clear,
@@ -38,15 +38,18 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
mysql = require('mysql'),
safe = require('safetydance'),
util = require('util');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasTarget', 'creationTime', 'membersJson', 'domain' ].join(',');
var MAILBOX_FIELDS = [ 'name', 'type', 'ownerId', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson;
data.membersOnly = !!data.membersOnly;
return data;
}
@@ -78,14 +81,15 @@ function updateMailboxOwner(name, domain, ownerId, callback) {
});
}
function addList(name, domain, members, callback) {
function addList(name, domain, members, membersOnly, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson) VALUES (?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members) ], function (error) {
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, membersJson, membersOnly) VALUES (?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', JSON.stringify(members), membersOnly ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mailbox already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -93,14 +97,15 @@ function addList(name, domain, members, callback) {
});
}
function updateList(name, domain, members, callback) {
function updateList(name, domain, members, membersOnly, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof callback, 'function');
database.query('UPDATE mailboxes SET membersJson = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), name, domain ], function (error, result) {
database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), membersOnly, name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
@@ -123,7 +128,7 @@ function del(name, domain, callback) {
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mailboxes WHERE (name=? OR aliasTarget = ?) AND domain = ?', [ name, name, domain ], function (error, result) {
database.query('DELETE FROM mailboxes WHERE ((name=? AND domain=?) OR (aliasName = ? AND aliasDomain=?))', [ name, domain, name, domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
@@ -200,20 +205,35 @@ function getMailbox(name, domain, callback) {
});
}
function listMailboxes(domain, page, perPage, callback) {
function getMailboxCount(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM mailboxes WHERE type = ? AND domain = ?', [ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results[0].total);
});
}
function listMailboxes(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ')';
query += 'ORDER BY name LIMIT ?,?';
results.forEach(function (result) { postProcess(result); });
database.query(query, [ exports.TYPE_MAILBOX, domain, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function listAllMailboxes(page, perPage, callback) {
@@ -221,8 +241,8 @@ function listAllMailboxes(page, perPage, callback) {
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX ], function (error, results) {
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ?,?`,
[ exports.TYPE_MAILBOX, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -231,18 +251,25 @@ function listAllMailboxes(page, perPage, callback) {
});
}
function getLists(domain, callback) {
function getLists(domain, search, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND domain = ?',
[ exports.TYPE_LIST, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
let query = `SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? AND domain = ?`;
if (search) query += ' AND (name LIKE ' + mysql.escape('%' + search + '%') + ' OR membersJson LIKE ' + mysql.escape('%' + search + '%') + ')';
results.forEach(function (result) { postProcess(result); });
query += 'ORDER BY name LIMIT ?,?';
callback(null, results);
});
database.query(query, [ exports.TYPE_LIST, domain, (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 getList(name, domain, callback) {
@@ -285,10 +312,10 @@ function setAliasesForName(name, domain, aliases, callback) {
var queries = [];
// clear existing aliases
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasTarget = ? AND domain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE aliasName = ? AND aliasDomain = ? AND type = ?', args: [ name, domain, exports.TYPE_ALIAS ] });
aliases.forEach(function (alias) {
queries.push({ query: 'INSERT INTO mailboxes (name, type, domain, aliasTarget, ownerId) VALUES (?, ?, ?, ?, ?)',
args: [ alias, exports.TYPE_ALIAS, domain, name, results[0].ownerId ] });
queries.push({ query: 'INSERT INTO mailboxes (name, domain, type, aliasName, aliasDomain, ownerId) VALUES (?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId ] });
});
database.transaction(queries, function (error) {
@@ -311,27 +338,10 @@ function getAliasesForName(name, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT name FROM mailboxes WHERE type = ? AND aliasTarget = ? AND domain = ? ORDER BY name',
database.query('SELECT name, domain FROM mailboxes WHERE type = ? AND aliasName = ? AND aliasDomain = ? ORDER BY name',
[ exports.TYPE_ALIAS, name, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results = results.map(function (r) { return r.name; });
callback(null, results);
});
}
function listAliases(domain, page, perPage, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE domain = ? AND type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ domain, exports.TYPE_ALIAS ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
+6 -33
View File
@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
list: list,
update: update,
@@ -19,7 +17,7 @@ var assert = require('assert'),
database = require('./database.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector' ].join(',');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector', 'bannerJson' ].join(',');
function postProcess(data) {
data.enabled = !!data.enabled; // int to boolean
@@ -31,23 +29,12 @@ function postProcess(data) {
data.relay = safe.JSON.parse(data.relayJson) || { provider: 'cloudron-smtp' };
delete data.relayJson;
data.banner = safe.JSON.parse(data.bannerJson) || { text: null, html: null };
delete data.bannerJson;
return data;
}
function add(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mail domain already exists'));
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 clear(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -58,20 +45,6 @@ function clear(callback) {
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null);
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -104,8 +77,8 @@ function update(domain, data, callback) {
var args = [ ];
var fields = [ ];
for (var k in data) {
if (k === 'catchAll') {
fields.push('catchAllJson = ?');
if (k === 'catchAll' || k === 'banner') {
fields.push(`${k}Json = ?`);
args.push(JSON.stringify(data[k]));
} else if (k === 'relay') {
fields.push('relayJson = ?');
+56
View File
@@ -0,0 +1,56 @@
'use strict';
exports = module.exports = {
getBlocklist,
setBlocklist
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
ipaddr = require('ipaddr.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
shell = require('./shell.js'),
validator = require('validator');
const SET_BLOCKLIST_CMD = path.join(__dirname, 'scripts/setblocklist.sh');
function getBlocklist(callback) {
assert.strictEqual(typeof callback, 'function');
const data = safe.fs.readFileSync(paths.FIREWALL_BLOCKLIST_FILE, 'utf8');
callback(null, data);
}
function setBlocklist(blocklist, auditSource, callback) {
assert.strictEqual(typeof blocklist, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const parsedIp = ipaddr.process(auditSource.ip);
for (const line of blocklist.split('\n')) {
if (!line || line.startsWith('#')) continue;
const rangeOrIP = line.trim();
if (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} is not a valid IP or range`));
if (rangeOrIP.indexOf('/') === -1) {
if (auditSource.ip === rangeOrIP) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`));
} else {
const parsedRange = ipaddr.parseCIDR(rangeOrIP);
if (parsedIp.match(parsedRange)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`));
}
}
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
shell.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`));
callback();
});
}
+41 -15
View File
@@ -7,6 +7,7 @@ map $http_upgrade $connection_upgrade {
# http server
server {
listen 80;
server_tokens off; # hide version
<% if (hasIPv6) { -%>
listen [::]:80;
<% } -%>
@@ -34,7 +35,14 @@ server {
location / {
# redirect everything to HTTPS
<% if ( endpoint === 'admin' ) { %>
return 301 https://$host$request_uri;
<% } else if ( endpoint === 'app' ) { %>
return 301 https://$host$request_uri;
<% } else if ( endpoint === 'redirect' ) { %>
return 301 https://<%= redirectTo %>$request_uri;
<% } %>
}
}
@@ -42,33 +50,33 @@ server {
server {
<% if (vhost) { -%>
server_name <%= vhost %>;
listen 443 http2;
listen 443 ssl http2;
<% if (hasIPv6) { -%>
listen [::]:443 http2;
listen [::]:443 ssl http2;
<% } -%>
<% } else { -%>
listen 443 http2 default_server;
listen 443 ssl http2 default_server;
<% if (hasIPv6) { -%>
listen [::]:443 http2 default_server;
listen [::]:443 ssl http2 default_server;
<% } -%>
<% } -%>
ssl on;
server_tokens off; # hide version
# paths are relative to prefix and not to this file
ssl_certificate <%= certFilePath %>;
ssl_certificate_key <%= keyFilePath %>;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
# https://cipherli.st/
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#25-use-forward-secrecy
# ciphers according to https://ssl-config.mozilla.org/#server=nginx&version=1.14.0&config=intermediate&openssl=1.1.1&guideline=5.4
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA256;
ssl_prefer_server_ciphers off;
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -136,8 +144,21 @@ server {
# internal means this is for internal routing and cannot be accessed as URL from browser
internal;
}
location /appstatus.html {
internal;
location @wellknown-upstream {
<% if ( endpoint === 'admin' ) { %>
proxy_pass http://127.0.0.1:3000;
<% } else if ( endpoint === 'app' ) { %>
proxy_pass http://127.0.0.1:<%= port %>;
<% } else if ( endpoint === 'redirect' ) { %>
return 302 https://<%= redirectTo %>$request_uri;
<% } %>
}
# user defined .well-known resources
location ~ ^/.well-known/(.*)$ {
root /home/yellowtent/boxdata/well-known/$host;
try_files /$1 @wellknown-upstream;
}
location / {
@@ -181,6 +202,11 @@ server {
client_max_body_size 0;
}
location ~ ^/api/v1/apps/.*/files/ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 0;
}
# graphite paths (uncomment block below and visit /graphite-web/dashboard)
# remember to comment out the CSP policy as well to access the graphite dashboard
# location ~ ^/graphite-web/ {
+13 -12
View File
@@ -155,12 +155,12 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
let title, message, program;
if (app) {
program = `App ${app.fqdn}`;
title = `The application ${app.fqdn} (${app.manifest.title}) ran out of memory.`;
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/apps/#increasing-the-memory-limit-of-an-app)';
title = `The application at ${app.fqdn} ran out of memory.`;
message = 'The application has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://docs.cloudron.io/apps/#memory-limit)';
} else if (addon) {
program = `${addon.name} service`;
title = `The ${addon.name} service ran out of memory`;
message = 'The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://cloudron.io/documentation/troubleshooting/#services)';
message = 'The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://docs.cloudron.io/troubleshooting/#services)';
} else { // this never happens currently
program = `Container ${containerId}`;
title = `The container ${containerId} ran out of memory`;
@@ -181,7 +181,7 @@ function appUp(eventId, app, callback) {
actionForAllAdmins([], function (admin, done) {
mailer.appUp(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application ${app.manifest.title} installed at ${app.fqdn} is back online.`, done);
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application installed at ${app.fqdn} is back online.`, done);
}, callback);
}
@@ -192,7 +192,7 @@ function appDied(eventId, app, callback) {
actionForAllAdmins([], function (admin, callback) {
mailer.appDied(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, callback);
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application installed at ${app.fqdn} is not responding.`, callback);
}, callback);
}
@@ -201,17 +201,19 @@ function appUpdated(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (!app.appStoreId) return callback(); // skip notification of dev apps
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
const upstreamVersion = (tmp && tmp[1]) ? tmp[1] : '';
const title = upstreamVersion ? `${app.manifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${app.manifest.version})`
: `${app.manifest.title} at ${app.fqdn} updated to package version ${app.manifest.version}`;
actionForAllAdmins([], function (admin, done) {
add(admin.id, eventId, title, `The application ${app.manifest.title} installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
add(admin.id, eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`, function (error) {
if (error) return callback(error);
mailer.appUpdated(admin.email, app, function (error) {
if (error) console.error('Failed to send app updated email', error); // non fatal
if (error) debug('appUpdated: Failed to send app updated email', error); // non fatal
done();
});
});
@@ -239,7 +241,7 @@ function boxUpdateError(eventId, errorMessage, callback) {
actionForAllAdmins([], function (admin, done) {
mailer.boxUpdateError(admin.email, errorMessage);
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}. Update will be retried in 4 hours`, done);
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, done);
}, callback);
}
@@ -263,7 +265,7 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`, callback);
}, callback);
}
@@ -273,7 +275,7 @@ function alert(id, title, message, callback) {
assert.strictEqual(typeof message, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`alert: id=${id} title=${title} message=${message}`);
debug(`alert: id=${id} title=${title}`);
const acknowledged = !message;
@@ -301,7 +303,7 @@ function alert(id, title, message, callback) {
});
});
}, function (error) {
if (error) console.error(error);
if (error) debug('alert: error notifying', error);
callback();
});
@@ -338,7 +340,6 @@ function onEvent(id, action, source, data, callback) {
return appUp(id, data.app, callback);
case eventlog.ACTION_APP_UPDATE_FINISH:
if (!data.app.appStoreId) return callback(); // skip notification of dev apps
return appUpdated(id, data.app, callback);
case eventlog.ACTION_CERTIFICATE_RENEWAL:
+7 -2
View File
@@ -17,12 +17,11 @@ exports = module.exports = {
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
LICENSE_FILE: '/etc/cloudron/LICENSE',
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'), // box data dir is part of box backup
ACME_CHALLENGES_DIR: path.join(baseDir(), 'platformdata/acme'),
ADDON_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons'),
@@ -46,10 +45,16 @@ exports = module.exports = {
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'),
FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'boxdata/firewall/blocklist.txt'),
FIREWALL_CONFIG_FILE: path.join(baseDir(), 'boxdata/firewall-config.json'),
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
BOX_LOG_FILE: path.join(baseDir(), 'platformdata/logs/box.log'),
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
// this pattern is for the cloudron logs API route to work
BACKUP_LOG_FILE: path.join(baseDir(), 'platformdata/logs/backup/app.log'),
+38 -42
View File
@@ -2,7 +2,7 @@
exports = module.exports = {
start: start,
stop: stop,
stopAllTasks: stopAllTasks,
// exported for testing
_isReady: false
@@ -26,8 +26,6 @@ var addons = require('./addons.js'),
tasks = require('./tasks.js'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function start(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -45,7 +43,7 @@ function start(callback) {
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
onPlatformReady();
onPlatformReady(false /* !infraChanged */);
return callback();
}
@@ -56,9 +54,8 @@ function start(callback) {
if (error) return callback(error);
async.series([
stopContainers.bind(null, existingInfra),
// mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
startApps.bind(null, existingInfra),
(next) => { if (existingInfra.version !== infra.version) removeAllContainers(existingInfra, next); else next(); },
markApps.bind(null, existingInfra), // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
graphs.startGraphite.bind(null, existingInfra),
sftp.startSftp.bind(null, existingInfra),
addons.startServices.bind(null, existingInfra),
@@ -68,24 +65,28 @@ function start(callback) {
locker.unlock(locker.OP_PLATFORM_START);
onPlatformReady();
onPlatformReady(true /* infraChanged */);
callback();
});
}
function stop(callback) {
function stopAllTasks(callback) {
tasks.stopAllTasks(callback);
}
function onPlatformReady() {
debug('onPlatformReady: platform is ready');
function onPlatformReady(infraChanged) {
debug(`onPlatformReady: platform is ready. infra changed: ${infraChanged}`);
exports._isReady = true;
apps.schedulePendingTasks(NOOP_CALLBACK);
let tasks = [ apps.schedulePendingTasks ];
if (infraChanged) tasks.push(applyPlatformConfig, pruneInfraImages);
applyPlatformConfig(NOOP_CALLBACK);
pruneInfraImages(NOOP_CALLBACK);
async.series(async.reflectAll(tasks), function (error, results) {
results.forEach((result, idx) => {
if (result.error) debug(`Startup task at index ${idx} failed: ${result.error.message}`);
});
});
}
function applyPlatformConfig(callback) {
@@ -130,42 +131,37 @@ function pruneInfraImages(callback) {
}, callback);
}
function stopContainers(existingInfra, callback) {
// always stop addons to restart them on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
// TODO: only nuke containers with isCloudronManaged=true
debug('stopping all containers for infra upgrade');
async.series([
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker rm -f')
], callback);
} else {
assert(typeof infra.images, 'object');
var changedAddons = [ ];
for (var imageName in existingInfra.images) { // do not use infra.images because we can only stop things which are existing
if (infra.images[imageName].tag !== existingInfra.images[imageName].tag) changedAddons.push(imageName);
}
function removeAllContainers(existingInfra, callback) {
debug('removeAllContainers: removing all containers for infra upgrade');
debug('stopContainer: stopping addons for incremental infra update: %j', changedAddons);
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker rm -f || true`)
], callback);
}
async.series([
shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
], callback);
}
function startApps(existingInfra, callback) {
function markApps(existingInfra, callback) {
if (existingInfra.version === 'none') { // cloudron is being restored from backup
debug('startApps: restoring installed apps');
debug('markApps: restoring installed apps');
apps.restoreInstalledApps(callback);
} else if (existingInfra.version !== infra.version) {
debug('startApps: reconfiguring installed apps');
debug('markApps: reconfiguring installed apps');
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
} else {
debug('startApps: apps are already uptodate');
callback();
let changedAddons = [];
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) changedAddons.push('mysql');
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) changedAddons.push('postgresql');
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) changedAddons.push('mongodb');
if (infra.images.redis.tag !== existingInfra.images.redis.tag) changedAddons.push('redis');
if (changedAddons.length) {
// restart apps if docker image changes since the IP changes and any "persistent" connections fail
debug(`markApps: changedAddons: ${JSON.stringify(changedAddons)}`);
apps.restartAppsUsingAddons(changedAddons, callback);
} else {
debug('markApps: apps are already uptodate');
callback();
}
}
}
+37 -52
View File
@@ -4,13 +4,10 @@ exports = module.exports = {
setup: setup,
restore: restore,
activate: activate,
getStatus: getStatus,
autoRegister: autoRegister
getStatus: getStatus
};
var appstore = require('./appstore.js'),
assert = require('assert'),
var assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
@@ -19,10 +16,7 @@ var appstore = require('./appstore.js'),
debug = require('debug')('box:provision'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
@@ -53,27 +47,6 @@ function setProgress(task, message, callback) {
callback();
}
function autoRegister(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (!fs.existsSync(paths.LICENSE_FILE)) return callback();
const license = safe.fs.readFileSync(paths.LICENSE_FILE, 'utf8');
if (!license) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Cannot read license'));
debug('Auto-registering cloudron');
appstore.registerWithLicense(license.trim(), domain, function (error) {
if (error && error.reason !== BoxError.CONFLICT) { // not already registered
debug('Failed to auto-register cloudron', error);
return callback(new BoxError(BoxError.LICENSE_ERROR, 'Failed to auto-register Cloudron with license. Please contact support@cloudron.io'));
}
callback();
});
}
function unprovision(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -81,7 +54,7 @@ function unprovision(callback) {
// TODO: also cancel any existing configureWebadmin task
async.series([
settings.setAdmin.bind(null, '', ''),
settings.setAdminLocation.bind(null, '', ''),
mail.clearDomains,
domains.clear
], callback);
@@ -121,29 +94,28 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' },
dkimSelector: 'cloudron'
};
domains.add(domain, data, auditSource, function (error) {
async.series([
settings.setMailLocation.bind(null, domain, `${constants.ADMIN_LOCATION}.${domain}`), // default mail location. do this before we add the domain for upserting mail DNS
domains.add.bind(null, domain, data, auditSource),
sysinfo.testConfig.bind(null, sysinfoConfig)
], function (error) {
if (error) return done(error);
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error);
callback(); // now that args are validated run the task in the background
callback(); // now that args are validated run the task in the background
async.series([
autoRegister.bind(null, domain),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
});
async.series([
settings.setSysinfoConfig.bind(null, sysinfoConfig),
cloudron.setupDnsAndCert.bind(null, constants.ADMIN_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
});
});
});
@@ -190,7 +162,7 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
assert.strictEqual(typeof callback, 'function');
if (!semver.valid(version)) return callback(new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver', { field: 'version' }));
if (constants.VERSION !== version) return callback(new BoxError(BoxError.BAD_STATE, `Run cloudron-setup with --version ${version} to restore from this backup`));
if (constants.VERSION !== version) return callback(new BoxError(BoxError.BAD_STATE, `Run "cloudron-setup --version ${version}" on a fresh Ubuntu installation to restore from this backup`));
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'));
@@ -206,9 +178,16 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
backups.testConfig(backupConfig, function (error) {
backups.testProviderConfig(backupConfig, function (error) {
if (error) return done(error);
if ('password' in backupConfig) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
} else {
backupConfig.encryption = null;
}
sysinfo.testConfig(sysinfoConfig, function (error) {
if (error) return done(error);
@@ -220,7 +199,13 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
(done) => {
const adminDomain = settings.adminDomain(); // load this fresh from after the backup.restore
async.series([
cloudron.setupDnsAndCert.bind(null, constants.ADMIN_LOCATION, adminDomain, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, adminDomain, auditSource)
], done);
},
settings.setBackupCredentials.bind(null, backupConfig), // update just the credentials and not the policy and flags
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
@@ -247,11 +232,11 @@ function getStatus(callback) {
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
activated: activated,
provider: settings.provider() // used by setup wizard of marketplace images
}, gProvisionStatus));
});
});

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