Compare commits

...

391 Commits
6.3 ... v7.0.4

Author SHA1 Message Date
Girish Ramakrishnan
7f45e1db06 send new login location to user email 2021-11-17 11:53:03 -08:00
Girish Ramakrishnan
2ab2255115 fix dhparam generation
it cannot be created in default config creation time since it is
already run pre-VM snapshot time
2021-11-17 11:48:06 -08:00
Girish Ramakrishnan
515b1db9d0 Fix tests 2021-11-17 11:35:44 -08:00
Girish Ramakrishnan
a7fe7b0aa3 boxerror: add acme error code 2021-11-17 10:54:26 -08:00
Girish Ramakrishnan
89389258d7 pass correct auditSource when raising notifications
this fixes the bug where automatic app update notification were not
raised.
2021-11-17 10:42:53 -08:00
Girish Ramakrishnan
1aacf65372 apps: pass the auditSource to addTask()
this is required for the notification logic to know what caused the
task (cron or manual, for example)
2021-11-17 10:38:02 -08:00
Girish Ramakrishnan
7ffcfc5206 auditSource: add PLATFORM 2021-11-17 10:33:28 -08:00
Girish Ramakrishnan
5ab2d9da8a notifications: remove dead code 2021-11-17 10:26:47 -08:00
Girish Ramakrishnan
cd302a7621 add missing await 2021-11-17 09:38:01 -08:00
Girish Ramakrishnan
1c8e699a71 generate dhparams per server
this way we don't need to save/restore it from the database.
2021-11-16 23:03:16 -08:00
Girish Ramakrishnan
c4db0d746d acme: if account key was revoked, generate new account key
the plan was to migrate only specific keys but this allows us the
flexibility to revoke keys after the release (since we have not
gotten response from DO about access to old 1-click images so far).
2021-11-16 22:57:40 -08:00
Girish Ramakrishnan
b7c5c99301 move turn secret generation 2021-11-16 22:37:42 -08:00
Girish Ramakrishnan
132c1872f4 sftp: move key generation to sftp code 2021-11-16 21:52:39 -08:00
Girish Ramakrishnan
0f04933dbf backups: fix issue where mail backups were not cleaned up 2021-11-16 19:52:51 -08:00
Girish Ramakrishnan
6d864d3621 ensure we have atleast 1GB before making an update 2021-11-16 18:20:40 -08:00
Girish Ramakrishnan
b6ee1fb662 mail: add non-tls ports for recvmail addon 2021-11-16 17:21:34 -08:00
Girish Ramakrishnan
649cd896fc throw error and not return 2021-11-16 14:46:58 -08:00
Girish Ramakrishnan
39be267805 restore: secrets must be copied over after downloading box backup 2021-11-16 11:14:41 -08:00
Girish Ramakrishnan
f6356b2dff speed up dhparam creation 2021-11-16 09:53:43 -08:00
Johannes Zellner
48574ce350 Add missing await 2021-11-16 18:48:13 +01:00
Girish Ramakrishnan
40a3145d92 Add more bad account keys and fix fresh cloudron migration 2021-11-16 00:56:59 -08:00
Girish Ramakrishnan
f42430b7c4 regenerate acme key of DO 1-click image
https://community.letsencrypt.org/t/receiving-expiration-emails-for-dozens-of-domains/165441
2021-11-16 00:25:59 -08:00
Girish Ramakrishnan
178d93033f 7.0.4 changes 2021-11-15 23:51:06 -08:00
Girish Ramakrishnan
01a1803625 provision: delay initialization of secrets until provision time
when we create the DO 1-click image, the key also gets snapshotted.

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

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

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

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

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

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

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

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

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

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

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

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

See also:

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

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

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

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

View File

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

104
CHANGES
View File

@@ -2295,3 +2295,107 @@
* mail: enable sieve extension editheader
* mail: update solr to 8.9.0
[6.3.4]
* Fix issue where old nginx configs where not removed before upgrade
[6.3.5]
* Fix permission issues with sshfs
* filemanager: reset selection if directory has changed
* branding: fix error highlight with empty cloudron name
* better text instead of "Cloudron in the wild"
* Make sso login hint translatable
* Give unread notifications a small left border
* Fix issue where clicking update indicator opened app in new tab
* Ensure notifications are only fetched and shown for at least admins
* setupaccount: Show input field errors below input field
* Set focus automatically for new alias or redirect
* eventlog: fix issue where old events are not periodically removed
* ssfs: fix chown
[6.3.6]
* Fix broken reboot button
* app updated notification shown despite failure
* Update translation for sso login information
* Hide groups/tags/state filter in app listing for normal users
* filemanager: Ensure breadcrumbs and hash are correctly updated on folder navigation
* cloudron-setup: check if nginx/docker is already installed
* Use the addresses of all available interfaces for port 53 binding
* refresh config on appstore login
* password reset: check 2fa when enabled
[7.0.0]
* Ubuntu 16 is not supported anymore
* Do not use Gravatar as the default but only an option
* redis: suppress password warning
* setup UI: fix dark mode
* wellknown: response to .wellknown/matrix/client
* purpose field is not required anymore during appstore signup
* sftp: fix symlink deletion
* Show correct/new app version info in updated finished notification
* Make new login email translatable
* Hide ticket form if cloudron.io mail is not verified
* Refactor code to use async/await
* postgresql: bump shm size and disable parallel queries
* update nodejs to 14.17.6
* external ldap: If we detect a local user with the same username as found on LDAP/AD we map it
* add basic eventlog for apps in app view
* Enable sshfs/cifs/nfs in app import UI
* Require password for fallback email change
* Make password reset logic translatable
* support: only verified email address can open support tickets
* Logout users without 2FA when mandatory 2fa is enabled
* notifications: better oom message for redis
* Add way to impersonate users for presetup
* mail: open up port 465 for mail submission (TLS)
* Implement operator role for apps
* sftp: normal users do not have SFTP access anymore. Use operator role instead
* eventlog: add service rebuild/restart/configure events
* upcloud: add object storage integration
* Each app can now have a custom crontab
* services: add recovery mode
* postgresql: fix restore issue with long table names
* recvmail: make the addon work again
* mail: update solr to 8.10.0
* mail: POP3 support
* update docker to 20.10.7
* volumes: add remount button
* mail: add spam eventlog filter type
* mail: configure dnsbl
* mail: add duplication detection for lists
* mail: add SRS for Sieve Forwarding
[7.0.1]
* Fix matrix wellKnown client migration
[7.0.2]
* mail: POP3 flag was not returned correctly
* external ldap: fix crash preventing users from logging in
* volumes: ensure we don't crash if mount status is unexpected
* backups: set default backup memory limit to 800
* users: allow admins to specify password recovery email
* retry startup tasks on database error
[7.0.3]
* support: fix remoe support not working for 'root' user
* Fix cog icon on app grid item hover for darkmode
* Disable password reset and impersonate button for self user instead of hiding them
* pop3: fix crash with auth of non-existent mailbox
* mail: fix direction field in eventlog of deferred mails
* mail: fix eventlog search
* mail: save message-id in eventlog
* backups: fix issue which resulted in incomplete backups when an app has backups disabled
* restore: do not redirect until mail data has been restored
* proxyauth: set viewport meta tag in login view
[7.0.4]
* Add password reveal button to login pages
* appstore: fix crash if account already registered
* Do not nuke all the logrotate configs on update
* Remove unused httpPaths from manifest
* cloudron-support: add option to reset cloudron.io account
* Fix flicker in login page
* Fix LE account key re-use issue in DO 1-click image
* mail: add non-tls ports for recvmail addon
* backups: fix issue where mail backups where not cleaned up
* notifications: fix automatic app update notifications

View File

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

View File

@@ -32,6 +32,7 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
mysql_package=$([[ "${ubuntu_version}" == "20.04" ]] && echo "mysql-server-8.0" || echo "mysql-server-5.7")
ntpd_package=$([[ "${ubuntu_version}" == "20.04" ]] && echo "systemd-timesyncd" || echo "")
apt-get -y install --no-install-recommends \
acl \
apparmor \
@@ -49,6 +50,7 @@ apt-get -y install --no-install-recommends \
logrotate \
$mysql_package \
nfs-common \
$ntpd_package \
openssh-server \
pwgen \
resolvconf \
@@ -74,7 +76,7 @@ apt-get -o Dpkg::Options::="--force-confold" install -y --no-install-recommends
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
echo "==> Installing node.js"
readonly node_version=14.15.4
readonly node_version=14.17.6
mkdir -p /usr/local/node-${node_version}
curl -sL https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-${node_version}
ln -sf /usr/local/node-${node_version}/bin/node /usr/bin/node
@@ -90,8 +92,8 @@ mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
# there are 3 packages for docker - containerd, CLI and the daemon
readonly docker_version=20.10.3
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.4.3-1_amd64.deb" -o /tmp/containerd.deb
readonly docker_version=20.10.7
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.4.6-1_amd64.deb" -o /tmp/containerd.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
# apt install with install deps (as opposed to dpkg -i)

79
box.js
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,11 +28,12 @@ CREATE TABLE IF NOT EXISTS users(
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
role VARCHAR(32),
inviteToken VARCHAR(128) DEFAULT "",
resetToken VARCHAR(128) DEFAULT "",
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT 1,
avatar MEDIUMBLOB,
locationJson TEXT, // { locations: [{ ip, userAgent, city, country, ts }] }
avatar MEDIUMBLOB NOT NULL,
loginLocationsJson MEDIUMTEXT, // { locations: [{ ip, userAgent, city, country, ts }] }
INDEX creationTime_index (creationTime),
PRIMARY KEY(id));
@@ -85,6 +86,9 @@ CREATE TABLE IF NOT EXISTS apps(
enableMailbox BOOLEAN DEFAULT 1, // whether sendmail addon is enabled
mailboxName VARCHAR(128), // mailbox of this app
mailboxDomain VARCHAR(128), // mailbox domain of this apps
enableInbox BOOLEAN DEFAULT 0, // whether recvmail addon is enabled
inboxName VARCHAR(128), // mailbox of this app
inboxDomain VARCHAR(128), // mailbox domain of this apps
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
@@ -117,7 +121,7 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
appId VARCHAR(128) NOT NULL,
addonId VARCHAR(32) NOT NULL,
name VARCHAR(128) NOT NULL,
value VARCHAR(512) NOT NULL,
value TEXT NOT NULL,
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS appEnvVars(
@@ -176,6 +180,7 @@ CREATE TABLE IF NOT EXISTS mail(
relayJson TEXT,
bannerJson TEXT,
dkimKeyJson MEDIUMTEXT,
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
FOREIGN KEY(domain) REFERENCES domains(domain),
@@ -202,6 +207,7 @@ CREATE TABLE IF NOT EXISTS mailboxes(
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
domain VARCHAR(128),
active BOOLEAN DEFAULT 1,
enablePop3 BOOLEAN DEFAULT 0,
FOREIGN KEY(domain) REFERENCES mail(domain),
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
@@ -222,6 +228,7 @@ CREATE TABLE IF NOT EXISTS subdomains(
CREATE TABLE IF NOT EXISTS tasks(
id int NOT NULL AUTO_INCREMENT,
type VARCHAR(32) NOT NULL,
argsJson TEXT,
percent INTEGER DEFAULT 0,
message TEXT,
errorJson TEXT,

786
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,14 +11,14 @@
"url": "https://git.cloudron.io/cloudron/box.git"
},
"dependencies": {
"@google-cloud/dns": "^2.1.0",
"@google-cloud/dns": "^2.2.0",
"@google-cloud/storage": "^5.8.5",
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
"async": "^3.2.0",
"aws-sdk": "^2.906.0",
"aws-sdk": "^2.936.0",
"basic-auth": "^2.0.1",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^5.10.2",
"cloudron-manifestformat": "^5.11.0",
"connect": "^3.7.0",
"connect-lastmile": "^2.1.1",
"connect-timeout": "^1.9.0",
@@ -33,11 +33,11 @@
"ejs": "^3.1.6",
"ejs-cli": "^2.2.1",
"express": "^4.17.1",
"ipaddr.js": "^2.0.0",
"ipaddr.js": "^2.0.1",
"js-yaml": "^4.1.0",
"json": "^11.0.0",
"jsonwebtoken": "^8.5.1",
"ldapjs": "^2.2.4",
"ldapjs": "^2.3.0",
"lodash": "^4.17.21",
"lodash.chunk": "^4.2.0",
"mime": "^2.5.2",
@@ -45,9 +45,9 @@
"moment-timezone": "^0.5.33",
"morgan": "^1.10.0",
"multiparty": "^4.2.2",
"mustache-express": "^1.3.0",
"mustache-express": "^1.3.1",
"mysql": "^2.18.1",
"nodemailer": "^6.6.0",
"nodemailer": "^6.6.2",
"nodemailer-smtp-transport": "^2.7.4",
"once": "^1.4.0",
"pretty-bytes": "^5.6.0",
@@ -56,9 +56,8 @@
"qrcode": "^1.4.4",
"readdirp": "^3.6.0",
"request": "^2.88.2",
"rimraf": "^3.0.2",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^2.0.1",
"safetydance": "^2.2.0",
"semver": "^7.3.5",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
@@ -71,17 +70,17 @@
"underscore": "^1.13.1",
"uuid": "^8.3.2",
"validator": "^13.6.0",
"ws": "^7.4.5",
"ws": "^7.5.1",
"xml2js": "^0.4.23"
},
"devDependencies": {
"expect.js": "*",
"hock": "^1.4.1",
"js2xmlparser": "^4.0.1",
"mocha": "^8.4.0",
"mocha": "^9.0.1",
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
"nock": "^13.0.11",
"node-sass": "^6.0.0",
"nock": "^13.1.0",
"node-sass": "^6.0.1",
"recursive-readdir": "^2.2.2"
},
"scripts": {

View File

@@ -22,7 +22,7 @@ fi
mkdir -p ${DATA_DIR}
cd ${DATA_DIR}
mkdir -p appsdata
mkdir -p boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
mkdir -p boxdata/box boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks platformdata/sftp/ssh platformdata/firewall platformdata/update
sudo mkdir -p /mnt/cloudron-test-music /media/cloudron-test-music # volume test
@@ -37,14 +37,15 @@ openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out
# clear out any containers if FAST is unset
if [[ -z ${FAST+x} ]]; then
echo "=> Delete all docker containers first"
docker ps -qa | xargs --no-run-if-empty docker rm -f
docker ps -qa --filter "label=isCloudronManaged" | xargs --no-run-if-empty docker rm -f
docker rm -f mysql-server
echo "==> To skip this run with: FAST=1 ./runTests"
else
echo "==> WARNING!! Skipping docker container cleanup, the database might not be pristine!"
fi
# create docker network (while the infra code does this, most tests skip infra setup)
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 --gateway 172.18.0.1 cloudron || true
# create the same mysql server version to test with
OUT=`docker inspect mysql-server` || true
@@ -62,6 +63,9 @@ while ! mysqladmin ping -h"${MYSQL_IP}" --silent; do
sleep 1
done
echo "=> Ensure local base image"
docker pull cloudron/base:3.0.0@sha256:455c70428723e3a823198c57472785437eb6eab082e79b3ff04ea584faf46e92
echo "=> Create iptables blocklist"
sudo ipset create cloudron_blocklist hash:net || true

View File

@@ -41,6 +41,11 @@ if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
exit 1
fi
if [[ "$(uname -m)" != "x86_64" ]]; then
echo "Error: Cloudron only supports amd64/x86_64"
exit 1
fi
# do not use is-active in case box service is down and user attempts to re-install
if systemctl cat box.service >/dev/null 2>&1; then
echo "Error: Cloudron is already installed. To reinstall, start afresh"
@@ -99,6 +104,11 @@ if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubu
exit 1
fi
if which nginx >/dev/null || which docker >/dev/null || which node > /dev/null; then
echo "Error: Some packages like nginx/docker/nodejs are already installed. Cloudron requires specific versions of these packages and will install them as part of it's installation. Please start with a fresh Ubuntu install and run this script again." > /dev/stderr
exit 1
fi
# Install MOTD file for stack script style installations. this is removed by the trap exit handler. Heredoc quotes prevents parameter expansion
cat > /etc/update-motd.d/91-cloudron-install-in-progress <<'EOF'
#!/bin/bash

View File

@@ -10,12 +10,13 @@ OUT="/tmp/cloudron-support.log"
LINE="\n========================================================\n"
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
This script collects diagnostic information to help debug server related issues.
Options:
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--reset-appstore-account Reset associated cloudron.io account
--help Show this message
"
# We require root
@@ -26,7 +27,7 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login,reset-appstore-account" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -39,11 +40,18 @@ while true; do
--owner-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' AND username IS NOT NULL ORDER BY creationTime LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
dashboard_domain=$(mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" 2>/dev/nul)
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > "${ghost_file}"
chown yellowtent:yellowtent "${ghost_file}" && chmod o-r,g-r "${ghost_file}"
echo "Login at https://${dashboard_domain} as ${admin_username} / ${admin_password} . This password may only be used once. ${ghost_file} will be automatically removed after use."
dashboard_domain=$(mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" 2>/dev/null)
mysql -NB -uroot -ppassword -e "INSERT INTO box.settings (name, value) VALUES ('ghosts_config', '{\"${admin_username}\":\"${admin_password}\"}') ON DUPLICATE KEY UPDATE name='ghosts_config', value='{\"${admin_username}\":\"${admin_password}\"}'" 2>/dev/null
echo "Login at https://${dashboard_domain} as ${admin_username} / ${admin_password} . This password may only be used once."
exit 0
;;
--reset-appstore-account)
echo -e "This will reset the Cloudron.io account associated with this Cloudron. Once reset, you can re-login with a different account in the Cloudron Dashboard. See https://docs.cloudron.io/appstore/#change-account for more information.\n"
read -e -p "Reset the Cloudron.io account? [y/N] " choice
[[ "$choice" != [Yy]* ]] && exit 1
mysql -uroot -ppassword -e "DELETE FROM box.settings WHERE name='cloudron_token';" 2>/dev/null
dashboard_domain=$(mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" 2>/dev/null)
echo "Account reset. Please re-login at https://${dashboard_domain}/#/appstore"
exit 0
;;
--) break;;
@@ -143,8 +151,7 @@ if [[ "${enableSSH}" == "true" ]]; then
fi
echo -n "Uploading information..."
# for some reason not using $(cat $OUT) will not contain newlines!?
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent --data-binary "@$OUT" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
echo "Done"
echo ""

View File

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

View File

@@ -73,10 +73,10 @@ log "Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION
log "updating docker"
readonly docker_version=20.10.3
readonly docker_version=20.10.7
if [[ $(docker version --format {{.Client.Version}}) != "${docker_version}" ]]; then
# there are 3 packages for docker - containerd, CLI and the daemon
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.4.3-1_amd64.deb" -o /tmp/containerd.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.4.6-1_amd64.deb" -o /tmp/containerd.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
@@ -115,13 +115,13 @@ if ! which sshfs; then
fi
log "updating node"
readonly node_version=14.15.4
readonly node_version=14.17.6
if [[ "$(node --version)" != "v${node_version}" ]]; then
mkdir -p /usr/local/node-${node_version}
$curl -sL https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-${node_version}
ln -sf /usr/local/node-${node_version}/bin/node /usr/bin/node
ln -sf /usr/local/node-${node_version}/bin/npm /usr/bin/npm
rm -rf /usr/local/node-10.18.1
rm -rf /usr/local/node-14.15.4
fi
# this is here (and not in updater.js) because rebuild requires the above node

View File

@@ -16,7 +16,7 @@ readonly HOME_DIR="/home/${USER}"
readonly BOX_SRC_DIR="${HOME_DIR}/box"
readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata"
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata"
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata"
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata/box"
readonly MAIL_DATA_DIR="${HOME_DIR}/boxdata/mail"
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -38,11 +38,11 @@ systemctl restart apparmor
usermod ${USER} -a -G docker
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 --gateway 172.18.0.1 cloudron || true
mkdir -p "${BOX_DATA_DIR}"
mkdir -p "${APPS_DATA_DIR}"
mkdir -p "${MAIL_DATA_DIR}/dkim"
mkdir -p "${MAIL_DATA_DIR}"
# keep these in sync with paths.js
log "Ensuring directories"
@@ -52,7 +52,8 @@ mkdir -p "${PLATFORM_DATA_DIR}/mysql"
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
mkdir -p "${PLATFORM_DATA_DIR}/redis"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner"
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner" \
"${PLATFORM_DATA_DIR}/addons/mail/dkim"
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
mkdir -p "${PLATFORM_DATA_DIR}/acme"
@@ -70,10 +71,6 @@ mkdir -p "${PLATFORM_DATA_DIR}/firewall"
mkdir -p /var/backups
chmod 777 /var/backups
# can be removed after 6.3
[[ -f "${BOX_DATA_DIR}/updatechecker.json" ]] && mv "${BOX_DATA_DIR}/updatechecker.json" "${PLATFORM_DATA_DIR}/update/updatechecker.json"
rm -rf "${BOX_DATA_DIR}/well-known"
log "Configuring journald"
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
@@ -84,13 +81,14 @@ sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
-i /lib/systemd/system/systemd-journald.service
# Give user access to system logs
usermod -a -G systemd-journal ${USER}
mkdir -p /var/log/journal # in some images, this directory is not created making system log to /run/systemd instead
chown root:systemd-journal /var/log/journal
usermod -a -G systemd-journal ${USER} # Give user access to system logs
if [[ ! -d /var/log/journal ]]; then # in some images, this directory is not created making system log to /run/systemd instead
mkdir -p /var/log/journal
chown root:systemd-journal /var/log/journal
chmod g+s /var/log/journal # sticky bit for group propagation
fi
systemctl daemon-reload
systemctl restart systemd-journald
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
# Give user access to nginx logs (uses adm group)
usermod -a -G adm ${USER}
@@ -148,7 +146,6 @@ log "Configuring logrotate"
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
fi
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
@@ -233,11 +230,9 @@ chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
# do not chown the boxdata/mail directory; dovecot gets upset
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${MAIL_DATA_DIR}" -exec chown -R "${USER}:${USER}" {} \;
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}"
# do not chown the boxdata/mail directory entirely; dovecot gets upset
chown "${USER}:${USER}" "${MAIL_DATA_DIR}"
chown "${USER}:${USER}" -R "${MAIL_DATA_DIR}/dkim" # this is owned by box currently since it generates the keys
log "Starting Cloudron"
systemctl start box

View File

@@ -81,8 +81,8 @@ for port in 2525 4190 9993; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
done
# msa, ldap, imap, sieve
for port in 2525 3002 4190 9993; do
# msa, ldap, imap, sieve, pop3
for port in 2525 3002 4190 9993 9995; do
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 500 -j CLOUDRON_RATELIMIT_LOG
done

View File

@@ -4,7 +4,7 @@
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
if [[ -z "$(ls -A /home/yellowtent/platformdata/addons/mail/dkim)" ]]; then
if [[ -f /tmp/.cloudron-motd-cache ]]; then
ip=$(cat /tmp/.cloudron-motd-cache)
elif ! ip=$(curl --fail --connect-timeout 2 --max-time 2 -q https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then

View File

@@ -59,3 +59,5 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/addmount.sh
Defaults!/home/yellowtent/box/src/scripts/rmmount.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmount.sh
Defaults!/home/yellowtent/box/src/scripts/remountmount.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remountmount.sh

View File

@@ -8,10 +8,7 @@ const assert = require('assert'),
BoxError = require('./boxerror.js'),
safe = require('safetydance'),
tokens = require('./tokens.js'),
users = require('./users.js'),
util = require('util');
const userGet = util.promisify(users.get);
users = require('./users.js');
async function verifyToken(accessToken) {
assert.strictEqual(typeof accessToken, 'string');
@@ -19,10 +16,8 @@ async function verifyToken(accessToken) {
const token = await tokens.getByAccessToken(accessToken);
if (!token) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'No such token');
const [error, user] = await safe(userGet(token.identifier));
if (error && error.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not found');
if (error) throw error;
const user = await users.get(token.identifier);
if (!user) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not found');
if (!user.active) throw new BoxError(BoxError.INVALID_CREDENTIALS, 'User not active');
await safe(tokens.update(token.id, { lastUsedTime: new Date() })); // ignore any error

View File

@@ -9,11 +9,11 @@ exports = module.exports = {
};
const assert = require('assert'),
async = require('async'),
blobs = require('./blobs.js'),
BoxError = require('./boxerror.js'),
crypto = require('crypto'),
debug = require('debug')('box:cert/acme2'),
domains = require('./domains.js'),
dns = require('./dns.js'),
fs = require('fs'),
os = require('os'),
path = require('path'),
@@ -32,7 +32,7 @@ const CA_PROD_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory',
function Acme2(options) {
assert.strictEqual(typeof options, 'object');
this.accountKeyPem = options.accountKeyPem; // Buffer
this.accountKeyPem = null; // Buffer .
this.email = options.email;
this.keyId = null;
this.caDirectory = options.prod ? CA_PROD_DIRECTORY_URL : CA_STAGING_DIRECTORY_URL;
@@ -88,10 +88,10 @@ Acme2.prototype.sendSignedRequest = async function (url, payload) {
let [error, response] = await safe(superagent.get(this.directory.newNonce).timeout(30000).ok(() => true));
if (error) throw new BoxError(BoxError.NETWORK_ERROR, `Network error sending signed request: ${error.message}`);
if (response.status !== 204) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
if (response.status !== 204) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code when fetching nonce : ${response.status}`);
const nonce = response.headers['Replay-Nonce'.toLowerCase()];
if (!nonce) throw new BoxError(BoxError.EXTERNAL_ERROR, 'No nonce in response');
if (!nonce) throw new BoxError(BoxError.ACME_ERROR, 'No nonce in response');
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
@@ -129,23 +129,43 @@ Acme2.prototype.updateContact = async function (registrationUri) {
};
const result = await this.sendSignedRequest(registrationUri, JSON.stringify(payload));
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to update contact. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
debug(`updateContact: contact of user updated to ${this.email}`);
};
Acme2.prototype.registerUser = async function () {
async function generateAccountKey() {
const acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096');
if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`);
return acmeAccountKey;
}
Acme2.prototype.ensureAccount = async function () {
const payload = {
termsOfServiceAgreed: true
};
debug('registerUser: registering user');
debug('ensureAccount: registering user');
this.accountKeyPem = await blobs.get(blobs.ACME_ACCOUNT_KEY);
if (!this.accountKeyPem) {
debug('ensureAccount: generating new account keys');
this.accountKeyPem = await generateAccountKey();
await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem);
}
let result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
if (result.status === 403 && result.body.type === 'urn:ietf:params:acme:error:unauthorized') {
debug(`ensureAccount: key was revoked. ${result.status} ${JSON.stringify(result.body)}. generating new account key`);
this.accountKeyPem = await generateAccountKey();
await blobs.set(blobs.ACME_ACCOUNT_KEY, this.accountKeyPem);
result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
}
const result = await this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload));
// 200 if already exists. 201 for new accounts
if (result.status !== 200 && result.status !== 201) return new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
if (result.status !== 200 && result.status !== 201) throw new BoxError(BoxError.ACME_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.status} ${JSON.stringify(result.body)}`);
debug(`registerUser: user registered keyid: ${result.headers.location}`);
debug(`ensureAccount: user registered keyid: ${result.headers.location}`);
this.keyId = result.headers.location;
@@ -166,15 +186,15 @@ Acme2.prototype.newOrder = async function (domain) {
const result = await this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload));
if (result.status === 403) throw new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`);
if (result.status !== 201) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`);
if (result.status !== 201) throw new BoxError(BoxError.ACME_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`);
debug('newOrder: created order %s %j', domain, result.body);
const order = result.body, orderUrl = result.headers.location;
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order');
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order');
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header');
if (!Array.isArray(order.authorizations)) throw new BoxError(BoxError.ACME_ERROR, 'invalid authorizations in order');
if (typeof order.finalize !== 'string') throw new BoxError(BoxError.ACME_ERROR, 'invalid finalize in order');
if (typeof orderUrl !== 'string') throw new BoxError(BoxError.ACME_ERROR, 'invalid order location in order header');
return { order, orderUrl };
};
@@ -190,14 +210,14 @@ Acme2.prototype.waitForOrder = async function (orderUrl) {
const result = await this.postAsGet(orderUrl);
if (result.status !== 200) {
debug(`waitForOrder: invalid response code getting uri ${result.status}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, `Bad response code: ${result.status}`);
throw new BoxError(BoxError.ACME_ERROR, `Bad response when waiting for order. code: ${result.status}`);
}
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`);
if (result.body.status === 'pending' || result.body.status === 'processing') throw new BoxError(BoxError.ACME_ERROR, `Request is in ${result.body.status} state`);
else if (result.body.status === 'valid' && result.body.certificate) return result.body.certificate;
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status or invalid response: ${JSON.stringify(result.body)}`);
else throw new BoxError(BoxError.ACME_ERROR, `Unexpected status or invalid response when waiting for order: ${JSON.stringify(result.body)}`);
});
};
@@ -229,7 +249,7 @@ Acme2.prototype.notifyChallengeReady = async function (challenge) {
};
const result = await this.sendSignedRequest(challenge.url, JSON.stringify(payload));
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
};
Acme2.prototype.waitForChallenge = async function (challenge) {
@@ -243,14 +263,14 @@ Acme2.prototype.waitForChallenge = async function (challenge) {
const result = await this.postAsGet(challenge.url);
if (result.status !== 200) {
debug(`waitForChallenge: invalid response code getting uri ${result.status}`);
throw new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode);
throw new BoxError(BoxError.ACME_ERROR, `Bad response code when waiting for challenge : ${result.status}`);
}
debug(`waitForChallenge: status is "${result.body.status}" "${JSON.stringify(result.body)}"`);
if (result.body.status === 'pending') throw new BoxError(BoxError.TRY_AGAIN);
if (result.body.status === 'pending') throw new BoxError(BoxError.ACME_ERROR, 'Challenge is in pending state');
else if (result.body.status === 'valid') return;
else throw new BoxError(BoxError.EXTERNAL_ERROR, `Unexpected status: ${result.body.status}`);
else throw new BoxError(BoxError.ACME_ERROR, `Unexpected status when waiting for challenge: ${result.body.status}`);
});
};
@@ -268,7 +288,7 @@ Acme2.prototype.signCertificate = async function (domain, finalizationUrl, csrDe
const result = await this.sendSignedRequest(finalizationUrl, JSON.stringify(payload));
// 429 means we reached the cert limit for this domain
if (result.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
if (result.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to sign certificate. Expecting 200, got ${result.status} ${JSON.stringify(result.body)}`);
};
Acme2.prototype.createKeyAndCsr = async function (hostname, keyFilePath, csrFilePath) {
@@ -319,8 +339,8 @@ Acme2.prototype.downloadCertificate = async function (hostname, certUrl, certFil
debug('downloadCertificate: downloading certificate');
const result = await this.postAsGet(certUrl);
if (result.statusCode === 202) throw new BoxError(BoxError.TRY_AGAIN, 'Retry downloading certificate');
if (result.statusCode !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
if (result.statusCode === 202) throw new BoxError(BoxError.ACME_ERROR, 'Retry downloading certificate');
if (result.statusCode !== 200) throw new BoxError(BoxError.ACME_ERROR, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`);
const fullChainPem = result.body; // buffer
@@ -338,7 +358,7 @@ Acme2.prototype.prepareHttpChallenge = async function (hostname, domain, authori
debug('prepareHttpChallenge: challenges: %j', authorization);
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
if (httpChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges');
if (httpChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no http challenges');
let challenge = httpChallenges[0];
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
@@ -387,7 +407,7 @@ Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authoriz
debug('prepareDnsChallenge: challenges: %j', authorization);
const dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
if (dnsChallenges.length === 0) throw new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges');
if (dnsChallenges.length === 0) throw new BoxError(BoxError.ACME_ERROR, 'no dns challenges');
const challenge = dnsChallenges[0];
const keyAuthorization = this.getKeyAuthorization(challenge.token);
@@ -399,17 +419,11 @@ Acme2.prototype.prepareDnsChallenge = async function (hostname, domain, authoriz
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
return new Promise((resolve, reject) => {
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return reject(error);
await dns.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ]);
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
if (error) return reject(error);
await dns.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 });
resolve(challenge);
});
});
});
return challenge;
};
Acme2.prototype.cleanupDnsChallenge = async function (hostname, domain, challenge) {
@@ -426,13 +440,7 @@ Acme2.prototype.cleanupDnsChallenge = async function (hostname, domain, challeng
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
return new Promise((resolve, reject) => {
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return reject(error);
resolve(null);
});
});
await dns.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ]);
};
Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizationUrl, acmeChallengesDir) {
@@ -444,7 +452,7 @@ Acme2.prototype.prepareChallenge = async function (hostname, domain, authorizati
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
const response = await this.postAsGet(authorizationUrl);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code getting authorization : ${response.status}`);
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code getting authorization : ${response.status}`);
const authorization = response.body;
@@ -477,7 +485,7 @@ Acme2.prototype.acmeFlow = async function (hostname, domain, paths) {
const { certFilePath, keyFilePath, csrFilePath, acmeChallengesDir } = paths;
await this.registerUser();
await this.ensureAccount();
const { order, orderUrl } = await this.newOrder(hostname);
for (let i = 0; i < order.authorizations.length; i++) {
@@ -504,11 +512,11 @@ Acme2.prototype.loadDirectory = async function () {
await promiseRetry({ times: 3, interval: 20000 }, async () => {
const response = await superagent.get(this.caDirectory).timeout(30000).ok(() => true);
if (response.status !== 200) throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response code when fetching directory : ${response.status}`);
if (response.status !== 200) throw new BoxError(BoxError.ACME_ERROR, `Invalid response code when fetching directory : ${response.status}`);
if (typeof response.body.newNonce !== 'string' ||
typeof response.body.newOrder !== 'string' ||
typeof response.body.newAccount !== 'string') throw new BoxError(BoxError.EXTERNAL_ERROR, `Invalid response body : ${response.body}`);
typeof response.body.newAccount !== 'string') throw new BoxError(BoxError.ACME_ERROR, `Invalid response body : ${response.body}`);
this.directory = response.body;
});
@@ -522,7 +530,7 @@ Acme2.prototype.getCertificate = async function (vhost, domain, paths) {
debug(`getCertificate: start acme flow for ${vhost} from ${this.caDirectory}`);
if (vhost !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
vhost = domains.makeWildcard(vhost);
vhost = dns.makeWildcard(vhost);
debug(`getCertificate: will get wildcard cert for ${vhost}`);
}
@@ -530,18 +538,17 @@ Acme2.prototype.getCertificate = async function (vhost, domain, paths) {
await this.acmeFlow(vhost, domain, paths);
};
function getCertificate(vhost, domain, paths, options, callback) {
async function getCertificate(vhost, domain, paths, options) {
assert.strictEqual(typeof vhost, 'string'); // this can also be a wildcard domain (for alias domains)
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof paths, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
let attempt = 1;
async.retry({ times: 3, interval: 0 }, function (retryCallback) {
await promiseRetry({ times: 3, interval: 0 }, async function () {
debug(`getCertificate: attempt ${attempt++}`);
let acme = new Acme2(options || { });
acme.getCertificate(vhost, domain, paths).then(callback).catch(retryCallback);
}, callback);
const acme = new Acme2(options || { });
return await acme.getCertificate(vhost, domain, paths);
});
}

81
src/addonconfigs.js Normal file
View File

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

View File

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

View File

@@ -1,10 +1,8 @@
'use strict';
const appdb = require('./appdb.js'),
apps = require('./apps.js'),
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
AuditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:apphealthmonitor'),
@@ -17,17 +15,15 @@ exports = module.exports = {
run
};
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
const UNHEALTHY_THRESHOLD = 20 * 60 * 1000; // 20 minutes
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // will only raise 1 oom event every hour
let gStartTime = null; // time when apphealthmonitor was started
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
function setHealth(app, health, callback) {
async function setHealth(app, health) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof health, 'string');
assert.strictEqual(typeof callback, 'function');
// app starts out with null health
// if it became healthy, we update immediately. this is required for ui to say "running" etc
@@ -42,79 +38,74 @@ function setHealth(app, health, callback) {
debug(`setHealth: ${app.id} (${app.fqdn}) switched from ${lastHealth} to healthy`);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, auditSource.HEALTH_MONITOR, { app: app });
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, AuditSource.HEALTH_MONITOR, { app: app });
}
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
if (lastHealth === apps.HEALTH_HEALTHY) {
debug(`setHealth: marking ${app.id} (${app.fqdn}) as unhealthy since not seen for more than ${UNHEALTHY_THRESHOLD/(60 * 1000)} minutes`);
// do not send mails for dev apps
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, auditSource.HEALTH_MONITOR, { app: app });
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, AuditSource.HEALTH_MONITOR, { app: app });
}
} else {
debug(`setHealth: ${app.id} (${app.fqdn}) waiting for ${(UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000} to update health`);
return callback(null);
return;
}
appdb.setHealth(app.id, health, healthTime, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null); // app uninstalled?
if (error) return callback(error);
const [error] = await safe(apps.setHealth(app.id, health, healthTime));
if (error && error.reason === BoxError.NOT_FOUND) return; // app uninstalled?
if (error) throw error;
app.health = health;
app.healthTime = healthTime;
callback(null);
});
app.health = health;
app.healthTime = healthTime;
}
// callback is called with error for fatal errors and not if health check failed
function checkAppHealth(app, callback) {
async function checkAppHealth(app, options) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options, 'object');
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
return callback(null);
}
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) return;
const manifest = app.manifest;
docker.inspect(app.containerId, function (error, data) {
if (error || !data || !data.State) return setHealth(app, apps.HEALTH_ERROR, callback);
if (data.State.Running !== true) return setHealth(app, apps.HEALTH_DEAD, callback);
const [error, data] = await safe(docker.inspect(app.containerId));
if (error || !data || !data.State) return await setHealth(app, apps.HEALTH_ERROR);
if (data.State.Running !== true) return await setHealth(app, apps.HEALTH_DEAD);
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
// non-appstore apps may not have healthCheckPath
if (!manifest.healthCheckPath) return await setHealth(app, apps.HEALTH_HEALTHY);
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
.redirects(0)
.timeout(HEALTHCHECK_INTERVAL)
.end(function (error, res) {
if (error && !error.response) {
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else if (res.statusCode > 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
} else {
setHealth(app, apps.HEALTH_HEALTHY, callback);
}
});
});
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
const [healthCheckError, response] = await safe(superagent
.get(healthCheckUrl)
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
.redirects(0)
.ok(() => true)
.timeout(options.timeout * 1000));
if (healthCheckError) {
await setHealth(app, apps.HEALTH_UNHEALTHY);
} else if (response.status > 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
await setHealth(app, apps.HEALTH_UNHEALTHY);
} else {
await setHealth(app, apps.HEALTH_HEALTHY);
}
}
function getContainerInfo(containerId, callback) {
docker.inspect(containerId, function (error, result) {
if (error) return callback(error);
async function getContainerInfo(containerId) {
const result = await docker.inspect(containerId);
const appId = safe.query(result, 'Config.Labels.appId', null);
const appId = safe.query(result, 'Config.Labels.appId', null);
if (appId) return { app: await apps.get(appId) }; // don't get by container id as this can be an exec container
if (!appId) return callback(null, null /* app */, { name: result.Name.slice(1) }); // addon . Name has a '/' in the beginning for some reason
apps.get(appId, callback); // don't get by container id as this can be an exec container
});
if (result.Name.startsWith('/redis-')) {
return { app: await apps.get(result.Name.slice('/redis-'.length)), addonName: 'redis' };
} else {
return { addonName: result.Name.slice(1) }; // addon . Name has a '/' in the beginning for some reason
}
}
/*
@@ -122,81 +113,69 @@ function getContainerInfo(containerId, callback) {
docker run -ti -m 100M cloudron/base:3.0.0 /bin/bash
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
function processDockerEvents(intervalSecs, callback) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
async function processDockerEvents(options) {
assert.strictEqual(typeof options, 'object');
const since = ((new Date().getTime() / 1000) - intervalSecs).toFixed(0);
const since = ((new Date().getTime() / 1000) - options.intervalSecs).toFixed(0);
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
if (error) return callback(error);
const stream = await docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) });
stream.setEncoding('utf8');
stream.on('data', async function (data) { // this is actually ldjson, we only process the first line for now
const event = safe.JSON.parse(data);
if (!event) return;
const containerId = String(event.id);
stream.setEncoding('utf8');
stream.on('data', function (data) {
const event = JSON.parse(data);
const containerId = String(event.id);
const [error, info] = await safe(getContainerInfo(containerId));
const program = error ? containerId : (info.addonName || info.app.fqdn);
const now = Date.now();
getContainerInfo(containerId, function (error, app, addon) {
const program = error ? containerId : (app ? app.fqdn : addon.name);
const now = Date.now();
const notifyUser = !(app && app.debugMode) && ((now - gLastOomMailTime) > OOM_EVENT_LIMIT);
// do not send mails for dev apps
const notifyUser = !(info.app && info.app.debugMode) && ((now - gLastOomMailTime) > OOM_EVENT_LIMIT);
debug(`OOM ${program} notifyUser: ${notifyUser}. lastOomTime: ${gLastOomMailTime} (now: ${now})`);
debug(`OOM ${program} notifyUser: ${notifyUser}. lastOomTime: ${gLastOomMailTime} (now: ${now})`);
// do not send mails for dev apps
if (notifyUser) {
// app can be null for addon containers
eventlog.add(eventlog.ACTION_APP_OOM, auditSource.HEALTH_MONITOR, { event, containerId, addon: addon || null, app: app || null });
if (notifyUser) {
await eventlog.add(eventlog.ACTION_APP_OOM, AuditSource.HEALTH_MONITOR, { event, containerId, addonName: info?.addonName || null, app: info?.app || null });
gLastOomMailTime = now;
}
});
});
stream.on('error', function (error) {
debug('Error reading docker events', error);
callback();
});
stream.on('end', callback);
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), 3000); // https://github.com/apocas/dockerode/issues/179
gLastOomMailTime = now;
}
});
stream.on('error', function (error) {
debug('Error reading docker events', error);
});
stream.on('end', function () {
// debug('Event stream ended');
});
// safety hatch if 'until' doesn't work (there are cases where docker is working with a different time)
setTimeout(stream.destroy.bind(stream), options.timeout); // https://github.com/apocas/dockerode/issues/179
}
function processApp(callback) {
assert.strictEqual(typeof callback, 'function');
async function processApp(options) {
assert.strictEqual(typeof options, 'object');
apps.getAll(function (error, allApps) {
if (error) return callback(error);
const allApps = await apps.list();
async.each(allApps, checkAppHealth, function (error) {
const alive = allApps
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
const healthChecks = allApps.map((app) => checkAppHealth(app, options)); // start healthcheck in parallel
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.` + (error ? ` ${error.reason}` : ''));
await Promise.allSettled(healthChecks); // wait for all promises to finish
callback(null);
});
});
const alive = allApps
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.`);
}
function run(intervalSecs, callback) {
async function run(intervalSecs) {
assert.strictEqual(typeof intervalSecs, 'number');
assert.strictEqual(typeof callback, 'function');
if (constants.TEST) return;
if (!gStartTime) gStartTime = new Date();
async.series([
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
processDockerEvents.bind(null, intervalSecs)
], function (error) {
if (error) debug(`run: could not check app health. ${error.message}`);
callback();
});
await processApp({ timeout: (intervalSecs - 3) * 1000 });
await processDockerEvents({ intervalSecs, timeout: 3000 });
}

88
src/apppasswords.js Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -20,12 +20,11 @@ let gPendingTasks = [ ];
let gInitialized = false;
const TASK_CONCURRENCY = 3;
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
function waitText(lockOperation) {
if (lockOperation === locker.OP_BOX_UPDATE) return 'Waiting for Cloudron to finish updating. See the Settings view';
if (lockOperation === locker.OP_PLATFORM_START) return 'Waiting for Cloudron to initialize';
if (lockOperation === locker.OP_FULL_BACKUP) return 'Wait for Cloudron to finish backup. See the Backups view';
if (lockOperation === locker.OP_FULL_BACKUP) return 'Waiting for Cloudron to finish backup. See the Backups view';
return ''; // cannot happen
}
@@ -36,31 +35,31 @@ function initializeSync() {
}
// callback is called when task is finished
function scheduleTask(appId, taskId, options, callback) {
function scheduleTask(appId, taskId, options, onFinished) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof onFinished, 'function');
if (!gInitialized) initializeSync();
if (appId in gActiveTasks) {
return callback(new BoxError(BoxError.CONFLICT, `Task for %s is already active: ${appId}`));
return onFinished(new BoxError(BoxError.CONFLICT, `Task for %s is already active: ${appId}`));
}
if (Object.keys(gActiveTasks).length >= TASK_CONCURRENCY) {
debug(`Reached concurrency limit, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, options, callback });
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' });
gPendingTasks.push({ appId, taskId, options, onFinished });
return;
}
var lockError = locker.recursiveLock(locker.OP_APPTASK);
const lockError = locker.recursiveLock(locker.OP_APPTASK);
if (lockError) {
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) }, NOOP_CALLBACK);
gPendingTasks.push({ appId, taskId, options, callback });
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) });
gPendingTasks.push({ appId, taskId, options, onFinished });
return;
}
@@ -73,7 +72,7 @@ function scheduleTask(appId, taskId, options, callback) {
scheduler.suspendJobs(appId);
tasks.startTask(taskId, Object.assign(options, { logFile }), function (error, result) {
callback(error, result);
onFinished(error, result);
delete gActiveTasks[appId];
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
@@ -88,5 +87,5 @@ function startNextTask() {
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
const t = gPendingTasks.shift();
scheduleTask(t.appId, t.taskId, t.options, t.callback);
scheduleTask(t.appId, t.taskId, t.options, t.onFinished);
}

View File

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

View File

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

306
src/backupcleaner.js Normal file
View File

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

View File

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

File diff suppressed because it is too large Load Diff

1065
src/backuptask.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,8 @@ exports = module.exports = {
set,
del,
initSecrets,
ACME_ACCOUNT_KEY: 'acme_account_key',
ADDON_TURN_SECRET: 'addon_turn_secret',
DHPARAMS: 'dhparams',
SFTP_PUBLIC_KEY: 'sftp_public_key',
SFTP_PRIVATE_KEY: 'sftp_private_key',
@@ -21,13 +18,7 @@ exports = module.exports = {
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
database = require('./database.js'),
debug = require('debug')('box:blobs'),
paths = require('./paths.js'),
safe = require('safetydance');
database = require('./database.js');
const BLOBS_FIELDS = [ 'id', 'value' ].join(',');
@@ -53,50 +44,3 @@ async function del(id) {
async function clear() {
await database.query('DELETE FROM blobs');
}
async function initSecrets() {
let acmeAccountKey = await get(exports.ACME_ACCOUNT_KEY);
if (!acmeAccountKey) {
acmeAccountKey = safe.child_process.execSync('openssl genrsa 4096');
if (!acmeAccountKey) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate acme account key: ${safe.error.message}`);
await set(exports.ACME_ACCOUNT_KEY, acmeAccountKey);
}
let turnSecret = await get(exports.ADDON_TURN_SECRET);
if (!turnSecret) {
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
await set(exports.ADDON_TURN_SECRET, Buffer.from(turnSecret));
}
if (!constants.TEST) {
let dhparams = await get(exports.DHPARAMS);
if (!dhparams) {
debug('initSecrets: generating dhparams.pem. this takes forever');
dhparams = safe.child_process.execSync('openssl dhparam 2048');
if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
await set(exports.DHPARAMS, dhparams);
} else if (!safe.fs.existsSync(paths.DHPARAMS_FILE)) {
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
}
}
let sftpPrivateKey = await get(exports.SFTP_PRIVATE_KEY);
let sftpPublicKey = await get(exports.SFTP_PUBLIC_KEY);
if (!sftpPrivateKey || !sftpPublicKey) {
debug('initSecrets: generate sftp keys');
if (constants.TEST) {
safe.fs.unlinkSync(paths.SFTP_PUBLIC_KEY_FILE);
safe.fs.unlinkSync(paths.SFTP_PRIVATE_KEY_FILE);
}
if (!safe.child_process.execSync(`ssh-keygen -m PEM -t rsa -f "${paths.SFTP_KEYS_DIR}/ssh_host_rsa_key" -q -N ""`)) throw new BoxError(BoxError.OPENSSL_ERROR, `Could not generate sftp ssh keys: ${safe.error.message}`);
sftpPublicKey = safe.fs.readFileSync(paths.SFTP_PUBLIC_KEY_FILE);
await set(exports.SFTP_PUBLIC_KEY, sftpPublicKey);
sftpPrivateKey = safe.fs.readFileSync(paths.SFTP_PRIVATE_KEY_FILE);
await set(exports.SFTP_PRIVATE_KEY, sftpPrivateKey);
} else if (!safe.fs.existsSync(paths.SFTP_PUBLIC_KEY_FILE) || !safe.fs.existsSync(paths.SFTP_PRIVATE_KEY_FILE)) {
if (!safe.fs.writeFileSync(paths.SFTP_PUBLIC_KEY_FILE, sftpPublicKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp public key: ${safe.error.message}`);
if (!safe.fs.writeFileSync(paths.SFTP_PRIVATE_KEY_FILE, sftpPrivateKey)) throw new BoxError(BoxError.FS_ERROR, `Could not save sftp private key: ${safe.error.message}`);
}
}

View File

@@ -33,6 +33,7 @@ function BoxError(reason, errorOrMessage, override) {
}
util.inherits(BoxError, Error);
BoxError.ACCESS_DENIED = 'Access Denied';
BoxError.ACME_ERROR = 'Acme Error';
BoxError.ADDONS_ERROR = 'Addons Error';
BoxError.ALREADY_EXISTS = 'Already Exists';
BoxError.BAD_FIELD = 'Bad Field';
@@ -60,6 +61,7 @@ BoxError.NGINX_ERROR = 'Nginx Error';
BoxError.NOT_FOUND = 'Not found';
BoxError.NOT_IMPLEMENTED = 'Not implemented';
BoxError.NOT_SIGNED = 'Not Signed';
BoxError.NOT_SUPPORTED = 'Not Supported';
BoxError.OPENSSL_ERROR = 'OpenSSL Error';
BoxError.PLAN_LIMIT = 'Plan Limit';
BoxError.SPAWN_ERROR = 'Spawn Error';
@@ -85,10 +87,12 @@ BoxError.toHttpError = function (error) {
case BoxError.ALREADY_EXISTS:
case BoxError.BAD_STATE:
case BoxError.CONFLICT:
case BoxError.NOT_SUPPORTED:
return new HttpError(409, error);
case BoxError.INVALID_CREDENTIALS:
return new HttpError(412, error);
case BoxError.EXTERNAL_ERROR:
case BoxError.ACME_ERROR:
case BoxError.NETWORK_ERROR:
case BoxError.FS_ERROR:
case BoxError.MOUNT_ERROR:

View File

@@ -25,14 +25,15 @@ exports = module.exports = {
const apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
AuditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
branding = require('./branding.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
delay = require('delay'),
dns = require('./dns.js'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
@@ -54,196 +55,157 @@ const apps = require('./apps.js'),
const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
async function initialize() {
runStartupTasks();
safe(runStartupTasks(), { debug }); // background
await notifyUpdate();
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
async.series([
cron.stopJobs,
platform.stopAllTasks
], callback);
async function uninitialize() {
await cron.stopJobs();
await platform.stopAllTasks();
}
function onActivated(options, callback) {
async function onActivated(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debug('onActivated: running post activation tasks');
// Starting the platform after a user is available means:
// 1. mail bounces can now be sent to the cloudron owner
// 2. the restore code path can run without sudo (since mail/ is non-root)
async.series([
platform.start.bind(null, options),
cron.startJobs,
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
// the UI some time to query the dashboard domain in the restore code path
(done) => setTimeout(() => reverseProxy.writeDefaultConfig({ activated :true }, done), 30000)
], callback);
await platform.start(options);
await cron.startJobs();
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
// the UI some time to query the dashboard domain in the restore code path
await delay(30000);
await reverseProxy.writeDefaultConfig({ activated :true });
}
async function notifyUpdate() {
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
if (version === constants.VERSION) return;
await eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { errorMessage: '', oldVersion: version || 'dev', newVersion: constants.VERSION });
await eventlog.add(eventlog.ACTION_UPDATE_FINISH, AuditSource.CRON, { errorMessage: '', oldVersion: version || 'dev', newVersion: constants.VERSION });
return new Promise((resolve, reject) => {
tasks.setCompletedByType(tasks.TASK_UPDATE, { error: null }, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) return reject(error); // when hotfixing, task may not exist
const [error] = await safe(tasks.setCompletedByType(tasks.TASK_UPDATE, { error: null }));
if (error && error.reason !== BoxError.NOT_FOUND) throw error; // when hotfixing, task may not exist
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
resolve();
});
});
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
}
// each of these tasks can fail. we will add some routes to fix/re-run them
function runStartupTasks() {
const tasks = [
// stop all the systemd tasks
platform.stopAllTasks,
async function runStartupTasks() {
const tasks = [];
// 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);
// stop all the systemd tasks
tasks.push(platform.stopAllTasks);
backups.configureCollectd(backupConfig, 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
tasks.push(async function () {
const backupConfig = await settings.getBackupConfig();
await backups.configureCollectd(backupConfig);
});
// always generate webadmin config since we have no versioning mechanism for the ejs
function (callback) {
if (!settings.dashboardDomain()) return callback();
// always generate webadmin config since we have no versioning mechanism for the ejs
tasks.push(async function () {
if (!settings.dashboardDomain()) return;
reverseProxy.writeDashboardConfig(settings.dashboardDomain(), callback);
},
await reverseProxy.writeDashboardConfig(settings.dashboardDomain());
});
tasks.push(async function () {
// check activation state and start the platform
function (callback) {
users.isActivated(function (error, activated) {
if (error) return callback(error);
const activated = await users.isActivated();
// 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({ activated: false }, callback);
}
onActivated({}, callback);
});
// 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 await reverseProxy.writeDefaultConfig({ activated: false });
}
];
await onActivated({});
});
// 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}`);
});
});
for (let i = 0; i < tasks.length; i++) {
const [error] = await safe(tasks[i]());
if (error) debug(`Startup task at index ${i} failed: ${error.message}`);
}
}
function getConfig(callback) {
assert.strictEqual(typeof callback, 'function');
async function getConfig() {
const release = safe.fs.readFileSync('/etc/lsb-release', 'utf-8');
if (release === null) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
if (release === null) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
const ubuntuVersion = release.match(/DISTRIB_DESCRIPTION="(.*)"/)[1];
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
const allSettings = await settings.list();
// be picky about what we send out here since this is sent for 'normal' users as well
callback(null, {
apiServerOrigin: settings.apiServerOrigin(),
webServerOrigin: settings.webServerOrigin(),
adminDomain: settings.dashboardDomain(),
adminFqdn: settings.dashboardFqdn(),
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
ubuntuVersion,
isDemo: settings.isDemo(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
features: appstore.getFeatures(),
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
});
});
// be picky about what we send out here since this is sent for 'normal' users as well
return {
apiServerOrigin: settings.apiServerOrigin(),
webServerOrigin: settings.webServerOrigin(),
adminDomain: settings.dashboardDomain(),
adminFqdn: settings.dashboardFqdn(),
mailFqdn: settings.mailFqdn(),
version: constants.VERSION,
ubuntuVersion,
isDemo: settings.isDemo(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
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) debug('reboot: failed to clear reboot notification.', error);
async function reboot() {
await notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '');
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
});
const [error] = await safe(shell.promises.sudo('reboot', [ REBOOT_CMD ], {}));
if (error) debug('reboot: could not reboot', error);
}
function isRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
async function isRebootRequired() {
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
callback(null, fs.existsSync('/var/run/reboot-required'));
return fs.existsSync('/var/run/reboot-required');
}
// called from cron.js
function runSystemChecks(callback) {
assert.strictEqual(typeof callback, 'function');
async function runSystemChecks() {
debug('runSystemChecks: checking status');
async.parallel([
checkMailStatus,
checkRebootRequired,
checkUbuntuVersion
], callback);
const checks = [
checkMailStatus(),
checkRebootRequired(),
checkUbuntuVersion()
];
await Promise.allSettled(checks);
}
function checkMailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
mail.checkConfiguration(function (error, message) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message, callback);
});
async function checkMailStatus() {
const message = await mail.checkConfiguration();
await notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message);
}
function checkRebootRequired(callback) {
assert.strictEqual(typeof callback, 'function');
isRebootRequired(function (error, rebootRequired) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish ubuntu security updates, a reboot is necessary.' : '', callback);
});
async function checkRebootRequired() {
const rebootRequired = await isRebootRequired();
await notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish ubuntu security updates, a reboot is necessary.' : '');
}
function checkUbuntuVersion(callback) {
assert.strictEqual(typeof callback, 'function');
async function checkUbuntuVersion() {
const isXenial = fs.readFileSync('/etc/lsb-release', 'utf-8').includes('16.04');
if (!isXenial) return callback();
if (!isXenial) return;
notifications.alert(notifications.ALERT_UPDATE_UBUNTU, 'Ubuntu upgrade required', 'Ubuntu 16.04 has reached end of life and will not receive security and maintenance updates. Please follow https://docs.cloudron.io/guides/upgrade-ubuntu-18/ to upgrade to Ubuntu 18 at the earliest.', callback);
await notifications.alert(notifications.ALERT_UPDATE_UBUNTU, 'Ubuntu upgrade required', 'Ubuntu 16.04 has reached end of life and will not receive security and maintenance updates. Please follow https://docs.cloudron.io/guides/upgrade-ubuntu-18/ to upgrade to Ubuntu 18 at the earliest.');
}
function getLogs(unit, options, callback) {
async function getLogs(unit, options) {
assert.strictEqual(typeof unit, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof options.lines, 'number');
assert.strictEqual(typeof options.format, 'string');
@@ -261,15 +223,15 @@ function getLogs(unit, options, callback) {
// need to handle box.log without subdir
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
else if (unit.startsWith('crash-')) args.push(path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log'));
else return callback(new BoxError(BoxError.BAD_FIELD, 'No such unit', { field: 'unit' }));
else throw new BoxError(BoxError.BAD_FIELD, 'No such unit', { field: 'unit' });
var cp = spawn('/usr/bin/tail', args);
const cp = spawn('/usr/bin/tail', args);
var transformStream = split(function mapper(line) {
const transformStream = split(function mapper(line) {
if (format !== 'json') return line + '\n';
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
var timestamp = (new Date(data[0])).getTime();
const data = line.split(' '); // logs are <ISOtimestamp> <msg>
let timestamp = (new Date(data[0])).getTime();
if (isNaN(timestamp)) timestamp = 0;
return JSON.stringify({
@@ -283,142 +245,98 @@ function getLogs(unit, options, callback) {
cp.stdout.pipe(transformStream);
return callback(null, transformStream);
return transformStream;
}
function prepareDashboardDomain(domain, auditSource, callback) {
async function prepareDashboardDomain(domain, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`prepareDashboardDomain: ${domain}`);
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
if (settings.isDemo()) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode');
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const domainObject = await domains.get(domain);
if (!domain) throw new BoxError(BoxError.NOT_FOUND, 'No such domain');
const fqdn = domains.fqdn(constants.DASHBOARD_LOCATION, domainObject);
const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject);
apps.getAll(function (error, result) {
if (error) return callback(error);
const result = await apps.list();
if (result.some(app => app.fqdn === fqdn)) throw new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app');
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'));
const taskId = await tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.DASHBOARD_LOCATION, domain, auditSource ]);
tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.DASHBOARD_LOCATION, domain, auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {});
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
});
});
return taskId;
}
// call this only pre activation since it won't start mail server
function setDashboardDomain(domain, auditSource, callback) {
async function setDashboardDomain(domain, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`setDashboardDomain: ${domain}`);
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
const domainObject = await domains.get(domain);
if (!domain) throw new BoxError(BoxError.NOT_FOUND, 'No such domain');
reverseProxy.writeDashboardConfig(domain, function (error) {
if (error) return callback(error);
await reverseProxy.writeDashboardConfig(domain);
const fqdn = dns.fqdn(constants.DASHBOARD_LOCATION, domainObject);
const fqdn = domains.fqdn(constants.DASHBOARD_LOCATION, domainObject);
await settings.setDashboardLocation(domain, fqdn);
settings.setDashboardLocation(domain, fqdn, function (error) {
if (error) return callback(error);
await safe(appstore.updateCloudron({ domain }));
appstore.updateCloudron({ domain }, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain, fqdn });
callback(null);
});
});
});
await eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain, fqdn });
}
// call this only post activation because it will restart mail server
function updateDashboardDomain(domain, auditSource, callback) {
async function updateDashboardDomain(domain, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug(`updateDashboardDomain: ${domain}`);
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
if (settings.isDemo()) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode');
setDashboardDomain(domain, auditSource, function (error) {
if (error) return callback(error);
await setDashboardDomain(domain, auditSource);
services.rebuildService('turn', NOOP_CALLBACK); // to update the realm variable
callback(null);
});
safe(services.rebuildService('turn', auditSource), { debug }); // to update the realm variable
}
function renewCerts(options, auditSource, callback) {
async function renewCerts(options, auditSource) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
tasks.add(tasks.TASK_CHECK_CERTS, [ options, auditSource ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
const taskId = await tasks.add(tasks.TASK_CHECK_CERTS, [ options, auditSource ]);
tasks.startTask(taskId, {});
return taskId;
}
function setupDnsAndCert(subdomain, domain, auditSource, progressCallback, callback) {
async function setupDnsAndCert(subdomain, domain, auditSource, progressCallback) {
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 domainObject = await domains.get(domain);
const dashboardFqdn = dns.fqdn(subdomain, domainObject);
const dashboardFqdn = domains.fqdn(subdomain, domainObject);
const ip = await sysinfo.getServerIp();
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.series([
(done) => { progressCallback({ message: `Updating DNS of ${dashboardFqdn}` }); done(); },
domains.upsertDnsRecords.bind(null, subdomain, domain, 'A', [ ip ]),
(done) => { progressCallback({ message: `Waiting for DNS of ${dashboardFqdn}` }); done(); },
domains.waitForDnsRecord.bind(null, subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }),
(done) => { progressCallback({ message: `Getting certificate of ${dashboardFqdn}` }); done(); },
reverseProxy.ensureCertificate.bind(null, domains.fqdn(subdomain, domainObject), domain, auditSource)
], function (error) {
if (error) return callback(error);
callback(null);
});
});
});
progressCallback({ message: `Updating DNS of ${dashboardFqdn}` });
await dns.upsertDnsRecords(subdomain, domain, 'A', [ ip ]);
progressCallback({ message: `Waiting for DNS of ${dashboardFqdn}` });
await dns.waitForDnsRecord(subdomain, domain, 'A', ip, { interval: 30000, times: 50000 });
progressCallback({ message: `Getting certificate of ${dashboardFqdn}` });
await reverseProxy.ensureCertificate(dns.fqdn(subdomain, domainObject), domain, auditSource);
}
function syncDnsRecords(options, callback) {
async function syncDnsRecords(options) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
tasks.add(tasks.TASK_SYNC_DNS_RECORDS, [ options ], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, NOOP_CALLBACK);
callback(null, taskId);
});
const taskId = await tasks.add(tasks.TASK_SYNC_DNS_RECORDS, [ options ]);
tasks.startTask(taskId, {});
return taskId;
}

View File

@@ -8,7 +8,6 @@ exports = module.exports = {
const assert = require('assert'),
BoxError = require('./boxerror.js'),
debug = require('debug')('collectd'),
fs = require('fs'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -16,40 +15,29 @@ const assert = require('assert'),
const CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh');
function addProfile(name, profile, callback) {
async function addProfile(name, profile) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof profile, 'string');
assert.strictEqual(typeof callback, 'function');
const configFilePath = path.join(paths.COLLECTD_APPCONFIG_DIR, `${name}.conf`);
// skip restarting collectd if the profile already exists with the same contents
const currentProfile = safe.fs.readFileSync(configFilePath, 'utf8') || '';
if (currentProfile === profile) return callback(null);
if (currentProfile === profile) return;
fs.writeFile(configFilePath, profile, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error writing collectd config: ${error.message}`));
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', name ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Could not add collectd config'));
callback(null);
});
});
if (!safe.fs.writeFileSync(configFilePath, profile)) throw new BoxError(BoxError.FS_ERROR, `Error writing collectd config: ${safe.error.message}`);
const [error] = await safe(shell.promises.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', name ], {}));
if (error) throw new BoxError(BoxError.COLLECTD_ERROR, 'Could not add collectd config');
}
function removeProfile(name, callback) {
async function removeProfile(name) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, `${name}.conf`), function (error) {
if (error && error.code !== 'ENOENT') debug('Error removing collectd profile', error);
if (!safe.fs.unlinkSync(path.join(paths.COLLECTD_APPCONFIG_DIR, `${name}.conf`))) {
if (safe.error.code !== 'ENOENT') debug('Error removing collectd profile', safe.error);
}
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', name ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Could not remove collectd config'));
callback(null);
});
});
const [error] = await safe(shell.promises.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', name ], {}));
if (error) throw new BoxError(BoxError.COLLECTD_ERROR, 'Could not remove collectd config');
}

View File

@@ -46,9 +46,15 @@ exports = module.exports = {
'io.github.sickchill.cloudronapp',
'to.couchpota.cloudronapp'
],
DEMO_APP_LIMIT: 20,
AUTOUPDATE_PATTERN_NEVER: 'never',
// the db field is a blob so we make this explicit
AVATAR_NONE: Buffer.from('', 'utf8'),
AVATAR_GRAVATAR: Buffer.from('gravatar', 'utf8'),
AVATAR_CUSTOM: Buffer.from('custom', 'utf8'), // this is not used here just for reference. The field will contain a byte buffer instead of the type string
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8), // also used in dashboard client.js
CLOUDRON: CLOUDRON,
@@ -58,6 +64,6 @@ exports = module.exports = {
FOOTER: '&copy; %YEAR% &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() : '6.0.1-test'
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '7.0.0-test'
};

View File

@@ -5,7 +5,8 @@ exports = module.exports = {
};
const assert = require('assert'),
auditSource = require('./auditsource.js'),
AuditSource = require('./auditsource.js'),
child_process = require('child_process'),
eventlog = require('./eventlog.js'),
safe = require('safetydance'),
path = require('path'),
@@ -17,43 +18,36 @@ const COLLECT_LOGS_CMD = path.join(__dirname, 'scripts/collectlogs.sh');
const CRASH_LOG_TIMESTAMP_OFFSET = 1000 * 60 * 60; // 60 min
const CRASH_LOG_TIMESTAMP_FILE = '/tmp/crashlog.timestamp';
function collectLogs(unitName, callback) {
async function collectLogs(unitName) {
assert.strictEqual(typeof unitName, 'string');
assert.strictEqual(typeof callback, 'function');
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + unitName, { encoding: 'utf8' });
if (!logs) return callback(safe.error);
callback(null, logs);
const logs = child_process.execSync(`sudo ${COLLECT_LOGS_CMD} ${unitName}`, { encoding: 'utf8' });
return logs;
}
function sendFailureLogs(unitName, callback) {
async function sendFailureLogs(unitName) {
assert.strictEqual(typeof unitName, 'string');
assert.strictEqual(typeof callback, 'function');
// check if we already sent a mail in the last CRASH_LOG_TIME_OFFSET window
const timestamp = safe.fs.readFileSync(CRASH_LOG_TIMESTAMP_FILE, 'utf8');
if (timestamp && (parseInt(timestamp) + CRASH_LOG_TIMESTAMP_OFFSET) > Date.now()) {
console.log('Crash log already sent within window');
return callback();
return;
}
collectLogs(unitName, async function (error, logs) {
if (error) {
console.error('Failed to collect logs.', error);
logs = util.format('Failed to collect logs.', error);
}
let [error, logs] = await safe(collectLogs(unitName));
if (error) {
console.error('Failed to collect logs.', error);
logs = util.format('Failed to collect logs.', error);
}
const crashId = `${new Date().toISOString()}`;
console.log(`Creating crash log for ${unitName} with id ${crashId}`);
const crashId = `${new Date().toISOString()}`;
console.log(`Creating crash log for ${unitName} with id ${crashId}`);
if (!safe.fs.writeFileSync(path.join(paths.CRASH_LOG_DIR, `${crashId}.log`), logs)) console.log(`Failed to stash logs to ${crashId}.log:`, safe.error);
if (!safe.fs.writeFileSync(path.join(paths.CRASH_LOG_DIR, `${crashId}.log`), logs)) console.log(`Failed to stash logs to ${crashId}.log:`, safe.error);
[error] = await safe(eventlog.add(eventlog.ACTION_PROCESS_CRASH, auditSource.HEALTH_MONITOR, { processName: unitName, crashId: crashId }));
if (error) console.log(`Error sending crashlog. Logs stashed at ${crashId}.log`);
[error] = await safe(eventlog.add(eventlog.ACTION_PROCESS_CRASH, AuditSource.HEALTH_MONITOR, { processName: unitName, crashId: crashId }));
if (error) console.log(`Error sending crashlog. Logs stashed at ${crashId}.log`);
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
callback();
});
safe.fs.writeFileSync(CRASH_LOG_TIMESTAMP_FILE, String(Date.now()));
}

View File

@@ -19,8 +19,7 @@ exports = module.exports = {
const appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
AuditSource = require('./auditsource.js'),
backups = require('./backups.js'),
cloudron = require('./cloudron.js'),
constants = require('./constants.js'),
@@ -29,6 +28,7 @@ const appHealthMonitor = require('./apphealthmonitor.js'),
dyndns = require('./dyndns.js'),
eventlog = require('./eventlog.js'),
janitor = require('./janitor.js'),
safe = require('safetydance'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
system = require('./system.js'),
@@ -52,8 +52,6 @@ const gJobs = {
appHealthMonitor: null
};
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// cron format
// Seconds: 0-59
// Minutes: 0-59
@@ -62,87 +60,81 @@ const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// Months: 0-11
// Day of Week: 0-6
function startJobs(callback) {
assert.strictEqual(typeof callback, 'function');
async function startJobs() {
debug('startJobs: starting cron jobs');
const randomTick = Math.floor(60*Math.random());
gJobs.systemChecks = new CronJob({
cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
onTick: async () => await safe(cloudron.runSystemChecks(), { debug }),
start: true
});
gJobs.diskSpaceChecker = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => system.checkDiskSpace(NOOP_CALLBACK),
onTick: async () => await safe(system.checkDiskSpace(), { debug }),
start: true
});
// 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),
onTick: async () => await safe(updateChecker.checkForUpdates({ automatic: true }), { debug }),
start: true
});
gJobs.cleanupTokens = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: janitor.cleanupTokens,
onTick: async () => await safe(janitor.cleanupTokens(), { debug }),
start: true
});
gJobs.cleanupBackups = new CronJob({
cronTime: DEFAULT_CLEANUP_BACKUPS_PATTERN,
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
onTick: async () => await safe(backups.startCleanupTask(AuditSource.CRON), { debug }),
start: true
});
gJobs.cleanupEventlog = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: eventlog.cleanup.bind(null, new Date(Date.now() - 60 * 60 * 24 * 10 * 1000)), // 10 days ago
onTick: async () => await safe(eventlog.cleanup({ creationTime: new Date(Date.now() - 60 * 60 * 24 * 10 * 1000) }), { debug }), // 10 days ago
start: true
});
gJobs.dockerVolumeCleaner = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: janitor.cleanupDockerVolumes,
onTick: async () => await safe(janitor.cleanupDockerVolumes(), { debug }),
start: true
});
gJobs.schedulerSync = new CronJob({
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
onTick: async () => await safe(scheduler.sync(), { debug }),
start: true
});
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: cloudron.renewCerts.bind(null, {}, auditSource.CRON, NOOP_CALLBACK),
onTick: async () => await safe(cloudron.renewCerts({}, AuditSource.CRON), { debug }),
start: true
});
gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
onTick: async () => await safe(appHealthMonitor.run(10), { debug }), // 10 is the max run time
start: true
});
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
const allSettings = await settings.list();
const tz = allSettings[settings.TIME_ZONE_KEY];
backupConfigChanged(allSettings[settings.BACKUP_CONFIG_KEY], tz);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY], tz);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
});
const tz = allSettings[settings.TIME_ZONE_KEY];
backupConfigChanged(allSettings[settings.BACKUP_CONFIG_KEY], tz);
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY], tz);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
}
// eslint-disable-next-line no-unused-vars
function handleSettingsChanged(key, value) {
async function handleSettingsChanged(key, value) {
assert.strictEqual(typeof key, 'string');
// value is a variant
@@ -152,10 +144,8 @@ function handleSettingsChanged(key, value) {
case settings.AUTOUPDATE_PATTERN_KEY:
case settings.DYNAMIC_DNS_KEY:
debug('handleSettingsChanged: recreating all jobs');
async.series([
stopJobs,
startJobs
], NOOP_CALLBACK);
await stopJobs();
await startJobs();
break;
default:
break;
@@ -172,7 +162,7 @@ function backupConfigChanged(value, tz) {
gJobs.backup = new CronJob({
cronTime: value.schedulePattern,
onTick: backups.startBackupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
onTick: async () => await safe(backups.startBackupTask(AuditSource.CRON), { debug }),
start: true,
timeZone: tz
});
@@ -190,19 +180,21 @@ function autoupdatePatternChanged(pattern, tz) {
gJobs.autoUpdater = new CronJob({
cronTime: pattern,
onTick: function() {
onTick: async function() {
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);
const [error] = await safe(updater.updateToLatest({ skipBackup: false }, AuditSource.CRON));
if (error) debug(`Failed to box autoupdate: ${error.message}`);
return;
}
const appUpdateInfo = _.omit(updateInfo, 'box');
if (Object.keys(appUpdateInfo).length > 0) {
debug('Starting app update to %j', appUpdateInfo);
apps.autoupdateApps(appUpdateInfo, auditSource.CRON, NOOP_CALLBACK);
const [error] = await safe(apps.autoupdateApps(appUpdateInfo, AuditSource.CRON));
if (error) debug(`Failed to app autoupdate: ${error.message}`);
} else {
debug('No app auto updates available');
}
@@ -221,7 +213,7 @@ function dynamicDnsChanged(enabled) {
if (enabled) {
gJobs.dynamicDns = new CronJob({
cronTime: '5 * * * * *', // we only update the records if the ip has changed.
onTick: dyndns.sync.bind(null, auditSource.CRON, NOOP_CALLBACK),
onTick: async () => await safe(dyndns.sync(AuditSource.CRON), { debug }),
start: true
});
} else {
@@ -230,14 +222,10 @@ function dynamicDnsChanged(enabled) {
}
}
function stopJobs(callback) {
assert.strictEqual(typeof callback, 'function');
for (var job in gJobs) {
async function stopJobs() {
for (const job in gJobs) {
if (!gJobs[job]) continue;
gJobs[job].stop();
gJobs[job] = null;
}
callback();
}

View File

@@ -15,14 +15,14 @@ exports = module.exports = {
const assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
child_process = require('child_process'),
constants = require('./constants.js'),
debug = require('debug')('box:database'),
mysql = require('mysql'),
once = require('once'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util');
var gConnectionPool = null;
let gConnectionPool = null;
const gDatabase = {
hostname: '127.0.0.1',
@@ -32,10 +32,8 @@ const gDatabase = {
name: 'box'
};
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
if (gConnectionPool !== null) return callback(null);
async function initialize() {
if (gConnectionPool !== null) return;
if (constants.TEST) {
// see setupTest script how the mysql-server is run
@@ -66,57 +64,49 @@ function initialize(callback) {
connection.query('USE ' + gDatabase.name);
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
});
callback(null);
}
function uninitialize(callback) {
if (!gConnectionPool) return callback(null);
async function uninitialize() {
if (!gConnectionPool) return;
gConnectionPool.end(callback);
gConnectionPool.end();
gConnectionPool = null;
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
var cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
async function clear() {
const cmd = util.format('mysql --host="%s" --user="%s" --password="%s" -Nse "SHOW TABLES" %s | grep -v "^migrations$" | while read table; do mysql --host="%s" --user="%s" --password="%s" -e "SET FOREIGN_KEY_CHECKS = 0; TRUNCATE TABLE $table" %s; done',
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name,
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name);
child_process.exec(cmd, callback);
await shell.promises.exec('clear_database', cmd);
}
function query() {
async function query() {
assert.notStrictEqual(gConnectionPool, null);
return new Promise((resolve, reject) => {
let args = Array.prototype.slice.call(arguments);
const callback = typeof args[args.length - 1] === 'function' ? args.pop() : null;
args.push(function queryCallback(error, result) {
if (error) return callback ? callback(error) : reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
if (error) return reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage || null }));
callback ? callback(null, result) : resolve(result);
resolve(result);
});
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
});
}
function transaction(queries) {
async function transaction(queries) {
assert(Array.isArray(queries));
const args = Array.prototype.slice.call(arguments);
const callback = typeof args[args.length - 1] === 'function' ? once(args.pop()) : null;
return new Promise((resolve, reject) => {
gConnectionPool.getConnection(function (error, connection) {
if (error) return callback ? callback(error) : reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
if (error) return reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
const releaseConnection = (error) => {
connection.release();
callback ? callback(error) : reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage }));
reject(new BoxError(BoxError.DATABASE_ERROR, error, { code: error.code, sqlMessage: error.sqlMessage || null }));
};
connection.beginTransaction(function (error) {
@@ -132,7 +122,7 @@ function transaction(queries) {
connection.release();
callback ? callback(null, results) : resolve(results);
resolve(results);
});
});
});
@@ -140,27 +130,25 @@ function transaction(queries) {
});
}
function importFromFile(file, callback) {
async function importFromFile(file) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`;
const cmd = `/usr/bin/mysql -h "${gDatabase.hostname}" -u ${gDatabase.username} -p${gDatabase.password} ${gDatabase.name} < ${file}`;
async.series([
query.bind(null, 'CREATE DATABASE IF NOT EXISTS box'),
child_process.exec.bind(null, cmd)
], callback);
await query('CREATE DATABASE IF NOT EXISTS box');
const [error] = await safe(shell.promises.exec('importFromFile', cmd));
if (error) throw new BoxError(BoxError.DATABASE_ERROR, error);
}
function exportToFile(file, callback) {
async function exportToFile(file) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
// 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 && require('fs').readFileSync('/etc/lsb-release', 'utf-8').includes('20.04')) ? '--column-statistics=0' : '';
const colStats = (!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}"`;
const cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${colStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
child_process.exec(cmd, callback);
const [error] = await safe(shell.promises.exec('exportToFile', cmd));
if (error) throw new BoxError(BoxError.DATABASE_ERROR, error);
}

View File

@@ -1,6 +1,6 @@
'use strict';
let assert = require('assert'),
const assert = require('assert'),
path = require('path');
class DataLayout {

335
src/dns.js Normal file
View File

@@ -0,0 +1,335 @@
'use strict';
module.exports = exports = {
fqdn,
getName,
getDnsRecords,
upsertDnsRecords,
removeDnsRecords,
waitForDnsRecord,
validateHostname,
makeWildcard,
registerLocations,
unregisterLocations,
checkDnsRecords,
syncDnsRecords,
resolve,
promises: {
resolve: require('util').promisify(resolve)
}
};
const apps = require('./apps.js'),
assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:dns'),
dns = require('dns'),
domains = require('./domains.js'),
mail = require('./mail.js'),
promiseRetry = require('./promise-retry.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
assert.strictEqual(typeof provider, 'string');
switch (provider) {
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 'vultr': return require('./dns/vultr.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'netcup': return require('./dns/netcup.js');
case 'noop': return require('./dns/noop.js');
case 'manual': return require('./dns/manual.js');
case 'wildcard': return require('./dns/wildcard.js');
default: return null;
}
}
function fqdn(location, domainObject) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
return location + (location ? '.' : '') + domainObject.domain;
}
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name (and not dns name)
function validateHostname(location, domainObject) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
const hostname = fqdn(location, domainObject);
const RESERVED_LOCATIONS = [
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
if (!tld.isValid(tmp)) return new BoxError(BoxError.BAD_FIELD, 'Hostname is not a valid domain name', { field: 'location' });
if (hostname.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters', { field: 'location' });
if (location) {
// label validation
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new BoxError(BoxError.BAD_FIELD, 'Invalid subdomain length', { field: 'location' });
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new BoxError(BoxError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot', { field: 'location' });
if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' });
}
return null;
}
// returns the 'name' that needs to be inserted into zone
// eslint-disable-next-line no-unused-vars
function getName(domain, location, type) {
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
if (location === '') return part;
return part ? `${location}.${part}` : location;
}
function maybePromisify(func) {
if (util.types.isAsyncFunction(func)) return func;
return util.promisify(func);
}
async function getDnsRecords(location, domain, type) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
const domainObject = await domains.get(domain);
return await maybePromisify(api(domainObject.provider).get)(domainObject, location, type);
}
async function checkDnsRecords(location, domain) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
const values = await getDnsRecords(location, domain, 'A');
const ip = await sysinfo.getServerIp();
if (values.length === 0) return { needsOverwrite: false }; // does not exist
if (values[0] === ip) return { needsOverwrite: false }; // exists but in sync
return { needsOverwrite: true };
}
// note: for TXT records the values must be quoted
async function upsertDnsRecords(location, domain, type, values) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
debug(`upsertDNSRecord: location ${location} on domain ${domain} of type ${type} with values ${JSON.stringify(values)}`);
const domainObject = await domains.get(domain);
await maybePromisify(api(domainObject.provider).upsert)(domainObject, location, type, values);
}
async function removeDnsRecords(location, domain, type, values) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values);
const domainObject = await domains.get(domain);
const [error] = await safe(maybePromisify(api(domainObject.provider).del)(domainObject, location, type, values));
if (error && error.reason !== BoxError.NOT_FOUND) throw error;
}
async function waitForDnsRecord(location, domain, type, value, options) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
const domainObject = await domains.get(domain);
// linode DNS takes ~15mins
if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000;
await maybePromisify(api(domainObject.provider).wait)(domainObject, location, type, value, options);
}
function makeWildcard(vhost) {
assert.strictEqual(typeof vhost, 'string');
// if the vhost is like *.example.com, this function will do nothing
let parts = vhost.split('.');
parts[0] = '*';
return parts.join('.');
}
async function registerLocations(locations, options, progressCallback) {
assert(Array.isArray(locations));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
debug(`registerLocations: Will register ${JSON.stringify(locations)} with options ${JSON.stringify(options)}`);
const overwriteDns = options.overwriteDns || false;
const ip = await sysinfo.getServerIp();
for (const location of locations) {
const error = await promiseRetry({ times: 200, interval: 5000 }, async function () {
progressCallback({ message: `Registering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` });
// get the current record before updating it
const [error, values] = await safe(getDnsRecords(location.subdomain, location.domain, 'A'));
if (error && error.reason === BoxError.EXTERNAL_ERROR) throw new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location }); // try again
// give up for other errors
if (error && error.reason === BoxError.ACCESS_DENIED) return new BoxError(BoxError.ACCESS_DENIED, error.message, { domain: location });
if (error && error.reason === BoxError.NOT_FOUND) return new BoxError(BoxError.NOT_FOUND, error.message, { domain: location });
if (error) return new BoxError(BoxError.EXTERNAL_ERROR, error.message, location);
if (values.length !== 0 && values[0] === ip) return null; // up-to-date
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwriteDns) return new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain: location });
const [upsertError] = await safe(upsertDnsRecords(location.subdomain, location.domain, 'A', [ ip ]));
if (upsertError && (upsertError.reason === BoxError.BUSY || upsertError.reason === BoxError.EXTERNAL_ERROR)) {
progressCallback({ message: `registerSubdomains: Upsert error. Will retry. ${upsertError.message}` });
throw new BoxError(BoxError.EXTERNAL_ERROR, upsertError.message, { domain: location }); // try again
}
return upsertError ? new BoxError(BoxError.EXTERNAL_ERROR, upsertError.message, location) : null;
});
if (error) throw error;
}
}
async function unregisterLocations(locations, progressCallback) {
assert(Array.isArray(locations));
assert.strictEqual(typeof progressCallback, 'function');
const ip = await sysinfo.getServerIp();
for (const location of locations) {
const error = await promiseRetry({ times: 30, interval: 5000 }, async function () {
progressCallback({ message: `Unregistering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` });
const [error] = await safe(removeDnsRecords(location.subdomain, location.domain, 'A', [ ip ]));
if (error && error.reason === BoxError.NOT_FOUND) return;
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
progressCallback({ message: `Error unregistering location. Will retry. ${error.message}`});
throw new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location }); // try again
}
return error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location }) : null; // give up for other errors
});
if (error) throw error;
}
}
async function syncDnsRecords(options, progressCallback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
if (options.domain && options.type === 'mail') return await mail.setDnsRecords(options.domain);
let allDomains = await domains.list();
if (options.domain) allDomains = allDomains.filter(d => d.domain === options.domain);
const mailSubdomain = settings.mailFqdn().substr(0, settings.mailFqdn().length - settings.mailDomain().length - 1);
const allApps = await apps.list();
let progress = 1, errors = [];
// we sync by domain only to get some nice progress
for (const domain of allDomains) {
progressCallback({ percent: progress, message: `Updating DNS of ${domain.domain}`});
progress += Math.round(100/(1+allDomains.length));
let locations = [];
if (domain.domain === settings.dashboardDomain()) locations.push({ subdomain: constants.DASHBOARD_LOCATION, domain: settings.dashboardDomain() });
if (domain.domain === settings.mailDomain() && settings.mailFqdn() !== settings.dashboardFqdn()) locations.push({ subdomain: mailSubdomain, domain: settings.mailDomain() });
allApps.forEach(function (app) {
const appLocations = [{ subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains);
locations = locations.concat(appLocations.filter(al => al.domain === domain.domain));
});
try {
await registerLocations(locations, { overwriteDns: true }, progressCallback);
progressCallback({ message: `Updating mail DNS of ${domain.domain}`});
await mail.setDnsRecords(domain.domain);
} catch (error) {
errors.push({ domain: domain.domain, message: error.message });
}
}
return { errors };
}
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
// are added for DNS server software to enclose spaces. Such quotes may also be returned
// by the DNS REST API of some providers
function resolve(hostname, rrtype, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof rrtype, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
const defaultOptions = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1
const resolver = new dns.Resolver();
options = _.extend({ }, defaultOptions, options);
// Only use unbound on a Cloudron
if (constants.CLOUDRON) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
resolver.resolve(hostname, rrtype, function (error, result) {
clearTimeout(timerId);
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
// for TXT records, result is 2d array of strings
callback(error, result);
});
}

View File

@@ -1,22 +1,21 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/cloudflare'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
@@ -115,7 +114,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
debug('upsert: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
@@ -186,7 +185,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
@@ -211,7 +210,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function(error, zone) {
if (error) return callback(error);
@@ -256,7 +255,7 @@ function wait(domainObject, location, type, value, options, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
debug('wait: %s for zone %s of type %s', fqdn, zoneName, type);

View File

@@ -15,8 +15,7 @@ const assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
@@ -87,7 +86,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
@@ -167,7 +166,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
@@ -190,7 +189,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
@@ -230,7 +229,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -1,26 +1,25 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gandi'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
var GANDI_API = 'https://dns.api.gandi.net/api/v5';
const GANDI_API = 'https://dns.api.gandi.net/api/v5';
function formatError(response) {
return util.format(`Gandi DNS error [${response.statusCode}] ${response.body.message}`);
@@ -44,7 +43,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
@@ -75,7 +74,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
@@ -103,7 +102,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
@@ -130,7 +129,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -1,23 +1,21 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/gcdns'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
GCDNS = require('@google-cloud/dns').DNS,
util = require('util'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
@@ -78,7 +76,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
@@ -120,7 +118,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
@@ -149,7 +147,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
getZoneByName(getDnsCredentials(dnsConfig), zoneName, function (error, zone) {
if (error) return callback(error);
@@ -183,7 +181,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -1,21 +1,20 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/godaddy'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
@@ -50,7 +49,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
@@ -91,7 +90,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug(`get: ${name} in zone ${zoneName} of type ${type}`);
@@ -123,7 +122,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug(`get: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
@@ -165,7 +164,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -7,18 +7,17 @@
// -------------------------------------------
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
util = require('util');
const assert = require('assert'),
BoxError = require('../boxerror.js');
function removePrivateFields(domainObject) {
// in-place removal of tokens and api keys with constants.SECRET_PLACEHOLDER

View File

@@ -10,13 +10,12 @@ exports = module.exports = {
verifyDnsConfig
};
let async = require('async'),
const 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'),
dns = require('../dns.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
@@ -117,7 +116,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
@@ -140,7 +139,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
@@ -222,7 +221,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
@@ -263,7 +262,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -10,12 +10,10 @@ exports = module.exports = {
verifyDnsConfig: verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
util = require('util'),
dns = require('../dns.js'),
waitForDns = require('./waitfordns.js');
function removePrivateFields(domainObject) {
@@ -66,7 +64,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -1,25 +1,25 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
verifyDnsConfig: verifyDnsConfig,
wait: wait
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
verifyDnsConfig,
wait
};
var assert = require('assert'),
const 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'),
dns = require('../dns.js'),
querystring = require('querystring'),
safe = require('safetydance'),
superagent = require('superagent'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js'),
xml2js = require('xml2js');
@@ -34,20 +34,17 @@ function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getQuery(dnsConfig, callback) {
async function getQuery(dnsConfig) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
const ip = await sysinfo.getServerIp();
callback(null, {
ApiUser: dnsConfig.username,
ApiKey: dnsConfig.token,
UserName: dnsConfig.username,
ClientIp: ip
});
});
return {
ApiUser: dnsConfig.username,
ApiKey: dnsConfig.token,
UserName: dnsConfig.username,
ClientIp: ip
};
}
function getZone(dnsConfig, zoneName, callback) {
@@ -55,7 +52,7 @@ function getZone(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
getQuery(dnsConfig, function (error, query) {
util.callbackify(getQuery)(dnsConfig, function (error, query) {
if (error) return callback(error);
query.Command = 'namecheap.domains.dns.getHosts';
@@ -93,7 +90,7 @@ function setZone(dnsConfig, zoneName, hosts, callback) {
assert(Array.isArray(hosts));
assert.strictEqual(typeof callback, 'function');
getQuery(dnsConfig, function (error, query) {
util.callbackify(getQuery)(dnsConfig, function (error, query) {
if (error) return callback(error);
query.Command = 'namecheap.domains.dns.setHosts';
@@ -151,7 +148,7 @@ function upsert(domainObject, subdomain, type, values, callback) {
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
subdomain = dns.getName(domainObject, subdomain, type) || '@';
debug('upsert: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
@@ -214,7 +211,7 @@ function get(domainObject, subdomain, type, callback) {
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
subdomain = dns.getName(domainObject, subdomain, type) || '@';
getZone(dnsConfig, zoneName, function (error, result) {
if (error) return callback(error);
@@ -241,7 +238,7 @@ function del(domainObject, subdomain, type, values, callback) {
const dnsConfig = domainObject.config;
const zoneName = domainObject.zoneName;
subdomain = domains.getName(domainObject, subdomain, type) || '@';
subdomain = dns.getName(domainObject, subdomain, type) || '@';
debug('del: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
@@ -316,7 +313,7 @@ function wait(domainObject, subdomain, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(subdomain, domainObject);
const fqdn = dns.fqdn(subdomain, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -1,24 +1,22 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/namecom'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
const NAMECOM_API = 'https://api.name.com/v4';
@@ -162,7 +160,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
debug(`upsert: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
@@ -183,7 +181,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
getInternal(dnsConfig, zoneName, name, type, function (error, result) {
if (error) return callback(error);
@@ -205,7 +203,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
debug(`del: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
@@ -235,7 +233,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -1,26 +1,25 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('../boxerror.js'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/netcup'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
var API_ENDPOINT = 'https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON';
const API_ENDPOINT = 'https://ccp.netcup.net/run/webservice/servers/endpoint.php?JSON';
function formatError(response) {
if (response.body) return util.format('Netcup DNS error [%s] %s', response.body.statuscode, response.body.longmessage);
@@ -95,7 +94,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
@@ -163,7 +162,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug('get: %s for zone %s of type %s', name, zoneName, type);
@@ -188,7 +187,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '@';
name = dns.getName(domainObject, location, type) || '@';
debug('del: %s for zone %s of type %s with values %j', name, zoneName, type, values);
@@ -249,7 +248,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -1,18 +1,17 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
debug = require('debug')('box:dns/noop'),
util = require('util');
const assert = require('assert'),
debug = require('debug')('box:dns/noop');
function removePrivateFields(domainObject) {
return domainObject;

View File

@@ -1,23 +1,21 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
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'),
util = require('util'),
dns = require('../dns.js'),
waitForDns = require('./waitfordns.js'),
_ = require('underscore');
@@ -102,7 +100,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
debug('add: %s for zone %s of type %s with values %j', fqdn, zoneName, type, values);
@@ -147,7 +145,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
@@ -183,7 +181,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
fqdn = domains.fqdn(location, domainObject);
fqdn = dns.fqdn(location, domainObject);
getZoneByName(dnsConfig, zoneName, function (error, zone) {
if (error) return callback(error);
@@ -241,7 +239,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -15,8 +15,7 @@ const async = require('async'),
constants = require('../constants.js'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/vultr'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
safe = require('safetydance'),
superagent = require('superagent'),
util = require('util'),
@@ -71,7 +70,7 @@ function getZoneRecords(dnsConfig, zoneName, name, type, callback) {
iteratorDone();
});
}, function (testDone) { return testDone(null, !!cursor); }, function (error) {
debug('getZoneRecords:', error, JSON.stringify(records));
debug('getZoneRecords: error:', error, JSON.stringify(records));
if (error) return callback(error);
@@ -87,7 +86,7 @@ function get(domainObject, location, type, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, records) {
if (error) return callback(error);
@@ -109,7 +108,7 @@ function upsert(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
@@ -190,7 +189,7 @@ function del(domainObject, location, type, values, callback) {
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
name = dns.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, records) {
if (error) return callback(error);
@@ -230,7 +229,7 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}

View File

@@ -2,11 +2,11 @@
exports = module.exports = waitForDns;
var assert = require('assert'),
const assert = require('assert'),
async = require('async'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/waitfordns'),
dns = require('../native-dns.js');
dns = require('../dns.js');
function resolveIp(hostname, options, callback) {
assert.strictEqual(typeof hostname, 'string');

View File

@@ -1,22 +1,21 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
removePrivateFields,
injectPrivateFields,
upsert,
get,
del,
wait,
verifyDnsConfig
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/manual'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
dns = require('../dns.js'),
safe = require('safetydance'),
sysinfo = require('../sysinfo.js'),
util = require('util'),
waitForDns = require('./waitfordns.js');
function removePrivateFields(domainObject) {
@@ -66,36 +65,32 @@ function wait(domainObject, location, type, value, options, callback) {
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
const fqdn = dns.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
async function verifyDnsConfig(domainObject) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const zoneName = domainObject.zoneName;
// Very basic check if the nameservers can be fetched
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new 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' }));
const [error, nameservers] = await safe(dns.promises.resolve(zoneName, 'NS', { timeout: 5000 }));
if (error && error.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' });
if (error || !nameservers) throw new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' });
const location = 'cloudrontestdns';
const fqdn = domains.fqdn(location, domainObject);
const location = 'cloudrontestdns';
const fqdn = dns.fqdn(location, domainObject);
dns.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }, function (error, result) {
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, `Unable to resolve ${fqdn}`, { field: 'nameservers' }));
if (error || !result) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : `Unable to resolve ${fqdn}`, { field: 'nameservers' }));
const [error2, result] = await safe(dns.promises.resolve(fqdn, 'A', { server: '127.0.0.1', timeout: 5000 }));
if (error2 && error2.code === 'ENOTFOUND') throw new BoxError(BoxError.BAD_FIELD, `Unable to resolve ${fqdn}`, { field: 'nameservers' });
if (error2 || !result) throw new BoxError(BoxError.BAD_FIELD, error2 ? error2.message : `Unable to resolve ${fqdn}`, { field: 'nameservers' });
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error.message}`));
const [error3, ip] = await safe(sysinfo.getServerIp());
if (error3) throw new BoxError(BoxError.EXTERNAL_ERROR, `Failed to detect IP of this server: ${error3.message}`);
if (result.length !== 1 || ip !== result[0]) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`));
if (result.length !== 1 || ip !== result[0]) throw new BoxError(BoxError.EXTERNAL_ERROR, `Domain resolves to ${JSON.stringify(result)} instead of ${ip}`);
callback(null, {});
});
});
});
return {};
}

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,8 @@ exports = module.exports = {
stop
};
var apps = require('./apps.js'),
const apps = require('./apps.js'),
assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
express = require('express'),
debug = require('debug')('box:dockerproxy'),
@@ -18,27 +17,27 @@ var apps = require('./apps.js'),
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
util = require('util'),
_ = require('underscore');
var gHttpServer = null;
let gHttpServer = null;
function authorizeApp(req, res, next) {
async function authorizeApp(req, res, next) {
// make the tests pass for now
if (constants.TEST) {
req.app = { id: 'testappid' };
return next();
}
apps.getByIpAddress(req.connection.remoteAddress, function (error, app) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error));
const [error, app] = await safe(apps.getByIpAddress(req.connection.remoteAddress));
if (error) return next(new HttpError(500, error));
if (!app) return next(new HttpError(401, 'Unauthorized'));
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
if (!('docker' in app.manifest.addons)) return next(new HttpError(401, 'Unauthorized'));
req.app = app;
req.app = app;
next();
});
next();
}
function attachDockerRequest(req, res, next) {
@@ -108,18 +107,17 @@ function process(req, res, next) {
}
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
async function start() {
assert(gHttpServer === null, 'Already started');
let json = middleware.json({ strict: true });
const 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();
const router = new express.Router();
router.post('/:version/containers/create', containersCreate);
let proxyServer = express();
const proxyServer = express();
if (constants.TEST) {
proxyServer.use(function (req, res, next) {
@@ -136,7 +134,6 @@ function start(callback) {
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
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);
@@ -170,14 +167,12 @@ function start(callback) {
client.pipe(remote).pipe(client);
});
});
await util.promisify(gHttpServer.listen.bind(gHttpServer))(constants.DOCKER_PROXY_PORT, '172.18.0.1');
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
async function stop() {
if (gHttpServer) gHttpServer.close();
gHttpServer = null;
callback();
}

View File

@@ -1,141 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
add,
get,
getAll,
update,
del,
clear
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
safe = require('safetydance');
const DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson', 'fallbackCertificateJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
delete data.configJson;
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
delete data.tlsConfigJson;
data.wellKnown = safe.JSON.parse(data.wellKnownJson);
delete data.wellKnownJson;
data.fallbackCertificate = safe.JSON.parse(data.fallbackCertificateJson);
delete data.fallbackCertificateJson;
return data;
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
postProcess(result[0]);
callback(null, result[0]);
});
}
function getAll(callback) {
database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`, function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
callback(null, results);
});
}
function add(name, data, callback) {
assert.strictEqual(typeof name, 'string');
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 data.fallbackCertificate, 'object');
assert.strictEqual(typeof callback, 'function');
let queries = [
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson, fallbackCertificateJson) VALUES (?, ?, ?, ?, ?, ?)',
args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig), JSON.stringify(data.fallbackCertificate) ] },
{ query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] },
];
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);
});
}
function update(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ], fields = [ ];
for (var k in domain) {
if (k === 'config' || k === 'tlsConfig' || k === 'wellKnown' || k === 'fallbackCertificate') { // json fields
fields.push(`${k}Json = ?`);
args.push(JSON.stringify(domain[k]));
} else {
fields.push(k + ' = ?');
args.push(domain[k]);
}
}
args.push(name);
database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
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).'));
if (error.message.indexOf('mail') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more mailboxes. Delete them first in the Email view.'));
return callback(new BoxError(BoxError.CONFLICT, error.message));
}
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
callback(null);
});
}
function clear(callback) {
database.query('DELETE FROM domains', function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
});
}

View File

@@ -3,53 +3,45 @@
module.exports = exports = {
add,
get,
getAll,
list,
update,
del,
clear,
fqdn,
getName,
getDnsRecords,
upsertDnsRecords,
removeDnsRecords,
waitForDnsRecord,
removePrivateFields,
removeRestrictedFields,
validateHostname,
makeWildcard,
parentDomain,
registerLocations,
unregisterLocations,
checkDnsRecords,
syncDnsRecords
};
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
const assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
database = require('./database.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
const DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'wellKnownJson', 'fallbackCertificateJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
delete data.configJson;
data.tlsConfig = safe.JSON.parse(data.tlsConfigJson);
delete data.tlsConfigJson;
data.wellKnown = safe.JSON.parse(data.wellKnownJson);
delete data.wellKnownJson;
data.fallbackCertificate = safe.JSON.parse(data.fallbackCertificateJson);
delete data.fallbackCertificateJson;
return data;
}
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
@@ -74,68 +66,28 @@ function api(provider) {
}
}
function parentDomain(domain) {
assert.strictEqual(typeof domain, 'string');
return domain.replace(/^\S+?\./, ''); // +? means non-greedy
function maybePromisify(func) {
if (util.types.isAsyncFunction(func)) return func;
return util.promisify(func);
}
function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
async function verifyDnsConfig(dnsConfig, domain, zoneName, provider) {
assert(dnsConfig && typeof dnsConfig === 'object'); // the dns config to test with
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof provider, 'string');
assert.strictEqual(typeof callback, 'function');
var backend = api(provider);
if (!backend) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid provider', { field: 'provider' }));
const backend = api(provider);
if (!backend) throw new BoxError(BoxError.BAD_FIELD, 'Invalid provider', { field: 'provider' });
const domainObject = { config: dnsConfig, domain: domain, zoneName: zoneName };
api(provider).verifyDnsConfig(domainObject, function (error, result) {
if (error && error.reason === BoxError.ACCESS_DENIED) return callback(new BoxError(BoxError.BAD_FIELD, `Access denied: ${error.message}`));
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_FIELD, `Zone not found: ${error.message}`));
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`));
if (error) return callback(error);
const [error, result] = await safe(maybePromisify(api(provider).verifyDnsConfig)(domainObject));
if (error && error.reason === BoxError.ACCESS_DENIED) return { error: new BoxError(BoxError.BAD_FIELD, `Access denied: ${error.message}`) };
if (error && error.reason === BoxError.NOT_FOUND) return { error: new BoxError(BoxError.BAD_FIELD, `Zone not found: ${error.message}`) };
if (error && error.reason === BoxError.EXTERNAL_ERROR) return { error: new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`) };
if (error) return { error };
callback(null, result);
});
}
function fqdn(location, domainObject) {
return location + (location ? '.' : '') + domainObject.domain;
}
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name (and not dns name)
function validateHostname(location, domainObject) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domainObject, 'object');
const hostname = fqdn(location, domainObject);
const RESERVED_LOCATIONS = [
constants.SMTP_LOCATION,
constants.IMAP_LOCATION
];
if (RESERVED_LOCATIONS.indexOf(location) !== -1) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
if (hostname === settings.dashboardFqdn()) return new BoxError(BoxError.BAD_FIELD, location + ' is reserved', { field: 'location' });
// workaround https://github.com/oncletom/tld.js/issues/73
var tmp = hostname.replace('_', '-');
if (!tld.isValid(tmp)) return new BoxError(BoxError.BAD_FIELD, 'Hostname is not a valid domain name', { field: 'location' });
if (hostname.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters', { field: 'location' });
if (location) {
// label validation
if (location.split('.').some(function (p) { return p.length > 63 || p.length < 1; })) return new BoxError(BoxError.BAD_FIELD, 'Invalid subdomain length', { field: 'location' });
if (location.match(/^[A-Za-z0-9-.]+$/) === null) return new BoxError(BoxError.BAD_FIELD, 'Subdomain can only contain alphanumeric, hyphen and dot', { field: 'location' });
if (/^[-.]/.test(location)) return new BoxError(BoxError.BAD_FIELD, 'Subdomain cannot start or end with hyphen or dot', { field: 'location' });
}
return null;
return { error: null, sanitizedConfig: result };
}
function validateTlsConfig(tlsConfig, dnsProvider) {
@@ -165,37 +117,37 @@ function validateWellKnown(wellKnown) {
return null;
}
function add(domain, data, auditSource, callback) {
async function add(domain, data, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.fallbackCertificate, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
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' }));
if (!tld.isValid(domain)) throw new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' });
if (domain.endsWith('.')) throw new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' });
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }));
if (zoneName.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }));
if (!tld.isValid(zoneName)) throw new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' });
if (zoneName.endsWith('.')) throw new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' });
} else {
zoneName = tld.getDomain(domain) || domain;
}
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', { domain, config }, fallbackCertificate);
if (error) return callback(error);
if (error) throw error;
} else {
fallbackCertificate = reverseProxy.generateFallbackCertificateSync(domain);
if (fallbackCertificate.error) return callback(fallbackCertificate.error);
fallbackCertificate = await reverseProxy.generateFallbackCertificate(domain);
}
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (error) throw error;
const dkimKey = await mail.generateDkimKey();
if (!dkimSelector) {
// create a unique suffix. this lets one add this domain can be added in another cloudron instance and not have their dkim selector conflict
@@ -203,47 +155,41 @@ function add(domain, data, auditSource, callback) {
dkimSelector = `cloudron-${suffix}`;
}
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
const result = await verifyDnsConfig(config, domain, zoneName, provider);
if (result.error) throw result.error;
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector, fallbackCertificate }, function (error) {
if (error) return callback(error);
let queries = [
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson, fallbackCertificateJson) VALUES (?, ?, ?, ?, ?, ?)',
args: [ domain, zoneName, provider, JSON.stringify(result.sanitizedConfig), JSON.stringify(tlsConfig), JSON.stringify(fallbackCertificate) ] },
{ query: 'INSERT INTO mail (domain, dkimKeyJson, dkimSelector) VALUES (?, ?, ?)', args: [ domain, JSON.stringify(dkimKey), dkimSelector || 'cloudron' ] },
];
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(error);
[error] = await safe(database.transaction(queries));
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, 'Domain already exists');
if (error) throw new BoxError(BoxError.DATABASE_ERROR, error);
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
await reverseProxy.setFallbackCertificate(domain, fallbackCertificate);
mail.onDomainAdded(domain, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
callback();
});
});
});
safe(mail.onDomainAdded(domain)); // background
}
function get(domain, callback) {
async function get(domain) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
domaindb.get(domain, function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
const result = await database.query(`SELECT ${DOMAINS_FIELDS} FROM domains WHERE domain=?`, [ domain ]);
if (result.length === 0) return null;
return postProcess(result[0]);
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.getAll(function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
async function list() {
const results = await database.query(`SELECT ${DOMAINS_FIELDS} FROM domains ORDER BY domain`);
results.forEach(postProcess);
return results;
}
function update(domain, data, auditSource, callback) {
async function update(domain, data, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
@@ -251,196 +197,97 @@ function update(domain, data, auditSource, callback) {
assert.strictEqual(typeof data.fallbackCertificate, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig, wellKnown } = data;
let error;
if (settings.isDemo() && (domain === settings.dashboardDomain())) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
if (settings.isDemo() && (domain === settings.dashboardDomain())) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode');
domaindb.get(domain, function (error, domainObject) {
if (error) return callback(error);
const domainObject = await get(domain);
if (zoneName) {
if (!tld.isValid(zoneName)) throw new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' });
} else {
zoneName = domainObject.zoneName;
}
if (zoneName) {
if (!tld.isValid(zoneName)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid zoneName', { field: 'zoneName' }));
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate);
if (error) throw error;
}
error = validateTlsConfig(tlsConfig, provider);
if (error) throw error;
error = validateWellKnown(wellKnown, provider);
if (error) throw error;
if (provider === domainObject.provider) api(provider).injectPrivateFields(config, domainObject.config);
const result = await verifyDnsConfig(config, domain, zoneName, provider);
if (result.error) throw result.error;
const newData = {
config: result.sanitizedConfig,
zoneName,
provider,
tlsConfig,
wellKnown,
};
if (fallbackCertificate) newData.fallbackCertificate = fallbackCertificate;
let args = [ ], fields = [ ];
for (const k in newData) {
if (k === 'config' || k === 'tlsConfig' || k === 'wellKnown' || k === 'fallbackCertificate') { // json fields
fields.push(`${k}Json = ?`);
args.push(JSON.stringify(newData[k]));
} else {
zoneName = domainObject.zoneName;
fields.push(k + ' = ?');
args.push(newData[k]);
}
}
args.push(domain);
if (fallbackCertificate) {
let error = reverseProxy.validateCertificate('test', domainObject, fallbackCertificate);
if (error) return callback(error);
}
[error] = await safe(database.query('UPDATE domains SET ' + fields.join(', ') + ' WHERE domain=?', args));
if (error && error.reason === BoxError.NOT_FOUND) throw new BoxError(BoxError.NOT_FOUND, 'Domain not found');
if (error) throw new BoxError(BoxError.DATABASE_ERROR, error);
error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (!fallbackCertificate) return;
error = validateWellKnown(wellKnown, provider);
if (error) return callback(error);
await reverseProxy.setFallbackCertificate(domain, fallbackCertificate);
if (provider === domainObject.provider) api(provider).injectPrivateFields(config, domainObject.config);
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
let newData = {
config: sanitizedConfig,
zoneName,
provider,
tlsConfig,
wellKnown,
};
if (fallbackCertificate) newData.fallbackCertificate = fallbackCertificate;
domaindb.update(domain, newData, function (error) {
if (error) return callback(error);
if (!fallbackCertificate) return callback();
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
callback();
});
});
});
});
eventlog.add(eventlog.ACTION_DOMAIN_UPDATE, auditSource, { domain, zoneName, provider });
}
function del(domain, auditSource, callback) {
async function del(domain, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.dashboardDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain'));
if (domain === settings.mailDomain()) return callback(new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain. Change the mail server location first'));
if (domain === settings.dashboardDomain()) throw new BoxError(BoxError.CONFLICT, 'Cannot remove admin domain');
if (domain === settings.mailDomain()) throw new BoxError(BoxError.CONFLICT, 'Cannot remove mail domain. Change the mail server location first');
domaindb.del(domain, function (error) {
if (error) return callback(error);
let queries = [
{ query: 'DELETE FROM mail WHERE domain = ?', args: [ domain ] },
{ query: 'DELETE FROM domains WHERE domain = ?', args: [ domain ] },
];
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
const [error, results] = await safe(database.transaction(queries));
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') {
if (error.message.indexOf('apps_mailDomain_constraint') !== -1) throw 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) throw new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more app(s).');
if (error.message.indexOf('mail') !== -1) throw new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more mailboxes. Delete them first in the Email view.');
throw new BoxError(BoxError.CONFLICT, error.message);
}
if (error) throw error;
if (results[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Domain not found');
mail.onDomainRemoved(domain, NOOP_CALLBACK);
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
return callback(null);
});
safe(mail.onDomainRemoved(domain));
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
domaindb.clear(function (error) {
if (error) return callback(error);
return callback(null);
});
}
// returns the 'name' that needs to be inserted into zone
// eslint-disable-next-line no-unused-vars
function getName(domain, location, type) {
const part = domain.domain.slice(0, -domain.zoneName.length - 1);
if (location === '') return part;
return part ? `${location}.${part}` : location;
}
function getDnsRecords(location, domain, type, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
get(domain, function (error, domainObject) {
if (error) return callback(error);
api(domainObject.provider).get(domainObject, location, type, function (error, values) {
if (error) return callback(error);
callback(null, values);
});
});
}
function checkDnsRecords(location, domain, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
getDnsRecords(location, domain, 'A', function (error, values) {
if (error) return callback(error);
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
if (values.length === 0) return callback(null, { needsOverwrite: false }); // does not exist
if (values[0] === ip) return callback(null, { needsOverwrite: false }); // exists but in sync
callback(null, { needsOverwrite: true });
});
});
}
// note: for TXT records the values must be quoted
function upsertDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('upsertDNSRecord: %s on %s type %s values', location, domain, type, values);
get(domain, function (error, domainObject) {
if (error) return callback(error);
api(domainObject.provider).upsert(domainObject, location, type, values, function (error) {
if (error) return callback(error);
callback(null);
});
});
}
function removeDnsRecords(location, domain, type, values, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert(Array.isArray(values));
assert.strictEqual(typeof callback, 'function');
debug('removeDNSRecord: %s on %s type %s values', location, domain, type, values);
get(domain, function (error, domainObject) {
if (error) return callback(error);
api(domainObject.provider).del(domainObject, location, type, values, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
callback(null);
});
});
}
function waitForDnsRecord(location, domain, type, value, options, callback) {
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof domain, 'string');
assert(type === 'A' || type === 'TXT');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
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);
});
async function clear() {
await database.query('DELETE FROM domains');
}
// removes all fields that are strictly private and should never be returned by API calls
@@ -457,135 +304,3 @@ function removeRestrictedFields(domain) {
return result;
}
function makeWildcard(vhost) {
assert.strictEqual(typeof vhost, 'string');
// if the vhost is like *.example.com, this function will do nothing
let parts = vhost.split('.');
parts[0] = '*';
return parts.join('.');
}
function registerLocations(locations, options, progressCallback, callback) {
assert(Array.isArray(locations));
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
debug(`registerLocations: Will register ${JSON.stringify(locations)} with options ${JSON.stringify(options)}`);
const overwriteDns = options.overwriteDns || false;
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(locations, function (location, iteratorDone) {
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
progressCallback({ message: `Registering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` });
// get the current record before updating it
getDnsRecords(location.subdomain, location.domain, 'A', function (error, values) {
if (error && error.reason === BoxError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again
if (error && error.reason === BoxError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain: location }));
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain: location }));
if (error) return retryCallback(null, new BoxError(BoxError.EXTERNAL_ERROR, error.message, location)); // give up for other errors
if (values.length !== 0 && values[0] === ip) return retryCallback(null); // up-to-date
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !overwriteDns) return retryCallback(null, new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain: location }));
upsertDnsRecords(location.subdomain, location.domain, 'A', [ ip ], function (error) {
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
progressCallback({ message: `registerSubdomains: Upsert error. Will retry. ${error.message}` });
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again
}
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, location) : null);
});
});
}, function (error, result) {
if (error || result) return iteratorDone(error || result);
iteratorDone(null);
});
}, callback);
});
}
function unregisterLocations(locations, progressCallback, callback) {
assert(Array.isArray(locations));
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
async.eachSeries(locations, function (location, iteratorDone) {
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
progressCallback({ message: `Unregistering location: ${location.subdomain ? (location.subdomain + '.') : ''}${location.domain}` });
removeDnsRecords(location.subdomain, location.domain, 'A', [ ip ], function (error) {
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
progressCallback({ message: `Error unregistering location. Will retry. ${error.message}`});
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location })); // try again
}
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain: location }) : null);
});
}, function (error, result) {
if (error || result) return iteratorDone(error || result);
iteratorDone();
});
}, callback);
});
}
function syncDnsRecords(options, progressCallback, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (options.domain && options.type === 'mail') return mail.setDnsRecords(options.domain, callback);
getAll(function (error, domains) {
if (error) return callback(error);
if (options.domain) domains = domains.filter(d => d.domain === options.domain);
const mailSubdomain = settings.mailFqdn().substr(0, settings.mailFqdn().length - settings.mailDomain().length - 1);
apps.getAll(function (error, allApps) {
if (error) return callback(error);
let progress = 1, errors = [];
// we sync by domain only to get some nice progress
async.eachSeries(domains, function (domain, iteratorDone) {
progressCallback({ percent: progress, message: `Updating DNS of ${domain.domain}`});
progress += Math.round(100/(1+domains.length));
let locations = [];
if (domain.domain === settings.dashboardDomain()) locations.push({ subdomain: constants.DASHBOARD_LOCATION, domain: settings.dashboardDomain() });
if (domain.domain === settings.mailDomain() && settings.mailFqdn() !== settings.dashboardFqdn()) locations.push({ subdomain: mailSubdomain, domain: settings.mailDomain() });
allApps.forEach(function (app) {
const appLocations = [{ subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains);
locations = locations.concat(appLocations.filter(al => al.domain === domain.domain));
});
async.series([
registerLocations.bind(null, locations, { overwriteDns: true }, progressCallback),
progressCallback.bind(null, { message: `Updating mail DNS of ${domain.domain}`}),
mail.setDnsRecords.bind(null, domain.domain)
], function (error) {
if (error) errors.push({ domain: domain.domain, message: error.message });
iteratorDone();
});
}, () => callback(null, { errors }));
});
});
}

View File

@@ -4,12 +4,11 @@ exports = module.exports = {
sync
};
let apps = require('./apps.js'),
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
constants = require('./constants.js'),
debug = require('debug')('box:dyndns'),
domains = require('./domains.js'),
dns = require('./dns.js'),
eventlog = require('./eventlog.js'),
paths = require('./paths.js'),
safe = require('safetydance'),
@@ -17,46 +16,33 @@ let apps = require('./apps.js'),
sysinfo = require('./sysinfo.js');
// called for dynamic dns setups where we have to update the IP
function sync(auditSource, callback) {
async function sync(auditSource) {
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(error);
const ip = await sysinfo.getServerIp();
let info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ip: null };
if (info.ip === ip) {
debug(`refreshDNS: no change in IP ${ip}`);
return callback();
}
let info = safe.JSON.parse(safe.fs.readFileSync(paths.DYNDNS_INFO_FILE, 'utf8')) || { ip: null };
if (info.ip === ip) {
debug(`refreshDNS: no change in IP ${ip}`);
return;
}
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
debug(`refreshDNS: updating ip from ${info.ip} to ${ip}`);
domains.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ip ], function (error) {
if (error) return callback(error);
await dns.upsertDnsRecords(constants.DASHBOARD_LOCATION, settings.dashboardDomain(), 'A', [ ip ]);
debug('refreshDNS: updated admin location');
debug('refreshDNS: updated admin location');
const result = await apps.list();
for (const app of result) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== apps.ISTATE_INSTALLED) continue;
apps.getAll(function (error, result) {
if (error) return callback(error);
await dns.upsertDnsRecords(app.location, app.domain, 'A', [ ip ]);
}
async.each(result, function (app, callback) {
// do not change state of installing apps since apptask will error if dns record already exists
if (app.installationState !== apps.ISTATE_INSTALLED) return callback();
debug('refreshDNS: updated apps');
domains.upsertDnsRecords(app.location, app.domain, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
debug('refreshDNS: updated apps');
eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, auditSource, { fromIp: info.ip, toIp: ip });
info.ip = ip;
safe.fs.writeFileSync(paths.DYNDNS_INFO_FILE, JSON.stringify(info), 'utf8');
callback();
});
});
});
});
await eventlog.add(eventlog.ACTION_DYNDNS_UPDATE, auditSource, { fromIp: info.ip, toIp: ip });
info.ip = ip;
safe.fs.writeFileSync(paths.DYNDNS_INFO_FILE, JSON.stringify(info), 'utf8');
}

View File

@@ -4,7 +4,7 @@ exports = module.exports = {
add,
upsertLoginEvent,
get,
getAllPaged,
listPaged,
cleanup,
_clear: clear,
@@ -19,6 +19,8 @@ exports = module.exports = {
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_APP_UPDATE_FINISH: 'app.update.finish',
ACTION_APP_BACKUP: 'app.backup',
ACTION_APP_BACKUP_FINISH: 'app.backup.finish',
ACTION_APP_LOGIN: 'app.login',
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
@@ -54,6 +56,11 @@ exports = module.exports = {
ACTION_PROVISION: 'cloudron.provision',
ACTION_RESTORE: 'cloudron.restore', // unused
ACTION_START: 'cloudron.start',
ACTION_SERVICE_CONFIGURE: 'service.configure',
ACTION_SERVICE_REBUILD: 'service.rebuild',
ACTION_SERVICE_RESTART: 'service.restart',
ACTION_UPDATE: 'cloudron.update',
ACTION_UPDATE_FINISH: 'cloudron.update.finish',
@@ -66,6 +73,7 @@ exports = module.exports = {
ACTION_VOLUME_ADD: 'volume.add',
ACTION_VOLUME_UPDATE: 'volume.update',
ACTION_VOLUME_REMOUNT: 'volume.remount',
ACTION_VOLUME_REMOVE: 'volume.remove',
ACTION_DYNDNS_UPDATE: 'dyndns.update',
@@ -78,7 +86,6 @@ exports = module.exports = {
const assert = require('assert'),
database = require('./database.js'),
debug = require('debug')('box:eventlog'),
mysql = require('mysql'),
notifications = require('./notifications.js'),
safe = require('safetydance'),
@@ -101,14 +108,9 @@ async function add(action, source, data) {
assert.strictEqual(typeof data, 'object');
const id = uuid.v4();
try {
await database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ]);
await notifications.onEvent(id, action, source, data);
return id;
} catch (error) {
debug('add: error adding event', error);
return null;
}
await database.query('INSERT INTO eventlog (id, action, source, data) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ]);
await notifications.onEvent(id, action, source, data);
return id;
}
// never throws, only logs because previously code did not take a callback
@@ -126,16 +128,11 @@ async function upsertLoginEvent(action, source, data) {
args: [ action, JSON.stringify(source) ]
}];
try {
const result = await database.transaction(queries);
if (result[0].affectedRows >= 1) return result[1][0].id;
const result = await database.transaction(queries);
if (result[0].affectedRows >= 1) return result[1][0].id;
// no existing eventlog found, create one
return await add(action, source, data);
} catch (error) {
debug('add: error adding event', error);
return null;
}
// no existing eventlog found, create one
return await add(action, source, data);
}
async function get(id) {
@@ -147,7 +144,7 @@ async function get(id) {
return postProcess(result[0]);
}
async function getAllPaged(actions, search, page, perPage) {
async function listPaged(actions, search, page, perPage) {
assert(Array.isArray(actions));
assert(typeof search === 'string' || search === null);
assert.strictEqual(typeof page, 'number');

View File

@@ -1,9 +1,8 @@
'use strict';
exports = module.exports = {
search,
verifyPassword,
createAndVerifyUserIfNotExist,
maybeCreateUser,
testConfig,
startSyncer,
@@ -14,18 +13,19 @@ exports = module.exports = {
sync
};
var assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
const assert = require('assert'),
AuditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:externalldap'),
groups = require('./groups.js'),
ldap = require('ldapjs'),
once = require('once'),
safe = require('safetydance'),
settings = require('./settings.js'),
tasks = require('./tasks.js'),
users = require('./users.js');
users = require('./users.js'),
util = require('util');
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.bindPassword === constants.SECRET_PLACEHOLDER) newConfig.bindPassword = currentConfig.bindPassword;
@@ -57,532 +57,431 @@ function validUserRequirements(user) {
}
// performs service bind if required
function getClient(externalLdapConfig, doBindAuth, callback) {
async function getClient(externalLdapConfig, options) {
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);
assert.strictEqual(typeof options, 'object');
// 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')); }
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, 'invalid baseDn'); }
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, 'invalid filter'); }
var config = {
const config = {
url: externalLdapConfig.url,
tlsOptions: {
rejectUnauthorized: externalLdapConfig.acceptSelfSignedCerts ? false : true
}
};
var client;
let client;
try {
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 (e instanceof ldap.ProtocolError) throw new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid');
throw new BoxError(BoxError.INTERNAL_ERROR, e);
}
// 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);
if (!externalLdapConfig.bindDn || !options.bind) return 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));
return await new Promise((resolve, reject) => {
reject = once(reject);
callback(null, client);
// ensure we don't just crash
client.on('error', function (error) {
reject(new BoxError(BoxError.EXTERNAL_ERROR, error));
});
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
if (error instanceof ldap.InvalidCredentialsError) return reject(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return reject(new BoxError(BoxError.EXTERNAL_ERROR, error));
resolve(client);
});
});
}
function ldapGetByDN(externalLdapConfig, dn, callback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
async function clientSearch(client, dn, searchOptions) {
assert.strictEqual(typeof client, 'object');
assert.strictEqual(typeof dn, 'string');
assert.strictEqual(typeof callback, 'function');
assert.strictEqual(typeof searchOptions, 'object');
getClient(externalLdapConfig, true, function (error, client) {
if (error) return callback(error);
debug(`clientSearch: Get objects at ${dn} with options ${JSON.stringify(searchOptions)}`);
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')); }
// basic validation to not crash
try { ldap.parseDN(dn); } catch (e) { throw new BoxError(BoxError.BAD_FIELD, 'invalid DN'); }
return await new Promise((resolve, reject) => {
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));
if (error instanceof ldap.NoSuchObjectError) return reject(new BoxError(BoxError.NOT_FOUND));
if (error) return reject(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('error', error => reject(new BoxError(BoxError.EXTERNAL_ERROR, error)));
result.on('end', function (result) {
client.unbind();
if (result.status !== 0) return reject(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
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]);
resolve(ldapObjects);
});
});
});
}
async function ldapGetByDN(externalLdapConfig, dn) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof dn, 'string');
const searchOptions = {
paged: true,
scope: 'sub' // We may have to make this configurable
};
debug(`ldapGetByDN: Get object at ${dn}`);
const client = await getClient(externalLdapConfig, { bind: true });
const result = await clientSearch(client, dn, searchOptions);
client.unbind();
if (result.length === 0) throw new BoxError(BoxError.NOT_FOUND);
return result[0];
}
// TODO support search by email
function ldapUserSearch(externalLdapConfig, options, callback) {
async function ldapUserSearch(externalLdapConfig, options) {
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);
const searchOptions = {
paged: true,
filter: ldap.parseFilter(externalLdapConfig.filter),
scope: 'sub' // We may have to make this configurable
};
let searchOptions = {
paged: true,
filter: ldap.parseFilter(externalLdapConfig.filter),
scope: 'sub' // We may have to make this configurable
};
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
const extraFilter = ldap.parseFilter(options.filter);
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
}
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 users at ${externalLdapConfig.baseDn} with filter ${searchOptions.filter.toString()}`);
debug(`Listing users at ${externalLdapConfig.baseDn} with filter ${searchOptions.filter.toString()}`);
client.search(externalLdapConfig.baseDn, 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 ldapUsers = [];
result.on('searchEntry', entry => ldapUsers.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, ldapUsers);
});
});
});
const client = await getClient(externalLdapConfig, { bind: true });
const result = await clientSearch(client, externalLdapConfig.baseDn, searchOptions);
client.unbind();
return result;
}
function ldapGroupSearch(externalLdapConfig, options, callback) {
async function ldapGroupSearch(externalLdapConfig, options) {
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);
const searchOptions = {
paged: true,
scope: 'sub' // We may have to make this configurable
};
let searchOptions = {
paged: true,
scope: 'sub' // We may have to make this configurable
};
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter);
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter);
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
const extraFilter = ldap.parseFilter(options.filter);
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
}
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()}`);
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);
});
});
});
const client = await getClient(externalLdapConfig, { bind: true });
const result = await clientSearch(client, externalLdapConfig.groupBaseDn, searchOptions);
client.unbind();
return result;
}
function testConfig(config, callback) {
async function testConfig(config) {
assert.strictEqual(typeof config, 'object');
assert.strictEqual(typeof callback, 'function');
if (config.provider === 'noop') return callback();
if (config.provider === 'noop') return null;
if (!config.url) return callback(new BoxError(BoxError.BAD_FIELD, 'url must not be empty'));
if (!config.url.startsWith('ldap://') && !config.url.startsWith('ldaps://')) return callback(new BoxError(BoxError.BAD_FIELD, 'url is missing ldap:// or ldaps:// prefix'));
if (!config.url) return new BoxError(BoxError.BAD_FIELD, 'url must not be empty');
if (!config.url.startsWith('ldap://') && !config.url.startsWith('ldaps://')) return new BoxError(BoxError.BAD_FIELD, 'url is missing ldap:// or ldaps:// prefix');
if (!config.usernameField) config.usernameField = 'uid';
// bindDn may not be a dn!
if (!config.baseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'basedn must not be empty'));
try { ldap.parseDN(config.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
if (!config.baseDn) return new BoxError(BoxError.BAD_FIELD, 'basedn must not be empty');
try { ldap.parseDN(config.baseDn); } catch (e) { return new BoxError(BoxError.BAD_FIELD, 'invalid baseDn'); }
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')); }
if (!config.filter) return new BoxError(BoxError.BAD_FIELD, 'filter must not be empty');
try { ldap.parseFilter(config.filter); } catch (e) { return new BoxError(BoxError.BAD_FIELD, 'invalid filter'); }
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 ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean');
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return 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.groupBaseDn) return new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty');
try { ldap.parseDN(config.groupBaseDn); } catch (e) { return 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.groupFilter) return new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty');
try { ldap.parseFilter(config.groupFilter); } catch (e) { return 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'));
if (!config.groupnameField || typeof config.groupnameField !== 'string') return new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty');
}
getClient(config, true, function (error, client) {
if (error) return callback(error);
const [error, client] = await safe(getClient(config, { bind: true }));
if (error) return error;
var opts = {
filter: config.filter,
scope: 'sub'
};
const opts = {
filter: config.filter,
scope: 'sub'
};
client.search(config.baseDn, opts, function (error, result) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
const [searchError, ] = await safe(clientSearch(client, config.baseDn, opts));
client.unbind();
if (searchError) return searchError;
result.on('searchEntry', function (/* entry */) {});
result.on('error', function (error) { client.unbind(); callback(new BoxError(BoxError.BAD_FIELD, `Unable to search directory: ${error.message}`)); });
result.on('end', function (/* result */) { client.unbind(); callback(); });
});
});
return null;
}
function search(identifier, callback) {
// eslint-disable-next-line no-unused-vars
async function search(identifier) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
if (error) return callback(error);
const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` });
// translate ldap properties to ours
let users = ldapUsers.map(function (u) { return translateUser(externalLdapConfig, u); });
// translate ldap properties to ours
let users = ldapUsers.map(function (u) { return translateUser(externalLdapConfig, u); });
callback(null, users);
});
});
return users;
}
function createAndVerifyUserIfNotExist(identifier, password, callback) {
async function maybeCreateUser(identifier) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
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'));
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
if (!externalLdapConfig.autoCreate) throw new BoxError(BoxError.BAD_STATE, 'auto create not enabled');
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));
const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` });
if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND);
if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT);
let user = translateUser(externalLdapConfig, ldapUsers[0]);
if (!validUserRequirements(user)) return callback(new BoxError(BoxError.BAD_FIELD));
const user = translateUser(externalLdapConfig, ldapUsers[0]);
if (!validUserRequirements(user)) throw new BoxError(BoxError.BAD_FIELD);
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) {
if (error) {
debug(`createAndVerifyUserIfNotExist: Failed to auto create user ${user.username}`, error);
return callback(new BoxError(BoxError.INTERNAL_ERROR));
}
const [error, userId] = await safe(users.add(user.email, { username: user.username, password: null, displayName: user.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_AUTO_CREATE));
if (error) {
debug(`maybeCreateUser: failed to auto create user ${user.username}`, error);
throw error;
}
verifyPassword(user, password, function (error) {
if (error) return callback(error);
callback(null, user);
});
});
});
});
// fetch the full record
return await users.get(userId);
}
function verifyPassword(user, password, callback) {
async function verifyPassword(user, password) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
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));
const ldapUsers = await ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` });
if (ldapUsers.length === 0) throw new BoxError(BoxError.NOT_FOUND);
if (ldapUsers.length > 1) throw new BoxError(BoxError.CONFLICT);
getClient(externalLdapConfig, false, function (error, client) {
if (error) return callback(error);
const client = await getClient(externalLdapConfig, { bind: false });
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));
const [error] = await safe(util.promisify(client.bind.bind(client))(ldapUsers[0].dn, password));
client.unbind();
if (error instanceof ldap.InvalidCredentialsError) throw new BoxError(BoxError.INVALID_CREDENTIALS);
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
});
});
});
});
return translateUser(externalLdapConfig, ldapUsers[0]);
}
function startSyncer(callback) {
assert.strictEqual(typeof callback, 'function');
async function startSyncer() {
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
const taskId = await tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, []);
tasks.add(tasks.TASK_SYNC_EXTERNAL_LDAP, [], function (error, taskId) {
if (error) return callback(error);
tasks.startTask(taskId, {}, function (error, result) {
debug('sync: done', error, result);
});
callback(null, taskId);
});
tasks.startTask(taskId, {}, function (error, result) {
debug('sync: done', error, result);
});
return taskId;
}
function syncUsers(externalLdapConfig, progressCallback, callback) {
async function syncUsers(externalLdapConfig, progressCallback) {
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);
const ldapUsers = await ldapUserSearch(externalLdapConfig, {});
debug(`Found ${ldapUsers.length} users`);
debug(`syncUsers: Found ${ldapUsers.length} users`);
let percent = 10;
let step = 30/(ldapUsers.length+1); // ensure no divide by 0
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);
// we ignore all errors here and just log them for now
for (let i = 0; i < ldapUsers.length; i++) {
let ldapUser = translateUser(externalLdapConfig, ldapUsers[i]);
if (!validUserRequirements(ldapUser)) continue;
if (!validUserRequirements(user)) return iteratorCallback();
percent += step;
progressCallback({ percent, message: `Syncing... ${ldapUser.username}` });
percent += step;
progressCallback({ percent, message: `Syncing... ${user.username}` });
const user = await users.getByUsername(ldapUser.username);
users.getByUsername(user.username, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
if (!user) {
debug(`syncUsers: [adding user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
if (!result) {
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
const [userAddError] = await safe(users.add(ldapUser.email, { username: ldapUser.username, password: null, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_TASK));
if (userAddError) debug('syncUsers: Failed to create user', ldapUser, userAddError);
} else if (user.source !== 'ldap') {
debug(`syncUsers: [mapping user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.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}`);
const [userMappingError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName, source: 'ldap' }, AuditSource.EXTERNAL_LDAP_TASK));
if (userMappingError) debug('Failed to map user', ldapUser, userMappingError);
} else if (user.email !== ldapUser.email || user.displayName !== ldapUser.displayName) {
debug(`syncUsers: [updating user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.displayName}`);
iteratorCallback();
} else if (result.email !== user.email || result.displayName !== user.displayName) {
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
const [userUpdateError] = await safe(users.update(user, { email: ldapUser.email, fallbackEmail: ldapUser.email, displayName: ldapUser.displayName }, AuditSource.EXTERNAL_LDAP_TASK));
if (userUpdateError) debug('Failed to update user', ldapUser, userUpdateError);
} else {
// user known and up-to-date
debug(`syncUsers: [up-to-date user] username=${ldapUser.username} email=${ldapUser.email} displayName=${ldapUser.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);
});
debug('syncUsers: done');
}
function syncGroups(externalLdapConfig, progressCallback, callback) {
async function syncGroups(externalLdapConfig, progressCallback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!externalLdapConfig.syncGroups) {
debug('Group sync is disabled');
debug('syncGroups: Group sync is disabled');
progressCallback({ percent: 70, message: 'Skipping group sync...' });
return callback(null, []);
return [];
}
ldapGroupSearch(externalLdapConfig, {}, function (error, ldapGroups) {
if (error) return callback(error);
const ldapGroups = await ldapGroupSearch(externalLdapConfig, {});
debug(`Found ${ldapGroups.length} groups`);
debug(`syncGroups: Found ${ldapGroups.length} groups`);
let percent = 40;
let step = 30/(ldapGroups.length+1); // ensure no divide by 0
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();
// we ignore all non internal errors here and just log them for now
for (const ldapGroup of ldapGroups) {
let groupName = ldapGroup[externalLdapConfig.groupnameField];
if (!groupName) return;
// some servers return empty array for unknown properties :-/
if (typeof groupName !== 'string') return;
// groups are lowercase
groupName = groupName.toLowerCase();
// groups are lowercase
groupName = groupName.toLowerCase();
percent += step;
progressCallback({ percent, message: `Syncing... ${groupName}` });
percent += step;
progressCallback({ percent, message: `Syncing... ${groupName}` });
groups.getByName(groupName, function (error, result) {
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
const result = await groups.getByName(groupName);
if (!result) {
debug(`[adding group] groupname=${groupName}`);
if (!result) {
debug(`syncGroups: [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}`);
const [error] = await safe(groups.add({ name: groupName, source: 'ldap' }));
if (error) debug('syncGroups: Failed to create group', groupName, error);
} else {
debug(`syncGroups: [up-to-date group] groupname=${groupName}`);
}
}
iteratorCallback();
}
});
}, function (error) {
if (error) return callback(error);
debug('sync: ldap sync is done', error);
callback(error);
});
});
debug('syncGroups: sync done');
}
function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
async function syncGroupUsers(externalLdapConfig, progressCallback) {
assert.strictEqual(typeof externalLdapConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
if (!externalLdapConfig.syncGroups) {
debug('Group users sync is disabled');
debug('syncGroupUsers: Group users sync is disabled');
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
return callback(null, []);
return [];
}
groups.getAll(function (error, result) {
if (error) return callback(error);
const allGroups = await groups.list();
const ldapGroups = allGroups.filter(function (g) { return g.source === 'ldap'; });
debug(`syncGroupUsers: Found ${ldapGroups.length} groups to sync users`);
var ldapGroups = result.filter(function (g) { return g.source === 'ldap'; });
debug(`Found ${ldapGroups.length} groups to sync users`);
for (const group of ldapGroups) {
debug(`syncGroupUsers: Sync users for group ${group.name}`);
async.eachSeries(ldapGroups, function (group, iteratorCallback) {
debug(`Sync users for group ${group.name}`);
const result = await ldapGroupSearch(externalLdapConfig, {});
if (!result || result.length === 0) {
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
continue;
}
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;
});
// 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.`);
continue;
}
if (!found) {
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
return callback();
}
let ldapGroupMembers = found.member || found.uniqueMember || [];
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 ];
// if only one entry is in the group ldap returns a string, not an array!
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
debug(`syncGroupUsers: Group ${group.name} has ${ldapGroupMembers.length} members.`);
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
for (const memberDn of ldapGroupMembers) {
const [ldapError, result] = await safe(ldapGetByDN(externalLdapConfig, memberDn));
if (ldapError) {
debug(`syncGroupUsers: Failed to get ${memberDn}:`, ldapError);
continue;
}
async.eachSeries(ldapGroupMembers, function (memberDn, iteratorCallback) {
ldapGetByDN(externalLdapConfig, memberDn, function (error, result) {
if (error) {
debug(`Failed to get ${memberDn}:`, error);
return iteratorCallback();
}
debug(`syncGroupUsers: Found member object at ${memberDn} adding to group ${group.name}`);
debug(`Found member object at ${memberDn} adding to group ${group.name}`);
const username = result[externalLdapConfig.usernameField];
if (!username) continue;
const username = result[externalLdapConfig.usernameField];
if (!username) return iteratorCallback();
const [getError, userObject] = await safe(users.getByUsername(username));
if (getError || !userObject) {
debug(`syncGroupUsers: Failed to get user by username ${username}`, getError ? getError : 'User not found');
continue;
}
users.getByUsername(username, function (error, result) {
if (error) {
debug(`syncGroupUsers: Failed to get user by username ${username}`, error);
return iteratorCallback();
}
const [addError] = await safe(groups.addMember(group.id, userObject.id));
if (addError && addError.reason !== BoxError.ALREADY_EXISTS) debug('syncGroupUsers: Failed to add member', addError);
}
}
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);
});
debug('syncGroupUsers: done');
}
function sync(progressCallback, callback) {
async function sync(progressCallback) {
assert.strictEqual(typeof progressCallback, 'function');
assert.strictEqual(typeof callback, 'function');
progressCallback({ percent: 10, message: 'Starting ldap user sync' });
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
if (error) return callback(error);
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
const externalLdapConfig = await settings.getExternalLdapConfig();
if (externalLdapConfig.provider === 'noop') throw new BoxError(BoxError.BAD_STATE, 'not enabled');
async.series([
syncUsers.bind(null, externalLdapConfig, progressCallback),
syncGroups.bind(null, externalLdapConfig, progressCallback),
syncGroupUsers.bind(null, externalLdapConfig, progressCallback)
], function (error) {
if (error) return callback(error);
await syncUsers(externalLdapConfig, progressCallback);
await syncGroups(externalLdapConfig, progressCallback);
await syncGroupUsers(externalLdapConfig, progressCallback);
progressCallback({ percent: 100, message: 'Done' });
progressCallback({ percent: 100, message: 'Done' });
debug('sync: ldap sync is done', error);
callback(error);
});
});
debug('sync: done');
}

View File

@@ -1,275 +0,0 @@
'use strict';
exports = module.exports = {
get,
getByName,
getWithMembers,
getAll,
getAllWithMembers,
add,
update,
del,
count,
getMembers,
addMember,
removeMember,
setMembers,
isMember,
getMembership,
setMembership,
_clear: clear
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js');
var GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(',');
function get(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE id = ? ORDER BY name', [ groupId ], 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 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');
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' WHERE userGroups.id = ? ' +
' GROUP BY userGroups.id', [ groupId ], 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'));
var result = results[0];
result.userIds = result.userIds ? result.userIds.split(',') : [ ];
callback(null, result);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
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);
});
}
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 ORDER BY name', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
callback(null, results);
});
}
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, 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));
callback(null);
});
}
function update(id, data, callback) {
assert.strictEqual(typeof id, 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ];
var fields = [ ];
for (var k in data) {
if (k === 'name') {
assert.strictEqual(typeof data.name, 'string');
fields.push(k + ' = ?');
args.push(data.name);
}
}
args.push(id);
database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('userGroups_name') !== -1) return callback(new BoxError(BoxError.ALREADY_EXISTS, 'name already exists'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
return callback(null);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
// also cleanup the groupMembers table
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
callback(error);
});
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT COUNT(*) AS total FROM userGroups', function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result[0].total);
});
}
function clear(callback) {
database.query('DELETE FROM groupMembers', function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
database.query('DELETE FROM userGroups', function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
});
});
}
function getMembers(groupId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ], 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')); // need to differentiate group with no members and invalid groupId
callback(error, result.map(function (r) { return r.userId; }));
});
}
function setMembers(groupId, userIds, callback) {
assert.strictEqual(typeof groupId, 'string');
assert(Array.isArray(userIds));
assert.strictEqual(typeof callback, 'function');
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ groupId ] });
for (var i = 0; i < userIds.length; i++) {
queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', args: [ groupId, userIds[i] ] });
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.CONFLICT, 'Duplicate member in list'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(error);
});
}
function getMembership(userId, callback) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ], 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')); // need to differentiate group with no members and invalid groupId
callback(error, result.map(function (r) { return r.groupId; }));
});
}
function setMembership(userId, groupIds, callback) {
assert.strictEqual(typeof userId, 'string');
assert(Array.isArray(groupIds));
assert.strictEqual(typeof callback, 'function');
var queries = [ ];
queries.push({ query: 'DELETE from groupMembers WHERE userId = ?', args: [ userId ] });
groupIds.forEach(function (gid) {
queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (? , ?)', args: [ gid, userId ] });
});
database.transaction(queries, function (error) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, error.message));
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.CONFLICT, 'Already member'));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function addMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ groupId, userId ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function removeMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM groupMembers WHERE groupId = ? AND userId = ?', [ groupId, userId ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
callback(null);
});
}
function isMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT 1 FROM groupMembers WHERE groupId=? AND userId=?', [ groupId, userId ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, result.length !== 0);
});
}

View File

@@ -1,14 +1,14 @@
'use strict';
exports = module.exports = {
create,
add,
remove,
get,
getByName,
update,
getWithMembers,
getAll,
getAllWithMembers,
list,
listWithMembers,
getMembers,
addMember,
@@ -18,16 +18,16 @@ exports = module.exports = {
setMembership,
getMembership,
count
};
var assert = require('assert'),
const assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
groupdb = require('./groupdb.js'),
uuid = require('uuid'),
_ = require('underscore');
database = require('./database.js'),
safe = require('safetydance'),
uuid = require('uuid');
const GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(',');
// keep this in sync with validateUsername
function validateGroupname(name) {
@@ -52,199 +52,187 @@ function validateGroupSource(source) {
return null;
}
function create(name, source, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof source, 'string');
assert.strictEqual(typeof callback, 'function');
async function add(group) {
assert.strictEqual(typeof group, 'object');
// we store names in lowercase
name = name.toLowerCase();
let { name, source } = group;
var error = validateGroupname(name);
if (error) return callback(error);
name = name.toLowerCase(); // we store names in lowercase
source = source || '';
let error = validateGroupname(name);
if (error) throw error;
error = validateGroupSource(source);
if (error) return callback(error);
if (error) throw error;
var id = 'gid-' + uuid.v4();
groupdb.add(id, name, source, function (error) {
if (error) return callback(error);
const id = `gid-${uuid.v4()}`;
callback(null, { id: id, name: name });
});
[error] = await safe(database.query('INSERT INTO userGroups (id, name, source) VALUES (?, ?, ?)', [ id, name, source ]));
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, error);
if (error) throw error;
return { id, name };
}
function remove(id, callback) {
async function remove(id) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.del(id, function (error) {
if (error) return callback(error);
// also cleanup the groupMembers table
let queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM userGroups WHERE id = ?', args: [ id ] });
callback(null);
});
const result = await database.transaction(queries);
if (result[1].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
}
function get(id, callback) {
async function get(id) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.get(id, function (error, result) {
if (error) return callback(error);
const result = await database.query(`SELECT ${GROUPS_FIELDS} FROM userGroups WHERE id = ? ORDER BY name`, [ id ]);
if (result.length === 0) return null;
return callback(null, result);
});
return result[0];
}
function getByName(name, callback) {
async function getByName(name) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getByName(name, function (error, result) {
if (error) return callback(error);
const result = await database.query(`SELECT ${GROUPS_FIELDS} FROM userGroups WHERE name = ?`, [ name ]);
if (result.length === 0) return null;
return callback(null, result);
});
return result[0];
}
function getWithMembers(id, callback) {
async function getWithMembers(id) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getWithMembers(id, function (error, result) {
if (error) return callback(error);
const results = await database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
' WHERE userGroups.id = ? ' +
' GROUP BY userGroups.id', [ id ]);
return callback(null, result);
});
if (results.length === 0) return null;
const result = results[0];
result.userIds = result.userIds ? result.userIds.split(',') : [ ];
return result;
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
groupdb.getAll(function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
async function list() {
const results = await database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups ORDER BY name');
return results;
}
function getAllWithMembers(callback) {
assert.strictEqual(typeof callback, 'function');
async function listWithMembers() {
const results = await 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 ORDER BY name');
groupdb.getAllWithMembers(function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
return results;
}
function getMembers(groupId, callback) {
async function getMembers(groupId) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getMembers(groupId, function (error, result) {
if (error) return callback(error);
const result = await database.query('SELECT userId FROM groupMembers WHERE groupId=?', [ groupId ]);
return callback(null, result);
});
return result.map(function (r) { return r.userId; });
}
function getMembership(userId, callback) {
async function getMembership(userId) {
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.getMembership(userId, function (error, result) {
if (error) return callback(error);
const result = await database.query('SELECT groupId FROM groupMembers WHERE userId=? ORDER BY groupId', [ userId ]);
return callback(null, result);
});
return result.map(function (r) { return r.groupId; });
}
function setMembership(userId, groupIds, callback) {
async function setMembership(userId, groupIds) {
assert.strictEqual(typeof userId, 'string');
assert(Array.isArray(groupIds));
assert.strictEqual(typeof callback, 'function');
groupdb.setMembership(userId, groupIds, function (error) {
if (error) return callback(error);
return callback(null);
let queries = [ ];
queries.push({ query: 'DELETE from groupMembers WHERE userId = ?', args: [ userId ] });
groupIds.forEach(function (gid) {
queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (? , ?)', args: [ gid, userId ] });
});
const [error] = await safe(database.transaction(queries));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, error.message);
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.CONFLICT, 'Already member');
if (error) throw error;
}
function addMember(groupId, userId, callback) {
async function addMember(groupId, userId) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.addMember(groupId, userId, function (error) {
if (error) return callback(error);
return callback(null);
});
const [error] = await safe(database.query('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ groupId, userId ]));
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.ALREADY_EXISTS, error);
if (error && error.code === 'ER_NO_REFERENCED_ROW_2' && error.sqlMessage.includes('userId')) throw new BoxError(BoxError.NOT_FOUND, 'User not found');
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
if (error) throw error;
}
function setMembers(groupId, userIds, callback) {
async function setMembers(groupId, userIds) {
assert.strictEqual(typeof groupId, 'string');
assert(Array.isArray(userIds));
assert.strictEqual(typeof callback, 'function');
groupdb.setMembers(groupId, userIds, function (error) {
if (error) return callback(error);
return callback(null);
});
}
function removeMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.removeMember(groupId, userId, function (error) {
if (error) return callback(error);
return callback(null);
});
}
function isMember(groupId, userId, callback) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
groupdb.isMember(groupId, userId, function (error, result) {
if (error) return callback(error);
return callback(null, result);
});
}
function update(groupId, data, callback) {
assert.strictEqual(typeof groupId, 'string');
assert(data && typeof data === 'object');
assert.strictEqual(typeof callback, 'function');
let error;
if ('name' in data) {
assert.strictEqual(typeof data.name, 'string');
error = validateGroupname(data.name);
if (error) return callback(error);
let queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ groupId ] });
for (let i = 0; i < userIds.length; i++) {
queries.push({ query: 'INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', args: [ groupId, userIds[i] ] });
}
groupdb.update(groupId, _.pick(data, 'name'), function (error) {
if (error) return callback(error);
callback(null);
});
const [error] = await safe(database.transaction(queries));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
if (error && error.code === 'ER_DUP_ENTRY') throw new BoxError(BoxError.CONFLICT, 'Duplicate member in list');
if (error) throw error;
}
function count(callback) {
assert.strictEqual(typeof callback, 'function');
async function removeMember(groupId, userId) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
groupdb.count(function (error, count) {
if (error) return callback(error);
callback(null, count);
});
const result = await database.query('DELETE FROM groupMembers WHERE groupId = ? AND userId = ?', [ groupId, userId ]);
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
}
async function isMember(groupId, userId) {
assert.strictEqual(typeof groupId, 'string');
assert.strictEqual(typeof userId, 'string');
const result = await database.query('SELECT 1 FROM groupMembers WHERE groupId=? AND userId=?', [ groupId, userId ]);
return result.length !== 0;
}
async function update(id, data) {
assert.strictEqual(typeof id, 'string');
assert(data && typeof data === 'object');
if ('name' in data) {
assert.strictEqual(typeof data.name, 'string');
const error = validateGroupname(data.name);
if (error) throw error;
}
const args = [];
const fields = [];
for (const k in data) {
if (k === 'name') {
assert.strictEqual(typeof data.name, 'string');
fields.push(k + ' = ?');
args.push(data.name);
}
}
args.push(id);
const [updateError, result] = await safe(database.query('UPDATE userGroups SET ' + fields.join(', ') + ' WHERE id = ?', args));
if (updateError && updateError.code === 'ER_DUP_ENTRY' && updateError.sqlMessage.indexOf('userGroups_name') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'name already exists');
if (updateError) throw updateError;
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Group not found');
}

View File

@@ -17,11 +17,11 @@ exports = module.exports = {
'images': {
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.3.1@sha256:759cafab7625ff538418a1f2ed5558b1d5bff08c576bba577d865d6d02b49091' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:3.0.7@sha256:6679c2fb96f8d6d62349b607748570640a90fc46b50aad80ca2c0161655d07f4' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:4.0.6@sha256:e583082e15e8e41b0e3b80c3efc917ec429f19fa08a19e14fc27144a8bfe446a' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:4.1.1@sha256:86e4e2f4fd43809efca7c9cb1def4d7608cf36cb9ea27052f9b64da4481db43a' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:4.0.2@sha256:9df297ccc3370f38c54f8d614e214e082b363777cd1c6c9522e29663cc8f5362' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.0.3@sha256:37e5222e01ae89bc5a742ce12030631de25a127b5deec8a0e992c68df0fdec10' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.3.3@sha256:b1093e6f38bebf4a9ae903ca385aea3a32e7cccae5ede7f2e01a34681e361a5f' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:3.0.4@sha256:5c60de75d078ae609da5565f32dcd91030f45907e945756cc976ff207b8c6199' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:3.5.0@sha256:e05d328ea1afa94e31e2eae9b035ff2edde8b90cae902ca49e06053b5bdb5fde' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:3.0.1@sha256:bed9f6b5d06fe2c5289e895e806cfa5b74ad62993d705be55d4554a67d128029' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.3.0@sha256:183c11150d5a681cb02f7d2bd542ddb8a8f097422feafb7fac8fdbca0ca55d47' }
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:3.4.2@sha256:810306478c3dac7caa7497e5f6381cc7ce2f68aafda849a4945d39a67cc04bc1' }
}
};

View File

@@ -7,6 +7,7 @@ exports = module.exports = {
const assert = require('assert');
// this code is used in migrations - 20201120212726-apps-add-containerIp.js
function intFromIp(address) {
assert.strictEqual(typeof address, 'string');
@@ -20,6 +21,7 @@ function intFromIp(address) {
(parseInt(parts[3], 10) << (8*0)) & 0x000000FF;
}
// this code is used in migrations - 20201120212726-apps-add-containerIp.js
function ipFromInt(input) {
assert.strictEqual(typeof input, 'number');

View File

@@ -1,7 +1,6 @@
'use strict';
const assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:janitor'),
Docker = require('dockerode'),
@@ -13,8 +12,6 @@ exports = module.exports = {
cleanupDockerVolumes
};
const NOOP_CALLBACK = function () { };
const gConnection = new Docker({ socketPath: '/var/run/docker.sock' });
async function cleanupTokens() {
@@ -26,44 +23,34 @@ async function cleanupTokens() {
debug(`Cleaned up ${result} expired tokens`,);
}
function cleanupTmpVolume(containerInfo, callback) {
async function cleanupTmpVolume(containerInfo) {
assert.strictEqual(typeof containerInfo, 'object');
assert.strictEqual(typeof callback, 'function');
var cmd = 'find /tmp -type f -mtime +10 -exec rm -rf {} +'.split(' '); // 10 day old files
const cmd = 'find /tmp -type f -mtime +10 -exec rm -rf {} +'.split(' '); // 10 day old files
debug('cleanupTmpVolume %j', containerInfo.Names);
gConnection.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }, function (error, execContainer) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Failed to exec container: ${error.message}`));
const [error, execContainer] = await safe(gConnection.getContainer(containerInfo.Id).exec({ Cmd: cmd, AttachStdout: true, AttachStderr: true, Tty: false }));
if (error) throw new BoxError(BoxError.DOCKER_ERROR, `Failed to exec container: ${error.message}`);
execContainer.start({ hijack: true }, function (error, stream) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Failed to start exec container: ${error.message}`));
const [startError, stream] = await safe(execContainer.start({ hijack: true }));
if (startError) throw new BoxError(BoxError.DOCKER_ERROR, `Failed to start exec container: ${startError.message}`);
stream.on('error', callback);
stream.on('end', callback);
gConnection.modem.demuxStream(stream, process.stdout, process.stderr);
gConnection.modem.demuxStream(stream, process.stdout, process.stderr);
});
return new Promise((resolve, reject) => {
stream.on('error', (error) => reject(new BoxError(BoxError.DOCKER_ERROR, `Failed to cleanup in exec container: ${error.message}`)));
stream.on('end', resolve);
});
}
function cleanupDockerVolumes(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
callback = callback || NOOP_CALLBACK;
async function cleanupDockerVolumes() {
debug('Cleaning up docker volumes');
gConnection.listContainers({ all: 0 }, function (error, containers) {
if (error) return callback(error);
const [error, containers] = await safe(gConnection.listContainers({ all: 0 }));
if (error) throw new BoxError(BoxError.DOCKER_ERROR, error);
async.eachSeries(containers, function (container, iteratorDone) {
cleanupTmpVolume(container, function (error) {
if (error) debug('Error cleaning tmp: %s', error);
iteratorDone(); // intentionally ignore error
});
}, callback);
});
for (const container of containers) {
await safe(cleanupTmpVolume(container), { debug }); // intentionally ignore error
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -55,7 +55,7 @@ Locker.prototype.recursiveLock = function (operation) {
Locker.prototype.unlock = function (operation) {
assert.strictEqual(typeof operation, 'string');
if (this._operation !== operation) throw BoxError(BoxError.BAD_STATE, 'Mismatched unlock. Current lock is for ' + this._operation); // throw because this is a programming error
if (this._operation !== operation) throw new BoxError(BoxError.BAD_STATE, 'Mismatched unlock. Current lock is for ' + this._operation); // throw because this is a programming error
if (--this._lockDepth === 0) {
debug('Released : %s', this._operation);

File diff suppressed because it is too large Load Diff

View File

@@ -2,15 +2,15 @@
<img src="<%= cloudronAvatarUrl %>" width="128px" height="128px"/>
<h3>We've noticed a new login on your Cloudron account.</h3>
<h3>{{ newLoginEmail.topic }}</h3>
<p>Hi <%= user %>,</p>
<p>{{ newLoginEmail.salutation }}</p>
<p>We noticed a login on your Cloudron account from a new device.</p>
<p>{{ newLoginEmail.notice }}</p>
<p>IP: <%= ip %> (<%= city %>, <%= country %>)</p>
<p>Browser: <%= userAgent %></p>
<p>If this was you, you can safely disregard this email. If this wasn't you, you should change your password immediately.</p>
<p>{{ newLoginEmail.action }}</p>
<br/>
<br/>

View File

@@ -1,14 +1,14 @@
We've noticed a new login on your Cloudron account.
{{ newLoginEmail.topic }}
Hi <%= user %>,
{{ newLoginEmail.salutation }}
We noticed a login on your Cloudron account from a new device.
{{ newLoginEmail.notice }}
IP: <%= ip %> (<%= city %>, <%= country %>)
Browser: <%= userAgent %>
If this was you, you can safely disregard this email. If this wasn't you, you should change your password immediately.
{{ newLoginEmail.action }}
Powered by https://cloudron.io

View File

@@ -1,408 +0,0 @@
'use strict';
exports = module.exports = {
addMailbox,
addList,
updateMailbox,
updateList,
del,
getMailboxCount,
listMailboxes,
getLists,
listAllMailboxes,
get,
getMailbox,
getList,
getAlias,
getAliasesForName,
setAliasesForName,
getByOwnerId,
delByOwnerId,
delByDomain,
updateName,
_clear: clear,
TYPE_MAILBOX: 'mailbox',
TYPE_LIST: 'list',
TYPE_ALIAS: 'alias'
};
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', 'ownerType', 'aliasName', 'aliasDomain', 'creationTime', 'membersJson', 'membersOnly', 'domain', 'active' ].join(',');
function postProcess(data) {
data.members = safe.JSON.parse(data.membersJson) || [ ];
delete data.membersJson;
data.membersOnly = !!data.membersOnly;
data.active = !!data.active;
return data;
}
function postProcessAliases(data) {
const aliasNames = JSON.parse(data.aliasNames), aliasDomains = JSON.parse(data.aliasDomains);
delete data.aliasNames;
delete data.aliasDomains;
data.aliases = [];
for (let i = 0; i < aliasNames.length; i++) { // NOTE: aliasNames is [ null ] when no aliases
if (aliasNames[i]) data.aliases[i] = { name: aliasNames[i], domain: aliasDomains[i] };
}
}
function addMailbox(name, domain, data, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const { ownerId, ownerType, active } = data;
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof active, 'boolean');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, active) VALUES (?, ?, ?, ?, ?, ?)', [ name, exports.TYPE_MAILBOX, domain, ownerId, ownerType, active ], 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));
callback(null);
});
}
function updateMailbox(name, domain, data, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const { ownerId, ownerType, active } = data;
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof ownerType, 'string');
assert.strictEqual(typeof active, 'boolean');
database.query('UPDATE mailboxes SET ownerId = ?, ownerType = ?, active = ? WHERE name = ? AND domain = ?', [ ownerId, ownerType, active, 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'));
callback(null);
});
}
function addList(name, domain, data, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const { members, membersOnly, active } = data;
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof active, 'boolean');
database.query('INSERT INTO mailboxes (name, type, domain, ownerId, ownerType, membersJson, membersOnly, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[ name, exports.TYPE_LIST, domain, 'admin', 'user', JSON.stringify(members), membersOnly, active ], 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));
callback(null);
});
}
function updateList(name, domain, data, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
const { members, membersOnly, active } = data;
assert(Array.isArray(members));
assert.strictEqual(typeof membersOnly, 'boolean');
assert.strictEqual(typeof active, 'boolean');
database.query('UPDATE mailboxes SET membersJson = ?, membersOnly = ?, active = ? WHERE name = ? AND domain = ?',
[ JSON.stringify(members), membersOnly, active, 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'));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('TRUNCATE TABLE mailboxes', [], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function del(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
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'));
callback(null);
});
}
function delByDomain(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM mailboxes WHERE domain = ?', [ domain ], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function delByOwnerId(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM mailboxes WHERE ownerId=?', [ id ], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function updateName(oldName, oldDomain, newName, newDomain, callback) {
assert.strictEqual(typeof oldName, 'string');
assert.strictEqual(typeof oldDomain, 'string');
assert.strictEqual(typeof newName, 'string');
assert.strictEqual(typeof newDomain, 'string');
assert.strictEqual(typeof callback, 'function');
// skip if no changes
if (oldName === newName && oldDomain === newDomain) return callback(null);
database.query('UPDATE mailboxes SET name=?, domain=? WHERE name=? AND domain = ?', [ newName, newDomain, oldName, oldDomain ], function (error, result) {
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));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null);
});
}
function get(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?',
[ name, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null, postProcess(results[0]));
});
}
function getMailbox(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?',
[ name, exports.TYPE_MAILBOX, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null, postProcess(results[0]));
});
}
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');
const escapedSearch = mysql.escape('%' + search + '%'); // this also quotes the string
const searchQuery = search ? ` HAVING (name LIKE ${escapedSearch} OR aliasNames LIKE ${escapedSearch} OR aliasDomains LIKE ${escapedSearch})` : ''; // having instead of where because of aggregated columns use
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' WHERE m1.domain = ?'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ searchQuery
+ ' ORDER BY name LIMIT ?,?';
database.query(query, [ domain, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
results.forEach(postProcessAliases);
callback(null, results);
});
}
function listAllMailboxes(page, perPage, callback) {
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
const query = 'SELECT m1.name AS name, m1.domain AS domain, m1.ownerId AS ownerId, m1.ownerType as ownerType, m1.active as active, JSON_ARRAYAGG(m2.name) AS aliasNames, JSON_ARRAYAGG(m2.domain) AS aliasDomains '
+ ` FROM (SELECT * FROM mailboxes WHERE type='${exports.TYPE_MAILBOX}') AS m1`
+ ` LEFT JOIN (SELECT * FROM mailboxes WHERE type='${exports.TYPE_ALIAS}') AS m2`
+ ' ON m1.name=m2.aliasName AND m1.domain=m2.aliasDomain AND m1.ownerId=m2.ownerId'
+ ' GROUP BY m1.name, m1.domain, m1.ownerId'
+ ' ORDER BY name LIMIT ?,?';
database.query(query, [ (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(postProcess);
results.forEach(postProcessAliases);
callback(null, results);
});
}
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');
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 + '%') + ')';
query += 'ORDER BY name LIMIT ?,?';
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) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE type = ? AND name = ? AND domain = ?',
[ exports.TYPE_LIST, name, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
callback(null, postProcess(results[0]));
});
}
function getByOwnerId(ownerId, callback) {
assert.strictEqual(typeof ownerId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE ownerId = ? ORDER BY name', [ ownerId ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function setAliasesForName(name, domain, aliases, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert(Array.isArray(aliases));
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND domain = ?', [ name, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
var queries = [];
// clear existing aliases
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, domain, type, aliasName, aliasDomain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?, ?, ?)',
args: [ alias.name, alias.domain, exports.TYPE_ALIAS, name, domain, results[0].ownerId, results[0].ownerType ] });
});
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY' && error.message.indexOf('mailboxes_name_domain_unique_index') !== -1) {
var aliasMatch = error.message.match(new RegExp(`^ER_DUP_ENTRY: Duplicate entry '(.*)-${domain}' for key 'mailboxes_name_domain_unique_index'$`));
if (!aliasMatch) return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
return callback(new BoxError(BoxError.ALREADY_EXISTS, `Mailbox, mailinglist or alias for ${aliasMatch[1]} already exists`));
}
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
});
}
function getAliasesForName(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
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));
callback(null, results);
});
}
function getAlias(name, domain, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND type = ? AND domain = ?',
[ name, exports.TYPE_ALIAS, domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mailbox not found'));
results.forEach(function (result) { postProcess(result); });
callback(null, results[0]);
});
}

View File

@@ -1,99 +0,0 @@
'use strict';
exports = module.exports = {
get,
list,
update,
clear,
TYPE_USER: 'user',
TYPE_APP: 'app',
TYPE_GROUP: 'group'
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js'),
safe = require('safetydance');
var MAILDB_FIELDS = [ 'domain', 'enabled', 'mailFromValidation', 'catchAllJson', 'relayJson', 'dkimSelector', 'bannerJson' ].join(',');
function postProcess(data) {
data.enabled = !!data.enabled; // int to boolean
data.mailFromValidation = !!data.mailFromValidation; // int to boolean
data.catchAll = safe.JSON.parse(data.catchAllJson) || [ ];
delete data.catchAllJson;
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 clear(callback) {
assert.strictEqual(typeof callback, 'function');
// using TRUNCATE makes it fail foreign key check
database.query('DELETE FROM mail', [], function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail WHERE domain = ?', [ domain ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null, postProcess(results[0]));
});
}
function list(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILDB_FIELDS + ' FROM mail ORDER BY domain', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function update(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
var args = [ ];
var fields = [ ];
for (var k in data) {
if (k === 'catchAll' || k === 'banner') {
fields.push(`${k}Json = ?`);
args.push(JSON.stringify(data[k]));
} else if (k === 'relay') {
fields.push('relayJson = ?');
args.push(JSON.stringify(data[k]));
} else {
fields.push(k + ' = ?');
args.push(data[k]);
}
}
args.push(domain);
database.query('UPDATE mail SET ' + fields.join(', ') + ' WHERE domain=?', args, function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null);
});
}

View File

@@ -25,68 +25,54 @@ const assert = require('assert'),
safe = require('safetydance'),
settings = require('./settings.js'),
translation = require('./translation.js'),
smtpTransport = require('nodemailer-smtp-transport');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
smtpTransport = require('nodemailer-smtp-transport'),
util = require('util');
const MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
// This will collect the most common details required for notification emails
function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
async function getMailConfig() {
const cloudronName = await settings.getCloudronName();
const supportConfig = await settings.getSupportConfig();
settings.getCloudronName(function (error, cloudronName) {
if (error) debug('Error getting cloudron name: ', error);
settings.getSupportConfig(function (error, supportConfig) {
if (error) debug('Error getting support config: ', error);
callback(null, {
cloudronName: cloudronName || '',
notificationFrom: `"${cloudronName}" <no-reply@${settings.dashboardDomain()}>`,
supportEmail: supportConfig.email
});
});
});
return {
cloudronName,
notificationFrom: `"${cloudronName}" <no-reply@${settings.dashboardDomain()}>`,
supportEmail: supportConfig.email
};
}
function sendMail(mailOptions, callback) {
async function sendMail(mailOptions) {
assert.strictEqual(typeof mailOptions, 'object');
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') {
exports._mailQueue.push(mailOptions);
return callback();
return;
}
mail.getMailAuth(function (error, data) {
if (error) return callback(error);
const data = await mail.getMailAuth();
var transport = nodemailer.createTransport(smtpTransport({
host: data.ip,
port: data.port,
auth: {
user: mailOptions.authUser || `no-reply@${settings.dashboardDomain()}`,
pass: data.relayToken
}
}));
const transport = nodemailer.createTransport(smtpTransport({
host: data.ip,
port: data.port,
auth: {
user: mailOptions.authUser || `no-reply@${settings.dashboardDomain()}`,
pass: data.relayToken
}
}));
transport.sendMail(mailOptions, function (error) {
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
debug(`Email "${mailOptions.subject}" sent to ${mailOptions.to}`);
callback(null);
});
});
const transportSendMail = util.promisify(transport.sendMail.bind(transport));
const [error] = await safe(transportSendMail(mailOptions));
if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, error);
debug(`Email "${mailOptions.subject}" sent to ${mailOptions.to}`);
}
function render(templateFile, params, translationAssets) {
assert.strictEqual(typeof templateFile, 'string');
assert.strictEqual(typeof params, 'object');
var content = null;
var raw = safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8');
let content = null;
let raw = safe.fs.readFileSync(path.join(MAIL_TEMPLATES_DIR, templateFile), 'utf8');
if (raw === null) {
debug(`Error loading ${templateFile}`);
return '';
@@ -103,42 +89,36 @@ function render(templateFile, params, translationAssets) {
return content;
}
function sendInvite(user, invitor, inviteLink) {
async function sendInvite(user, invitor, email, inviteLink) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof invitor, 'object');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof inviteLink, 'string');
debug('Sending invite mail');
const mailConfig = await getMailConfig();
const translationAssets = await translation.getTranslations();
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
const templateData = {
user: user.displayName || user.username || user.email,
webadminUrl: settings.dashboardOrigin(),
inviteLink: inviteLink,
invitor: invitor ? invitor.email : null,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
};
translation.getTranslations(function (error, translationAssets) {
if (error) return debug('Error getting translations:', error);
const mailOptions = {
from: mailConfig.notificationFrom,
to: email,
subject: ejs.render(translation.translate('{{ welcomeEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('welcome_user-text.ejs', templateData, translationAssets),
html: render('welcome_user-html.ejs', templateData, translationAssets)
};
var templateData = {
user: user.displayName || user.username || user.email,
webadminUrl: settings.dashboardOrigin(),
inviteLink: inviteLink,
invitor: invitor ? invitor.email : null,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
};
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: ejs.render(translation.translate('{{ welcomeEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('welcome_user-text.ejs', templateData, translationAssets),
html: render('welcome_user-html.ejs', templateData, translationAssets)
};
sendMail(mailOptions);
});
});
await sendMail(mailOptions);
}
function sendNewLoginLocation(user, loginLocation) {
async function sendNewLoginLocation(user, loginLocation) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof loginLocation, 'object');
@@ -149,124 +129,103 @@ function sendNewLoginLocation(user, loginLocation) {
assert.strictEqual(typeof country, 'string');
assert.strictEqual(typeof city, 'string');
debug('Sending new login location mail');
const mailConfig = await getMailConfig();
const translationAssets = await translation.getTranslations();
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
const templateData = {
user: user.displayName || user.username || user.email,
ip,
userAgent: userAgent || 'unknown',
country,
city,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
};
translation.getTranslations(function (error, translationAssets) {
if (error) return debug('Error getting translations:', error);
const mailOptions = {
from: mailConfig.notificationFrom,
to: user.email,
subject: ejs.render(translation.translate('{{ newLoginEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('new_login_location-text.ejs', templateData, translationAssets),
html: render('new_login_location-html.ejs', templateData, translationAssets)
};
const templateData = {
user: user.displayName || user.username || user.email,
ip,
userAgent: userAgent || 'unknown',
country,
city,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
};
const mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: `[${mailConfig.cloudronName}] New login on your account`,
text: render('new_login_location-text.ejs', templateData, translationAssets),
html: render('new_login_location-html.ejs', templateData, translationAssets)
};
sendMail(mailOptions);
});
});
await sendMail(mailOptions);
}
function passwordReset(user) {
async function passwordReset(user, email, resetLink) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof resetLink, 'string');
debug('Sending mail for password reset for user %s.', user.email, user.id);
const mailConfig = await getMailConfig();
const translationAssets = await translation.getTranslations();
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
const templateData = {
user: user.displayName || user.username || user.email,
resetLink: resetLink,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
};
translation.getTranslations(function (error, translationAssets) {
if (error) return debug('Error getting translations:', error);
const mailOptions = {
from: mailConfig.notificationFrom,
to: email,
subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('password_reset-text.ejs', templateData, translationAssets),
html: render('password_reset-html.ejs', templateData, translationAssets)
};
var templateData = {
user: user.displayName || user.username || user.email,
resetLink: `${settings.dashboardOrigin()}/login.html?resetToken=${user.resetToken}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.dashboardOrigin() + '/api/v1/cloudron/avatar'
};
var mailOptions = {
from: mailConfig.notificationFrom,
to: user.fallbackEmail,
subject: ejs.render(translation.translate('{{ passwordResetEmail.subject }}', translationAssets.translations || {}, translationAssets.fallback || {}), { cloudron: mailConfig.cloudronName }),
text: render('password_reset-text.ejs', templateData, translationAssets),
html: render('password_reset-html.ejs', templateData, translationAssets)
};
sendMail(mailOptions);
});
});
await sendMail(mailOptions);
}
function backupFailed(mailTo, errorMessage, logUrl, callback) {
async function backupFailed(mailTo, errorMessage, logUrl) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof logUrl, 'string');
assert.strictEqual(typeof callback, 'function');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
const mailConfig = await getMailConfig();
var mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Failed to backup`,
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: errorMessage, logUrl, format: 'text' })
};
const mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Failed to backup`,
text: render('backup_failed.ejs', { cloudronName: mailConfig.cloudronName, message: errorMessage, logUrl, format: 'text' })
};
sendMail(mailOptions, callback);
});
await sendMail(mailOptions);
}
function certificateRenewalError(mailTo, domain, message, callback) {
async function certificateRenewalError(mailTo, domain, message) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof message, 'string');
assert.strictEqual(typeof callback, 'function');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
const mailConfig = await getMailConfig();
const mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Certificate renewal error`,
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
const mailOptions = {
from: mailConfig.notificationFrom,
to: mailTo,
subject: `[${mailConfig.cloudronName}] Certificate renewal error`,
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
sendMail(mailOptions, callback);
});
await sendMail(mailOptions);
}
function sendTestMail(domain, email, callback) {
async function sendTestMail(domain, email) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof callback, 'function');
getMailConfig(function (error, mailConfig) {
if (error) return debug('Error getting mail details:', error);
const mailConfig = await getMailConfig();
var mailOptions = {
authUser: `no-reply@${domain}`,
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
to: email,
subject: `[${mailConfig.cloudronName}] Test Email`,
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
};
const mailOptions = {
authUser: `no-reply@${domain}`,
from: `"${mailConfig.cloudronName}" <no-reply@${domain}>`,
to: email,
subject: `[${mailConfig.cloudronName}] Test Email`,
text: render('test.ejs', { cloudronName: mailConfig.cloudronName, format: 'text'})
};
sendMail(mailOptions, callback);
});
await sendMail(mailOptions);
}

View File

@@ -1,15 +1,19 @@
'use strict';
exports = module.exports = {
isMountProvider,
mountObjectFromBackupConfig,
tryAddMount,
removeMount,
validateMountOptions,
getStatus,
remount
};
const assert = require('assert'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
debug = require('debug')('box:mounts'),
ejs = require('ejs'),
fs = require('fs'),
path = require('path'),
@@ -19,6 +23,7 @@ const assert = require('assert'),
const ADD_MOUNT_CMD = path.join(__dirname, 'scripts/addmount.sh');
const RM_MOUNT_CMD = path.join(__dirname, 'scripts/rmmount.sh');
const REMOUNT_MOUNT_CMD = path.join(__dirname, 'scripts/remountmount.sh');
const SYSTEMD_MOUNT_EJS = fs.readFileSync(path.join(__dirname, 'systemd-mount.ejs'), { encoding: 'utf8' });
// https://man7.org/linux/man-pages/man8/mount.8.html
@@ -51,18 +56,31 @@ function validateMountOptions(type, options) {
if (typeof options.diskPath !== 'string') return new BoxError(BoxError.BAD_FIELD, 'diskPath is not a string');
return null;
default:
return new BoxError(BoxError.BAD_FIELD, 'Bad volume mount type');
return new BoxError(BoxError.BAD_FIELD, 'Bad mount type');
}
}
function isMountProvider(provider) {
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'ext4';
}
function mountObjectFromBackupConfig(backupConfig) {
return {
name: 'backup',
hostPath: backupConfig.mountPoint,
mountType: backupConfig.provider,
mountOptions: backupConfig.mountOptions
};
}
// https://www.man7.org/linux/man-pages/man8/mount.8.html for various mount option flags
// nfs - no_root_squash is mode on server to map all root to 'nobody' user. all_squash does this for all users (making it like ftp)
// sshfs - supports users/permissions
// cifs - does not support permissions
function renderMountFile(volume) {
assert.strictEqual(typeof volume, 'object');
function renderMountFile(mount) {
assert.strictEqual(typeof mount, 'object');
const {name, hostPath, mountType, mountOptions} = volume;
const {name, hostPath, mountType, mountOptions} = mount;
let options, what, type;
switch (mountType) {
@@ -96,14 +114,14 @@ function renderMountFile(volume) {
return ejs.render(SYSTEMD_MOUNT_EJS, { name, what, where: hostPath, options, type });
}
async function removeMount(volume) {
assert.strictEqual(typeof volume, 'object');
async function removeMount(mount) {
assert.strictEqual(typeof mount, 'object');
const { hostPath, mountType, mountOptions } = volume;
const { hostPath, mountType, mountOptions } = mount;
if (constants.TEST) return;
await safe(shell.promises.sudo('removeMount', [ RM_MOUNT_CMD, hostPath ], {})); // ignore any error
await safe(shell.promises.sudo('removeMount', [ RM_MOUNT_CMD, hostPath ], {}), { debug }); // ignore any error
if (mountType === 'sshfs') {
const keyFilePath = path.join(paths.SSHFS_KEYS_DIR, `id_rsa_${mountOptions.host}`);
@@ -117,25 +135,32 @@ async function getStatus(mountType, hostPath) {
if (mountType === 'filesystem') return { state: 'active', message: 'Mounted' };
if (mountType === 'mountpoint') {
if (safe.child_process.execSync(`mountpoint -q -- ${hostPath}`)) {
return { state: 'active', message: 'Mounted' };
} else {
return { state: 'inactive', message: 'Not mounted' };
}
}
const state = safe.child_process.execSync(`mountpoint -q -- ${hostPath}`) ? 'active' : 'inactive';
let output = safe.child_process.execSync(`systemctl show -p ActiveState $(systemd-escape -p --suffix=mount ${hostPath})`, { encoding: 'utf8' }); // --value does not work in ubuntu 16
const state = output ? output.trim().split('=')[1] : '';
if (mountType === 'mountpoint') return { state, message: state === 'active' ? 'Mounted' : 'Not mounted' };
// we used to rely on "systemctl show -p ActiveState" output before but some mounts like sshfs.fuse show the status as "active" event though the mount commant failed (on ubuntu 18)
let message;
if (state !== 'active') { // find why it failed
output = safe.child_process.execSync(`journalctl -u $(systemd-escape -p --suffix=mount ${hostPath}) -n 10 --no-pager -o cat`, { encoding: 'utf8' });
const logsJson = safe.child_process.execSync(`journalctl -u $(systemd-escape -p --suffix=mount ${hostPath}) -n 10 --no-pager -o json`, { encoding: 'utf8' });
if (output) {
const rlines = output.split('\n').reverse();
const idx = rlines.findIndex(l => /^mount./.test(l) || l.includes('failed') || l.includes('error') || l.includes('reset'));
if (idx !== -1) message = rlines[idx];
if (logsJson) {
const lines = logsJson.trim().split('\n').map(l => JSON.parse(l)); // array of json
let start = -1, end = -1; // start and end of error message block
for (let idx = lines.length - 1; idx >= 0; idx--) { // reverse
const line = lines[idx];
const match = line['SYSLOG_IDENTIFIER'] === 'mount' || (line['_EXE'] && line['_EXE'].includes('mount')) || (line['_COMM'] && line['_COMM'].includes('mount'));
if (match) {
if (end === -1) end = idx;
start = idx;
continue;
}
if (end !== -1) break; // no match and we already found a block
}
if (end !== -1) message = lines.slice(start, end+1).map(line => line['MESSAGE']).join('\n');
}
if (!message) message = `Could not determine failure reason. ${safe.error ? safe.error.message : ''}`;
} else {
@@ -145,27 +170,40 @@ async function getStatus(mountType, hostPath) {
return { state, message };
}
async function tryAddMount(volume, options) {
assert.strictEqual(typeof volume, 'object');
assert.strictEqual(typeof options, 'object');
async function tryAddMount(mount, options) {
assert.strictEqual(typeof mount, 'object'); // { name, hostPath, mountType, mountOptions }
assert.strictEqual(typeof options, 'object'); // { timeout, skipCleanup }
if (volume.mountType === 'mountpoint') return;
if (mount.mountType === 'mountpoint') return;
if (constants.TEST) return;
if (volume.mountType === 'sshfs') {
const keyFilePath = path.join(paths.SSHFS_KEYS_DIR, `id_rsa_${volume.mountOptions.host}`);
if (mount.mountType === 'sshfs') {
const keyFilePath = path.join(paths.SSHFS_KEYS_DIR, `id_rsa_${mount.mountOptions.host}`);
safe.fs.mkdirSync(paths.SSHFS_KEYS_DIR);
if (!safe.fs.writeFileSync(keyFilePath, `${volume.mountOptions.privateKey}\n`, { mode: 0o600 })) throw new BoxError(BoxError.FS_ERROR, safe.error);
if (!safe.fs.writeFileSync(keyFilePath, `${mount.mountOptions.privateKey}\n`, { mode: 0o600 })) throw new BoxError(BoxError.FS_ERROR, safe.error);
}
const [error] = await safe(shell.promises.sudo('addMount', [ ADD_MOUNT_CMD, renderMountFile(volume), options.timeout ], {}));
const [error] = await safe(shell.promises.sudo('addMount', [ ADD_MOUNT_CMD, renderMountFile(mount), options.timeout ], {}));
if (error && error.code === 2) throw new BoxError(BoxError.MOUNT_ERROR, 'Failed to unmount existing mount'); // at this point, the old mount config is still there
const status = await getStatus(volume.mountType, volume.hostPath);
if (options.skipCleanup) return;
const status = await getStatus(mount.mountType, mount.hostPath);
if (status.state !== 'active') { // cleanup
await removeMount(volume);
await removeMount(mount);
throw new BoxError(BoxError.MOUNT_ERROR, `Failed to mount (${status.state}): ${status.message}`);
}
}
async function remount(mount) {
assert.strictEqual(typeof mount, 'object'); // { name, hostPath, mountType, mountOptions }
if (mount.mountType === 'mountpoint') return;
if (constants.TEST) return;
const [error] = await safe(shell.promises.sudo('remountMount', [ REMOUNT_MOUNT_CMD, mount.hostPath ], {}));
if (error && error.code === 2) throw new BoxError(BoxError.MOUNT_ERROR, 'Failed to remount existing mount'); // at this point, the old mount config is still there
}

View File

@@ -1,43 +0,0 @@
'use strict';
exports = module.exports = {
resolve
};
var assert = require('assert'),
constants = require('./constants.js'),
dns = require('dns'),
_ = require('underscore');
const DEFAULT_OPTIONS = { server: '127.0.0.1', timeout: 5000 }; // unbound runs on 127.0.0.1
// a note on TXT records. It doesn't have quotes ("") at the DNS level. Those quotes
// are added for DNS server software to enclose spaces. Such quotes may also be returned
// by the DNS REST API of some providers
function resolve(hostname, rrtype, options, callback) {
assert.strictEqual(typeof hostname, 'string');
assert.strictEqual(typeof rrtype, 'string');
assert(options && typeof options === 'object');
assert.strictEqual(typeof callback, 'function');
const resolver = new dns.Resolver();
options = _.extend({ }, DEFAULT_OPTIONS, options);
// Only use unbound on a Cloudron
if (constants.CLOUDRON) resolver.setServers([ options.server ]);
// should callback with ECANCELLED but looks like we might hit https://github.com/nodejs/node/issues/14814
const timerId = setTimeout(resolver.cancel.bind(resolver), options.timeout || 5000);
resolver.resolve(hostname, rrtype, function (error, result) {
clearTimeout(timerId);
if (error && error.code === 'ECANCELLED') error.code = 'TIMEOUT';
// result is an empty array if there was no error but there is no record. when you query a random
// domain, it errors with ENOTFOUND. But if you query an existing domain (A record) but with different
// type (CNAME) it is not an error and empty array
// for TXT records, result is 2d array of strings
callback(error, result);
});
}

View File

@@ -17,48 +17,36 @@ const assert = require('assert'),
const SET_BLOCKLIST_CMD = path.join(__dirname, 'scripts/setblocklist.sh');
function getBlocklist(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getFirewallBlocklist(function (error, blocklist) {
if (error) return callback(error);
callback(null, blocklist);
});
async function getBlocklist() {
return await settings.getFirewallBlocklist();
}
function setBlocklist(blocklist, auditSource, callback) {
async function setBlocklist(blocklist, auditSource) {
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 (!validator.isIP(rangeOrIP) && !validator.isIPRange(rangeOrIP)) throw 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`));
if (auditSource.ip === rangeOrIP) throw new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`);
} else {
const parsedRange = ipaddr.parseCIDR(rangeOrIP); // returns [addr, range]
if (parsedRange[0].kind() === parsedIp.kind() && parsedIp.match(parsedRange)) return callback(new BoxError(BoxError.BAD_FIELD, `${rangeOrIP} includes client IP. Cannot block yourself`));
if (parsedRange[0].kind() === parsedIp.kind() && parsedIp.match(parsedRange)) throw 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 (settings.isDemo()) throw new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode');
settings.setFirewallBlocklist(blocklist, function (error) {
if (error) return callback(error);
await settings.setFirewallBlocklist(blocklist);
// this is done only because it's easier for the shell script and the firewall service to get the value
if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) return callback(new BoxError(BoxError.FS_ERROR, safe.error.message));
// this is done only because it's easier for the shell script and the firewall service to get the value
if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) throw 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(null);
});
});
const [error] = await safe(shell.promises.sudo('setBlocklist', [ SET_BLOCKLIST_CMD ], {}));
if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`);
}

View File

@@ -87,7 +87,10 @@ server {
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;
<% if (endpoint !== 'ip' && endpoint !== 'setup') { -%>
# dhparams is generated only after dns setup
ssl_dhparam /home/yellowtent/platformdata/dhparams.pem;
<% } -%>
add_header Strict-Transport-Security "max-age=63072000";
<% if ( ocsp ) { -%>
@@ -214,7 +217,7 @@ server {
client_max_body_size 1m;
}
location ~ ^/api/v1/(developer|session)/login$ {
location ~ ^/api/v1/cloudron/login$ {
proxy_pass http://127.0.0.1:3000;
client_max_body_size 1m;
limit_req zone=admin_login burst=5;
@@ -275,6 +278,9 @@ server {
if ($http_user_agent ~* "docker") {
return 401;
}
if ($http_user_agent ~* "container") {
return 401;
}
return 302 /login?redirect=$request_uri;
}
@@ -298,13 +304,6 @@ server {
}
<% } %>
<% Object.keys(httpPaths).forEach(function (path) { -%>
location "<%= path %>" {
# the trailing / will replace part of the original URI matched by the location.
proxy_pass http://<%= ip %>:<%= httpPaths[path] %>/;
}
<% }); %>
<% } else if ( endpoint === 'redirect' ) { %>
location / {
# redirect everything to the app. this is temporary because there is no way

View File

@@ -22,16 +22,14 @@ exports = module.exports = {
};
const assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
AuditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
changelog = require('./changelog.js'),
database = require('./database.js'),
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
settings = require('./settings.js'),
users = require('./users.js'),
util = require('util');
users = require('./users.js');
const NOTIFICATION_FIELDS = [ 'id', 'eventId', 'title', 'message', 'creationTime', 'acknowledged' ];
@@ -122,38 +120,44 @@ async function list(filters, page, perPage) {
return results;
}
async function oomEvent(eventId, app, addon, containerId /*, event*/) {
async function oomEvent(eventId, containerId, app, addonName /*, event*/) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof addon, 'object');
assert.strictEqual(typeof containerId, 'string');
assert.strictEqual(typeof app, 'object');
assert(addonName === null || typeof addonName === 'string');
assert(app || addon);
assert(app || addonName);
let title, message;
if (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](${settings.dashboardOrigin()}/#/app/${app.id}/resources)`;
} else if (addon) {
title = `The ${addon.name} service ran out of memory`;
if (addonName) {
if (app) {
title = `The ${addonName} service of the app at ${app.fqdn} ran out of memory`;
} else {
title = `The ${addonName} service ran out of memory`;
}
message = `The service has been restarted automatically. If you see this notification often, consider increasing the [memory limit](${settings.dashboardOrigin()}/#/services)`;
} else if (app) {
title = `The app at ${app.fqdn} ran out of memory.`;
message = `The app has been restarted automatically. If you see this notification often, consider increasing the [memory limit](${settings.dashboardOrigin()}/#/app/${app.id}/resources)`;
}
await add(eventId, title, message);
}
async function appUpdated(eventId, app) {
async function appUpdated(eventId, app, fromManifest, toManifest) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof fromManifest, 'object');
assert.strictEqual(typeof toManifest, 'object');
if (!app.appStoreId) return; // skip notification of dev apps
const tmp = app.manifest.description.match(/<upstream>(.*)<\/upstream>/i);
const tmp = toManifest.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}`;
const title = upstreamVersion ? `${toManifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${toManifest.version})`
: `${toManifest.title} at ${app.fqdn} updated to package version ${toManifest.version}`;
await add(eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${app.manifest.changelog}\n`);
await add(eventId, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${toManifest.changelog}\n`);
}
async function boxUpdated(eventId, oldVersion, newVersion) {
@@ -179,21 +183,12 @@ async function certificateRenewalError(eventId, vhost, errorMessage) {
assert.strictEqual(typeof vhost, 'string');
assert.strictEqual(typeof errorMessage, 'string');
return new Promise((resolve, reject) => {
users.getAdmins(function (error, admins) {
if (error) return reject(error);
await add(eventId, `Certificate renewal of ${vhost} failed`, `Failed to renew certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours.`);
async.eachSeries(admins, function (admin, iteratorDone) {
mailer.certificateRenewalError(admin.email, vhost, errorMessage, iteratorDone);
}, async function (error) {
if (error) return reject(error);
await add(eventId, `Certificate renewal of ${vhost} failed`, `Failed to renew certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours.`);
resolve();
});
});
});
const admins = await users.getAdmins();
for (const admin of admins) {
await mailer.certificateRenewalError(admin.email, vhost, errorMessage);
}
}
async function backupFailed(eventId, taskId, errorMessage) {
@@ -204,27 +199,18 @@ async function backupFailed(eventId, taskId, errorMessage) {
await add(eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`);
// only send mail if the past 3 automated backups failed
const backupEvents = await eventlog.getAllPaged([eventlog.ACTION_BACKUP_FINISH], null /* search */, 1, 20);
const backupEvents = await eventlog.listPaged([eventlog.ACTION_BACKUP_FINISH], null /* search */, 1, 20);
let count = 0;
for (const event of backupEvents) {
if (!event.data.errorMessage) return; // successful backup (manual or cron)
if (event.source.username === auditSource.CRON.username && ++count === 3) break; // last 3 consecutive crons have failed
if (event.source.username === AuditSource.CRON.username && ++count === 3) break; // last 3 consecutive crons have failed
}
if (count !== 3) return; // less than 3 failures
return new Promise((resolve, reject) => {
users.getSuperadmins(function (error, superadmins) {
if (error) return reject(error);
async.eachSeries(superadmins, function (superadmin, iteratorDone) {
mailer.backupFailed(superadmin.email, errorMessage, `${settings.dashboardOrigin()}/logs.html?taskId=${taskId}`, iteratorDone);
}, function (error) {
if (error) return reject(error);
resolve();
});
});
});
const superadmins = await users.getSuperadmins();
for (const superadmin of superadmins) {
await mailer.backupFailed(superadmin.email, errorMessage, `${settings.dashboardOrigin()}/logs.html?taskId=${taskId}`);
}
}
// id is unused but nice to search code
@@ -260,15 +246,14 @@ async function onEvent(id, action, source, data) {
assert.strictEqual(typeof source, 'object');
assert.strictEqual(typeof data, 'object');
// external ldap syncer does not generate notifications - FIXME username might be an issue here
if (source.username === auditSource.EXTERNAL_LDAP_TASK.username) return;
switch (action) {
case eventlog.ACTION_APP_OOM:
return await oomEvent(id, data.app, data.addon, data.containerId, data.event);
return await oomEvent(id, data.containerId, data.app, data.addonName, data.event);
case eventlog.ACTION_APP_UPDATE_FINISH:
return await appUpdated(id, data.app);
if (source.username !== AuditSource.CRON.username) return; // updated by user
if (data.errorMessage) return; // the update indicator will still appear, so no need to notify user
return await appUpdated(id, data.app, data.fromManifest, data.toManifest);
case eventlog.ACTION_CERTIFICATE_RENEWAL:
case eventlog.ACTION_CERTIFICATE_NEW:
@@ -278,7 +263,7 @@ async function onEvent(id, action, source, data) {
case eventlog.ACTION_BACKUP_FINISH:
if (!data.errorMessage) return;
if (source.username !== auditSource.CRON.username && !data.timedOut) return; // manual stop by user
if (source.username !== AuditSource.CRON.username && !data.timedOut) return; // manual stop by user
return await backupFailed(id, data.taskId, data.errorMessage); // only notify for automated backups or timedout

View File

@@ -4,7 +4,7 @@ var constants = require('./constants.js'),
path = require('path');
function baseDir() {
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
const homeDir = process.env.HOME;
if (constants.CLOUDRON) return homeDir;
if (constants.TEST) return path.join(homeDir, '.cloudron_test');
// cannot reach
@@ -25,10 +25,10 @@ exports = module.exports = {
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
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'),
MAIL_CONFIG_DIR: path.join(baseDir(), 'platformdata/addons/mail'),
COLLECTD_APPCONFIG_DIR: path.join(baseDir(), 'platformdata/collectd/collectd.conf.d'),
LOGROTATE_CONFIG_DIR: path.join(baseDir(), 'platformdata/logrotate.d'),
NGINX_CONFIG_DIR: path.join(baseDir(), 'platformdata/nginx'),
@@ -49,7 +49,7 @@ exports = module.exports = {
SFTP_PRIVATE_KEY_FILE: path.join(baseDir(), 'platformdata/sftp/ssh/ssh_host_rsa_key'),
FIREWALL_BLOCKLIST_FILE: path.join(baseDir(), 'platformdata/firewall/blocklist.txt'),
// this is not part of appdata because an icon may be set before install
BOX_DATA_DIR: path.join(baseDir(), 'boxdata/box'),
MAIL_DATA_DIR: path.join(baseDir(), 'boxdata/mail'),
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),

View File

@@ -10,8 +10,10 @@ exports = module.exports = {
const apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
AuditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:platform'),
delay = require('delay'),
fs = require('fs'),
infra = require('./infra_version.js'),
locker = require('./locker.js'),
@@ -21,17 +23,15 @@ const apps = require('./apps.js'),
services = require('./services.js'),
shell = require('./shell.js'),
tasks = require('./tasks.js'),
volumes = require('./volumes.js'),
_ = require('underscore');
function start(options, callback) {
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return callback();
async function start(options) {
if (process.env.BOX_ENV === 'test' && !process.env.TEST_CREATE_INFRA) return;
debug('initializing addon infrastructure');
var existingInfra = { version: 'none' };
let existingInfra = { version: 'none' };
if (fs.existsSync(paths.INFRA_VERSION_FILE)) {
existingInfra = safe.JSON.parse(fs.readFileSync(paths.INFRA_VERSION_FILE, 'utf8'));
if (!existingInfra) existingInfra = { version: 'corrupt' };
@@ -40,60 +40,63 @@ function start(options, callback) {
// short-circuit for the restart case
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
onPlatformReady(false /* !infraChanged */);
return callback();
await onPlatformReady(false /* !infraChanged */);
return;
}
debug('Updating infrastructure from %s to %s', existingInfra.version, infra.version);
var error = locker.lock(locker.OP_PLATFORM_START);
if (error) return callback(error);
const error = locker.lock(locker.OP_PLATFORM_START);
if (error) throw error;
async.series([
(next) => { if (existingInfra.version !== infra.version) removeAllContainers(next); else next(); },
markApps.bind(null, existingInfra, options), // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
services.startServices.bind(null, existingInfra),
fs.writeFile.bind(fs, paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4))
], function (error) {
if (error) return callback(error);
for (let attempt = 0; attempt < 5; attempt++) {
try {
if (existingInfra.version !== infra.version) await removeAllContainers();
if (existingInfra.version === 'none') await volumes.mountAll(); // when restoring, mount all volumes
await markApps(existingInfra, options); // mark app state before we start addons. this gives the db import logic a chance to mark an app as errored
await services.startServices(existingInfra);
await fs.promises.writeFile(paths.INFRA_VERSION_FILE, JSON.stringify(infra, null, 4));
break;
} catch (error) {
// for some reason, mysql arbitrary restarts making startup tasks fail. this makes the box update stuck
// LOST is when existing connection breaks. REFUSED is when new connection cannot connect at all
const retry = error.reason === BoxError.DATABASE_ERROR && (error.code === 'PROTOCOL_CONNECTION_LOST' || error.code === 'ECONNREFUSED');
debug(`Failed to start services. retry=${retry} (attempt ${attempt}): ${error.message}`);
if (!retry) break;
await delay(10000);
}
}
locker.unlock(locker.OP_PLATFORM_START);
locker.unlock(locker.OP_PLATFORM_START);
onPlatformReady(true /* infraChanged */);
callback();
});
await onPlatformReady(true /* infraChanged */);
}
function stopAllTasks(callback) {
tasks.stopAllTasks(callback);
async function stopAllTasks() {
await tasks.stopAllTasks();
}
function onPlatformReady(infraChanged) {
async function onPlatformReady(infraChanged) {
debug(`onPlatformReady: platform is ready. infra changed: ${infraChanged}`);
exports._isReady = true;
let tasks = [ apps.schedulePendingTasks ];
if (infraChanged) tasks.push(pruneInfraImages);
if (infraChanged) await safe(pruneInfraImages(), { debug }); // ignore error
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}`);
});
});
await apps.schedulePendingTasks(AuditSource.PLATFORM);
}
function pruneInfraImages(callback) {
async function pruneInfraImages() {
debug('pruneInfraImages: checking existing images');
// cannot blindly remove all unused images since redis image may not be used
const images = infra.baseImages.concat(Object.keys(infra.images).map(function (addon) { return infra.images[addon]; }));
async.eachSeries(images, function (image, iteratorCallback) {
for (const image of images) {
let output = safe.child_process.execSync(`docker images --digests ${image.repo} --format "{{.ID}} {{.Repository}}:{{.Tag}}@{{.Digest}}"`, { encoding: 'utf8' });
if (output === null) return iteratorCallback(safe.error);
if (output === null) {
debug(`Failed to list images of ${image}`, safe.error);
throw safe.error;
}
let lines = output.trim().split('\n');
for (let line of lines) {
@@ -105,32 +108,27 @@ function pruneInfraImages(callback) {
let result = safe.child_process.execSync(`docker rmi ${parts[0]}`, { encoding: 'utf8' });
if (result === null) debug(`Error removing image ${parts[0]}: ${safe.error.mesage}`);
}
iteratorCallback();
}, callback);
}
}
function removeAllContainers(callback) {
async function removeAllContainers() {
debug('removeAllContainers: removing all containers for infra upgrade');
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);
await shell.promises.exec('removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop');
await shell.promises.exec('removeAllContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f');
}
function markApps(existingInfra, options, callback) {
async function markApps(existingInfra, options) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (existingInfra.version === 'none') { // cloudron is being restored from backup
debug('markApps: restoring installed apps');
apps.restoreInstalledApps(options, callback);
await apps.restoreInstalledApps(options, AuditSource.PLATFORM);
} else if (existingInfra.version !== infra.version) {
debug('markApps: reconfiguring installed apps');
reverseProxy.removeAppConfigs(); // should we change the cert location, nginx will not start
apps.configureInstalledApps(callback);
await apps.configureInstalledApps(AuditSource.PLATFORM);
} else {
let changedAddons = [];
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) changedAddons.push('mysql');
@@ -141,10 +139,9 @@ function markApps(existingInfra, options, callback) {
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);
await apps.restartAppsUsingAddons(changedAddons, AuditSource.PLATFORM);
} else {
debug('markApps: apps are already uptodate');
callback();
}
}
}

View File

@@ -8,8 +8,8 @@ exports = module.exports = {
};
const assert = require('assert'),
async = require('async'),
backups = require('./backups.js'),
backuptask = require('./backuptask.js'),
BoxError = require('./boxerror.js'),
branding = require('./branding.js'),
constants = require('./constants.js'),
@@ -17,6 +17,7 @@ const assert = require('assert'),
debug = require('debug')('box:provision'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
mail = require('./mail.js'),
mounts = require('./mounts.js'),
reverseProxy = require('./reverseproxy.js'),
@@ -24,15 +25,14 @@ const assert = require('assert'),
semver = require('semver'),
settings = require('./settings.js'),
sysinfo = require('./sysinfo.js'),
paths = require('./paths.js'),
users = require('./users.js'),
tld = require('tldjs'),
tokens = require('./tokens.js'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// we cannot use tasks since the tasks table gets overwritten when db is imported
let gProvisionStatus = {
const gProvisionStatus = {
setup: {
active: false,
message: '',
@@ -48,145 +48,167 @@ let gProvisionStatus = {
function setProgress(task, message, callback) {
debug(`setProgress: ${task} - ${message}`);
gProvisionStatus[task].message = message;
callback();
if (callback) callback();
}
function unprovision(callback) {
assert.strictEqual(typeof callback, 'function');
debug('unprovision');
async function ensureDhparams() {
if (fs.existsSync(paths.DHPARAMS_FILE)) return;
debug('ensureDhparams: generating dhparams');
const dhparams = safe.child_process.execSync('openssl dhparam -dsaparam 2048');
if (!dhparams) throw new BoxError(BoxError.OPENSSL_ERROR, safe.error);
if (!safe.fs.writeFileSync(paths.DHPARAMS_FILE, dhparams)) throw new BoxError(BoxError.FS_ERROR, `Could not save dhparams.pem: ${safe.error.message}`);
}
async function unprovision() {
// TODO: also cancel any existing configureWebadmin task
async.series([
settings.setDashboardLocation.bind(null, '', ''),
mail.clearDomains,
domains.clear
], callback);
await settings.setDashboardLocation('', '');
await mail.clearDomains();
await domains.clear();
}
async function setupTask(domain, auditSource) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof auditSource, 'object');
function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
try {
await cloudron.setupDnsAndCert(constants.DASHBOARD_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message));
await ensureDhparams();
await cloudron.setDashboardDomain(domain, auditSource);
setProgress('setup', 'Done'),
await eventlog.add(eventlog.ACTION_PROVISION, auditSource, {});
} catch (error) {
gProvisionStatus.setup.errorMessage = error ? error.message : '';
}
gProvisionStatus.setup.active = false;
}
async function setup(dnsConfig, sysinfoConfig, auditSource) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof sysinfoConfig, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) return callback(new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring'));
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring');
gProvisionStatus.setup = { active: true, errorMessage: '', message: 'Adding domain' };
function done(error) {
try {
const activated = await users.isActivated();
if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated', { activate: true });
await unprovision();
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
debug(`setup: Setting up Cloudron with domain ${domain} and zone ${zoneName}`);
const data = {
zoneName: zoneName,
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' },
dkimSelector: 'cloudron'
};
await settings.setMailLocation(domain, `${constants.DASHBOARD_LOCATION}.${domain}`); // default mail location. do this before we add the domain for upserting mail DNS
await domains.add(domain, data, auditSource);
await settings.setSysinfoConfig(sysinfoConfig);
safe(setupTask(domain, auditSource), { debug }); // now that args are validated run the task in the background
} catch (error) {
debug('setup: error', error);
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
callback(error);
gProvisionStatus.setup.errorMessage = error.message;
throw error;
}
users.isActivated(function (error, activated) {
if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated', { activate: true }));
unprovision(function (error) {
if (error) return done(error);
const domain = dnsConfig.domain.toLowerCase();
const zoneName = dnsConfig.zoneName ? dnsConfig.zoneName : (tld.getDomain(domain) || domain);
debug(`provision: Setting up Cloudron with domain ${domain} and zone ${zoneName}`);
let data = {
zoneName: zoneName,
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' },
dkimSelector: 'cloudron'
};
async.series([
settings.setMailLocation.bind(null, domain, `${constants.DASHBOARD_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);
callback(); // now that args are validated run the task in the background
async.series([
settings.setSysinfoConfig.bind(null, sysinfoConfig),
cloudron.setupDnsAndCert.bind(null, constants.DASHBOARD_LOCATION, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
setProgress.bind(null, 'setup', 'Done'),
async () => eventlog.add(eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
gProvisionStatus.setup.active = false;
gProvisionStatus.setup.errorMessage = error ? error.message : '';
});
});
});
});
}
function activate(username, password, email, displayName, ip, auditSource, callback) {
async function activate(username, password, email, displayName, ip, auditSource) {
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof email, 'string');
assert.strictEqual(typeof displayName, 'string');
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
debug('activating user:%s email:%s', username, email);
debug(`activate: user: ${username} email:${email}`);
users.createOwner(username, password, email, displayName, auditSource, async function (error, userObject) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Already activated'));
if (error) return callback(error);
const [error, ownerId] = await safe(users.createOwner(email, username, password, displayName, auditSource));
if (error && error.reason === BoxError.ALREADY_EXISTS) throw new BoxError(BoxError.CONFLICT, 'Already activated');
if (error) throw error;
const token = { clientId: tokens.ID_WEBADMIN, identifier: userObject.id, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS };
let result;
[error, result] = await safe(tokens.add(token));
if (error) return callback(error);
const token = { clientId: tokens.ID_WEBADMIN, identifier: ownerId, expires: Date.now() + constants.DEFAULT_TOKEN_EXPIRATION_MSECS };
const result = await tokens.add(token);
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, {});
callback(null, {
userId: userObject.id,
token: result.accessToken,
expires: result.expires
});
setImmediate(() => safe(cloudron.onActivated({}), { debug }));
setImmediate(cloudron.onActivated.bind(null, {}, NOOP_CALLBACK)); // hack for now to not block the above http response
});
return {
userId: ownerId,
token: result.accessToken,
expires: result.expires
};
}
function restore(backupConfig, backupId, version, sysinfoConfig, options, auditSource, callback) {
async function restoreTask(backupConfig, backupId, sysinfoConfig, options, auditSource) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof sysinfoConfig, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
try {
setProgress('restore', 'Downloading box backup');
await backuptask.restore(backupConfig, backupId, (progress) => setProgress('restore', progress.message));
setProgress('restore', 'Downloading mail backup');
const mailBackups = await backups.getByIdentifierAndStatePaged(backups.BACKUP_IDENTIFIER_MAIL, backups.BACKUP_STATE_NORMAL, 1, 1);
if (mailBackups.length === 0) throw new BoxError(BoxError.NOT_FOUND, 'mail backup not found');
const mailRestoreConfig = { backupConfig, backupId: mailBackups[0].id, backupFormat: mailBackups[0].format };
await backuptask.downloadMail(mailRestoreConfig, (progress) => setProgress('restore', progress.message));
await ensureDhparams();
await settings.setSysinfoConfig(sysinfoConfig);
await reverseProxy.restoreFallbackCertificates();
const dashboardDomain = settings.dashboardDomain(); // load this fresh from after the backup.restore
if (!options.skipDnsSetup) await cloudron.setupDnsAndCert(constants.DASHBOARD_LOCATION, dashboardDomain, auditSource, (progress) => setProgress('restore', progress.message));
await cloudron.setDashboardDomain(dashboardDomain, auditSource);
await settings.setBackupCredentials(backupConfig); // update just the credentials and not the policy and flags
await eventlog.add(eventlog.ACTION_RESTORE, auditSource, { backupId });
setImmediate(() => safe(cloudron.onActivated(options), { debug }));
} catch (error) {
gProvisionStatus.restore.errorMessage = error ? error.message : '';
}
gProvisionStatus.restore.active = false;
}
async function restore(backupConfig, backupId, version, sysinfoConfig, options, auditSource) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof sysinfoConfig, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof auditSource, 'object');
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 --version ${version}" on a fresh Ubuntu installation to restore from this backup`));
if (!semver.valid(version)) throw new BoxError(BoxError.BAD_FIELD, 'version is not a valid semver', { field: 'version' });
if (constants.VERSION !== version) throw 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'));
if (gProvisionStatus.setup.active || gProvisionStatus.restore.active) throw new BoxError(BoxError.BAD_STATE, 'Already setting up or restoring');
gProvisionStatus.restore = { active: true, errorMessage: '', message: 'Testing backup config' };
function done(error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
callback(error);
}
try {
const activated = await users.isActivated();
if (activated) throw new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.');
users.isActivated(async function (error, activated) {
if (error) return done(error);
if (activated) return done(new BoxError(BoxError.CONFLICT, 'Already activated. Restore with a fresh Cloudron installation.'));
if (backups.isMountProvider(backupConfig.provider)) {
if (mounts.isMountProvider(backupConfig.provider)) {
error = mounts.validateMountOptions(backupConfig.provider, backupConfig.mountOptions);
if (error) return done(error);
if (error) throw error;
const newMount = {
name: 'backup',
@@ -195,75 +217,44 @@ function restore(backupConfig, backupId, version, sysinfoConfig, options, auditS
mountOptions: backupConfig.mountOptions
};
[error] = await safe(mounts.tryAddMount(newMount, { timeout: 10 })); // 10 seconds
if (error) return done(error);
await safe(mounts.tryAddMount(newMount, { timeout: 10 })); // 10 seconds
}
backups.testProviderConfig(backupConfig, function (error) {
if (error) return done(error);
let error = await backups.testProviderConfig(backupConfig);
if (error) throw error;
if ('password' in backupConfig) {
backupConfig.encryption = backups.generateEncryptionKeysSync(backupConfig.password);
delete backupConfig.password;
} else {
backupConfig.encryption = null;
}
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);
error = await sysinfo.testConfig(sysinfoConfig);
if (error) throw error;
debug(`restore: restoring from ${backupId} from provider ${backupConfig.provider} with format ${backupConfig.format}`);
callback(); // now that the fields are validated, continue task in the background
async.series([
setProgress.bind(null, 'restore', 'Downloading backup'),
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
reverseProxy.restoreFallbackCertificates,
(done) => {
const dashboardDomain = settings.dashboardDomain(); // load this fresh from after the backup.restore
async.series([
(next) => {
if (options.skipDnsSetup) return next();
cloudron.setupDnsAndCert(constants.DASHBOARD_LOCATION, dashboardDomain, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK), next);
},
cloudron.setDashboardDomain.bind(null, dashboardDomain, auditSource)
], done);
},
settings.setBackupCredentials.bind(null, backupConfig), // update just the credentials and not the policy and flags
async () => eventlog.add(eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
if (!error) cloudron.onActivated(options, NOOP_CALLBACK);
});
});
});
});
safe(restoreTask(backupConfig, backupId, sysinfoConfig, options, auditSource), { debug }); // now that args are validated run the task in the background
} catch (error) {
gProvisionStatus.restore.active = false;
gProvisionStatus.restore.errorMessage = error ? error.message : '';
throw error;
}
}
function getStatus(callback) {
assert.strictEqual(typeof callback, 'function');
async function getStatus() {
const activated = await users.isActivated();
users.isActivated(function (error, activated) {
if (error) return callback(error);
const allSettings = await settings.list();
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
callback(null, _.extend({
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
adminFqdn: settings.dashboardDomain() ? settings.dashboardFqdn() : null,
language: allSettings[settings.LANGUAGE_KEY],
activated: activated,
provider: settings.provider() // used by setup wizard of marketplace images
}, gProvisionStatus));
});
});
return _.extend({
version: constants.VERSION,
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: branding.renderFooter(allSettings[settings.FOOTER_KEY] || constants.FOOTER),
adminFqdn: settings.dashboardDomain() ? settings.dashboardFqdn() : null,
language: allSettings[settings.LANGUAGE_KEY],
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