Compare commits

...

248 Commits

Author SHA1 Message Date
Girish Ramakrishnan
5515324fd4 coturn -> turn in docker repo name 2020-04-02 19:51:14 -07:00
Girish Ramakrishnan
e72622ed4f Fix crash during auto-update 2020-04-02 19:47:29 -07:00
Girish Ramakrishnan
e821733a58 add note on exposed ports 2020-04-02 18:09:26 -07:00
Girish Ramakrishnan
a03c0e4475 mail: disable hostname validation 2020-04-02 15:00:11 -07:00
Girish Ramakrishnan
3203821546 typo 2020-04-02 12:29:20 -07:00
Girish Ramakrishnan
16f3cee5c5 install custom nginx only on xenial
https://nginx.org/en/linux_packages.html#Ubuntu
http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/
2020-04-02 11:54:22 -07:00
Johannes Zellner
57afb46cbd Ensure nginx installation will not overwrite our conf files 2020-04-02 16:57:55 +02:00
Johannes Zellner
91dde5147a add-apt-repository does not call apt-get update 2020-04-02 13:54:39 +02:00
Johannes Zellner
d0692f7379 Ensure we have latest nginx 2020-04-02 12:37:02 +02:00
Girish Ramakrishnan
e360658c6e More changes 2020-04-01 17:00:01 -07:00
Girish Ramakrishnan
e7dc77e6de bump mail container for mailbox size fix 2020-04-01 16:31:07 -07:00
Girish Ramakrishnan
e240a8b58f add comment on the struct 2020-04-01 16:26:16 -07:00
Girish Ramakrishnan
38d4f2c27b Add note on what df output is 2020-04-01 15:59:48 -07:00
Girish Ramakrishnan
552e2a036c Use block size instead of apparent size in du
https://stackoverflow.com/questions/5694741/why-is-the-output-of-du-often-so-different-from-du-b

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

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

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

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

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

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

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

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

https://forum.cloudron.io/topic/2222/impersonate-user-privilege-escalation
2020-03-12 10:24:21 -07:00
Girish Ramakrishnan
3e66feb514 mail: add mailbox acl 2020-03-10 22:12:15 -07:00
Girish Ramakrishnan
cd91a5ef64 5.0.3 changes 2020-03-10 17:18:21 -07:00
Girish Ramakrishnan
cf89609633 mail: acl was enabled by mistake 2020-03-10 17:15:23 -07:00
Girish Ramakrishnan
67c24c1282 mail: make spamd_user case insensitive 2020-03-10 12:08:43 -07:00
Girish Ramakrishnan
7d3df3c55f Fix sa usage 2020-03-10 09:22:41 -07:00
Girish Ramakrishnan
dfe5cec46f Show the public IP to finish setup 2020-03-09 15:18:39 -07:00
Girish Ramakrishnan
17c881da47 Fix spam training 2020-03-09 13:51:17 -07:00
Girish Ramakrishnan
6e30c4917c Do not wait for dns when re-configured 2020-03-09 12:36:29 -07:00
Girish Ramakrishnan
c6d4f0d2f0 mail: fix word boundary regexp 2020-03-07 19:16:10 -08:00
Girish Ramakrishnan
b32128bebf Fix quoting in emails 2020-03-07 19:12:39 -08:00
Girish Ramakrishnan
a3f3d86908 More spam fixes 2020-03-07 18:52:20 -08:00
Girish Ramakrishnan
b29c82087a Bump the mail container version 2020-03-07 17:08:35 -08:00
Johannes Zellner
657beda7c9 Copy 5.0.0 changes for 5.0.1 2020-03-07 16:56:40 -08:00
Girish Ramakrishnan
b4f5ecb304 mail: fix eventlog search 2020-03-07 15:56:56 -08:00
Girish Ramakrishnan
3dabad5e91 Detect that domain is in use by app correctly 2020-03-07 14:52:34 -08:00
Johannes Zellner
890b46836b Do not allow lower level roles to edit higher level ones 2020-03-07 13:53:01 -08:00
Girish Ramakrishnan
835b3224c6 disable getting user token in demo mode 2020-03-07 11:44:38 -08:00
Girish Ramakrishnan
f8d27f3139 mail: Fix ownership issue with /app/data 2020-03-07 11:40:49 -08:00
Girish Ramakrishnan
33f263ebb9 Fix spamd logs 2020-03-07 01:00:08 -08:00
Girish Ramakrishnan
027925c0ba Only do spam processing when we have incoming domains 2020-03-07 00:22:00 -08:00
Girish Ramakrishnan
17c4819d41 eventlog updates 2020-03-06 23:16:32 -08:00
Johannes Zellner
017d19a8c8 Do not send internal link for update notification 2020-03-06 19:18:01 -08:00
Girish Ramakrishnan
46b6e319f5 add some spacing in the footer 2020-03-06 19:13:37 -08:00
Johannes Zellner
8f087e1c30 Take default footer from constants and keep settingsdb pristine 2020-03-06 18:08:26 -08:00
Johannes Zellner
c3fc0e83a8 Optimize collectd restart to be skipped if profile hasn't actually changed 2020-03-06 17:44:31 -08:00
Johannes Zellner
7ed0ef7b37 Ensure collectd backup config on startup 2020-03-06 17:44:31 -08:00
Girish Ramakrishnan
46ede3d60d search for request_uri in try_files
this lets us put images in app_not_responding.html
2020-03-06 17:01:48 -08:00
Girish Ramakrishnan
7a63fd4711 Failed quickly if docker image not found 2020-03-06 16:39:20 -08:00
Girish Ramakrishnan
65f573b773 mail container update 2020-03-06 16:11:52 -08:00
Johannes Zellner
afa2fe8177 Improve role add/edit error message 2020-03-06 13:16:50 -08:00
Girish Ramakrishnan
ad72a8a929 Add comment 2020-03-06 13:05:31 -08:00
Johannes Zellner
a7b00bad63 Fixup status code typo 2020-03-06 11:59:31 -08:00
Johannes Zellner
85fd74135c Bring back legacy ldap mailbox search for old sogo 2020-03-06 11:48:51 -08:00
Girish Ramakrishnan
970ccf1ddb send footer in status route
required for login screen to be customized
2020-03-06 01:16:48 -08:00
Johannes Zellner
b237eb03f6 Add support feature flag 2020-03-06 01:08:45 -08:00
Girish Ramakrishnan
a569294f86 Better changelog 2020-03-06 01:03:52 -08:00
Johannes Zellner
16f85a23d2 Clear reboot notification if reboot is triggered 2020-03-06 00:49:00 -08:00
Johannes Zellner
fcee8aa5f3 Improve LDAP mailbox searches to better suit sogo 2020-03-06 00:48:41 -08:00
Johannes Zellner
d85eabce02 Update reboot required notification text 2020-03-05 21:01:15 -08:00
Johannes Zellner
de23d1aa03 Do not allow to set active flag for the operating user 2020-03-05 21:00:59 -08:00
Johannes Zellner
1766bc6ee3 For now we enable all features 2020-03-05 13:37:07 -08:00
Girish Ramakrishnan
c1801d6e71 Add linode-oneclick provider 2020-03-05 11:25:43 -08:00
Girish Ramakrishnan
64844045ca mail: various pam related fixes 2020-03-04 15:00:37 -08:00
Girish Ramakrishnan
e90da46967 spam: add default corpus and global db 2020-03-02 21:45:48 -08:00
Girish Ramakrishnan
d10957d6df remove galaxygate from cloudron-setup help 2020-02-28 11:14:06 -08:00
Girish Ramakrishnan
50dc90d7ae remove galaxygate 2020-02-28 11:13:44 -08:00
Johannes Zellner
663bedfe39 Sync default features 2020-02-28 15:18:16 +01:00
Girish Ramakrishnan
ce9834757e restore: carefully replace backup config
do not replace the backup policy and other flags
2020-02-27 12:38:37 -08:00
Girish Ramakrishnan
cc932328ff fix comment 2020-02-27 10:36:35 -08:00
Girish Ramakrishnan
4ebe143a98 improve the error message on domain removal 2020-02-27 10:12:39 -08:00
Johannes Zellner
82aff74fc2 Make app passwords stronger 2020-02-27 13:07:01 +01:00
Girish Ramakrishnan
6adc099455 Fix crash 2020-02-26 15:49:41 -08:00
Girish Ramakrishnan
35efc8c650 add linode objectstorage backend 2020-02-26 09:08:30 -08:00
Girish Ramakrishnan
3f63d79905 Fixup version of next release 2020-02-26 09:01:48 -08:00
Girish Ramakrishnan
00096f4dcd fix comment 2020-02-26 09:01:35 -08:00
Girish Ramakrishnan
c3e0d9086e cloudron-support: backups and appsdata can be empty 2020-02-24 14:12:25 -08:00
Girish Ramakrishnan
f1dfe3c7e8 mail: Fix crash when determining usage 2020-02-24 11:45:17 -08:00
Johannes Zellner
6f96ff790f Groups are part of user manager role 2020-02-24 17:49:22 +01:00
Johannes Zellner
ccb218f243 setPassword wants the full user object 2020-02-24 13:21:17 +01:00
Girish Ramakrishnan
9ac194bbea fix missing quote in debug message 2020-02-23 11:15:30 -08:00
Girish Ramakrishnan
0191907ce2 mail: use limit plugin instead of rcpt_to.max_count 2020-02-23 11:15:30 -08:00
Johannes Zellner
e80069625b Fix typo in migration script 2020-02-22 15:26:16 +01:00
Girish Ramakrishnan
0e156b9376 migrate permissions and admin flag to user.role 2020-02-21 16:49:20 -08:00
Johannes Zellner
a8f1b0241e Add route to obtain an appstore accessToken 2020-02-21 12:34:54 +01:00
Girish Ramakrishnan
6715cf23d7 Add mail usage info 2020-02-20 12:09:06 -08:00
Girish Ramakrishnan
82a173f7d8 proxy requests to mail server 2020-02-20 10:10:34 -08:00
Johannes Zellner
857504c409 Add function to retrieve appstore user access token 2020-02-20 17:05:07 +01:00
Johannes Zellner
4b4586c1e5 Get features from the appstore 2020-02-20 16:04:22 +01:00
Girish Ramakrishnan
6679fe47df mail: add X-Envelope-From/To headers 2020-02-19 22:14:23 -08:00
Girish Ramakrishnan
e7a98025a2 disable update of domain in demo mode
we removed the locked flag, so we have to add this check
2020-02-19 10:45:55 -08:00
Girish Ramakrishnan
2870f24bec mail eventlog: add remote info 2020-02-18 21:31:28 -08:00
Girish Ramakrishnan
037440034b Move collectd logs to platformdata and rotate it 2020-02-18 20:36:50 -08:00
Johannes Zellner
15cc1f92e3 Fix typo 2020-02-17 13:47:21 +01:00
Girish Ramakrishnan
00c6ad675e add usermanager tests 2020-02-14 14:34:29 -08:00
Girish Ramakrishnan
655a740b0c split tests into various sections 2020-02-14 14:04:51 -08:00
Girish Ramakrishnan
028852740d Make users-test work 2020-02-14 13:23:17 -08:00
Johannes Zellner
c8000fdf90 Fix the features selection 2020-02-14 15:21:56 +01:00
Johannes Zellner
995e56d7e4 Also grant education and contributor subscriptions all features 2020-02-14 15:13:21 +01:00
Johannes Zellner
c20d3b62b0 Determin features based on subscription and cloudron creation 2020-02-14 15:07:25 +01:00
Girish Ramakrishnan
c537dfabb2 add manage user permission 2020-02-13 22:49:58 -08:00
Girish Ramakrishnan
11b5304cb9 userdb: only pass specific fields to update 2020-02-13 22:45:14 -08:00
Girish Ramakrishnan
fd8abbe2ab remove ROLE_USER
every authenticated user has ROLE_USER. So, this role is superfluous
2020-02-13 21:53:57 -08:00
Girish Ramakrishnan
25d871860d domains: remove locked field
we will do this as part of access control if needed later
2020-02-13 21:16:46 -08:00
Girish Ramakrishnan
d1911be28c user: load the resource with middleware 2020-02-13 20:59:17 -08:00
Girish Ramakrishnan
938ca6402c mail: add search param 2020-02-13 09:08:47 -08:00
Johannes Zellner
0aaecf6e46 Cannot use Infinity 2020-02-13 17:09:28 +01:00
Johannes Zellner
b06d84984b Add features to config object 2020-02-13 16:34:29 +01:00
Girish Ramakrishnan
51b50688e4 mail eventlog: fix bounce event 2020-02-12 23:33:43 -08:00
Girish Ramakrishnan
066d7ab972 Update mail container 2020-02-12 22:11:11 -08:00
Girish Ramakrishnan
e092074d77 2020 is unused 2020-02-11 22:12:34 -08:00
Girish Ramakrishnan
83bdcb8cc4 remove unused domain stats route 2020-02-11 22:10:57 -08:00
Girish Ramakrishnan
f80f40cbcd repair: take optional docker image for re-configure 2020-02-11 21:05:01 -08:00
Girish Ramakrishnan
4b93b31c3d SCOPE_* vars are unused now 2020-02-11 17:37:12 -08:00
Girish Ramakrishnan
4d050725b7 storage: done events must be called next tick
It seems that listDir() returns synchronously (!), not sure how.
This results in the done event getting called with an error but
the EE event handlers are not setup yet.
2020-02-11 11:48:49 -08:00
Girish Ramakrishnan
57597bd103 s3: bucket name cannot contain / 2020-02-11 11:19:47 -08:00
Girish Ramakrishnan
fb52c2b684 backupupload: it is either result or message 2020-02-11 10:03:26 -08:00
Girish Ramakrishnan
de547df9bd Show docker image in the error 2020-02-10 21:54:08 -08:00
Girish Ramakrishnan
a05342eaa0 Add mail eventlog 2020-02-10 15:36:30 -08:00
Girish Ramakrishnan
fb931b7a3a More 4.5 changes 2020-02-10 14:32:15 -08:00
Girish Ramakrishnan
d1c07b6d30 cron: rework recreation of jobs based on timezone 2020-02-10 13:12:20 -08:00
Johannes Zellner
7f0ad2afa0 Move login tests to cloudron route tests 2020-02-10 16:40:07 +01:00
Johannes Zellner
d8e0639db4 Empty or missing username/password results in 400 2020-02-10 16:14:22 +01:00
Johannes Zellner
4d91351845 Get config should succeed for non-admins also 2020-02-10 13:10:56 +01:00
Johannes Zellner
d3f08ef580 Fix apps test to use latest test-app 2020-02-08 00:43:57 +01:00
Johannes Zellner
5e11a9c8ed Fixup typo 2020-02-07 23:12:53 +01:00
Johannes Zellner
957e1a7708 Cleanup unused tokendb apis 2020-02-07 23:06:45 +01:00
Johannes Zellner
7c86ed9783 Add ability to specify the login purpose for further use
In this case the cli will specify a different token type
2020-02-07 23:03:53 +01:00
Girish Ramakrishnan
799b588693 More 4.5 changes 2020-02-07 11:29:16 -08:00
Girish Ramakrishnan
596f4c01a4 cloudron-setup: remove support for pre-4.2 2020-02-07 09:15:12 -08:00
Girish Ramakrishnan
f155de0f17 Revert "Read the provider from the settings, not the migration PROVIDER_FILE"
This reverts commit 001749564d.

PROVIDER is still very much alive and active. sysinfo provider is for the network
interface
2020-02-07 09:13:33 -08:00
Johannes Zellner
476ba1ad69 Fix token expiresAt 2020-02-07 16:42:15 +01:00
Johannes Zellner
ac4aa4bd3d Add tokens routes 2020-02-07 16:20:05 +01:00
Girish Ramakrishnan
237f2c5112 Better error message for domain conflict 2020-02-06 15:51:32 -08:00
Johannes Zellner
cbc6785eb5 Fix typo 2020-02-06 17:29:45 +01:00
Johannes Zellner
26c4cdbf17 Rename tokens.addTokenByUserId() to simply tokens.add() 2020-02-06 17:26:17 +01:00
Johannes Zellner
fb78f31891 cleanup accesscontrol route tests for now 2020-02-06 17:26:17 +01:00
Johannes Zellner
2b6bf8d195 Remove Oauth clients code 2020-02-06 17:26:15 +01:00
Johannes Zellner
2854462e0e Remove token scope business 2020-02-06 16:44:46 +01:00
Johannes Zellner
b4e4b11ab3 Remove now redundant developer login code 2020-02-06 15:47:44 +01:00
Johannes Zellner
7c5a258af3 Move 2fa validation in one place 2020-02-06 15:36:14 +01:00
Johannes Zellner
12aa8ac0ad Remove passport 2020-02-06 14:56:28 +01:00
Johannes Zellner
58d8f688e5 Update schema since authcodes is gone 2020-02-06 11:11:15 +01:00
Girish Ramakrishnan
7efb9e817e oauth2 is gone 2020-02-05 14:46:09 -08:00
Girish Ramakrishnan
5145ea3530 Add supportConfig in database 2020-02-05 14:46:05 -08:00
Girish Ramakrishnan
2f6933102c put appstore whitelist/blacklist in db 2020-02-05 11:58:10 -08:00
Girish Ramakrishnan
25ef5ab636 Move custom pages to a subdirectory 2020-02-05 11:42:17 -08:00
Johannes Zellner
4ae12ac10b Remove oauth
A whole bunch of useless stuff
2020-02-05 18:15:59 +01:00
Johannes Zellner
bfffde5f89 Remove oauth based account setup page 2020-02-05 17:10:55 +01:00
Johannes Zellner
aa7ec53257 Also send display name with invite link 2020-02-05 16:34:34 +01:00
Johannes Zellner
1f41e6dc0f Fix audit source usage 2020-02-05 16:12:40 +01:00
Johannes Zellner
1fbbaa82ab Generate the user invite link only in one location 2020-02-05 15:53:05 +01:00
Johannes Zellner
68b1d1dde1 Fixup account setup link 2020-02-05 15:21:55 +01:00
Johannes Zellner
d773cb4873 Add REST route for account setup
This replaces the server side rendered form
2020-02-05 15:04:59 +01:00
Johannes Zellner
d3c7616120 Remove csurf
New views will be using the REST api not session, so this won't apply
2020-02-05 12:49:37 +01:00
Johannes Zellner
6a92af3db3 Remove password reset views from oauth 2020-02-05 11:43:33 +01:00
Girish Ramakrishnan
763e14f55d Make app error page customizable 2020-02-04 17:52:30 -08:00
Girish Ramakrishnan
4f57d97fff add api to get/set footer and remove all use of custom.js 2020-02-04 13:27:19 -08:00
Girish Ramakrishnan
3153fb5cbe custom: remove alerts section 2020-02-04 13:09:00 -08:00
Girish Ramakrishnan
c9e96cd97a custom: remove support section 2020-02-04 13:07:36 -08:00
Girish Ramakrishnan
c41042635f custom: remove subscription.configurable 2020-02-04 12:58:32 -08:00
Girish Ramakrishnan
141b2d2691 custom: remove app whitelist/blacklist 2020-02-04 12:58:11 -08:00
Girish Ramakrishnan
e71e8043cf custom: remove config.uiSpec.domains 2020-02-04 12:56:10 -08:00
Girish Ramakrishnan
49d80dbfc4 custom: remove backups.configurable 2020-02-04 12:49:41 -08:00
Johannes Zellner
8d6eca2349 Fix typos 2020-02-04 18:32:43 +01:00
Johannes Zellner
13d0491759 Send out new password reset link 2020-02-04 17:11:31 +01:00
Johannes Zellner
37e2d78d6a Users without a username have to sign up first 2020-02-04 17:07:45 +01:00
Johannes Zellner
6745221e0f Password reset does not need an email 2020-02-04 17:05:08 +01:00
Johannes Zellner
18bbe70364 Add route to set new password 2020-02-04 16:47:57 +01:00
Johannes Zellner
eec8d4bdac We want to redirect to login.html 2020-02-04 15:59:12 +01:00
Johannes Zellner
86029c1068 Add new password reset route 2020-02-04 15:27:22 +01:00
Johannes Zellner
0ae9be4de9 Add basic login/logout logic 2020-02-04 14:35:25 +01:00
Girish Ramakrishnan
57e3180737 typo 2020-02-01 18:12:33 -08:00
Girish Ramakrishnan
a84cdc3d09 app password: add tests for the rest routes 2020-02-01 10:19:14 -08:00
Girish Ramakrishnan
a5f35f39fe oom notification for backup disk as well 2020-01-31 22:20:34 -08:00
Girish Ramakrishnan
3427db3983 Add app passwords feature 2020-01-31 22:03:19 -08:00
Girish Ramakrishnan
e3878fa381 mysqldump: Add --column-statistics=0
mysqldump: Couldn't execute 'SELECT COLUMN_NAME,                       JSON_EXTRACT(HISTOGRAM, '$."number-of-buckets-specified"')                FROM information_schema.COLUMN_STATISTICS                WHERE SCHEMA_NAME = 'box' AND TABLE_NAME = 'appAddonConfigs';': Unknown table 'COLUMN_STATISTICS' in information_schema (1109)
2020-01-31 18:42:44 -08:00
Girish Ramakrishnan
e1ded9f7b5 Add collectd for backups 2020-01-31 14:56:41 -08:00
Girish Ramakrishnan
1981493398 refactor code into collectd.js 2020-01-31 13:33:19 -08:00
Girish Ramakrishnan
dece7319cc Update packages carefully 2020-01-31 10:25:47 -08:00
131 changed files with 5354 additions and 8384 deletions

91
CHANGES
View File

@@ -1776,3 +1776,94 @@
* Update nodejs to 12.14.1
* Update MySQL addon packet size to 64M
[5.0.0]
* Show backup disk usage in graphs
* Add per-user app passwords
* Make app not responding page customizable
* Make footer customizable
* Add UI to import backups
* Display timestamps in browser timezone in the UI
* Mail eventlog and usage
* Add user roles - owner, admin, user manager and user
* Setup logrotate configs for collectd since upstream does not set it up
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
* linode: add object storage backend
* restore: carefully replace backup config
* spam: add default corpus and global db
[5.0.1]
* Show backup disk usage in graphs
* Add per-user app passwords
* Make app not responding page customizable
* Make footer customizable
* Add UI to import backups
* Display timestamps in browser timezone in the UI
* Mail eventlog and usage
* Add user roles - owner, admin, user manager and user
* Setup logrotate configs for collectd since upstream does not set it up
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
* linode: add object storage backend
* restore: carefully replace backup config
* spam: add default corpus and global db
[5.0.2]
* Show backup disk usage in graphs
* Add per-user app passwords
* Make app not responding page customizable
* Make footer customizable
* Add UI to import backups
* Display timestamps in browser timezone in the UI
* Mail eventlog and usage
* Add user roles - owner, admin, user manager and user
* Setup logrotate configs for collectd since upstream does not set it up
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
* linode: add object storage backend
* restore: carefully replace backup config
* spam: per mailbox bayes db and training
[5.0.3]
* Show backup disk usage in graphs
* Add per-user app passwords
* Make app not responding page customizable
* Make footer customizable
* Add UI to import backups
* Display timestamps in browser timezone in the UI
* Mail eventlog and usage
* Add user roles - owner, admin, user manager and user
* Setup logrotate configs for collectd since upstream does not set it up
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
* linode: add object storage backend
* restore: carefully replace backup config
* spam: per mailbox bayes db and training
[5.0.4]
* Fix potential previlige escalation because of ghost file
* linode: dns backend
* make branding routes owner only
* add branding API
* Add app start/stop/restart events
* Use the primary email for LE account
* make mail eventlog more descriptive
[5.0.5]
* Fix bug where incoming mail from dynamic hostnames was rejected
* Increase token expiry
* Fix bug in tag UI where tag removal did not work
[5.0.6]
* Make mail eventlog only visible to owners
* Make app password work with sftp
[5.1.0]
* Add turn addon
* Fix disk usage display
* Drop support for TLSv1 and TLSv1.1
* Make cert validation work for ECC certs
* Add type filter to mail eventlog
* mail: Fix listing of mailboxes and aliases in the UI
* branding: fix login page title
* Only a Cloudron owner can install/update/exec apps with the docker addon
* security: reset tokens are only valid for a day
* mail: fix eventlog db perms
* Fix various bugs in the disk graphs

View File

@@ -44,7 +44,6 @@ apt-get -y install \
linux-generic \
logrotate \
mysql-server-5.7 \
nginx-full \
openssh-server \
pwgen \
resolvconf \
@@ -54,6 +53,17 @@ apt-get -y install \
unbound \
xfsprogs
if [[ "${ubuntu_version}" == "16.04" ]]; then
echo "==> installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y /tmp/nginx.deb
rm /tmp/nginx.deb
else
apt install -y nginx-full
fi
# on some providers like scaleway the sudo file is changed and we want to keep the old one
apt-get -o Dpkg::Options::="--force-confold" install -y sudo

View File

@@ -0,0 +1,26 @@
'use strict';
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE appPasswords(' +
'id VARCHAR(128) NOT NULL UNIQUE,' +
'name VARCHAR(128) NOT NULL,' +
'userId VARCHAR(128) NOT NULL,' +
'identifier VARCHAR(128) NOT NULL,' +
'hashedPassword VARCHAR(1024) NOT NULL,' +
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
'FOREIGN KEY(userId) REFERENCES users(id),' +
'UNIQUE (name, userId),' +
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('DROP TABLE appPasswords', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,22 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('DROP TABLE authcodes', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
var cmd = `CREATE TABLE IF NOT EXISTS authcodes(
authCode VARCHAR(128) NOT NULL UNIQUE,
userId VARCHAR(128) NOT NULL,
clientId VARCHAR(128) NOT NULL,
expiresAt BIGINT NOT NULL,
PRIMARY KEY(authCode)) CHARACTER SET utf8 COLLATE utf8_bin`;
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,24 @@
'use strict';
exports.up = function(db, callback) {
db.runSql('DROP TABLE clients', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
var cmd = `CREATE TABLE IF NOT EXISTS clients(
id VARCHAR(128) NOT NULL UNIQUE,
appId VARCHAR(128) NOT NULL,
type VARCHAR(16) NOT NULL,
clientSecret VARCHAR(512) NOT NULL,
redirectURI VARCHAR(512) NOT NULL,
scope VARCHAR(512) NOT NULL,
PRIMARY KEY(id)) CHARACTER SET utf8 COLLATE utf8_bin`;
db.runSql(cmd, function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

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

View File

@@ -0,0 +1,40 @@
'use strict';
var async = require('async');
exports.up = function(db, callback) {
async.series([
db.runSql.bind(db, 'START TRANSACTION;'),
db.runSql.bind(db, 'ALTER TABLE users ADD COLUMN role VARCHAR(32)'),
function migrateAdminFlag(done) {
db.all('SELECT * FROM users ORDER BY createdAt', function (error, results) {
if (error) return done(error);
let ownerFound = false;
async.eachSeries(results, function (user, next) {
let role;
if (!ownerFound && user.admin) {
role = 'owner';
ownerFound = true;
console.log(`Designating ${user.username} ${user.email} ${user.id} as the owner of this cloudron`);
} else {
role = user.admin ? 'admin' : 'user';
}
db.runSql('UPDATE users SET role=? WHERE id=?', [ role, user.id ], next);
}, done);
});
},
db.runSql.bind(db, 'ALTER TABLE users DROP COLUMN admin'),
db.runSql.bind(db, 'ALTER TABLE users MODIFY role VARCHAR(32) NOT NULL'),
db.runSql.bind(db, 'COMMIT')
], callback);
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN role', 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 ADD COLUMN resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
if (error) console.error(error);
callback(error);
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN resetTokenCreationTime', function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -0,0 +1,28 @@
'use strict';
let async = require('async');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY mailboxDomain VARCHAR(128)', [], function (error) { // make it nullable
if (error) console.error(error);
// clear mailboxName/Domain for apps that do not use mail addons
db.all('SELECT * FROM apps', function (error, apps) {
if (error) return callback(error);
async.eachSeries(apps, function (app, iteratorDone) {
var manifest = JSON.parse(app.manifestJson);
if (manifest.addons['sendmail'] || manifest.addons['recvmail']) return iteratorDone();
db.runSql('UPDATE apps SET mailboxName=?, mailboxDomain=? WHERE id=?', [ null, null, app.id ], iteratorDone);
}, callback);
});
});
};
exports.down = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(128) NOT NULL', [], function (error) {
if (error) console.error(error);
callback(error);
});
};

View File

@@ -26,8 +26,11 @@ CREATE TABLE IF NOT EXISTS users(
fallbackEmail VARCHAR(512) DEFAULT "",
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
admin BOOLEAN DEFAULT false,
source VARCHAR(128) DEFAULT "",
role VARCHAR(32),
resetToken VARCHAR(128) DEFAULT "",
resetTokenCreationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
active BOOLEAN DEFAULT 1,
PRIMARY KEY(id));
@@ -52,15 +55,6 @@ CREATE TABLE IF NOT EXISTS tokens(
expires BIGINT NOT NULL, // FIXME: make this a timestamp
PRIMARY KEY(accessToken));
CREATE TABLE IF NOT EXISTS clients(
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
appId VARCHAR(128) NOT NULL, // name of the client (for external apps) or id of app (for built-in apps)
type VARCHAR(16) NOT NULL,
clientSecret VARCHAR(512) NOT NULL,
redirectURI VARCHAR(512) NOT NULL,
scope VARCHAR(512) NOT NULL,
PRIMARY KEY(id));
CREATE TABLE IF NOT EXISTS apps(
id VARCHAR(128) NOT NULL UNIQUE,
appStoreId VARCHAR(128) NOT NULL, // empty for custom apps
@@ -85,8 +79,8 @@ CREATE TABLE IF NOT EXISTS apps(
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
enableAutomaticUpdate BOOLEAN DEFAULT 1,
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
mailboxDomain VARCHAR(128) NOT NULL, // mailbox domain of this apps
mailboxName VARCHAR(128), // mailbox of this app
mailboxDomain VARCHAR(128), // mailbox domain of this apps
label VARCHAR(128), // display name
tagsJson VARCHAR(2048), // array of tags
dataDir VARCHAR(256) UNIQUE,
@@ -105,13 +99,6 @@ CREATE TABLE IF NOT EXISTS appPortBindings(
FOREIGN KEY(appId) REFERENCES apps(id),
PRIMARY KEY(hostPort));
CREATE TABLE IF NOT EXISTS authcodes(
authCode VARCHAR(128) NOT NULL UNIQUE,
userId VARCHAR(128) NOT NULL,
clientId VARCHAR(128) NOT NULL,
expiresAt BIGINT NOT NULL, // ## FIXME: make this a timestamp
PRIMARY KEY(authCode));
CREATE TABLE IF NOT EXISTS settings(
name VARCHAR(128) NOT NULL UNIQUE,
value TEXT,
@@ -158,7 +145,6 @@ CREATE TABLE IF NOT EXISTS domains(
provider VARCHAR(16) NOT NULL,
configJson TEXT, /* JSON containing the dns backend provider config */
tlsConfigJson TEXT, /* JSON containing the tls provider config */
locked BOOLEAN,
PRIMARY KEY (domain))
@@ -232,4 +218,16 @@ CREATE TABLE IF NOT EXISTS notifications(
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS appPasswords(
id VARCHAR(128) NOT NULL UNIQUE,
name VARCHAR(128) NOT NULL,
userId VARCHAR(128) NOT NULL,
identifier VARCHAR(128) NOT NULL, // resourceId: app id or mail or webadmin
hashedPassword VARCHAR(1024) NOT NULL,
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(userId) REFERENCES users(id),
UNIQUE (name, userId),
PRIMARY KEY (id)
);
CHARACTER SET utf8 COLLATE utf8_bin;

377
package-lock.json generated
View File

@@ -338,7 +338,7 @@
},
"amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
@@ -411,7 +411,7 @@
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
"assertion-error": {
@@ -483,17 +483,12 @@
},
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"resolved": false,
"integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=",
"requires": {
"precond": "0.2"
}
},
"bagpipe": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/bagpipe/-/bagpipe-0.3.5.tgz",
"integrity": "sha1-40HRZPyyTN8E6n4Ft2XsEMiupqE="
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
@@ -626,7 +621,7 @@
},
"buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
},
"buffer-fill": {
@@ -641,7 +636,7 @@
},
"bunyan": {
"version": "1.8.12",
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.12.tgz",
"resolved": false,
"integrity": "sha1-8VDw9nSKvdcq6uhPBEA74u8RN5c=",
"requires": {
"dtrace-provider": "~0.8",
@@ -741,18 +736,23 @@
}
},
"cloudron-manifestformat": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-4.0.0.tgz",
"integrity": "sha512-St/Quu8ofQOf0rUAMaIsOL0u0dZ46irweU8rYVMvAXU0CGwSD9KDaeLW5NjGRg3FVjNzladUDVUE/BGD4rwEvA==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-5.1.1.tgz",
"integrity": "sha512-1mArahTp9qkYRQsUJfpT/x6est1qW+gKPF+HoFU0hPuOVuBdMkfu6UUmwZDYmQF4FrbQkir46GyQAJADaXBg6g==",
"requires": {
"cron": "^1.7.2",
"cron": "^1.8.2",
"java-packagename-regex": "^1.0.0",
"safetydance": "0.7.1",
"semver": "^6.3.0",
"safetydance": "1.0.0",
"semver": "^7.1.3",
"tv4": "^1.3.0",
"validator": "^12.0.0"
"validator": "^12.2.0"
},
"dependencies": {
"semver": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA=="
},
"validator": {
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-12.2.0.tgz",
@@ -762,7 +762,7 @@
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"dev": true
},
@@ -807,7 +807,7 @@
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
@@ -872,11 +872,6 @@
}
}
},
"connect-ensure-login": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/connect-ensure-login/-/connect-ensure-login-0.1.1.tgz",
"integrity": "sha1-F03MUSQ7nqwj+NmCFa62aU4uihI="
},
"connect-lastmile": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-1.2.2.tgz",
@@ -944,20 +939,6 @@
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
},
"cookie": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
"integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
},
"cookie-parser": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz",
"integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==",
"requires": {
"cookie": "0.3.1",
"cookie-signature": "1.0.6"
}
},
"cookie-session": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/cookie-session/-/cookie-session-1.4.0.tgz",
@@ -1011,7 +992,7 @@
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cron": {
@@ -1037,46 +1018,6 @@
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
"integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4="
},
"csrf": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
"integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"requires": {
"rndm": "1.2.0",
"tsscmp": "1.0.6",
"uid-safe": "2.1.5"
}
},
"csurf": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/csurf/-/csurf-1.11.0.tgz",
"integrity": "sha512-UCtehyEExKTxgiu8UHdGvHj4tnpE/Qctue03Giq5gPgMQ9cg/ciod5blZQ5a4uCEenNQjxyGuzygLdKUmee/bQ==",
"requires": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"csrf": "3.1.0",
"http-errors": "~1.7.3"
},
"dependencies": {
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
}
}
},
"currently-unhandled": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
@@ -1093,7 +1034,7 @@
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"resolved": false,
"integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
"requires": {
"assert-plus": "^1.0.0"
@@ -1168,7 +1109,7 @@
},
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"resolved": false,
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"deep-eql": {
@@ -1435,7 +1376,7 @@
},
"ent": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
"resolved": false,
"integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0="
},
"error-ex": {
@@ -1539,7 +1480,7 @@
},
"expect.js": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz",
"resolved": false,
"integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=",
"dev": true
},
@@ -1600,51 +1541,6 @@
}
}
},
"express-session": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.0.tgz",
"integrity": "sha512-t4oX2z7uoSqATbMfsxWMbNjAL0T5zpvcJCk3Z9wnPPN7ibddhnmDZXHfEcoBMG2ojKXZoCyPMc5FbtK+G7SoDg==",
"requires": {
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.0",
"uid-safe": "~2.1.5"
},
"dependencies": {
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"safe-buffer": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -1778,16 +1674,6 @@
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"fs-extra": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
"integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
"requires": {
"graceful-fs": "^4.2.0",
"jsonfile": "^4.0.0",
"universalify": "^0.1.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -2381,7 +2267,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
@@ -2417,7 +2303,7 @@
},
"is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"resolved": false,
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
"dev": true
},
@@ -2499,12 +2385,12 @@
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"isstream": {
@@ -2579,14 +2465,6 @@
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
},
"jsonfile": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
"integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
"requires": {
"graceful-fs": "^4.1.6"
}
},
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
@@ -2637,11 +2515,6 @@
"tsscmp": "1.0.6"
}
},
"kruptein": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/kruptein/-/kruptein-2.0.5.tgz",
"integrity": "sha512-1WRXAil8d5jZ6jNXC0/pOC2lJsu2GhL3zhzL7DDvtCsef+NC1JvZUXnyNnM6Kuv0aan54moM/hsPvUAkx3y7/w=="
},
"lcid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
@@ -2653,7 +2526,7 @@
},
"ldap-filter": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.2.2.tgz",
"resolved": false,
"integrity": "sha1-8rhCvguG2jNSeYUFsx68rlkNd9A=",
"requires": {
"assert-plus": "0.1.5"
@@ -2661,7 +2534,7 @@
"dependencies": {
"assert-plus": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.1.5.tgz",
"resolved": false,
"integrity": "sha1-7nQAlBMALYTOxyGcasgRgS5yMWA="
}
}
@@ -2860,19 +2733,19 @@
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"mkdirp": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"requires": {
"minimist": "0.0.8"
@@ -3070,7 +2943,7 @@
},
"mv": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz",
"resolved": false,
"integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=",
"optional": true,
"requires": {
@@ -3081,7 +2954,7 @@
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"resolved": false,
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"optional": true,
"requires": {
@@ -3094,13 +2967,13 @@
},
"ncp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=",
"optional": true
},
"rimraf": {
"version": "2.4.5",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz",
"resolved": false,
"integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=",
"optional": true,
"requires": {
@@ -3388,7 +3261,7 @@
},
"nopt": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
"resolved": false,
"integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
"dev": true,
"requires": {
@@ -3437,7 +3310,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"dev": true
},
@@ -3446,35 +3319,11 @@
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"oauth2orize": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/oauth2orize/-/oauth2orize-1.11.0.tgz",
"integrity": "sha1-eTzvJR1F696sMq5AqLaBT6qx1IM=",
"requires": {
"debug": "2.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"dev": true
},
"object-inspect": {
"version": "1.7.0",
@@ -3621,7 +3470,7 @@
},
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
"resolved": false,
"integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
"dev": true,
"requires": {
@@ -3638,52 +3487,6 @@
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
},
"passport": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz",
"integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==",
"requires": {
"passport-strategy": "1.x.x",
"pause": "0.0.1"
}
},
"passport-http": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/passport-http/-/passport-http-0.3.0.tgz",
"integrity": "sha1-juU9Q4C+nGDfIVGSUCmCb3cRVgM=",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-http-bearer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/passport-http-bearer/-/passport-http-bearer-1.0.1.tgz",
"integrity": "sha1-FHRp6jZp4qhMYWfvmdu3fh8AmKg=",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-local": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz",
"integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-oauth2-client-password": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/passport-oauth2-client-password/-/passport-oauth2-client-password-0.1.2.tgz",
"integrity": "sha1-TzeLZ4uS0W270jOmxwZSAJPlYbo=",
"requires": {
"passport-strategy": "1.x.x"
}
},
"passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ="
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
@@ -3691,7 +3494,7 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-key": {
@@ -3734,11 +3537,6 @@
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
"dev": true
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
},
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
@@ -3786,7 +3584,7 @@
},
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"resolved": false,
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"pretty-bytes": {
@@ -3854,7 +3652,7 @@
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=",
"dev": true
},
@@ -4122,7 +3920,7 @@
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"resolved": false,
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
},
"require-main-filename": {
@@ -4138,11 +3936,6 @@
"path-parse": "^1.0.6"
}
},
"retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
"integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs="
},
"retry-request": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/retry-request/-/retry-request-4.1.1.tgz",
@@ -4165,11 +3958,6 @@
"glob": "^7.1.3"
}
},
"rndm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz",
"integrity": "sha1-8z/pz7Urv9UgqhgyO8ZdsRCht2w="
},
"s3-block-read-stream": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/s3-block-read-stream/-/s3-block-read-stream-0.5.0.tgz",
@@ -4192,9 +3980,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"safetydance": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.7.1.tgz",
"integrity": "sha1-FOtNQqHKr8UUVVK2zmnJuIJN0qo="
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-1.0.0.tgz",
"integrity": "sha512-ji9T/p5poiGgx4N3OFORGChAS9L8YL8S0/RpYvEPmQucCPrzmQMfdzbjFG/4+0BhJUvNA19KZUVj3YE8gkLpjA=="
},
"sass-graph": {
"version": "2.2.4",
@@ -4404,34 +4192,9 @@
"send": "0.17.1"
}
},
"session-file-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/session-file-store/-/session-file-store-1.4.0.tgz",
"integrity": "sha512-jBeQwoHoHMOBoELxf+f/IDtMI94PqkFrwCP6O3DlvJlsCU6gY6pgWJGxUOtLWxauIdvAqmgoAbpJi7vJih9bvA==",
"requires": {
"bagpipe": "^0.3.5",
"fs-extra": "^8.0.1",
"kruptein": "^2.0.4",
"object-assign": "^4.1.1",
"retry": "^0.12.0",
"write-file-atomic": "1.3.1"
},
"dependencies": {
"write-file-atomic": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.1.tgz",
"integrity": "sha1-fUW6MjFjKN0ex9kPYOvA2EW7dZo=",
"requires": {
"graceful-fs": "^4.1.11",
"imurmurhash": "^0.1.4",
"slide": "^1.1.5"
}
}
}
},
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
},
"setprototypeof": {
@@ -4491,14 +4254,9 @@
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
},
"slide": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
"integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc="
},
"smtp-connection": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz",
@@ -4590,7 +4348,7 @@
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"resolved": false,
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
},
"sqlstring": {
@@ -4757,7 +4515,7 @@
},
"stubs": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz",
"resolved": false,
"integrity": "sha1-6NK6H6nJBXAwPAMLaQD31fiavls="
},
"superagent": {
@@ -5054,7 +4812,7 @@
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"resolved": false,
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uid-safe": {
@@ -5065,11 +4823,6 @@
"random-bytes": "~1.0.0"
}
},
"uid2": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz",
"integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I="
},
"underscore": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.2.tgz",
@@ -5083,11 +4836,6 @@
"crypto-random-string": "^1.0.0"
}
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -5136,7 +4884,7 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utile": {
@@ -5169,11 +4917,6 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"valid-url": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz",
"integrity": "sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA="
},
"validate-npm-package-license": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -5196,7 +4939,7 @@
},
"vasync": {
"version": "1.6.4",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-1.6.4.tgz",
"resolved": false,
"integrity": "sha1-3+k2Fq0OeugBszKp2Iv8XNyOHR8=",
"requires": {
"verror": "1.6.0"
@@ -5204,7 +4947,7 @@
"dependencies": {
"verror": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.6.0.tgz",
"resolved": false,
"integrity": "sha1-fROyex+swuLakEBetepuW90lLqU=",
"requires": {
"extsprintf": "1.2.0"
@@ -5214,7 +4957,7 @@
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"resolved": false,
"integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
"requires": {
"assert-plus": "^1.0.0",
@@ -5237,7 +4980,7 @@
},
"which-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
"resolved": false,
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
},
"wide-align": {
@@ -5324,7 +5067,7 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"write-file-atomic": {

View File

@@ -20,68 +20,56 @@
"async": "^2.6.3",
"aws-sdk": "^2.610.0",
"body-parser": "^1.19.0",
"cloudron-manifestformat": "^4.0.0",
"cloudron-manifestformat": "^5.1.1",
"connect": "^3.7.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^1.2.1",
"connect-lastmile": "^1.2.2",
"connect-timeout": "^1.9.0",
"cookie-parser": "^1.4.4",
"cookie-session": "^1.3.3",
"cron": "^1.7.1",
"csurf": "^1.10.0",
"cookie-session": "^1.4.0",
"cron": "^1.8.2",
"db-migrate": "^0.11.6",
"db-migrate-mysql": "^1.1.10",
"debug": "^4.1.1",
"dockerode": "^2.5.8",
"ejs": "^2.6.1",
"ejs-cli": "^2.0.1",
"ejs-cli": "^2.1.1",
"express": "^4.17.1",
"express-session": "^1.16.2",
"js-yaml": "^3.13.1",
"json": "^9.0.6",
"ldapjs": "^1.0.2",
"lodash": "^4.17.11",
"lodash": "^4.17.15",
"lodash.chunk": "^4.2.0",
"mime": "^2.4.4",
"moment-timezone": "^0.5.25",
"moment-timezone": "^0.5.27",
"morgan": "^1.9.1",
"multiparty": "^4.2.1",
"mysql": "^2.17.1",
"nodemailer": "^6.2.1",
"mysql": "^2.18.1",
"nodemailer": "^6.4.2",
"nodemailer-smtp-transport": "^2.7.4",
"oauth2orize": "^1.11.0",
"once": "^1.4.0",
"parse-links": "^0.1.0",
"passport": "^0.4.0",
"passport-http": "^0.3.0",
"passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2",
"pretty-bytes": "^5.3.0",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.15.0",
"qrcode": "^1.3.3",
"readdirp": "^3.0.2",
"qrcode": "^1.4.4",
"readdirp": "^3.3.0",
"request": "^2.88.0",
"rimraf": "^2.6.3",
"s3-block-read-stream": "^0.5.0",
"safetydance": "^0.7.1",
"safetydance": "^1.0.0",
"semver": "^6.1.1",
"session-file-store": "^1.3.1",
"showdown": "^1.9.0",
"showdown": "^1.9.1",
"speakeasy": "^2.0.0",
"split": "^1.0.1",
"superagent": "^5.0.9",
"superagent": "^5.2.1",
"supererror": "^0.7.2",
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
"tar-stream": "^2.1.0",
"tldjs": "^2.3.1",
"underscore": "^1.9.1",
"uuid": "^3.3.2",
"valid-url": "^1.0.9",
"underscore": "^1.9.2",
"uuid": "^3.4.0",
"validator": "^11.0.0",
"ws": "^7.0.0",
"xml2js": "^0.4.19"
"ws": "^7.2.1",
"xml2js": "^0.4.23"
},
"devDependencies": {
"expect.js": "*",

View File

@@ -92,7 +92,7 @@ fi
echo "Running cloudron-setup with args : $@" > "${LOG_FILE}"
# validate arguments in the absence of data
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
if [[ -z "${provider}" ]]; then
echo "--provider is required ($AVAILABLE_PROVIDERS)"
exit 1
@@ -107,13 +107,13 @@ elif [[ \
"${provider}" != "digitalocean-mp" && \
"${provider}" != "ec2" && \
"${provider}" != "exoscale" && \
"${provider}" != "galaxygate" && \
"${provider}" != "gce" && \
"${provider}" != "hetzner" && \
"${provider}" != "interox" && \
"${provider}" != "interox-image" && \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "linode-oneclick" && \
"${provider}" != "linode-stackscript" && \
"${provider}" != "netcup" && \
"${provider}" != "netcup-image" && \
@@ -203,21 +203,11 @@ if [[ "${initBaseImage}" == "true" ]]; then
echo ""
fi
# NOTE: this install script only supports 3.x and above
# NOTE: this install script only supports 4.2 and above
echo "=> Installing version ${version} (this takes some time) ..."
mkdir -p /etc/cloudron
# this file is used >= 4.2
echo "${provider}" > /etc/cloudron/PROVIDER
# this file is unused <= 4.2 and exists to make legacy installations work. the start script will remove this file anyway
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
{
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"provider": "${provider}"
}
CONF_END
[[ -n "${license}" ]] && echo -n "$license" > /etc/cloudron/LICENSE
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
@@ -225,7 +215,6 @@ if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
exit 1
fi
# only needed for >= 4.2
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('api_server_origin', '${apiServerOrigin}');" 2>/dev/null
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web_server_origin', '${webServerOrigin}');" 2>/dev/null
@@ -238,7 +227,10 @@ while true; do
sleep 10
done
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
if ! ip=$(curl --fail --connect-timeout 2 --max-time 2 -q https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
ip='<IP>'
fi
echo -e "\n\n${GREEN}Visit https://${ip} and accept the self-signed certificate to finish setup.${DONE}\n"
if [[ "${rebootServer}" == "true" ]]; then
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables

View File

@@ -13,7 +13,7 @@ HELP_MESSAGE="
This script collects diagnostic information to help debug server related issues
Options:
--admin-login Login as administrator
--owner-login Login as owner
--enable-ssh Enable SSH access for the Cloudron support team
--help Show this message
"
@@ -26,7 +26,7 @@ fi
enableSSH="false"
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -34,10 +34,15 @@ while true; do
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
--enable-ssh) enableSSH="true"; shift;;
--admin-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE admin=1 LIMIT 1" 2>/dev/null)
# fall through
;&
--owner-login)
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' LIMIT 1" 2>/dev/null)
admin_password=$(pwgen -1s 12)
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > /tmp/cloudron_ghost.json
echo "Login as ${admin_username} / ${admin_password} . Remove /tmp/cloudron_ghost.json when done."
ghost_file=/home/yellowtent/platformdata/cloudron_ghost.json
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > "${ghost_file}"
chown yellowtent:yellowtent "${ghost_file}" && chmod o-r,g-r "${ghost_file}"
echo "Login as ${admin_username} / ${admin_password} . Remove ${ghost_file} when done."
exit 0
;;
--) break;;
@@ -80,13 +85,13 @@ echo -e $LINE"Filesystem stats"$LINE >> $OUT
df -h &>> $OUT
echo -e $LINE"Appsdata stats"$LINE >> $OUT
du -hcsL /home/yellowtent/appsdata/* &>> $OUT
du -hcsL /home/yellowtent/appsdata/* &>> $OUT || true
echo -e $LINE"Boxdata stats"$LINE >> $OUT
du -hcsL /home/yellowtent/boxdata/* &>> $OUT
echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
du -hcsL /var/backups/* &>> $OUT
du -hcsL /var/backups/* &>> $OUT || true
echo -e $LINE"System daemon status"$LINE >> $OUT
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT

View File

@@ -56,6 +56,15 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
fi
readonly nginx_version=$(nginx -v)
if [[ "${nginx_version}" != *"1.14."* && "${ubuntu_version}" == "16.04" ]]; then
echo "==> installer: installing nginx for xenial for TLSv3 support"
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.14.0-1~xenial_amd64.deb -o /tmp/nginx.deb
# apt install with install deps (as opposed to dpkg -i)
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
rm /tmp/nginx.deb
fi
echo "==> installer: updating node"
if [[ "$(node --version)" != "v10.18.1" ]]; then
mkdir -p /usr/local/node-10.18.1

View File

@@ -47,7 +47,8 @@ mkdir -p "${PLATFORM_DATA_DIR}/backup"
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
"${PLATFORM_DATA_DIR}/logs/updater" \
"${PLATFORM_DATA_DIR}/logs/tasks" \
"${PLATFORM_DATA_DIR}/logs/crash"
"${PLATFORM_DATA_DIR}/logs/crash" \
"${PLATFORM_DATA_DIR}/logs/collectd"
mkdir -p "${PLATFORM_DATA_DIR}/update"
mkdir -p "${BOX_DATA_DIR}/appicons"
@@ -114,7 +115,7 @@ rm -f /etc/sudoers.d/${USER}
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
echo "==> Configuring collectd"
rm -rf /etc/collectd
rm -rf /etc/collectd /var/log/collectd.log
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
cp "${script_dir}/start/collectd/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
systemctl restart collectd

View File

@@ -12,6 +12,11 @@ iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
# ssh is allowed alternately on port 202
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
# turn and stun service
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:51000 -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT

View File

@@ -3,10 +3,17 @@
printf "**********************************************************************\n\n"
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
if [[ -f /tmp/.cloudron-motd-cache ]]; then
ip=$(cat /tmp/.cloudron-motd-cache)
elif ! ip=$(curl --fail --connect-timeout 2 --max-time 2 -q https://api.cloudron.io/api/v1/helper/public_ip | sed -n -e 's/.*"ip": "\(.*\)"/\1/p'); then
ip='<IP>'
fi
echo "${ip}" > /tmp/.cloudron-motd-cache
printf "\t\t\tWELCOME TO CLOUDRON\n"
printf "\t\t\t-------------------\n"
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://<IP> on your browser and accept the self-signed certificate to finish setup."
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://${ip} on your browser and accept the self-signed certificate to finish setup."
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
else

View File

@@ -57,7 +57,7 @@ LoadPlugin logfile
<Plugin logfile>
LogLevel "info"
File "/var/log/collectd.log"
File "/home/yellowtent/platformdata/logs/collectd/collectd.log"
Timestamp true
PrintSeverity false
</Plugin>

View File

@@ -6,7 +6,8 @@ PATHS = [] # { name, dir, exclude }
INTERVAL = 60 * 60 * 12 # twice a day. change values in docker-graphite if you change this
def du(pathinfo):
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
# -B1 makes du print block sizes and not apparent sizes (to match df which also uses block sizes)
cmd = 'timeout 1800 du -DsB1 "{}"'.format(pathinfo['dir'])
if pathinfo['exclude'] != '':
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
@@ -26,6 +27,7 @@ def parseSize(size):
def dockerSize():
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
# https://godoc.org/github.com/docker/go-units#HumanSize is used. so it's 1000 (KB) and not 1024 (KiB)
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
try:
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')

View File

@@ -1,40 +0,0 @@
# add customizations here
# after making changes run "sudo systemctl restart box"
# appstore:
# blacklist:
# - io.wekan.cloudronapp
# - io.cloudron.openvpn
# whitelist:
# org.wordpress.cloudronapp: {}
# chat.rocket.cloudronapp: {}
# com.nextcloud.cloudronapp: {}
#
# backups:
# configurable: true
#
# domains:
# dynamicDns: true
# changeDashboardDomain: true
#
# subscription:
# configurable: true
#
# support:
# email: support@cloudron.io
# remoteSupport: true
#
# ticketFormBody: |
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
# * [Forum](https://forum.cloudron.io/)
#
# submitTickets: true
#
# alerts:
# email: support@cloudron.io
# notifyCloudronAdmins: false
#
# footer:
# body: '&copy; 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'

View File

@@ -9,6 +9,7 @@
/home/yellowtent/platformdata/logs/sftp/*.log
/home/yellowtent/platformdata/logs/redis-*/*.log
/home/yellowtent/platformdata/logs/crash/*.log
/home/yellowtent/platformdata/logs/collectd/*.log
/home/yellowtent/platformdata/logs/updater/*.log {
# only keep one rotated file, we currently do not send that over the api
rotate 1

View File

@@ -1,140 +1,29 @@
'use strict';
exports = module.exports = {
SCOPE_APPS_READ: 'apps:read',
SCOPE_APPS_MANAGE: 'apps:manage',
SCOPE_APPSTORE: 'appstore',
SCOPE_CLIENTS: 'clients',
SCOPE_CLOUDRON: 'cloudron',
SCOPE_DOMAINS_READ: 'domains:read',
SCOPE_DOMAINS_MANAGE: 'domains:manage',
SCOPE_MAIL: 'mail',
SCOPE_PROFILE: 'profile',
SCOPE_SETTINGS: 'settings',
SCOPE_SUBSCRIPTION: 'subscription',
SCOPE_USERS_READ: 'users:read',
SCOPE_USERS_MANAGE: 'users:manage',
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'subscription', 'users' ], // keep this sorted
SCOPE_ANY: '*',
validateScopeString: validateScopeString,
hasScopes: hasScopes,
canonicalScopeString: canonicalScopeString,
intersectScopes: intersectScopes,
validateToken: validateToken,
scopesForUser: scopesForUser
verifyToken: verifyToken
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:accesscontrol'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
_ = require('underscore');
users = require('./users.js');
// returns scopes that does not have wildcards and is sorted
function canonicalScopeString(scope) {
if (scope === exports.SCOPE_ANY) return exports.VALID_SCOPES.join(',');
return scope.split(',').sort().join(',');
}
function intersectScopes(allowedScopes, wantedScopes) {
assert(Array.isArray(allowedScopes), 'Expecting sorted array');
assert(Array.isArray(wantedScopes), 'Expecting sorted array');
if (_.isEqual(allowedScopes, wantedScopes)) return allowedScopes; // quick path
let wantedScopesMap = new Map();
let results = [];
// make a map of scope -> [ subscopes ]
for (let w of wantedScopes) {
let parts = w.split(':');
let subscopes = wantedScopesMap.get(parts[0]) || new Set();
subscopes.add(parts[1] || '*');
wantedScopesMap.set(parts[0], subscopes);
}
for (let a of allowedScopes) {
let parts = a.split(':');
let as = parts[1] || '*';
let subscopes = wantedScopesMap.get(parts[0]);
if (!subscopes) continue;
if (subscopes.has('*') || subscopes.has(as)) {
results.push(a);
} else if (as === '*') {
results = results.concat(Array.from(subscopes).map(function (ss) { return `${a}:${ss}`; }));
}
}
return results;
}
function validateScopeString(scope) {
assert.strictEqual(typeof scope, 'string');
if (scope === '') return new BoxError(BoxError.BAD_FIELD, 'Empty scope not allowed', { field: 'scope' });
// NOTE: this function intentionally does not allow '*'. This is only allowed in the db to allow
// us not write a migration script every time we add a new scope
var allValid = scope.split(',').every(function (s) { return exports.VALID_SCOPES.indexOf(s.split(':')[0]) !== -1; });
if (!allValid) return new BoxError(BoxError.BAD_FIELD, 'Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '), { field: 'scope' });
return null;
}
// tests if all requiredScopes are attached to the request
function hasScopes(authorizedScopes, requiredScopes) {
assert(Array.isArray(authorizedScopes), 'Expecting array');
assert(Array.isArray(requiredScopes), 'Expecting array');
if (authorizedScopes.indexOf(exports.SCOPE_ANY) !== -1) return null;
for (var i = 0; i < requiredScopes.length; ++i) {
const scopeParts = requiredScopes[i].split(':');
// this allows apps:write if the token has a higher apps scope
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
debug('scope: missing scope "%s".', requiredScopes[i]);
return new BoxError(BoxError.NOT_FOUND, 'Missing required scope "' + requiredScopes[i] + '"');
}
}
return null;
}
function scopesForUser(user, callback) {
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
if (user.admin) return callback(null, exports.VALID_SCOPES);
callback(null, [ 'profile', 'apps:read' ]);
}
function validateToken(accessToken, callback) {
function verifyToken(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.getByAccessToken(accessToken, function (error, token) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error) return callback(error); // this triggers 'internal error' in passport
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(error);
users.get(token.identifier, function (error, user) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
if (error) return callback(error);
if (!user.active) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
if (!user.active) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
const authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
callback(null, user, { authorizedScopes }); // ends up in req.authInfo
});
callback(null, user);
});
});
}

View File

@@ -20,22 +20,18 @@ exports = module.exports = {
getMountsSync: getMountsSync,
getContainerNamesSync: getContainerNamesSync,
// exported for testing
_setupOauth: setupOauth,
_teardownOauth: teardownOauth,
getServiceDetails: getServiceDetails,
SERVICE_STATUS_STARTING: 'starting', // container up, waiting for healthcheck
SERVICE_STATUS_ACTIVE: 'active',
SERVICE_STATUS_STOPPED: 'stopped'
};
var accesscontrol = require('./accesscontrol.js'),
appdb = require('./appdb.js'),
var appdb = require('./appdb.js'),
apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
clients = require('./clients.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
debug = require('debug')('box:addons'),
@@ -67,6 +63,13 @@ const RMADDONDIR_CMD = path.join(__dirname, 'scripts/rmaddondir.sh');
// setup can be called multiple times for the same app (configure crash restart) and existing data must not be lost
// teardown is destructive. app data stored with the addon is lost
var KNOWN_ADDONS = {
turn: {
setup: setupTurn,
teardown: teardownTurn,
backup: NOOP,
restore: NOOP,
clear: NOOP
},
email: {
setup: setupEmail,
teardown: teardownEmail,
@@ -102,13 +105,6 @@ var KNOWN_ADDONS = {
restore: restoreMySql,
clear: clearMySql,
},
oauth: {
setup: setupOauth,
teardown: teardownOauth,
backup: NOOP,
restore: setupOauth,
clear: NOOP,
},
postgresql: {
setup: setupPostgreSql,
teardown: teardownPostgreSql,
@@ -154,6 +150,11 @@ var KNOWN_ADDONS = {
};
const KNOWN_SERVICES = {
turn: {
status: statusTurn,
restart: restartContainer.bind(null, 'turn'),
defaultMemoryLimit: 256 * 1024 * 1024
},
mail: {
status: containerStatus.bind(null, 'mail', 'CLOUDRON_MAIL_TOKEN'),
restart: mail.restartMail,
@@ -237,6 +238,7 @@ function rebuildService(serviceName, callback) {
// this attempts to recreate the service docker container if they don't exist but platform infra version is unchanged
// passing an infra version of 'none' will not attempt to purge existing data, not sure if this is good or bad
if (serviceName === 'turn') return startTurn({ version: 'none' }, callback);
if (serviceName === 'mongodb') return startMongodb({ version: 'none' }, callback);
if (serviceName === 'postgresql') return startPostgresql({ version: 'none' }, callback);
if (serviceName === 'mysql') return startMysql({ version: 'none' }, callback);
@@ -650,6 +652,7 @@ function startServices(existingInfra, callback) {
if (existingInfra.version !== infra.version) {
debug(`startServices: ${existingInfra.version} -> ${infra.version}. starting all services`);
startFuncs.push(
startTurn.bind(null, existingInfra),
startMysql.bind(null, existingInfra),
startPostgresql.bind(null, existingInfra),
startMongodb.bind(null, existingInfra),
@@ -658,6 +661,7 @@ function startServices(existingInfra, callback) {
} else {
assert.strictEqual(typeof existingInfra.images, 'object');
if (!existingInfra.images.turn || infra.images.turn.tag !== existingInfra.images.turn.tag) startFuncs.push(startTurn.bind(null, existingInfra));
if (infra.images.mysql.tag !== existingInfra.images.mysql.tag) startFuncs.push(startMysql.bind(null, existingInfra));
if (infra.images.postgresql.tag !== existingInfra.images.postgresql.tag) startFuncs.push(startPostgresql.bind(null, existingInfra));
if (infra.images.mongodb.tag !== existingInfra.images.mongodb.tag) startFuncs.push(startMongodb.bind(null, existingInfra));
@@ -677,7 +681,7 @@ function getEnvironment(app, callback) {
appdb.getAddonConfigByAppId(app.id, function (error, result) {
if (error) return callback(error);
if (app.manifest.addons['docker']) result.push({ name: 'DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
if (app.manifest.addons['docker']) result.push({ name: 'CLOUDRON_DOCKER_HOST', value: `tcp://172.18.0.1:${constants.DOCKER_PROXY_PORT}` });
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
});
@@ -768,52 +772,37 @@ function teardownLocalStorage(app, options, callback) {
], callback);
}
function setupOauth(app, options, callback) {
function setupTurn(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'setupOauth');
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
if (!turnSecret) console.error('No turn secret set. Will leave emtpy, but this is a problem!');
if (!app.sso) return callback(null);
const env = [
{ name: 'CLOUDRON_STUN_SERVER', value: settings.adminFqdn() },
{ name: 'CLOUDRON_STUN_PORT', value: '3478' },
{ name: 'CLOUDRON_STUN_TLS_PORT', value: '5349' },
{ name: 'CLOUDRON_TURN_SERVER', value: settings.adminFqdn() },
{ name: 'CLOUDRON_TURN_PORT', value: '3478' },
{ name: 'CLOUDRON_TURN_TLS_PORT', value: '5349' },
{ name: 'CLOUDRON_TURN_SECRET', value: turnSecret }
];
var appId = app.id;
var redirectURI = 'https://' + app.fqdn;
var scope = accesscontrol.SCOPE_PROFILE;
debugApp(app, 'Setting up TURN');
clients.delByAppIdAndType(appId, clients.TYPE_OAUTH, function (error) { // remove existing creds
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
clients.add(appId, clients.TYPE_OAUTH, redirectURI, scope, function (error, result) {
if (error) return callback(error);
const envPrefix = app.manifest.manifestVersion <= 1 ? '' : 'CLOUDRON_';
var env = [
{ name: `${envPrefix}OAUTH_CLIENT_ID`, value: result.id },
{ name: `${envPrefix}OAUTH_CLIENT_SECRET`, value: result.clientSecret },
{ name: `${envPrefix}OAUTH_ORIGIN`, value: settings.adminOrigin() }
];
debugApp(app, 'Setting oauth addon config to %j', env);
appdb.setAddonConfig(appId, 'oauth', env, callback);
});
});
appdb.setAddonConfig(app.id, 'turn', env, callback);
}
function teardownOauth(app, options, callback) {
function teardownTurn(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'teardownOauth');
debugApp(app, 'Tearing down TURN');
clients.delByAppIdAndType(app.id, clients.TYPE_OAUTH, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) debug(error);
appdb.unsetAddonConfig(app.id, 'oauth', callback);
});
appdb.unsetAddonConfig(app.id, 'turn', callback);
}
function setupEmail(app, options, callback) {
@@ -1369,6 +1358,43 @@ function restorePostgreSql(app, options, callback) {
});
}
function startTurn(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
// get and ensure we have a turn secret
var turnSecret = safe.fs.readFileSync(paths.ADDON_TURN_SECRET_FILE, 'utf8');
if (!turnSecret) {
turnSecret = 'a' + crypto.randomBytes(15).toString('hex'); // prefix with a to ensure string starts with a letter
safe.fs.writeFileSync(paths.ADDON_TURN_SECRET_FILE, turnSecret, 'utf8');
}
const tag = infra.images.turn.tag;
const memoryLimit = 256;
const realm = settings.adminFqdn();
if (existingInfra.version === infra.version && existingInfra.images.turn && infra.images.turn.tag === existingInfra.images.turn.tag) return callback();
// this exports 3478/tcp, 5349/tls and 50000-51000/udp
const cmd = `docker run --restart=always -d --name="turn" \
--hostname turn \
--net host \
--log-driver syslog \
--log-opt syslog-address=udp://127.0.0.1:2514 \
--log-opt syslog-format=rfc5424 \
--log-opt tag=turn \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-e CLOUDRON_TURN_SECRET="${turnSecret}" \
-e CLOUDRON_REALM="${realm}" \
--label isCloudronManaged=true \
--read-only -v /tmp -v /run "${tag}"`;
shell.exec('startTurn', cmd, callback);
}
function startMongodb(existingInfra, callback) {
assert.strictEqual(typeof existingInfra, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -1728,6 +1754,27 @@ function restoreRedis(app, options, callback) {
});
}
function statusTurn(callback) {
assert.strictEqual(typeof callback, 'function');
docker.inspect('turn', function (error, container) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, { status: exports.SERVICE_STATUS_STOPPED });
if (error) return callback(error);
docker.memoryUsage(container.Id, function (error, result) {
if (error) return callback(error);
var tmp = {
status: container.State.Running ? exports.SERVICE_STATUS_ACTIVE : exports.SERVICE_STATUS_STOPPED,
memoryUsed: result.memory_stats.usage,
memoryPercent: parseInt(100 * result.memory_stats.usage / result.memory_stats.limit)
};
callback(null, tmp);
});
});
}
function statusDocker(callback) {
assert.strictEqual(typeof callback, 'function');

View File

@@ -349,12 +349,13 @@ function del(id, callback) {
{ 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 apps WHERE id = ?', args: [ id ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (results[3].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
if (results[4].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
callback(null);
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
'use strict';
exports = module.exports = {
getFeatures: getFeatures,
getApps: getApps,
getApp: getApp,
getAppVersion: getAppVersion,
@@ -14,6 +16,7 @@ exports = module.exports = {
purchaseApp: purchaseApp,
unpurchaseApp: unpurchaseApp,
getUserToken: getUserToken,
getSubscription: getSubscription,
isFreePlan: isFreePlan,
@@ -30,13 +33,13 @@ var apps = require('./apps.js'),
async = require('async'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
custom = require('./custom.js'),
debug = require('debug')('box:appstore'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
groups = require('./groups.js'),
mail = require('./mail.js'),
os = require('os'),
paths = require('./paths.js'),
safe = require('safetydance'),
semver = require('semver'),
settings = require('./settings.js'),
@@ -46,6 +49,38 @@ var apps = require('./apps.js'),
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// These are the default options and will be adjusted once a subscription state is obtained
// Keep in sync with appstore/routes/cloudrons.js
let gFeatures = {
userMaxCount: null,
externalLdap: true,
eventLog: true,
privateDockerRegistry: true,
branding: true,
userManager: true,
multiAdmin: true,
support: true
};
// attempt to load feature cache in case appstore would be down
let tmp = safe.JSON.parse(safe.fs.readFileSync(paths.FEATURES_INFO_FILE, 'utf8'));
if (tmp) gFeatures = tmp;
function getFeatures() {
return gFeatures;
}
function isAppAllowed(appstoreId, listingConfig) {
assert.strictEqual(typeof listingConfig, 'object');
assert.strictEqual(typeof appstoreId, 'string');
if (listingConfig.blacklist && listingConfig.blacklist.includes(appstoreId)) return false;
if (listingConfig.whitelist) return listingConfig.whitelist.includes(appstoreId);
return true;
}
function getCloudronToken(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -99,6 +134,25 @@ function registerUser(email, password, callback) {
});
}
function getUserToken(callback) {
assert.strictEqual(typeof callback, 'function');
if (settings.isDemo()) return callback(new BoxError(BoxError.BAD_FIELD, 'Not allowed in demo mode'));
getCloudronToken(function (error, token) {
if (error) return callback(error);
const url = `${settings.apiServerOrigin()}/api/v1/user_token`;
superagent.post(url).send({}).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `getUserToken status code: ${result.status}`));
callback(null, result.body.accessToken);
});
});
}
function getSubscription(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -113,7 +167,11 @@ function getSubscription(callback) {
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}`));
callback(null, result.body); // { email, subscription }
// update the features cache
gFeatures = result.body.features;
safe.fs.writeFileSync(paths.FEATURES_INFO_FILE, JSON.stringify(gFeatures), 'utf8');
callback(null, result.body);
});
});
}
@@ -471,7 +529,7 @@ function createTicket(info, auditSource, callback) {
let url = settings.apiServerOrigin() + '/api/v1/ticket';
info.supportEmail = custom.spec().support.email; // destination address for tickets
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
@@ -481,7 +539,7 @@ function createTicket(info, auditSource, callback) {
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
callback(null);
callback(null, { message: `An email for sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
});
});
});
@@ -504,7 +562,13 @@ function getApps(callback) {
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)));
callback(null, result.body.apps);
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return callback(error);
const filteredApps = result.body.apps.filter(app => isAppAllowed(app.id, listingConfig));
callback(null, filteredApps);
});
});
});
});
@@ -515,20 +579,26 @@ function getAppVersion(appId, version, callback) {
assert.strictEqual(typeof version, 'string');
assert.strictEqual(typeof callback, 'function');
getCloudronToken(function (error, token) {
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return callback(error);
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
if (!isAppAllowed(appId, listingConfig)) return callback(new BoxError(BoxError.FEATURE_DISABLED));
superagent.get(url).query({ accessToken: token }).timeout(10 * 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)));
getCloudronToken(function (error, token) {
if (error) return callback(error);
callback(null, result.body);
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
if (version !== 'latest') url += `/versions/${version}`;
superagent.get(url).query({ accessToken: token }).timeout(10 * 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)));
callback(null, result.body);
});
});
});
}

View File

@@ -27,6 +27,7 @@ var addons = require('./addons.js'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
collectd = require('./collectd.js'),
constants = require('./constants.js'),
debug = require('debug')('box:apptask'),
df = require('@sindresorhus/df'),
@@ -51,8 +52,7 @@ var addons = require('./addons.js'),
util = require('util'),
_ = require('underscore');
const COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
const COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd/app.ejs', { encoding: 'utf8' }),
MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'),
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
@@ -229,29 +229,14 @@ function addCollectdProfile(app, callback) {
assert.strictEqual(typeof callback, 'function');
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId, appDataDir: apps.getDataDir(app, app.dataDir) });
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error writing collectd config: ${error.message}`));
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Could not add collectd config'));
callback(null);
});
});
collectd.addProfile(app.id, collectdConf, callback);
}
function removeCollectdProfile(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, function (error) {
if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Could not remove collectd config'));
callback(null);
});
});
collectd.removeProfile(app.id, callback);
}
function addLogrotateConfig(app, callback) {
@@ -446,12 +431,12 @@ function waitForDnsPropagation(app, callback) {
sysinfo.getServerIp(function (error, ip) {
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`));
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 }, function (error) {
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: app.location, domain: app.domain }));
// now wait for alternateDomains, if any
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 }, function (error) {
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: domain.subdomain, domain: domain.domain }));
iteratorCallback();
@@ -791,9 +776,6 @@ function configure(app, args, progressCallback, callback) {
startApp.bind(null, app),
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
configureReverseProxy.bind(null, app),

View File

@@ -1,78 +0,0 @@
/* jslint node:true */
'use strict';
exports = module.exports = {
get: get,
add: add,
del: del,
delExpired: delExpired,
_clear: clear
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js');
var AUTHCODES_FIELDS = [ 'authCode', 'userId', 'clientId', 'expiresAt' ].join(',');
function get(authCode, callback) {
assert.strictEqual(typeof authCode, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + AUTHCODES_FIELDS + ' FROM authcodes WHERE authCode = ? AND expiresAt > ?', [ authCode, Date.now() ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Authcode not found'));
callback(null, result[0]);
});
}
function add(authCode, clientId, userId, expiresAt, callback) {
assert.strictEqual(typeof authCode, 'string');
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO authcodes (authCode, clientId, userId, expiresAt) VALUES (?, ?, ?, ?)',
[ authCode, clientId, userId, expiresAt ], function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function del(authCode, callback) {
assert.strictEqual(typeof authCode, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM authcodes WHERE authCode = ?', [ authCode ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Authcode not found'));
callback(null);
});
}
function delExpired(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM authcodes WHERE expiresAt <= ?', [ Date.now() ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
return callback(null, result.affectedRows);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM authcodes', function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}

View File

@@ -30,6 +30,8 @@ exports = module.exports = {
checkConfiguration: checkConfiguration,
configureCollectd: configureCollectd,
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
// for testing
@@ -44,12 +46,14 @@ var addons = require('./addons.js'),
assert = require('assert'),
backupdb = require('./backupdb.js'),
BoxError = require('./boxerror.js'),
collectd = require('./collectd.js'),
constants = require('./constants.js'),
crypto = require('crypto'),
database = require('./database.js'),
DataLayout = require('./datalayout.js'),
debug = require('debug')('box:backups'),
df = require('@sindresorhus/df'),
ejs = require('ejs'),
eventlog = require('./eventlog.js'),
fs = require('fs'),
locker = require('./locker.js'),
@@ -69,6 +73,7 @@ var addons = require('./addons.js'),
zlib = require('zlib');
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
const COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd/cloudron-backup.ejs', { encoding: 'utf8' });
function debugApp(app) {
assert(typeof app === 'object');
@@ -88,6 +93,7 @@ function api(provider) {
case 'exoscale-sos': return require('./storage/s3.js');
case 'wasabi': return require('./storage/s3.js');
case 'scaleway-objectstorage': return require('./storage/s3.js');
case 'linode-objectstorage': return require('./storage/s3.js');
case 'noop': return require('./storage/noop.js');
default: return null;
}
@@ -722,8 +728,8 @@ function runBackupUpload(uploadConfig, progressCallback, callback) {
}
callback();
}).on('message', function (progress) { // script sends either 'message' or 'result' property
if (!progress.result) return progressCallback({ message: `${progress.message} (${progressTag})` });
}).on('message', function (progress) { // this is { message } or { result }
if ('message' in progress) return progressCallback({ message: `${progress.message} (${progressTag})` });
debug(`runBackupUpload: result - ${JSON.stringify(progress)}`);
result = progress.result;
});
@@ -1302,3 +1308,15 @@ function checkConfiguration(callback) {
callback(null, message);
});
}
function configureCollectd(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
if (backupConfig.provider === 'filesystem') {
const collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { backupDir: backupConfig.backupFolder });
collectd.addProfile('cloudron-backup', collectdConf, callback);
} else {
collectd.removeProfile('cloudron-backup', callback);
}
}

View File

@@ -45,6 +45,7 @@ BoxError.DATABASE_ERROR = 'Database Error';
BoxError.DNS_ERROR = 'DNS Error';
BoxError.DOCKER_ERROR = 'Docker Error';
BoxError.EXTERNAL_ERROR = 'External Error'; // use this for external API errors
BoxError.FEATURE_DISABLED = 'Feature Disabled';
BoxError.FS_ERROR = 'FileSystem Error';
BoxError.INACTIVE = 'Inactive';
BoxError.INTERNAL_ERROR = 'Internal Error';
@@ -77,6 +78,8 @@ BoxError.toHttpError = function (error) {
return new HttpError(402, error);
case BoxError.NOT_FOUND:
return new HttpError(404, error);
case BoxError.FEATURE_DISABLED:
return new HttpError(405, error);
case BoxError.ALREADY_EXISTS:
case BoxError.BAD_STATE:
case BoxError.CONFLICT:

View File

@@ -285,7 +285,7 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
}
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
debug('waitForChallenge: status is "%s" %j', result.body.status, result.body);
if (result.body.status === 'pending') return retryCallback(new BoxError(BoxError.TRY_AGAIN));
else if (result.body.status === 'valid') return retryCallback();
@@ -452,7 +452,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
if (error) return callback(error);
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
if (error) return callback(error);
callback(null, challenge);

View File

@@ -1,179 +0,0 @@
'use strict';
exports = module.exports = {
get: get,
getAll: getAll,
getAllWithTokenCount: getAllWithTokenCount,
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
add: add,
del: del,
getByAppId: getByAppId,
getByAppIdAndType: getByAppIdAndType,
upsert: upsert,
delByAppId: delByAppId,
delByAppIdAndType: delByAppIdAndType,
_clear: clear
};
var assert = require('assert'),
BoxError = require('./boxerror.js'),
database = require('./database.js');
var CLIENTS_FIELDS = [ 'id', 'appId', 'type', 'clientSecret', 'redirectURI', 'scope' ].join(',');
var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.type', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(',');
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE 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, `Client not found: ${id}`));
callback(null, result[0]);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients ORDER BY appId', function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function getAllWithTokenCount(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId GROUP BY clients.id', [], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function getAllWithTokenCountByIdentifier(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null, results);
});
}
function getByAppId(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? LIMIT 1', [ appId ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
callback(null, result[0]);
});
}
function getByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? AND type = ? LIMIT 1', [ appId, type ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
callback(null, result[0]);
});
}
function add(id, appId, type, clientSecret, redirectURI, scope, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof clientSecret, 'string');
assert.strictEqual(typeof redirectURI, 'string');
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
var data = [ id, appId, type, clientSecret, redirectURI, scope ];
database.query('INSERT INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error || result.affectedRows === 0) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function upsert(id, appId, type, clientSecret, redirectURI, scope, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof clientSecret, 'string');
assert.strictEqual(typeof redirectURI, 'string');
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
var data = [ id, appId, type, clientSecret, redirectURI, scope ];
database.query('REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
if (error || result.affectedRows === 0) 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 clients WHERE id = ?', [ id ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, `Client not found: ${id}`));
callback(null);
});
}
function delByAppId(appId, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE appId=?', [ appId ], function (error, result) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
callback(null);
});
}
function delByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients WHERE appId=? AND type=?', [ appId, 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, 'Client not found'));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM clients', function (error) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}

View File

@@ -1,329 +0,0 @@
'use strict';
exports = module.exports = {
add: add,
get: get,
del: del,
getAll: getAll,
getByAppIdAndType: getByAppIdAndType,
getTokensByUserId: getTokensByUserId,
delTokensByUserId: delTokensByUserId,
delByAppIdAndType: delByAppIdAndType,
addTokenByUserId: addTokenByUserId,
delToken: delToken,
issueDeveloperToken: issueDeveloperToken,
addDefaultClients: addDefaultClients,
removeTokenPrivateFields: removeTokenPrivateFields,
// client ids. we categorize them so we can have different restrictions based on the client
ID_WEBADMIN: 'cid-webadmin', // dashboard oauth
ID_SDK: 'cid-sdk', // created by user via dashboard
ID_CLI: 'cid-cli', // created via cli tool
// client type enums
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
TYPE_OAUTH: 'addon-oauth',
TYPE_PROXY: 'addon-proxy'
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
BoxError = require('./boxerror.js'),
clientdb = require('./clientdb.js'),
constants = require('./constants.js'),
debug = require('debug')('box:clients'),
eventlog = require('./eventlog.js'),
hat = require('./hat.js'),
accesscontrol = require('./accesscontrol.js'),
tokendb = require('./tokendb.js'),
users = require('./users.js'),
uuid = require('uuid'),
_ = require('underscore');
function validateClientName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 character', { field: 'name' });
if (name.length > 128) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' });
if (/[^a-zA-Z0-9-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'name can only contain alphanumerals and dash', { field: 'name' });
return null;
}
function validateTokenName(name) {
assert.strictEqual(typeof name, 'string');
if (name.length > 64) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' });
return null;
}
function add(appId, type, redirectURI, scope, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof redirectURI, 'string');
assert.strictEqual(typeof scope, 'string');
assert.strictEqual(typeof callback, 'function');
var error = accesscontrol.validateScopeString(scope);
if (error) return callback(error);
error = validateClientName(appId);
if (error) return callback(error);
var id = 'cid-' + uuid.v4();
var clientSecret = hat(8 * 128);
clientdb.add(id, appId, type, clientSecret, redirectURI, scope, function (error) {
if (error) return callback(error);
var client = {
id: id,
appId: appId,
type: type,
clientSecret: clientSecret,
redirectURI: redirectURI,
scope: scope
};
callback(null, client);
});
}
function get(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.get(id, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function del(id, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.del(id, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function getAll(callback) {
assert.strictEqual(typeof callback, 'function');
clientdb.getAll(function (error, results) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, []);
if (error) return callback(error);
var tmp = [];
async.each(results, function (record, callback) {
if (record.type === exports.TYPE_EXTERNAL || record.type === exports.TYPE_BUILT_IN) {
// the appId in this case holds the name
record.name = record.appId;
tmp.push(record);
return callback(null);
}
apps.get(record.appId, function (error, result) {
if (error) {
console.error('Failed to get app details for oauth client', record.appId, error);
return callback(null); // ignore error so we continue listing clients
}
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
record.domain = result.fqdn;
tmp.push(record);
callback(null);
});
}, function (error) {
if (error) return callback(error);
callback(null, tmp);
});
});
}
function getByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
clientdb.getByAppIdAndType(appId, type, function (error, result) {
if (error) return callback(error);
callback(null, result);
});
}
function getTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
callback(null, []);
});
return;
}
if (error) return callback(error);
callback(null, result || []);
});
}
function delTokensByUserId(clientId, userId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof callback, 'function');
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
if (error && error.reason === BoxError.NOT_FOUND) {
// this can mean either that there are no tokens or the clientId is actually unknown
get(clientId, function (error/*, result*/) {
if (error) return callback(error);
callback(null);
});
return;
}
if (error) return callback(error);
callback(null);
});
}
function delByAppIdAndType(appId, type, callback) {
assert.strictEqual(typeof appId, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
getByAppIdAndType(appId, type, function (error, result) {
if (error) return callback(error);
tokendb.delByClientId(result.id, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
clientdb.delByAppIdAndType(appId, type, function (error) {
if (error) return callback(error);
callback(null);
});
});
});
}
function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof userId, 'string');
assert.strictEqual(typeof expiresAt, 'number');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
const name = options.name || '';
let error = validateTokenName(name);
if (error) return callback(error);
get(clientId, function (error, result) {
if (error) return callback(error);
users.get(userId, function (error, user) {
if (error) return callback(error);
accesscontrol.scopesForUser(user, function (error, userScopes) {
if (error) return callback(error);
const scope = accesscontrol.canonicalScopeString(result.scope);
const authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
const token = {
id: 'tid-' + uuid.v4(),
accessToken: hat(8 * 32),
identifier: userId,
clientId: result.id,
expires: expiresAt,
scope: authorizedScopes.join(','),
name: name
};
tokendb.add(token, function (error) {
if (error) return callback(error);
callback(null, {
accessToken: token.accessToken,
tokenScopes: authorizedScopes,
identifier: userId,
clientId: result.id,
expires: expiresAt
});
});
});
});
});
}
function issueDeveloperToken(userObject, auditSource, callback) {
assert.strictEqual(typeof userObject, 'object');
assert.strictEqual(typeof auditSource, 'object');
assert.strictEqual(typeof callback, 'function');
const expiresAt = Date.now() + (30 * 24 * 60 * 60 * 1000); // cli tokens are valid for a month
addTokenByUserId(exports.ID_CLI, userObject.id, expiresAt, {}, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
callback(null, result);
});
}
function delToken(clientId, tokenId, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof tokenId, 'string');
assert.strictEqual(typeof callback, 'function');
get(clientId, function (error) {
if (error) return callback(error);
tokendb.del(tokenId, function (error) {
if (error) return callback(error);
callback(null);
});
});
}
function addDefaultClients(origin, callback) {
assert.strictEqual(typeof origin, 'string');
assert.strictEqual(typeof callback, 'function');
debug('Adding default clients');
// The domain might have changed, therefor we have to update the record
// id, appId, type, clientSecret, redirectURI, scope
async.series([
clientdb.upsert.bind(null, exports.ID_WEBADMIN, 'Settings', 'built-in', 'secret-webadmin', origin, '*'),
clientdb.upsert.bind(null, exports.ID_SDK, 'SDK', 'built-in', 'secret-sdk', origin, '*'),
clientdb.upsert.bind(null, exports.ID_CLI, 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
], callback);
}
function removeTokenPrivateFields(token) {
return _.pick(token, 'id', 'identifier', 'clientId', 'scope', 'expires', 'name');
}

View File

@@ -21,23 +21,22 @@ exports = module.exports = {
runSystemChecks: runSystemChecks,
};
var apps = require('./apps.js'),
var addons = require('./addons.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
clients = require('./clients.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
debug = require('debug')('box:cloudron'),
domains = require('./domains.js'),
eventlog = require('./eventlog.js'),
custom = require('./custom.js'),
fs = require('fs'),
mail = require('./mail.js'),
notifications = require('./notifications.js'),
os = require('os'),
path = require('path'),
paths = require('./paths.js'),
platform = require('./platform.js'),
@@ -107,6 +106,12 @@ function runStartupTasks() {
// configure nginx to be reachable by IP
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
settings.getBackupConfig(function (error, backupConfig) {
if (error) return console.error('Failed to read backup config.', error);
backups.configureCollectd(backupConfig, NOOP_CALLBACK);
});
// always generate webadmin config since we have no versioning mechanism for the ejs
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
@@ -136,13 +141,18 @@ function getConfig(callback) {
isDemo: settings.isDemo(),
provider: settings.provider(),
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
uiSpec: custom.uiSpec()
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
features: appstore.getFeatures()
});
});
}
function reboot(callback) {
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) {
if (error) console.error('Failed to clear reboot notification.', error);
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
});
}
function isRebootRequired(callback) {
@@ -195,7 +205,7 @@ function checkRebootRequired(callback) {
isRebootRequired(function (error, rebootRequired) {
if (error) return callback(error);
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish security updates, a [reboot](/#/system) is necessary.' : '', callback);
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish ubuntu security updates, a reboot is necessary.' : '', callback);
});
}
@@ -252,6 +262,8 @@ function prepareDashboardDomain(domain, auditSource, callback) {
debug(`prepareDashboardDomain: ${domain}`);
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
domains.get(domain, function (error, domainObject) {
if (error) return callback(error);
@@ -290,10 +302,7 @@ function setDashboardDomain(domain, auditSource, callback) {
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
async.series([
(done) => settings.setAdmin(domain, fqdn, done),
(done) => clients.addDefaultClients(settings.adminOrigin(), done)
], function (error) {
settings.setAdmin(domain, fqdn, function (error) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
@@ -312,10 +321,13 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
debug(`setDashboardAndMailDomain: ${domain}`);
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
setDashboardDomain(domain, auditSource, function (error) {
if (error) return callback(error);
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
addons.restartService('turn', NOOP_CALLBACK); // to update the realm variable
callback(null);
});

55
src/collectd.js Normal file
View File

@@ -0,0 +1,55 @@
'use strict';
exports = module.exports = {
addProfile,
removeProfile
};
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'),
shell = require('./shell.js');
const CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh');
function addProfile(name, profile, callback) {
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);
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);
});
});
}
function removeProfile(name, callback) {
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);
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);
});
});
}

View File

@@ -0,0 +1,9 @@
<Plugin python>
<Module du>
<Path>
Instance "cloudron-backup"
Dir "<%= backupDir %>"
</Path>
</Module>
</Plugin>

View File

@@ -32,9 +32,7 @@ exports = module.exports = {
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
DEFAULT_TOKEN_EXPIRATION: 7 * 24 * 60 * 60 * 1000, // 1 week
DEFAULT_TOKEN_EXPIRATION: 365 * 24 * 60 * 60 * 1000, // 1 year
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
@@ -47,6 +45,10 @@ exports = module.exports = {
CLOUDRON: CLOUDRON,
TEST: TEST,
SUPPORT_EMAIL: 'support@cloudron.io',
FOOTER: '&copy; 2020 &nbsp; [Cloudron](https://cloudron.io) &nbsp; &nbsp; &nbsp; [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '4.2.0-test'
};

View File

@@ -12,6 +12,7 @@ var appHealthMonitor = require('./apphealthmonitor.js'),
apps = require('./apps.js'),
appstore = require('./appstore.js'),
assert = require('assert'),
async = require('async'),
auditSource = require('./auditsource.js'),
backups = require('./backups.js'),
cloudron = require('./cloudron.js'),
@@ -59,148 +60,141 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function startJobs(callback) {
assert.strictEqual(typeof callback, 'function');
var randomHourMinute = Math.floor(60*Math.random());
const randomMinute = Math.floor(60*Math.random());
gJobs.alive = new CronJob({
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
cronTime: '00 ' + randomMinute + ' * * * *', // every hour on a random minute
onTick: appstore.sendAliveStatus,
start: true
});
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
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),
start: true
});
gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
start: true
});
gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
start: true
});
gJobs.cleanupTokens = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: janitor.cleanupTokens,
start: true
});
gJobs.cleanupBackups = new CronJob({
cronTime: '00 45 1,3,5,23 * * *', // every 6 hours. try not to overlap with ensureBackup job
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true
});
gJobs.cleanupEventlog = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: eventlog.cleanup,
start: true
});
gJobs.dockerVolumeCleaner = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: janitor.cleanupDockerVolumes,
start: true
});
gJobs.schedulerSync = new CronJob({
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
start: true
});
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: cloudron.renewCerts.bind(null, {}, auditSource.CRON, NOOP_CALLBACK),
start: true
});
gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
start: true
});
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
const tz = allSettings[settings.TIME_ZONE_KEY];
backupConfigChanged(allSettings[settings.BACKUP_CONFIG_KEY], tz);
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY], tz);
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY], tz);
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
callback();
});
}
// eslint-disable-next-line no-unused-vars
function handleSettingsChanged(key, value) {
assert.strictEqual(typeof key, 'string');
// value is a variant
switch (key) {
case settings.TIME_ZONE_KEY: recreateJobs(value); break;
case settings.APP_AUTOUPDATE_PATTERN_KEY: appAutoupdatePatternChanged(value); break;
case settings.BOX_AUTOUPDATE_PATTERN_KEY: boxAutoupdatePatternChanged(value); break;
case settings.DYNAMIC_DNS_KEY: dynamicDnsChanged(value); break;
default: break;
case settings.TIME_ZONE_KEY:
case settings.BACKUP_CONFIG_KEY:
case settings.APP_AUTOUPDATE_PATTERN_KEY:
case settings.BOX_AUTOUPDATE_PATTERN_KEY:
case settings.DYNAMIC_DNS_KEY:
debug('handleSettingsChanged: recreating all jobs');
async.series([
stopJobs,
startJobs
], NOOP_CALLBACK);
break;
default:
break;
}
}
function recreateJobs(tz) {
function backupConfigChanged(value, tz) {
assert.strictEqual(typeof value, 'object');
assert.strictEqual(typeof tz, 'string');
debug('Creating jobs with timezone %s', tz);
debug(`backupConfigChanged: interval ${value.intervalSecs} (${tz})`);
if (gJobs.backup) gJobs.backup.stop();
let pattern;
if (value.intervalSecs <= 6 * 60 * 60) {
pattern = '00 00 1,7,13,19 * * *'; // no option but to backup in the middle of the day
} else {
pattern = '00 00 1,3,5,23 * * *'; // avoid middle of the day backups
}
gJobs.backup = new CronJob({
cronTime: '00 00 1,3,5,23 * * *',
cronTime: pattern,
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gJobs.systemChecks) gJobs.systemChecks.stop();
gJobs.systemChecks = new CronJob({
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gJobs.diskSpaceChecker) gJobs.diskSpaceChecker.stop();
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),
start: true,
timeZone: tz
});
// randomized pattern per cloudron every hour
var randomMinute = Math.floor(60*Math.random());
if (gJobs.boxUpdateCheckerJob) gJobs.boxUpdateCheckerJob.stop();
gJobs.boxUpdateCheckerJob = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gJobs.appUpdateChecker) gJobs.appUpdateChecker.stop();
gJobs.appUpdateChecker = new CronJob({
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gJobs.cleanupTokens) gJobs.cleanupTokens.stop();
gJobs.cleanupTokens = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: janitor.cleanupTokens,
start: true,
timeZone: tz
});
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
gJobs.cleanupBackups = new CronJob({
cronTime: '00 45 1,3,5,23 * * *', // every 6 hours. try not to overlap with ensureBackup job
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gJobs.cleanupEventlog) gJobs.cleanupEventlog.stop();
gJobs.cleanupEventlog = new CronJob({
cronTime: '00 */30 * * * *', // every 30 minutes
onTick: eventlog.cleanup,
start: true,
timeZone: tz
});
if (gJobs.dockerVolumeCleaner) gJobs.dockerVolumeCleaner.stop();
gJobs.dockerVolumeCleaner = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: janitor.cleanupDockerVolumes,
start: true,
timeZone: tz
});
if (gJobs.schedulerSync) gJobs.schedulerSync.stop();
gJobs.schedulerSync = new CronJob({
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
onTick: scheduler.sync,
start: true,
timeZone: tz
});
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
gJobs.certificateRenew = new CronJob({
cronTime: '00 00 */12 * * *', // every 12 hours
onTick: cloudron.renewCerts.bind(null, {}, auditSource.CRON, NOOP_CALLBACK),
start: true,
timeZone: tz
});
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
gJobs.appHealthMonitor = new CronJob({
cronTime: '*/10 * * * * *', // every 10 seconds
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
start: true,
timeZone: tz
});
}
function boxAutoupdatePatternChanged(pattern) {
function boxAutoupdatePatternChanged(pattern, tz) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
assert.strictEqual(typeof tz, 'string');
debug('Box auto update pattern changed to %s', pattern);
debug(`boxAutoupdatePatternChanged: pattern - ${pattern} (${tz})`);
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
@@ -218,15 +212,15 @@ function boxAutoupdatePatternChanged(pattern) {
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
timeZone: tz
});
}
function appAutoupdatePatternChanged(pattern) {
function appAutoupdatePatternChanged(pattern, tz) {
assert.strictEqual(typeof pattern, 'string');
assert(gJobs.boxUpdateCheckerJob);
assert.strictEqual(typeof tz, 'string');
debug('Apps auto update pattern changed to %s', pattern);
debug(`appAutoupdatePatternChanged: pattern ${pattern} (${tz})`);
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
@@ -244,13 +238,12 @@ function appAutoupdatePatternChanged(pattern) {
}
},
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
timeZone: tz
});
}
function dynamicDnsChanged(enabled) {
assert.strictEqual(typeof enabled, 'boolean');
assert(gJobs.boxUpdateCheckerJob);
debug('Dynamic DNS setting changed to %s', enabled);
@@ -258,8 +251,7 @@ function dynamicDnsChanged(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),
start: true,
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
start: true
});
} else {
if (gJobs.dynamicDns) gJobs.dynamicDns.stop();

View File

@@ -1,66 +0,0 @@
'use strict';
let debug = require('debug')('box:custom'),
lodash = require('lodash'),
paths = require('./paths.js'),
safe = require('safetydance'),
yaml = require('js-yaml');
exports = module.exports = {
uiSpec: uiSpec,
spec: spec
};
const DEFAULT_SPEC = {
appstore: {
blacklist: [],
whitelist: null // null imples, not set. this is an object and not an array
},
backups: {
configurable: true
},
domains: {
dynamicDns: true,
changeDashboardDomain: true
},
subscription: {
configurable: true
},
support: {
email: 'support@cloudron.io',
remoteSupport: true,
ticketFormBody:
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
+ '* [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)\n'
+ '* [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)\n'
+ '* [Forum](https://forum.cloudron.io/)\n\n',
submitTickets: true
},
alerts: {
email: '',
notifyCloudronAdmins: true
},
footer: {
body: '&copy; 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
}
};
const gSpec = (function () {
try {
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return DEFAULT_SPEC;
const c = yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
return lodash.merge({}, DEFAULT_SPEC, c);
} catch (e) {
debug(`Error loading features file from ${paths.CUSTOM_FILE} : ${e.message}`);
return DEFAULT_SPEC;
}
})();
// flags sent to the UI. this is separate because we have values that are secret to the backend
function uiSpec() {
return gSpec;
}
function spec() {
return gSpec;
}

View File

@@ -104,10 +104,7 @@ function clear(callback) {
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name,
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name);
async.series([
child_process.exec.bind(null, cmd),
require('./clients.js').addDefaultClients.bind(null, 'https://admin-localhost')
], callback);
child_process.exec(cmd, callback);
}
function beginTransaction(callback) {
@@ -204,7 +201,11 @@ function exportToFile(file, callback) {
assert.strictEqual(typeof file, 'string');
assert.strictEqual(typeof callback, 'function');
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
// latest mysqldump enables column stats by default which is not present in MySQL 5.7 server
// this option must not be set in production cloudrons which still use the old mysqldump
const disableColStats = (constants.TEST && process.env.DESKTOP_SESSION !== 'ubuntu') ? '--column-statistics=0' : '';
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
child_process.exec(cmd, callback);
}

309
src/dns/linode.js Normal file
View File

@@ -0,0 +1,309 @@
'use strict';
exports = module.exports = {
removePrivateFields: removePrivateFields,
injectPrivateFields: injectPrivateFields,
upsert: upsert,
get: get,
del: del,
wait: wait,
verifyDnsConfig: verifyDnsConfig
};
let async = require('async'),
assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:dns/linode'),
dns = require('../native-dns.js'),
domains = require('../domains.js'),
superagent = require('superagent'),
util = require('util'),
waitForDns = require('./waitfordns.js');
const LINODE_ENDPOINT = 'https://api.linode.com/v4';
function formatError(response) {
return util.format('Linode DNS error [%s] %j', response.statusCode, response.body);
}
function removePrivateFields(domainObject) {
domainObject.config.token = domains.SECRET_PLACEHOLDER;
return domainObject;
}
function injectPrivateFields(newConfig, currentConfig) {
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
}
function getZoneId(dnsConfig, zoneName, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof callback, 'function');
// returns 100 at a time
superagent.get(`${LINODE_ENDPOINT}/domains`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
if (!Array.isArray(result.body.data)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
const zone = result.body.data.find(d => d.domain === zoneName);
if (!zone || !zone.id) return callback(new BoxError(BoxError.NOT_FOUND, 'Zone not found'));
debug(`getZoneId: zone id of ${zoneName} is ${zone.id}`);
callback(null, zone.id);
});
}
function getZoneRecords(dnsConfig, zoneName, name, type, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof zoneName, 'string');
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`);
getZoneId(dnsConfig, zoneName, function (error, zoneId) {
if (error) return callback(error);
let page = 0, more = false;
let records = [];
async.doWhilst(function (iteratorDone) {
const url = `${LINODE_ENDPOINT}/domains/${zoneId}/records?page=${++page}`;
superagent.get(url)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
records = records.concat(result.body.data.filter(function (record) {
return (record.type === type && record.name === name);
}));
more = result.body.page !== result.body.pages;
iteratorDone();
});
}, function () { return more; }, function (error) {
debug('getZoneRecords:', error, JSON.stringify(records));
if (error) return callback(error);
callback(null, { zoneId, records });
});
});
}
function get(domainObject, location, type, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { records }) {
if (error) return callback(error);
var tmp = records.map(function (record) { return record.target; });
debug('get: %j', tmp);
return callback(null, tmp);
});
}
function upsert(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
let i = 0, recordIds = []; // used to track available records to update instead of create
async.eachSeries(values, function (value, iteratorCallback) {
let data = {
type: type,
ttl_sec: 300 // lowest
};
if (type === 'MX') {
data.priority = parseInt(value.split(' ')[0], 10);
data.target = value.split(' ')[1];
} else if (type === 'TXT') {
data.target = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes
} else {
data.target = value;
}
if (i >= records.length) {
data.name = name; // only set for new records
superagent.post(`${LINODE_ENDPOINT}/domains/${zoneId}/records`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(result.body.id);
return iteratorCallback(null);
});
} else {
superagent.put(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${records[i].id}`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.send(data)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
// increment, as we have consumed the record
++i;
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
recordIds.push(result.body.id);
return iteratorCallback(null);
});
}
}, function (error) {
if (error) return callback(error);
debug('upsert: completed with recordIds:%j', recordIds);
callback();
});
});
}
function del(domainObject, location, type, values, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert(util.isArray(values));
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName,
name = domains.getName(domainObject, location, type) || '';
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
if (error) return callback(error);
if (records.length === 0) return callback(null);
var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); });
debug('del: %j', tmp);
if (tmp.length === 0) return callback(null);
// FIXME we only handle the first one currently
superagent.del(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${tmp[0].id}`)
.set('Authorization', 'Bearer ' + dnsConfig.token)
.timeout(30 * 1000)
.retry(5)
.end(function (error, result) {
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
if (result.statusCode === 404) return callback(null);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
debug('del: done');
return callback(null);
});
});
}
function wait(domainObject, location, type, value, options, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof location, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof value, 'string');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
const fqdn = domains.fqdn(location, domainObject);
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
}
function verifyDnsConfig(domainObject, callback) {
assert.strictEqual(typeof domainObject, 'object');
assert.strictEqual(typeof callback, 'function');
const dnsConfig = domainObject.config,
zoneName = domainObject.zoneName;
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
const ip = '127.0.0.1';
var credentials = {
token: dnsConfig.token
};
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.linode.com') === -1) {
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode', { field: 'nameservers' }));
}
const location = 'cloudrontestdns';
upsert(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record added');
del(domainObject, location, 'A', [ ip ], function (error) {
if (error) return callback(error);
debug('verifyDnsConfig: Test A record removed again');
callback(null, credentials);
});
});
});
}

View File

@@ -143,7 +143,8 @@ function pullImage(manifest, callback) {
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`);
gConnection.pull(manifest.dockerImage, { authconfig: authConfig }, function (error, stream) {
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to pull image. Please check the network or if the image needs authentication. statusCode: ' + error.statusCode));
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to pull image ${manifest.dockerImage}. message: ${error.message} statusCode: ${error.statusCode}`));
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${manifest.dockerImage}. Please check the network or if the image needs authentication. statusCode: ${error.statusCode}`));
// https://github.com/dotcloud/docker/issues/1074 says each status message
// is emitted as a chunk
@@ -180,14 +181,10 @@ function downloadImage(manifest, callback) {
var attempt = 1;
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
pullImage(manifest, function (error) {
if (error) console.error(error);
retryCallback(error);
});
pullImage(manifest, retryCallback);
}, callback);
}

View File

@@ -23,11 +23,6 @@ var apps = require('./apps.js'),
var gHttpServer = null;
function authorizeApp(req, res, next) {
// TODO add here some authorization
// - block apps not using the docker addon
// - block calls regarding platform containers
// - only allow managing and inspection of containers belonging to the app
// make the tests pass for now
if (constants.TEST) {
req.app = { id: 'testappid' };
@@ -64,6 +59,8 @@ function attachDockerRequest(req, res, next) {
dockerResponse.pipe(res, { end: true });
});
req.dockerRequest.on('error', () => {}); // abort() throws
next();
}
@@ -74,22 +71,21 @@ function containersCreate(req, res, next) {
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data');
debug('Original volume binds:', req.body.HostConfig.Binds);
debug('Original bind mounts:', req.body.HostConfig.Binds);
let binds = [];
for (let bind of (req.body.HostConfig.Binds || [])) {
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
else binds.push(`${dockerDataDir}/${bind}`);
if (!bind.startsWith('/app/data/')) {
req.dockerRequest.abort();
return next(new HttpError(400, 'Binds must be under /app/data/'));
}
binds.push(bind.replace(new RegExp('^/app/data/'), appDataDir + '/'));
}
// cleanup the paths from potential double slashes
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
debug('Rewritten volume binds:', binds);
debug('Rewritten bind mounts:', binds);
safe.set(req.body, 'HostConfig.Binds', binds);
let plainBody = JSON.stringify(req.body);
@@ -117,6 +113,9 @@ function start(callback) {
assert(gHttpServer === null, 'Already started');
let json = middleware.json({ strict: true });
// we protect container create as the app/admin can otherwise mount random paths (like the ghost file)
// protected other paths is done by preventing install/exec access of apps using docker addon
let router = new express.Router();
router.post('/:version/containers/create', containersCreate);
@@ -137,7 +136,7 @@ function start(callback) {
.use(middleware.lastMile());
gHttpServer = http.createServer(proxyServer);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '172.18.0.1', callback);
// Overwrite the default 2min request timeout. This is required for large builds for example
gHttpServer.setTimeout(60 * 60 * 1000);

View File

@@ -16,7 +16,7 @@ var assert = require('assert'),
database = require('./database.js'),
safe = require('safetydance');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'locked' ].join(',');
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
function postProcess(data) {
data.config = safe.JSON.parse(data.configJson);
@@ -24,8 +24,6 @@ function postProcess(data) {
delete data.configJson;
delete data.tlsConfigJson;
data.locked = !!data.locked; // make it bool
return data;
}
@@ -53,16 +51,21 @@ function getAll(callback) {
});
}
function add(name, domain, callback) {
function add(name, data, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof domain, 'object');
assert.strictEqual(typeof domain.zoneName, 'string');
assert.strictEqual(typeof domain.provider, 'string');
assert.strictEqual(typeof domain.config, 'object');
assert.strictEqual(typeof domain.tlsConfig, 'object');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof data.zoneName, 'string');
assert.strictEqual(typeof data.provider, 'string');
assert.strictEqual(typeof data.config, 'object');
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
let queries = [
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig) ] },
{ query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] },
];
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
@@ -102,10 +105,22 @@ function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
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 (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
if (results[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
callback(null);
});

View File

@@ -40,6 +40,7 @@ var assert = require('assert'),
debug = require('debug')('box:domains'),
domaindb = require('./domaindb.js'),
eventlog = require('./eventlog.js'),
mail = require('./mail.js'),
reverseProxy = require('./reverseproxy.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
@@ -48,6 +49,8 @@ var assert = require('assert'),
util = require('util'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
// choose which subdomain backend we use for test purpose we use route53
function api(provider) {
assert.strictEqual(typeof provider, 'string');
@@ -60,6 +63,7 @@ function api(provider) {
case 'digitalocean': return require('./dns/digitalocean.js');
case 'gandi': return require('./dns/gandi.js');
case 'godaddy': return require('./dns/godaddy.js');
case 'linode': return require('./dns/linode.js');
case 'namecom': return require('./dns/namecom.js');
case 'namecheap': return require('./dns/namecheap.js');
case 'noop': return require('./dns/noop.js');
@@ -170,7 +174,7 @@ function add(domain, data, auditSource, callback) {
assert.strictEqual(typeof data.tlsConfig, 'object');
assert.strictEqual(typeof callback, 'function');
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
let { zoneName, provider, config, fallbackCertificate, tlsConfig, dkimSelector } = data;
if (!tld.isValid(domain)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
if (domain.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
@@ -193,10 +197,12 @@ function add(domain, data, auditSource, callback) {
let error = validateTlsConfig(tlsConfig, provider);
if (error) return callback(error);
if (!dkimSelector) dkimSelector = 'cloudron-' + settings.adminDomain().replace(/\./g, '');
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
if (error) return callback(error);
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector }, function (error) {
if (error) return callback(error);
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
@@ -204,6 +210,8 @@ function add(domain, data, auditSource, callback) {
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
mail.onDomainAdded(domain, NOOP_CALLBACK);
callback();
});
});
@@ -253,6 +261,8 @@ function update(domain, data, auditSource, callback) {
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
if (settings.isDemo() && (domain === settings.adminDomain())) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
domaindb.get(domain, function (error, domainObject) {
if (error) return callback(error);
@@ -311,6 +321,8 @@ function del(domain, auditSource, callback) {
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
mail.onDomainRemoved(domain, NOOP_CALLBACK);
return callback(null);
});
}
@@ -434,19 +446,22 @@ function waitForDnsRecord(location, domain, type, value, options, callback) {
get(domain, function (error, domainObject) {
if (error) return callback(error);
// linode DNS takes ~15mins
if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000;
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
});
}
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate');
return api(result.provider).removePrivateFields(result);
}
// removes all fields that are not accessible by a normal user
function removeRestrictedFields(domain) {
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'locked');
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
// always ensure config object
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };

View File

@@ -7,7 +7,7 @@ exports = module.exports = {
getByCreationTime: getByCreationTime,
cleanup: cleanup,
// keep in sync with webadmin index.js filter and CLI tool
// keep in sync with webadmin index.js filter
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CLONE: 'app.clone',
ACTION_APP_CONFIGURE: 'app.configure',
@@ -21,6 +21,9 @@ exports = module.exports = {
ACTION_APP_OOM: 'app.oom',
ACTION_APP_UP: 'app.up',
ACTION_APP_DOWN: 'app.down',
ACTION_APP_START: 'app.start',
ACTION_APP_STOP: 'app.stop',
ACTION_APP_RESTART: 'app.restart',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
@@ -87,11 +90,9 @@ function add(action, source, data, callback) {
api(uuid.v4(), action, source, data, function (error, id) {
if (error) return callback(error);
notifications.onEvent(id, action, source, data, function (error) {
if (error) return callback(error);
callback(null, { id: id });
callback(null, { id: id });
});
notifications.onEvent(id, action, source, data, NOOP_CALLBACK);
});
}

View File

@@ -301,7 +301,7 @@ function sync(progressCallback, callback) {
} else if (result.email !== user.email || result.displayName !== user.displayName) {
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
users.update(result.id, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
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();

View File

@@ -15,12 +15,13 @@ exports = module.exports = {
// a major version bump in the db containers will trigger the restore logic that uses the db dumps
// docker inspect --format='{{index .RepoDigests 0}}' $IMAGE to get the sha256
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.1.0@sha256:eee0dfd3829d563f2063084bc0d7c8802c4bdd6e233159c6226a17ff7a9a3503' },
'turn': { repo: 'cloudron/turn', tag: 'cloudron/turn:1.0.1@sha256:d7e8b3a88e30986a459a4da9742fbd50a1d150f07408942b91e279c41e6af059' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:2.2.0@sha256:440c8a9ca4d2958d51a375359f8158ef702b83395aa9ac4f450c51825ec09239' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:2.0.2@sha256:6dcee0731dfb9b013ed94d56205eee219040ee806c7e251db3b3886eaa4947ff' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:2.1.0@sha256:6d1bf221cfe6124957e2c58b57c0a47214353496009296acb16adf56df1da9d5' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:2.1.0@sha256:f2cda21bd15c21bbf44432df412525369ef831a2d53860b5c5b1675e6f384de2' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.5.0@sha256:086ae1c9433d90a820326aa43914a2afe94ad707074ef2bc05a7ef4798e83655' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:2.7.2@sha256:f20d112ff9a97e052a9187063eabbd8d484ce369114d44186e344169a1b3ef6b' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:2.2.0@sha256:fc9ca69d16e6ebdbd98ed53143d4a0d2212eef60cb638dc71219234e6f427a2c' },
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:0.1.0@sha256:e177c5bf5f38c84ce1dea35649c22a1b05f96eec67a54a812c5a35e585670f0f' }
'sftp': { repo: 'cloudron/sftp', tag: 'cloudron/sftp:1.0.0@sha256:3b70aac36700225945a4a39b5a400c28e010e980879d0dcca76e4a37b04a16ed' }
}
};

View File

@@ -2,7 +2,6 @@
var assert = require('assert'),
async = require('async'),
authcodedb = require('./authcodedb.js'),
BoxError = require('./boxerror.js'),
debug = require('debug')('box:janitor'),
Docker = require('dockerode'),
@@ -39,26 +38,13 @@ function cleanupExpiredTokens(callback) {
});
}
function cleanupExpiredAuthCodes(callback) {
assert.strictEqual(typeof callback, 'function');
authcodedb.delExpired(function (error, result) {
if (error) return callback(error);
debug('Cleaned up %s expired authcodes.', result);
callback(null);
});
}
function cleanupTokens(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
debug('Cleaning up expired tokens');
async.series([
ignoreError(cleanupExpiredTokens),
ignoreError(cleanupExpiredAuthCodes)
ignoreError(cleanupExpiredTokens)
], callback);
}

View File

@@ -126,16 +126,16 @@ function userSearch(req, res, next) {
var results = [];
// send user objects
result.forEach(function (entry) {
result.forEach(function (user) {
// skip entries with empty username. Some apps like owncloud can't deal with this
if (!entry.username) return;
if (!user.username) return;
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var dn = ldap.parseDN('cn=' + user.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
if (users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
var displayName = user.displayName || user.username || ''; // displayName can be empty and username can be null
var nameParts = displayName.split(' ');
var firstName = nameParts[0];
var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists
@@ -145,16 +145,16 @@ function userSearch(req, res, next) {
attributes: {
objectclass: ['user', 'inetorgperson', 'person' ],
objectcategory: 'person',
cn: entry.id,
uid: entry.id,
entryuuid: entry.id, // to support OpenLDAP clients
mail: entry.email,
mailAlternateAddress: entry.fallbackEmail,
cn: user.id,
uid: user.id,
entryuuid: user.id, // to support OpenLDAP clients
mail: user.email,
mailAlternateAddress: user.fallbackEmail,
displayname: displayName,
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
isadmin: entry.admin,
username: user.username,
samaccountname: user.username, // to support ActiveDirectory clients
isadmin: users.compareRoles(user.role, users.ROLE_ADMIN) >= 0,
memberof: groups
}
};
@@ -194,7 +194,7 @@ function groupSearch(req, res, next) {
groups.forEach(function (group) {
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
var members = group.admin ? result.filter(function (user) { return users.compareRoles(user.role, users.ROLE_ADMIN) >= 0; }) : result;
var obj = {
dn: dn.toString(),
@@ -242,8 +242,8 @@ function groupAdminsCompare(req, res, next) {
// we only support memberuid here, if we add new group attributes later add them here
if (req.attribute === 'memberuid') {
var found = result.find(function (u) { return u.id === req.value; });
if (found && found.admin) return res.end(true);
var user = result.find(function (u) { return u.id === req.value; });
if (user && users.compareRoles(user.role, users.ROLE_ADMIN) >= 0) return res.end(true);
}
res.end(false);
@@ -284,10 +284,11 @@ function mailboxSearch(req, res, next) {
res.end();
}
});
} else if (req.dn.rdns[0].attrs.domain) {
} else if (req.dn.rdns[0].attrs.domain) { // legacy ldap mailbox search for old sogo
var domain = req.dn.rdns[0].attrs.domain.value.toLowerCase();
mailboxdb.listMailboxes(domain, 1, 1000, function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
var results = [];
@@ -310,7 +311,6 @@ function mailboxSearch(req, res, next) {
// ensure all filter values are also lowercase
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
results.push(obj);
}
@@ -318,8 +318,55 @@ function mailboxSearch(req, res, next) {
finalSend(results, req, res, next);
});
} else {
return next(new ldap.NoSuchObjectError(req.dn.toString()));
} else { // new sogo
mailboxdb.listAllMailboxes(1, 1000, function (error, mailboxes) {
if (error) return next(new ldap.OperationsError(error.toString()));
var results = [];
// send mailbox objects
async.eachSeries(mailboxes, function (mailbox, callback) {
var dn = ldap.parseDN(`cn=${mailbox.name}@${mailbox.domain},ou=mailboxes,dc=cloudron`);
users.get(mailbox.ownerId, function (error, userObject) {
if (error) return callback(); // skip mailboxes with unknown owner
var obj = {
dn: dn.toString(),
attributes: {
objectclass: ['mailbox'],
objectcategory: 'mailbox',
displayname: userObject.displayName,
cn: `${mailbox.name}@${mailbox.domain}`,
uid: `${mailbox.name}@${mailbox.domain}`,
mail: `${mailbox.name}@${mailbox.domain}`
}
};
mailboxdb.getAliasesForName(mailbox.name, mailbox.domain, function (error, aliases) {
if (error) return callback(error);
aliases.forEach(function (a, idx) {
obj.attributes['mail' + idx] = `${a}@${mailbox.domain}`;
});
// ensure all filter values are also lowercase
var lowerCaseFilter = safe(function () { return ldap.parseFilter(req.filter.toString().toLowerCase()); }, null);
if (!lowerCaseFilter) return next(new ldap.OperationsError(safe.error.toString()));
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && lowerCaseFilter.matches(obj.attributes)) {
results.push(obj);
}
callback();
});
});
}, function (error) {
if (error) return next(new ldap.OperationsError(error.toString()));
finalSend(results, req, res, next);
});
});
}
}
@@ -420,7 +467,7 @@ function authenticateUser(req, res, next) {
api = users.verifyWithUsername;
}
api(commonName, req.credentials || '', function (error, user) {
api(commonName, req.credentials || '', req.app.id, function (error, user) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
@@ -466,7 +513,7 @@ function authenticateUserMailbox(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
users.verify(mailbox.ownerId, req.credentials || '', users.AP_MAIL, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
@@ -487,13 +534,16 @@ function authenticateSftp(req, res, next) {
var parts = email.split('@');
if (parts.length !== 2) return next(new ldap.NoSuchObjectError(req.dn.toString()));
// actual user bind
users.verifyWithUsername(parts[0], req.credentials, function (error) {
apps.getByFqdn(parts[1], function (error, app) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
debug('sftp auth: success');
users.verifyWithUsername(parts[0], req.credentials, app.id, function (error) {
if (error) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
res.end();
debug('sftp auth: success');
res.end();
});
});
}
@@ -577,7 +627,7 @@ function authenticateMailAddon(req, res, next) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));
users.verify(mailbox.ownerId, req.credentials || '', function (error, result) {
users.verify(mailbox.ownerId, req.credentials || '', users.AP_MAIL, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
if (error) return next(new ldap.OperationsError(error.message));

View File

@@ -7,10 +7,11 @@ exports = module.exports = {
getDomains: getDomains,
getDomain: getDomain,
addDomain: addDomain,
removeDomain: removeDomain,
clearDomains: clearDomains,
onDomainAdded: onDomainAdded,
onDomainRemoved: onDomainRemoved,
removePrivateFields: removePrivateFields,
setDnsRecords: setDnsRecords,
@@ -101,7 +102,6 @@ function checkOutboundPort25(callback) {
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.comcast.net',
'smtp.1und1.de',
]);
@@ -313,9 +313,8 @@ function checkDmarc(domain, callback) {
if (txtRecords.length !== 0) {
dmarc.value = txtRecords[0].join('');
// allow extra fields in dmarc like rua
const actual = txtToDict(dmarc.value), expected = txtToDict(dmarc.expected);
dmarc.status = Object.keys(expected).every(k => expected[k] === actual[k]);
const actual = txtToDict(dmarc.value);
dmarc.status = actual.v === 'DMARC1'; // see box#666
}
callback(null, dmarc);
@@ -456,7 +455,7 @@ function getStatus(domain, callback) {
// ensure we always have a valid toplevel properties for the api
var results = {
dns: {}, // { mx/dmarc/dkim/spf/ptr: { expected, value, name, domain, type } }
dns: {}, // { mx/dmarc/dkim/spf/ptr: { expected, value, name, domain, type, status } }
rbl: {}, // { status, ip, servers: [{name,site,dns}]} optional. only for cloudron-smtp
relay: {} // { status, value } always checked
};
@@ -654,7 +653,6 @@ function configureMail(mailFqdn, mailDomain, callback) {
-v "${paths.MAIL_DATA_DIR}:/app/data" \
-v "${paths.PLATFORM_DATA_DIR}/addons/mail:/etc/mail" \
${ports} \
-p 127.0.0.1:2020:2020 \
--label isCloudronManaged=true \
--read-only -v /run -v /tmp ${tag}`;
@@ -907,37 +905,21 @@ function onMailFqdnChanged(callback) {
});
}
function addDomain(domain, callback) {
function onDomainAdded(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
const dkimSelector = domain === settings.adminDomain() ? 'cloudron' : ('cloudron-' + settings.adminDomain().replace(/\./g, ''));
maildb.add(domain, { dkimSelector }, function (error) {
if (error) return callback(error);
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], NOOP_CALLBACK); // do these asynchronously
callback();
});
async.series([
upsertDnsRecords.bind(null, domain, settings.mailFqdn()), // do this first to ensure DKIM keys
restartMailIfActivated
], callback);
}
function removeDomain(domain, callback) {
function onDomainRemoved(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
if (domain === settings.adminDomain()) return callback(new BoxError(BoxError.CONFLICT));
maildb.del(domain, function (error) {
if (error) return callback(error);
restartMail(NOOP_CALLBACK);
callback();
});
restartMail(callback);
}
function clearDomains(callback) {

View File

@@ -5,7 +5,7 @@ Dear <%= user.displayName || user.username || user.email %>,
Welcome to <%= cloudronName %>!
Follow the link to get started.
<%- setupLink %>
<%- inviteLink %>
<% if (invitor && invitor.email) { %>
You are receiving this email because you were invited by <%= invitor.email %>.
@@ -25,7 +25,7 @@ Powered by https://cloudron.io
<h2>Welcome to <%= cloudronName %>!</h2>
<p>
<a href="<%= setupLink %>">Get started</a>.
<a href="<%= inviteLink %>">Get started</a>.
</p>
<br/>

View File

@@ -12,6 +12,8 @@ exports = module.exports = {
listMailboxes: listMailboxes,
getLists: getLists,
listAllMailboxes: listAllMailboxes,
get: get,
getMailbox: getMailbox,
getList: getList,
@@ -214,6 +216,21 @@ function listMailboxes(domain, page, perPage, callback) {
});
}
function listAllMailboxes(page, perPage, callback) {
assert.strictEqual(typeof page, 'number');
assert.strictEqual(typeof perPage, 'number');
assert.strictEqual(typeof callback, 'function');
database.query(`SELECT ${MAILBOX_FIELDS} FROM mailboxes WHERE type = ? ORDER BY name LIMIT ${(page-1)*perPage},${perPage}`,
[ exports.TYPE_MAILBOX ], function (error, results) {
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getLists(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');

View File

@@ -1,8 +1,6 @@
'use strict';
exports = module.exports = {
add: add,
del: del,
get: get,
list: list,
update: update,
@@ -34,20 +32,6 @@ function postProcess(data) {
return data;
}
function add(domain, data, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof data, 'object');
assert.strictEqual(typeof callback, 'function');
database.query('INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', [ domain, data.dkimSelector || 'cloudron' ], function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'mail domain already exists'));
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND), 'no such domain');
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -58,20 +42,6 @@ function clear(callback) {
});
}
function del(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
// deletes aliases as well
database.query('DELETE FROM mail WHERE domain=?', [ domain ], function (error, result) {
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Mail domain not found'));
callback(null);
});
}
function get(domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');

View File

@@ -3,7 +3,7 @@
exports = module.exports = {
userAdded: userAdded,
userRemoved: userRemoved,
adminChanged: adminChanged,
roleChanged: roleChanged,
passwordReset: passwordReset,
appUpdatesAvailable: appUpdatesAvailable,
@@ -26,7 +26,6 @@ exports = module.exports = {
var assert = require('assert'),
BoxError = require('./boxerror.js'),
custom = require('./custom.js'),
debug = require('debug')('box:mailer'),
ejs = require('ejs'),
mail = require('./mail.js'),
@@ -47,15 +46,16 @@ function getMailConfig(callback) {
assert.strictEqual(typeof callback, 'function');
settings.getCloudronName(function (error, cloudronName) {
// this is not fatal
if (error) {
debug(error);
cloudronName = 'Cloudron';
}
if (error) debug('Error getting cloudron name: ', error);
callback(null, {
cloudronName: cloudronName,
notificationFrom: `"${cloudronName}" <no-reply@${settings.adminDomain()}>`
settings.getSupportConfig(function (error, supportConfig) {
if (error) debug('Error getting support config: ', error);
callback(null, {
cloudronName: cloudronName || '',
notificationFrom: `"${cloudronName}" <no-reply@${settings.adminDomain()}>`,
supportEmail: supportConfig.email
});
});
});
}
@@ -125,9 +125,10 @@ function mailUserEvent(mailTo, user, event) {
});
}
function sendInvite(user, invitor) {
function sendInvite(user, invitor, inviteLink) {
assert.strictEqual(typeof user, 'object');
assert(typeof invitor === 'object');
assert.strictEqual(typeof invitor, 'object');
assert.strictEqual(typeof inviteLink, 'string');
debug('Sending invite mail');
@@ -137,7 +138,7 @@ function sendInvite(user, invitor) {
var templateData = {
user: user,
webadminUrl: settings.adminOrigin(),
setupLink: `${settings.adminOrigin()}/api/v1/session/account/setup.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
inviteLink: inviteLink,
invitor: invitor,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
@@ -203,14 +204,13 @@ function userRemoved(mailTo, user) {
mailUserEvent(mailTo, user, 'was removed');
}
function adminChanged(mailTo, user, isAdmin) {
function roleChanged(mailTo, user) {
assert.strictEqual(typeof mailTo, 'string');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof isAdmin, 'boolean');
debug('Sending mail for adminChanged');
debug('Sending mail for roleChanged');
mailUserEvent(mailTo, user, isAdmin ? 'is now an admin' : 'is no more an admin');
mailUserEvent(mailTo, user, `now has the role '${user.role}'`);
}
function passwordReset(user) {
@@ -223,7 +223,7 @@ function passwordReset(user) {
var templateData = {
user: user,
resetLink: `${settings.adminOrigin()}/api/v1/session/password/reset.html?reset_token=${user.resetToken}&email=${encodeURIComponent(user.email)}`,
resetLink: `${settings.adminOrigin()}/login.html?resetToken=${user.resetToken}`,
cloudronName: mailConfig.cloudronName,
cloudronAvatarUrl: settings.adminOrigin() + '/api/v1/cloudron/avatar'
};
@@ -279,7 +279,7 @@ function appDied(mailTo, app) {
from: mailConfig.notificationFrom,
to: mailTo,
subject: util.format('[%s] App %s is down', mailConfig.cloudronName, app.fqdn),
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: custom.spec().support.email, format: 'text' })
text: render('app_down.ejs', { title: app.manifest.title, appFqdn: app.fqdn, supportEmail: mailConfig.supportEmail, format: 'text' })
};
sendMail(mailOptions);

View File

@@ -1,15 +1,12 @@
'use strict';
exports = module.exports = {
cookieParser: require('cookie-parser'),
cors: require('./cors'),
csrf: require('csurf'),
json: require('body-parser').json,
morgan: require('morgan'),
proxy: require('proxy-middleware'),
lastMile: require('connect-lastmile'),
multipart: require('./multipart.js'),
session: require('express-session'),
timeout: require('connect-timeout'),
urlencoded: require('body-parser').urlencoded
};

View File

@@ -58,17 +58,16 @@ server {
ssl_certificate <%= certFilePath %>;
ssl_certificate_key <%= keyFilePath %>;
ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
# https://cipherli.st/
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
# https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices#25-use-forward-secrecy
# ciphers according to https://ssl-config.mozilla.org/#server=nginx&version=1.14.0&config=intermediate&openssl=1.1.1&guideline=5.4
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# ciphers according to https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=nginx-1.10.3&openssl=1.0.2g&hsts=yes&profile=modern
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
@@ -128,7 +127,14 @@ server {
# only serve up the status page if we get proxy gateway errors
root <%= sourceDir %>/dashboard/dist;
# some apps use 503 to indicate updating or maintenance
error_page 502 504 /appstatus.html;
error_page 502 504 /app_error_page;
location /app_error_page {
root /home/yellowtent/boxdata;
# the first argument looks for file under the root
try_files /custom_pages/$request_uri /custom_pages/app_not_responding.html /appstatus.html;
# internal means this is for internal routing and cannot be accessed as URL from browser
internal;
}
location /appstatus.html {
internal;
}

View File

@@ -25,7 +25,6 @@ let assert = require('assert'),
auditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
changelog = require('./changelog.js'),
custom = require('./custom.js'),
debug = require('debug')('box:notifications'),
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
@@ -100,7 +99,8 @@ function actionForAllAdmins(skippingUserIds, iterator, callback) {
assert.strictEqual(typeof iterator, 'function');
assert.strictEqual(typeof callback, 'function');
users.getAllAdmins(function (error, result) {
users.getAdmins(function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return callback();
if (error) return callback(error);
// filter out users we want to skip (like the user who did the action or the user the action was performed on)
@@ -134,14 +134,14 @@ function userRemoved(performedBy, eventId, user, callback) {
}, callback);
}
function adminChanged(performedBy, eventId, user, callback) {
function roleChanged(performedBy, eventId, user, callback) {
assert.strictEqual(typeof performedBy, 'string');
assert.strictEqual(typeof user, 'object');
assert.strictEqual(typeof callback, 'function');
actionForAllAdmins([ performedBy, user.id ], function (admin, done) {
mailer.adminChanged(admin.email, user, user.admin);
add(admin.id, eventId, `User '${user.displayName} ' ${user.admin ? 'is now an admin' : 'is no more an admin'}`, `User '${user.username || user.email || user.fallbackEmail}' ${user.admin ? 'is now an admin' : 'is no more an admin'}.`, done);
mailer.roleChanged(admin.email, user);
add(admin.id, eventId, `User '${user.displayName}'s role changed`, `User '${user.username || user.email || user.fallbackEmail}' now has the role ${user.role}.`, done);
}, callback);
}
@@ -167,9 +167,6 @@ function oomEvent(eventId, app, addon, containerId, event, callback) {
message = 'The container has been restarted automatically. Consider increasing the [memory limit](https://docs.docker.com/v17.09/edge/engine/reference/commandline/update/#update-a-containers-kernel-memory-constraints)';
}
if (custom.spec().alerts.email) mailer.oomEvent(custom.spec().alerts.email, program, event);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.oomEvent(admin.email, program, event);
@@ -182,9 +179,6 @@ function appUp(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.appUp(custom.spec().alerts.email, app);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.appUp(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is back online`, `The application ${app.manifest.title} installed at ${app.fqdn} is back online.`, done);
@@ -196,9 +190,6 @@ function appDied(eventId, app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.appDied(custom.spec().alerts.email, app);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.appDied(admin.email, app);
add(admin.id, eventId, `App ${app.fqdn} is down`, `The application ${app.manifest.title} installed at ${app.fqdn} is not responding.`, callback);
@@ -246,9 +237,6 @@ function boxUpdateError(eventId, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.boxUpdateError(custom.spec().alerts.email, errorMessage);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, done) {
mailer.boxUpdateError(admin.email, errorMessage);
add(admin.id, eventId, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}. Update will be retried in 4 hours`, done);
@@ -261,9 +249,6 @@ function certificateRenewalError(eventId, vhost, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.certificateRenewalError(custom.spec().alerts.email, vhost, errorMessage);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.certificateRenewalError(admin.email, vhost, errorMessage);
add(admin.id, eventId, `Certificate renewal of ${vhost} failed`, `Failed to new certs of ${vhost}: ${errorMessage}. Renewal will be retried in 12 hours`, callback);
@@ -276,9 +261,6 @@ function backupFailed(eventId, taskId, errorMessage, callback) {
assert.strictEqual(typeof errorMessage, 'string');
assert.strictEqual(typeof callback, 'function');
if (custom.spec().alerts.email) mailer.backupFailed(custom.spec().alerts.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
if (!custom.spec().alerts.notifyCloudronAdmins) return callback();
actionForAllAdmins([], function (admin, callback) {
mailer.backupFailed(admin.email, errorMessage, `${settings.adminOrigin()}/logs.html?taskId=${taskId}`);
add(admin.id, eventId, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}). Will be retried in 4 hours`, callback);
@@ -343,8 +325,8 @@ function onEvent(id, action, source, data, callback) {
return userRemoved(source.userId, id, data.user, callback);
case eventlog.ACTION_USER_UPDATE:
if (!data.adminStatusChanged) return callback();
return adminChanged(source.userId, id, data.user, callback);
if (!data.roleChanged) return callback();
return roleChanged(source.userId, id, data.user, callback);
case eventlog.ACTION_APP_OOM:
return oomEvent(id, data.app, data.addon, data.containerId, data.event, callback);

View File

@@ -1,82 +0,0 @@
<% include header %>
<!-- tester -->
<script>
'use strict';
// very basic angular app
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', function ($scope) {
$scope.username = '<%= (user && user.username) ? user.username : '' %>';
$scope.displayName = '<%= (user && user.displayName) ? user.displayName : '' %>';
}]);
</script>
<div class="layout-content">
<center>
<br/>
<h4>Hello <%= (user && user.email) ? user.email : '' %>, welcome to <%= cloudronName %>!</h4>
<h2>Setup your account and password</h2>
</center>
<div class="container" ng-app="Application" ng-controller="Controller">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/account/setup" method="post" name="setupForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="email" value="<%= email %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<center><p class="has-error"><%= error %></p></center>
<% if (user && user.username) { %>
<div class="form-group"">
<label class="control-label">Username</label>
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
</div>
<% } else { %>
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
<label class="control-label">Username</label>
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
</div>
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
</div>
<% } %>
<div class="form-group">
<label class="control-label">Full Name</label>
<input type="displayName" class="form-control" ng-model="displayName" name="displayName" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
<label class="control-label">New Password</label>
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be atleast 8 characters</small>
</div>
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^.{8,}$/" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
<label class="control-label">Repeat Password</label>
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
</div>
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<center><input class="btn btn-primary btn-outline" type="submit" value="Setup" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/></center>
</form>
</div>
</div>
</div>
</div>
<% include footer %>

View File

@@ -1,45 +0,0 @@
<!-- callback tester -->
<script>
'use strict';
var code = null;
var redirectURI = null;
var accessToken = null;
var tokenType = null;
var state = null;
var args = window.location.search.slice(1).split('&');
args.forEach(function (arg) {
var tmp = arg.split('=');
if (tmp[0] === 'redirectURI') {
redirectURI = window.decodeURIComponent(tmp[1]);
} else if (tmp[0] === 'code') {
code = window.decodeURIComponent(tmp[1]);
} else if (tmp[0] === 'state') {
state = window.decodeURIComponent(tmp[1]);
}
});
args = window.location.hash.slice(1).split('&');
args.forEach(function (arg) {
var tmp = arg.split('=');
if (tmp[0] === 'access_token') {
accessToken = window.decodeURIComponent(tmp[1]);
} else if (tmp[0] === 'token_type') {
tokenType = window.decodeURIComponent(tmp[1]);
} else if (tmp[0] === 'state') {
state = window.decodeURIComponent(tmp[1]);
}
});
if (code && redirectURI) {
window.location.href = redirectURI + (redirectURI.indexOf('?') !== -1 ? '&' : '?') + 'code=' + code + (state ? '&state=' + state : '');
} else if (redirectURI && accessToken) {
window.location.href = redirectURI + (redirectURI.indexOf('?') !== -1 ? '&' : '?') + 'token=' + accessToken + (state ? '&state=' + state : '');
} else {
window.location.href = '/api/v1/session/login';
}
</script>

View File

@@ -1,27 +0,0 @@
<% include header %>
<!-- error tester -->
<div class="layout-content">
<div class="container" style="margin-top: 50px;">
<div class="row">
<div class="col-md-2"></div>
<div class="col-md-8">
<div class="alert alert-danger">
<%- message %>
</div>
</div>
<div class="col-md-2"></div>
</div>
<div class="row">
<div class="col-md-2"></div>
<div class="col-md-8 text-center">
<a href="<%- adminOrigin %>">Back</a>
</div>
<div class="col-md-2"></div>
</div>
</div>
</div>
<% include footer %>

View File

@@ -1,9 +0,0 @@
<footer class="text-center">
<span class="text-muted">&copy; 2016-20 <a href="https://cloudron.io" target="_blank">Cloudron</a></span>
</footer>
</div>
</body>
</html>

View File

@@ -1,41 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self'; img-src 'self'" />
<title> <%= title %> </title>
<link href="/api/v1/cloudron/avatar?<%= Math.random() %>" rel="icon" type="image/png">
<!-- Theme CSS -->
<link href="<%= adminOrigin %>/theme.css" rel="stylesheet">
<!-- Custom Fonts -->
<link href="<%= adminOrigin %>/3rdparty/fontawesome/css/all.min.css" rel="stylesheet" rel="stylesheet" type="text/css">
<!-- jQuery-->
<script src="<%= adminOrigin %>/3rdparty/js/jquery.min.js"></script>
<!-- Bootstrap Core JavaScript -->
<script src="<%= adminOrigin %>/3rdparty/js/bootstrap.min.js"></script>
<!-- Angularjs scripts -->
<script src="<%= adminOrigin %>/3rdparty/js/angular.min.js"></script>
<script src="<%= adminOrigin %>/3rdparty/js/angular-loader.min.js"></script>
</head>
<body>
<div class="layout-root">
<nav class="navbar navbar-default navbar-static-top shadow" role="navigation" style="margin-bottom: 0">
<div class="container-fluid">
<div class="navbar-header">
<a href="/" class="navbar-brand navbar-brand-icon"><img src="/api/v1/cloudron/avatar?<%= Math.random() %>" width="40" height="40"/></a>
<a href="/" class="navbar-brand"><%= cloudronName %></a>
</div>
</div>
</nav>

View File

@@ -1,58 +0,0 @@
<% include header %>
<!-- login tester -->
<div class="layout-content">
<div class="card" style="padding: 20px; margin-top: 50px; max-width: 620px;">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<img width="128" height="128" src="<%= applicationLogo %>?<%= Math.random() %>"/>
<h1><small>Login to</small> <%= applicationName %></h1>
<br/>
</div>
</div>
<br/>
<% if (error) { -%>
<div class="row">
<div class="col-md-12">
<h4 class="has-error"><%= error %></h4>
</div>
</div>
<% } -%>
<div class="row">
<div class="col-md-12">
<form id="loginForm" action="" method="post">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputUsername">Username</label>
<input type="text" class="form-control" id="inputUsername" name="username" value="<%= username %>" autofocus required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">Password</label>
<input type="password" class="form-control" name="password" id="inputPassword" value="<%= password %>" required>
</div>
<div class="form-group">
<label class="control-label" for="inputPassword">2FA Token (if enabled)</label>
<input type="text" class="form-control" name="totpToken" id="inputTotpToken" value="">
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Sign in"/>
</form>
<a href="/api/v1/session/password/resetRequest.html">Reset password</a>
</div>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var search = window.location.search.slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
document.getElementById('loginForm').action = '/api/v1/session/login?returnTo=' + search.returnTo;
})();
</script>
<% include footer %>

View File

@@ -1,53 +0,0 @@
<% include header %>
<!-- tester -->
<script>
'use strict';
// very basic angular app
var app = angular.module('Application', []);
app.controller('Controller', [function () {}]);
</script>
<div class="layout-content">
<center>
<h2>Hello <%= user.username %>, set a new password</h2>
</center>
<br/>
<div class="container" ng-app="Application" ng-controller="Controller">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/password/reset" method="post" name="resetForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<input type="hidden" name="email" value="<%= email %>"/>
<input type="hidden" name="resetToken" value="<%= resetToken %>"/>
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
<label class="control-label" for="inputPassword">New Password</label>
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be atleast 8 characters</small>
</div>
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^.{8,30}$/" autofocus required>
</div>
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
</div>
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Set New Password" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
</form>
</div>
</div>
</div>
</div>
<% include footer %>

View File

@@ -1,30 +0,0 @@
<% include header %>
<!-- tester -->
<div class="layout-content">
<center>
<h2>Reset password</h2>
</center>
<br/>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">
<form action="/api/v1/session/password/resetRequest" method="post" autocomplete="off">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group">
<label class="control-label" for="inputIdentifier">Username</label>
<input type="text" class="form-control" id="inputIdentifier" name="identifier" autofocus required>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Reset"/>
</form>
<a href="/api/v1/session/login">Login</a>
</div>
</div>
</div>
</div>
<% include footer %>

View File

@@ -1,25 +0,0 @@
<% include header %>
<!-- tester -->
<div class="layout-content">
<center>
<h2>Password reset successful</h2>
</center>
<br/>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3 text-center">
<p>An email was sent to you with a link to set a new password.</p>
<br/>
<br/>
If you have not received any email, simply <a href="/api/v1/session/password/resetRequest.html">try again</a>.
</div>
</div>
</div>
</div>
<% include footer %>

View File

@@ -18,12 +18,11 @@ exports = module.exports = {
INFRA_VERSION_FILE: path.join(baseDir(), 'platformdata/INFRA_VERSION'),
LICENSE_FILE: '/etc/cloudron/LICENSE',
PROVIDER_FILE: '/etc/cloudron/PROVIDER',
PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'),
APPS_DATA_DIR: path.join(baseDir(), 'appsdata'),
BOX_DATA_DIR: path.join(baseDir(), 'boxdata'),
CUSTOM_FILE: path.join(baseDir(), 'boxdata/custom.yml'),
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'),
@@ -36,11 +35,9 @@ exports = module.exports = {
UPDATE_DIR: path.join(baseDir(), 'platformdata/update'),
SNAPSHOT_INFO_FILE: path.join(baseDir(), 'platformdata/backup/snapshot-info.json'),
DYNDNS_INFO_FILE: path.join(baseDir(), 'platformdata/dyndns-info.json'),
FEATURES_INFO_FILE: path.join(baseDir(), 'platformdata/features-info.json'),
VERSION_FILE: path.join(baseDir(), 'platformdata/VERSION'),
SESSION_SECRET_FILE: path.join(baseDir(), 'boxdata/session.secret'),
SESSION_DIR: path.join(baseDir(), 'platformdata/sessions'),
// this is not part of appdata because an icon may be set before install
APP_ICONS_DIR: path.join(baseDir(), 'boxdata/appicons'),
PROFILE_ICONS_DIR: path.join(baseDir(), 'boxdata/profileicons'),
@@ -49,11 +46,14 @@ exports = module.exports = {
APP_CERTS_DIR: path.join(baseDir(), 'boxdata/certs'),
CLOUDRON_AVATAR_FILE: path.join(baseDir(), 'boxdata/avatar.png'),
UPDATE_CHECKER_FILE: path.join(baseDir(), 'boxdata/updatechecker.json'),
ADDON_TURN_SECRET_FILE: path.join(baseDir(), 'boxdata/addon-turn-secret'),
LOG_DIR: path.join(baseDir(), 'platformdata/logs'),
TASKS_LOG_DIR: path.join(baseDir(), 'platformdata/logs/tasks'),
CRASH_LOG_DIR: path.join(baseDir(), 'platformdata/logs/crash'),
GHOST_USER_FILE: path.join(baseDir(), 'platformdata/cloudron_ghost.json'),
// this pattern is for the cloudron logs API route to work
BACKUP_LOG_FILE: path.join(baseDir(), 'platformdata/logs/backup/app.log'),
UPDATER_LOG_FILE: path.join(baseDir(), 'platformdata/logs/updater/app.log')

View File

@@ -133,11 +133,10 @@ function pruneInfraImages(callback) {
function stopContainers(existingInfra, callback) {
// always stop addons to restart them on any infra change, regardless of minor or major update
if (existingInfra.version !== infra.version) {
// TODO: only nuke containers with isCloudronManaged=true
debug('stopping all containers for infra upgrade');
async.series([
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'network=cloudron\' | xargs --no-run-if-empty docker rm -f')
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker stop'),
shell.exec.bind(null, 'stopContainers', 'docker ps -qa --filter \'label=isCloudronManaged\' | xargs --no-run-if-empty docker rm -f')
], callback);
} else {
assert(typeof infra.images, 'object');
@@ -150,8 +149,8 @@ function stopContainers(existingInfra, callback) {
let filterArg = changedAddons.map(function (c) { return `--filter 'name=${c}'`; }).join(' '); // name=c matches *c*. required for redis-{appid}
// ignore error if container not found (and fail later) so that this code works across restarts
async.series([
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'network=cloudron' | xargs --no-run-if-empty docker rm -f || true`)
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker stop || true`),
shell.exec.bind(null, 'stopContainers', `docker ps -qa ${filterArg} --filter 'label=isCloudronManaged' | xargs --no-run-if-empty docker rm -f || true`)
], callback);
}
}

View File

@@ -15,7 +15,6 @@ var appstore = require('./appstore.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
clients = require('./clients.js'),
cloudron = require('./cloudron.js'),
debug = require('debug')('box:provision'),
domains = require('./domains.js'),
@@ -29,6 +28,7 @@ var appstore = require('./appstore.js'),
sysinfo = require('./sysinfo.js'),
users = require('./users.js'),
tld = require('tldjs'),
tokens = require('./tokens.js'),
_ = require('underscore');
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
@@ -121,7 +121,8 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
provider: dnsConfig.provider,
config: dnsConfig.config,
fallbackCertificate: dnsConfig.fallbackCertificate || null,
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' }
tlsConfig: dnsConfig.tlsConfig || { provider: 'letsencrypt-prod' },
dkimSelector: 'cloudron'
};
domains.add(domain, data, auditSource, function (error) {
@@ -137,7 +138,6 @@ function setup(dnsConfig, sysinfoConfig, auditSource, callback) {
settings.setSysinfoConfig.bind(null, sysinfoConfig),
domains.prepareDashboardDomain.bind(null, domain, auditSource, (progress) => setProgress('setup', progress.message, NOOP_CALLBACK)),
cloudron.setDashboardDomain.bind(null, domain, auditSource),
mail.addDomain.bind(null, domain), // this relies on settings.mailFqdn() and settings.adminDomain()
setProgress.bind(null, 'setup', 'Done'),
eventlog.add.bind(null, eventlog.ACTION_PROVISION, auditSource, { })
], function (error) {
@@ -165,7 +165,7 @@ function activate(username, password, email, displayName, ip, auditSource, callb
if (error && error.reason === BoxError.ALREADY_EXISTS) return callback(new BoxError(BoxError.CONFLICT, 'Already activated'));
if (error) return callback(error);
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(error);
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
@@ -221,7 +221,7 @@ function restore(backupConfig, backupId, version, sysinfoConfig, auditSource, ca
backups.restore.bind(null, backupConfig, backupId, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setSysinfoConfig.bind(null, sysinfoConfig),
cloudron.setupDashboard.bind(null, auditSource, (progress) => setProgress('restore', progress.message, NOOP_CALLBACK)),
settings.setBackupConfig.bind(null, backupConfig), // update with the latest backupConfig
settings.setBackupCredentials.bind(null, backupConfig), // update just the credentials and not the policy and flags
eventlog.add.bind(null, eventlog.ACTION_RESTORE, auditSource, { backupId }),
], function (error) {
gProvisionStatus.restore.active = false;
@@ -240,7 +240,7 @@ function getStatus(callback) {
users.isActivated(function (error, activated) {
if (error) return callback(error);
settings.getCloudronName(function (error, cloudronName) {
settings.getAll(function (error, allSettings) {
if (error) return callback(error);
callback(null, _.extend({
@@ -248,7 +248,8 @@ function getStatus(callback) {
apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool
webServerOrigin: settings.webServerOrigin(), // used by CaaS tool
provider: settings.provider(),
cloudronName: cloudronName,
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
adminFqdn: settings.adminDomain() ? settings.adminFqdn() : null,
activated: activated,
}, gProvisionStatus));

View File

@@ -79,7 +79,7 @@ function getCertApi(domainObject, callback) {
// we simply update the account with the latest email we have each time when getting letsencrypt certs
// https://github.com/ietf-wg-acme/acme/issues/30
users.getOwner(function (error, owner) {
options.email = error ? 'support@cloudron.io' : (owner.fallbackEmail || owner.email); // can error if not activated yet
options.email = error ? 'support@cloudron.io' : owner.email; // can error if not activated yet
callback(null, api, options);
});
@@ -146,19 +146,19 @@ function validateCertificate(location, domainObject, certificate) {
// -checkhost checks for SAN or CN exclusively. SAN takes precedence and if present, ignores the CN.
const fqdn = domains.fqdn(location, domainObject);
var result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
let result = safe.child_process.execSync(`openssl x509 -noout -checkhost "${fqdn}"`, { encoding: 'utf8', input: cert });
if (result === null) return new BoxError(BoxError.BAD_FIELD, 'Unable to get certificate subject:' + safe.error.message, { field: 'cert' });
if (result.indexOf('does match certificate') === -1) return new BoxError(BoxError.BAD_FIELD, `Certificate is not valid for this domain. Expecting ${fqdn}`, { field: 'cert' });
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
if (certModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get cert modulus: ${safe.error.message}`, { field: 'cert' });
// check if public key in the cert and private key matches. pkey below works for RSA and ECDSA keys
const pubKeyFromCert = safe.child_process.execSync('openssl x509 -noout -pubkey', { encoding: 'utf8', input: cert });
if (pubKeyFromCert === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from cert: ${safe.error.message}`, { field: 'cert' });
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (keyModulus === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get key modulus: ${safe.error.message}`, { field: 'cert' });
const pubKeyFromKey = safe.child_process.execSync('openssl pkey -pubout', { encoding: 'utf8', input: key });
if (pubKeyFromKey === null) return new BoxError(BoxError.BAD_FIELD, `Unable to get public key from private key: ${safe.error.message}`, { field: 'cert' });
if (certModulus !== keyModulus) return new BoxError(BoxError.BAD_FIELD, 'Key does not match the certificate.', { field: 'cert' });
if (pubKeyFromCert !== pubKeyFromKey) return new BoxError(BoxError.BAD_FIELD, 'Public key does not match the certificate.', { field: 'cert' });
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });

View File

@@ -1,159 +1,134 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
passwordAuth: passwordAuth,
tokenAuth: tokenAuth,
scope: scope,
authorize: authorize,
websocketAuth: websocketAuth
};
var accesscontrol = require('../accesscontrol.js'),
assert = require('assert'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
BoxError = require('../boxerror.js'),
clients = require('../clients.js'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
LocalStrategy = require('passport-local').Strategy,
passport = require('passport'),
users = require('../users.js');
users = require('../users.js'),
speakeasy = require('speakeasy');
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
function passwordAuth(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
// serialize user into session
passport.serializeUser(function (user, callback) {
callback(null, user.id);
});
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'A username must be non-empty string'));
if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'A password must be non-empty string'));
// deserialize user from session
passport.deserializeUser(function(userId, callback) {
users.get(userId, function (error, result) {
if (error) return callback(null, null /* user */, error.message); // will end up as a 401. can happen if user with active session got deleted
const username = req.body.username;
const password = req.body.password;
callback(null, result);
});
});
function check2FA(user) {
assert.strictEqual(typeof user, 'object');
if (!user.ghost && !user.appPassword && user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
// used when username/password is sent in request body. used in CLI login & oauth2 login route
passport.use(new LocalStrategy(function (username, password, callback) {
// TODO we should only do this for dashboard logins
function createAndVerifyUserIfNotExist(identifier, password, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
externalLdap.createAndVerifyUserIfNotExist(identifier.toLowerCase(), password, function (error, result) {
if (error && error.reason === BoxError.BAD_STATE) return callback(null, false);
if (error && error.reason === BoxError.BAD_FIELD) return callback(null, false);
if (error && error.reason === BoxError.CONFLICT) return callback(null, false);
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false);
if (error) return callback(error);
callback(null, result);
});
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
if (username.indexOf('@') === -1) {
users.verifyWithUsername(username, password, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password, callback);
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
} else {
users.verifyWithEmail(username, password, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password, callback);
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return callback(null, false);
if (error) return callback(error);
if (!result) return callback(null, false);
callback(null, result);
});
}
}));
// Used to authenticate a OAuth2 client which uses clientId and clientSecret in the Authorization header
passport.use(new BasicStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function (error, client) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.clientSecret !== clientSecret) return callback(null, false);
callback(null, client);
});
}));
// Used to authenticate a OAuth2 client which uses clientId and clientSecret in the request body (client_id, client_secret)
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
clients.get(clientId, function(error, client) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error) { return callback(error); }
if (client.clientSecret !== clientSecret) { return callback(null, false); }
callback(null, client);
});
}));
// used for "Authorization: Bearer token" or access_token query param authentication
passport.use(new BearerStrategy(function (token, callback) {
accesscontrol.validateToken(token, callback);
}));
callback(null);
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
callback(null);
}
// The scope middleware provides an auth middleware for routes.
//
// It is used for API routes, which are authenticated using accesstokens.
// Those accesstokens carry OAuth scopes and the middleware takes the required
// scope as an argument and will verify the accesstoken against it.
//
// See server.js:
// var profileScope = routes.oauth2.scope('profile');
//
function scope(requiredScope) {
assert.strictEqual(typeof requiredScope, 'string');
var requiredScopes = requiredScope.split(',');
return [
passport.authenticate(['bearer'], { session: false }),
function (req, res, next) {
assert(req.authInfo && typeof req.authInfo === 'object');
var error = accesscontrol.hasScopes(req.authInfo.authorizedScopes, requiredScopes);
if (error) return next(new HttpError(403, error.message));
next();
}
];
}
function websocketAuth(requiredScopes, req, res, next) {
assert(Array.isArray(requiredScopes));
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
accesscontrol.validateToken(req.query.access_token, function (error, user, info) {
if (error) return next(new HttpError(500, error.message));
if (!user) return next(new HttpError(401, 'Unauthorized'));
req.user = user;
var e = accesscontrol.hasScopes(info.authorizedScopes, requiredScopes);
if (e) return next(new HttpError(403, e.message));
next();
}
function createAndVerifyUserIfNotExist(identifier, password) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof password, 'string');
externalLdap.createAndVerifyUserIfNotExist(identifier.toLowerCase(), password, function (error, result) {
if (error && error.reason === BoxError.BAD_STATE) return next(new HttpError(401, 'Unauthorized'));
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(401, 'Unauthorized'));
if (error && error.reason === BoxError.CONFLICT) return next(new HttpError(401, 'Unauthorized'));
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(401, 'Unauthorized'));
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error));
check2FA(result);
});
}
if (username.indexOf('@') === -1) {
users.verifyWithUsername(username, password, users.AP_WEBADMIN, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password);
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error));
if (!result) return next(new HttpError(401, 'Unauthorized'));
check2FA(result);
});
} else {
users.verifyWithEmail(username, password, users.AP_WEBADMIN, function (error, result) {
if (error && error.reason === BoxError.NOT_FOUND) return createAndVerifyUserIfNotExist(username, password);
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error));
if (!result) return next(new HttpError(401, 'Unauthorized'));
check2FA(result);
});
}
}
function tokenAuth(req, res, next) {
var token;
// this determines the priority
if (req.body && req.body.access_token) token = req.body.access_token;
if (req.query && req.query.access_token) token = req.query.access_token;
if (req.headers && req.headers.authorization) {
var parts = req.headers.authorization.split(' ');
if (parts.length == 2) {
var scheme = parts[0];
var credentials = parts[1];
if (/^Bearer$/i.test(scheme)) token = credentials;
}
}
if (!token) return next(new HttpError(401, 'Unauthorized'));
accesscontrol.verifyToken(token, function (error, user) {
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error.message));
req.user = user;
next();
});
}
function authorize(requiredRole) {
assert.strictEqual(typeof requiredRole, 'string');
return function (req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (users.compareRoles(req.user.role, requiredRole) < 0) return next(new HttpError(403, `role '${requiredRole}' is required but user has only '${req.user.role}'`));
next();
};
}
function websocketAuth(requiredRole, req, res, next) {
assert.strictEqual(typeof requiredRole, 'string');
if (typeof req.query.access_token !== 'string') return next(new HttpError(401, 'Unauthorized'));
accesscontrol.verifyToken(req.query.access_token, function (error, user) {
if (error && error.reason === BoxError.INVALID_CREDENTIALS) return next(new HttpError(401, 'Unauthorized'));
if (error) return next(new HttpError(500, error.message));
req.user = user;
if (users.compareRoles(req.user.role, requiredRole) < 0) return next(new HttpError(403, `role '${requiredRole}' is required but user has only '${user.role}'`));
next();
});

View File

@@ -0,0 +1,61 @@
'use strict';
exports = module.exports = {
list: list,
get: get,
del: del,
add: add
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
users = require('../users.js');
function get(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.params.id, 'string');
users.getAppPassword(req.params.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result));
});
}
function add(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be string'));
if (typeof req.body.identifier !== 'string') return next(new HttpError(400, 'identifier must be string'));
users.addAppPassword(req.user.id, req.body.identifier, req.body.name, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, result));
});
}
function list(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
users.getAppPasswords(req.user.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { appPasswords: result }));
});
}
function del(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.params.id, 'string');
// TODO: verify userId owns the id ?
users.delAppPassword(req.params.id, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, {}));
});
}

View File

@@ -4,16 +4,16 @@ exports = module.exports = {
getApp: getApp,
getApps: getApps,
getAppIcon: getAppIcon,
installApp: installApp,
uninstallApp: uninstallApp,
restoreApp: restoreApp,
install: install,
uninstall: uninstall,
restore: restore,
importApp: importApp,
backupApp: backupApp,
updateApp: updateApp,
backup: backup,
update: update,
getLogs: getLogs,
getLogStream: getLogStream,
listBackups: listBackups,
repairApp: repairApp,
repair: repair,
setAccessRestriction: setAccessRestriction,
setLabel: setLabel,
@@ -31,16 +31,18 @@ exports = module.exports = {
setLocation: setLocation,
setDataDir: setDataDir,
stopApp: stopApp,
startApp: startApp,
restartApp: restartApp,
stop: stop,
start: start,
restart: restart,
exec: exec,
execWebSocket: execWebSocket,
cloneApp: cloneApp,
clone: clone,
uploadFile: uploadFile,
downloadFile: downloadFile
downloadFile: downloadFile,
load: load
};
var apps = require('../apps.js'),
@@ -51,19 +53,28 @@ var apps = require('../apps.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
users = require('../users.js'),
util = require('util'),
WebSocket = require('ws');
function getApp(req, res, next) {
function load(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
apps.get(req.params.id, function (error, app) {
apps.get(req.params.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, apps.removeInternalFields(app)));
req.resource = result;
next();
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
next(new HttpSuccess(200, apps.removeInternalFields(req.resource)));
}
function getApps(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
@@ -77,19 +88,19 @@ function getApps(req, res, next) {
}
function getAppIcon(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
apps.getIconPath(req.params.id, { original: req.query.original }, function (error, iconPath) {
apps.getIconPath(req.resource, { original: req.query.original }, function (error, iconPath) {
if (error) return next(BoxError.toHttpError(error));
res.sendFile(iconPath);
});
}
function installApp(req, res, next) {
function install(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
var data = req.body;
const data = req.body;
// atleast one
if ('manifest' in data && typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
@@ -133,22 +144,28 @@ function installApp(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
debug('Installing app :%j', data);
apps.install(data, req.user, auditSource.fromRequest(req), function (error, result) {
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to install app with docker addon'));
data.appStoreId = appStoreId;
data.manifest = manifest;
apps.install(data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { id: result.id, taskId: result.taskId }));
});
});
}
function setAccessRestriction(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.accessRestriction !== 'object') return next(new HttpError(400, 'accessRestriction must be an object'));
apps.setAccessRestriction(req.params.id, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
apps.setAccessRestriction(req.resource, req.body.accessRestriction, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -157,11 +174,11 @@ function setAccessRestriction(req, res, next) {
function setLabel(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.label !== 'string') return next(new HttpError(400, 'label must be a string'));
apps.setLabel(req.params.id, req.body.label, auditSource.fromRequest(req), function (error) {
apps.setLabel(req.resource, req.body.label, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -170,12 +187,12 @@ function setLabel(req, res, next) {
function setTags(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (!Array.isArray(req.body.tags)) return next(new HttpError(400, 'tags must be an array'));
if (req.body.tags.some((t) => typeof t !== 'string')) return next(new HttpError(400, 'tags array must contain strings'));
apps.setTags(req.params.id, req.body.tags, auditSource.fromRequest(req), function (error) {
apps.setTags(req.resource, req.body.tags, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -184,11 +201,11 @@ function setTags(req, res, next) {
function setIcon(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.icon !== null && typeof req.body.icon !== 'string') return next(new HttpError(400, 'icon is null or a base-64 image string'));
apps.setIcon(req.params.id, req.body.icon, auditSource.fromRequest(req), function (error) {
apps.setIcon(req.resource, req.body.icon, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -197,11 +214,11 @@ function setIcon(req, res, next) {
function setMemoryLimit(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit is not a number'));
apps.setMemoryLimit(req.params.id, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
apps.setMemoryLimit(req.resource, req.body.memoryLimit, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -210,11 +227,11 @@ function setMemoryLimit(req, res, next) {
function setCpuShares(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.cpuShares !== 'number') return next(new HttpError(400, 'cpuShares is not a number'));
apps.setCpuShares(req.params.id, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
apps.setCpuShares(req.resource, req.body.cpuShares, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -223,11 +240,11 @@ function setCpuShares(req, res, next) {
function setAutomaticBackup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticBackup(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
apps.setAutomaticBackup(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -236,11 +253,11 @@ function setAutomaticBackup(req, res, next) {
function setAutomaticUpdate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable must be a boolean'));
apps.setAutomaticUpdate(req.params.id, req.body.enable, auditSource.fromRequest(req), function (error) {
apps.setAutomaticUpdate(req.resource, req.body.enable, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -249,13 +266,13 @@ function setAutomaticUpdate(req, res, next) {
function setReverseProxyConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.robotsTxt !== null && typeof req.body.robotsTxt !== 'string') return next(new HttpError(400, 'robotsTxt is not a string'));
if (req.body.csp !== null && typeof req.body.csp !== 'string') return next(new HttpError(400, 'csp is not a string'));
apps.setReverseProxyConfig(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
apps.setReverseProxyConfig(req.resource, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -264,14 +281,14 @@ function setReverseProxyConfig(req, res, next) {
function setCertificate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.key !== null && typeof req.body.cert !== 'string') return next(new HttpError(400, 'cert must be a string'));
if (req.body.cert !== null && typeof req.body.key !== 'string') return next(new HttpError(400, 'key must be a string'));
if (req.body.cert && !req.body.key) return next(new HttpError(400, 'key must be provided'));
if (!req.body.cert && req.body.key) return next(new HttpError(400, 'cert must be provided'));
apps.setCertificate(req.params.id, req.body, auditSource.fromRequest(req), function (error) {
apps.setCertificate(req.resource, req.body, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
@@ -280,12 +297,12 @@ function setCertificate(req, res, next) {
function setEnvironment(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (!req.body.env || typeof req.body.env !== 'object') return next(new HttpError(400, 'env must be an object'));
if (Object.keys(req.body.env).some((key) => typeof req.body.env[key] !== 'string')) return next(new HttpError(400, 'env must contain values as strings'));
apps.setEnvironment(req.params.id, req.body.env, auditSource.fromRequest(req), function (error, result) {
apps.setEnvironment(req.resource, req.body.env, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -294,11 +311,11 @@ function setEnvironment(req, res, next) {
function setDebugMode(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.debugMode !== null && typeof req.body.debugMode !== 'object') return next(new HttpError(400, 'debugMode must be an object'));
apps.setDebugMode(req.params.id, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
apps.setDebugMode(req.resource, req.body.debugMode, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -307,12 +324,12 @@ function setDebugMode(req, res, next) {
function setMailbox(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.mailboxName !== null && typeof req.body.mailboxName !== 'string') return next(new HttpError(400, 'mailboxName must be a string'));
if (typeof req.body.mailboxDomain !== 'string') return next(new HttpError(400, 'mailboxDomain must be a string'));
apps.setMailbox(req.params.id, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
apps.setMailbox(req.resource, req.body.mailboxName, req.body.mailboxDomain, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -321,7 +338,7 @@ function setMailbox(req, res, next) {
function setLocation(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.body.location !== 'string') return next(new HttpError(400, 'location must be string')); // location may be an empty string
if (!req.body.domain) return next(new HttpError(400, 'domain is required'));
@@ -336,7 +353,7 @@ function setLocation(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.setLocation(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
apps.setLocation(req.resource, req.body, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -345,47 +362,49 @@ function setLocation(req, res, next) {
function setDataDir(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
if (req.body.dataDir !== null && typeof req.body.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string'));
apps.setDataDir(req.params.id, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
apps.setDataDir(req.resource, req.body.dataDir, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function repairApp(req, res, next) {
function repair(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
debug('Repair app id:%s', req.params.id);
assert.strictEqual(typeof req.resource, 'object');
const data = req.body;
if ('manifest' in data) {
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'backupId must be an object'));
if (!data.manifest || typeof data.manifest !== 'object') return next(new HttpError(400, 'manifest must be an object'));
if (safe.query(data.manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to repair app with docker addon'));
}
apps.repair(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
if ('dockerImage' in data) {
if (!data.dockerImage || typeof data.dockerImage !== 'string') return next(new HttpError(400, 'dockerImage must be a string'));
}
apps.repair(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function restoreApp(req, res, next) {
function restore(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
debug('Restore app id:%s', req.params.id);
if (!data.backupId || typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be non-empty string'));
apps.restore(req.params.id, data.backupId, auditSource.fromRequest(req), function (error, result) {
apps.restore(req.resource, data.backupId, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
@@ -394,12 +413,10 @@ function restoreApp(req, res, next) {
function importApp(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
debug('Importing app id:%s', req.params.id);
if ('backupId' in data) { // if not provided, we import in-place
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be string'));
if (typeof data.backupFormat !== 'string') return next(new HttpError(400, 'backupFormat must be string'));
@@ -418,21 +435,19 @@ function importApp(req, res, next) {
}
}
apps.importApp(req.params.id, data, auditSource.fromRequest(req), function (error, result) {
apps.importApp(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function cloneApp(req, res, next) {
function clone(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
debug('Clone app id:%s', req.params.id);
if (typeof data.backupId !== 'string') return next(new HttpError(400, 'backupId must be a string'));
if (typeof data.location !== 'string') return next(new HttpError(400, 'location is required'));
if (typeof data.domain !== 'string') return next(new HttpError(400, 'domain is required'));
@@ -440,76 +455,66 @@ function cloneApp(req, res, next) {
if ('overwriteDns' in req.body && typeof req.body.overwriteDns !== 'boolean') return next(new HttpError(400, 'overwriteDns must be boolean'));
apps.clone(req.params.id, data, req.user, auditSource.fromRequest(req), function (error, result) {
apps.clone(req.resource, data, req.user, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { id: result.id, taskId: result.taskId }));
});
}
function backupApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function backup(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Backup app id:%s', req.params.id);
apps.backup(req.params.id, function (error, result) {
apps.backup(req.resource, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function uninstallApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function uninstall(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Uninstalling app id:%s', req.params.id);
apps.uninstall(req.params.id, auditSource.fromRequest(req), function (error, result) {
apps.uninstall(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function startApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function start(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Start app id:%s', req.params.id);
apps.start(req.params.id, function (error, result) {
apps.start(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function stopApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function stop(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Stop app id:%s', req.params.id);
apps.stop(req.params.id, function (error, result) {
apps.stop(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function restartApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function restart(req, res, next) {
assert.strictEqual(typeof req.resource, 'object');
debug('Restart app id:%s', req.params.id);
apps.restart(req.params.id, function (error, result) {
apps.restart(req.resource, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
}
function updateApp(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
function update(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.resource, 'object');
var data = req.body;
@@ -521,20 +526,24 @@ function updateApp(req, res, next) {
if ('skipBackup' in data && typeof data.skipBackup !== 'boolean') return next(new HttpError(400, 'skipBackup must be a boolean'));
if ('force' in data && typeof data.force !== 'boolean') return next(new HttpError(400, 'force must be a boolean'));
debug('Update app id:%s to manifest:%j', req.params.id, data.manifest);
apps.update(req.params.id, req.body, auditSource.fromRequest(req), function (error, result) {
apps.downloadManifest(data.appStoreId, data.manifest, function (error, appStoreId, manifest) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
if (safe.query(manifest, 'addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is required to update app with docker addon'));
data.appStoreId = appStoreId;
data.manifest = manifest;
apps.update(req.resource, data, auditSource.fromRequest(req), function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId: result.taskId }));
});
});
}
// this route is for streaming logs
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Getting logstream of app id:%s', req.params.id);
assert.strictEqual(typeof req.resource, 'object');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
@@ -549,7 +558,7 @@ function getLogStream(req, res, next) {
format: 'json'
};
apps.getLogs(req.params.id, options, function (error, logStream) {
apps.getLogs(req.resource, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
@@ -571,20 +580,18 @@ function getLogStream(req, res, next) {
}
function getLogs(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10;
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug('Getting logs of app id:%s', req.params.id);
var options = {
lines: lines,
follow: false,
format: req.query.format || 'json'
};
apps.getLogs(req.params.id, options, function (error, logStream) {
apps.getLogs(req.resource, options, function (error, logStream) {
if (error) return next(BoxError.toHttpError(error));
res.writeHead(200, {
@@ -620,9 +627,7 @@ function demuxStream(stream, stdin) {
}
function exec(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Execing into app id:%s and cmd:%s', req.params.id, req.query.cmd);
assert.strictEqual(typeof req.resource, 'object');
var cmd = null;
if (req.query.cmd) {
@@ -636,9 +641,11 @@ function exec(req, res, next) {
var rows = req.query.rows ? parseInt(req.query.rows, 10) : null;
if (isNaN(rows)) return next(new HttpError(400, 'rows must be a number'));
var tty = req.query.tty === 'true' ? true : false;
var tty = req.query.tty === 'true';
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (safe.query(req.resource, 'manifest.addons.docker') && req.user.role !== users.ROLE_OWNER) return next(new HttpError(403, '"owner" role is requied to exec app with docker addon'));
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
if (req.headers['upgrade'] !== 'tcp') return next(new HttpError(404, 'exec requires TCP upgrade'));
@@ -660,9 +667,7 @@ function exec(req, res, next) {
}
function execWebSocket(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('Execing websocket into app id:%s and cmd:%s', req.params.id, req.query.cmd);
assert.strictEqual(typeof req.resource, 'object');
var cmd = null;
if (req.query.cmd) {
@@ -678,11 +683,9 @@ function execWebSocket(req, res, next) {
var tty = req.query.tty === 'true' ? true : false;
apps.exec(req.params.id, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
apps.exec(req.resource, { cmd: cmd, rows: rows, columns: columns, tty: tty }, function (error, duplexStream) {
if (error) return next(BoxError.toHttpError(error));
debug('Connected to terminal');
req.clearTimeout();
res.handleUpgrade(function (ws) {
@@ -710,7 +713,7 @@ function execWebSocket(req, res, next) {
}
function listBackups(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resource, 'object');
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
@@ -718,7 +721,7 @@ function listBackups(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
apps.listBackups(page, perPage, req.params.id, function (error, result) {
apps.listBackups(req.resource, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backups: result }));
@@ -726,30 +729,24 @@ function listBackups(req, res, next) {
}
function uploadFile(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('uploadFile: %s %j -> %s', req.params.id, req.files, req.query.file);
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
if (!req.files.file) return next(new HttpError(400, 'file must be provided as multipart'));
apps.uploadFile(req.params.id, req.files.file.path, req.query.file, function (error) {
apps.uploadFile(req.resource, req.files.file.path, req.query.file, function (error) {
if (error) return next(BoxError.toHttpError(error));
debug('uploadFile: done');
next(new HttpSuccess(202, {}));
});
}
function downloadFile(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
debug('downloadFile: ', req.params.id, req.query.file);
assert.strictEqual(typeof req.resource, 'object');
if (typeof req.query.file !== 'string' || !req.query.file) return next(new HttpError(400, 'file query argument must be provided'));
apps.downloadFile(req.params.id, req.query.file, function (error, stream, info) {
apps.downloadFile(req.resource, req.query.file, function (error, stream, info) {
if (error) return next(BoxError.toHttpError(error));
var headers = {

View File

@@ -5,6 +5,7 @@ exports = module.exports = {
getApp: getApp,
getAppVersion: getAppVersion,
createUserToken: createUserToken,
registerCloudron: registerCloudron,
getSubscription: getSubscription
};
@@ -12,35 +13,20 @@ exports = module.exports = {
var appstore = require('../appstore.js'),
assert = require('assert'),
BoxError = require('../boxerror.js'),
custom = require('../custom.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function isAppAllowed(appstoreId) {
if (custom.spec().appstore.blacklist.includes(appstoreId)) return false;
if (!custom.spec().appstore.whitelist) return true;
if (!custom.spec().appstore.whitelist[appstoreId]) return false;
return true;
}
function getApps(req, res, next) {
appstore.getApps(function (error, apps) {
if (error) return next(BoxError.toHttpError(error));
let filteredApps = apps.filter((app) => !custom.spec().appstore.blacklist.includes(app.id));
if (custom.spec().appstore.whitelist) filteredApps = filteredApps.filter((app) => app.id in custom.spec().appstore.whitelist);
next(new HttpSuccess(200, { apps: filteredApps }));
next(new HttpSuccess(200, { apps }));
});
}
function getApp(req, res, next) {
assert.strictEqual(typeof req.params.appstoreId, 'string');
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
appstore.getApp(req.params.appstoreId, function (error, app) {
if (error) return next(BoxError.toHttpError(error));
@@ -52,8 +38,6 @@ function getAppVersion(req, res, next) {
assert.strictEqual(typeof req.params.appstoreId, 'string');
assert.strictEqual(typeof req.params.versionId, 'string');
if (!isAppAllowed(req.params.appstoreId)) return next(new HttpError(405, 'feature disabled by admin'));
appstore.getAppVersion(req.params.appstoreId, req.params.versionId, function (error, manifest) {
if (error) return next(BoxError.toHttpError(error));
@@ -61,6 +45,14 @@ function getAppVersion(req, res, next) {
});
}
function createUserToken(req, res, next) {
appstore.getUserToken(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { accessToken: result }));
});
}
function registerCloudron(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
@@ -83,6 +75,6 @@ function getSubscription(req, res, next) {
appstore.getSubscription(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result)); // { email, cloudronId, plan, cancel_at, status }
next(new HttpSuccess(200, result)); // { email, cloudronId, cloudronCreatedAt, plan, current_period_end, canceled_at, cancel_at, status, features }
});
}

140
src/routes/branding.js Normal file
View File

@@ -0,0 +1,140 @@
'use strict';
exports = module.exports = {
get,
set,
getCloudronAvatar
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
settings = require('../settings.js'),
_ = require('underscore');
function getFooter(req, res, next) {
settings.getFooter(function (error, footer) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { footer }));
});
}
function setFooter(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.footer !== 'string') return next(new HttpError(400, 'footer is required'));
settings.setFooter(req.body.footer, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
});
}
function setCloudronName(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
settings.setCloudronName(req.body.name, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getCloudronName(req, res, next) {
settings.getCloudronName(function (error, name) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { name: name }));
});
}
function setAppstoreListingConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const listingConfig = _.pick(req.body, 'whitelist', 'blacklist');
if (Object.keys(listingConfig).length === 0) return next(new HttpError(400, 'blacklist or whitelist is required'));
if ('whitelist' in listingConfig) {
if (listingConfig.whitelist !== null && !Array.isArray(listingConfig.whitelist)) return next(new HttpError(400, 'whitelist is null or an array of strings'));
if (listingConfig.whitelist && !listingConfig.whitelist.every(id => typeof id === 'string')) return next(new HttpError(400, 'whitelist must be array of strings'));
}
if ('blacklist' in listingConfig) {
if (!Array.isArray(listingConfig.blacklist)) return next(new HttpError(400, 'blacklist an array of strings'));
if (!listingConfig.blacklist.every(id => typeof id === 'string')) return next(new HttpError(400, 'blacklist must be array of strings'));
}
settings.setAppstoreListingConfig(listingConfig, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getAppstoreListingConfig(req, res, next) {
settings.getAppstoreListingConfig(function (error, listingConfig) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, listingConfig));
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
if (!req.files.avatar) return next(new HttpError(400, 'avatar must be provided'));
var avatar = safe.fs.readFileSync(req.files.avatar.path);
settings.setCloudronAvatar(avatar, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getCloudronAvatar(req, res, next) {
settings.getCloudronAvatar(function (error, avatar) {
if (error) return next(BoxError.toHttpError(error));
// avoid caching the avatar on the client to see avatar changes immediately
res.set('Cache-Control', 'no-cache');
res.set('Content-Type', 'image/png');
res.status(200).send(avatar);
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.setting, 'string');
switch (req.params.setting) {
case settings.APPSTORE_LISTING_CONFIG_KEY: return getAppstoreListingConfig(req, res, next);
case settings.CLOUDRON_AVATAR_KEY: return getCloudronAvatar(req, res, next);
case settings.CLOUDRON_NAME_KEY: return getCloudronName(req, res, next);
case settings.FOOTER_KEY: return getFooter(req, res, next);
default: return next(new HttpError(404, 'No such setting'));
}
}
function set(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
switch (req.params.setting) {
case settings.APPSTORE_LISTING_CONFIG_KEY: return setAppstoreListingConfig(req, res, next);
case settings.CLOUDRON_AVATAR_KEY: return setCloudronAvatar(req, res, next);
case settings.CLOUDRON_NAME_KEY: return setCloudronName(req, res, next);
case settings.FOOTER_KEY: return setFooter(req, res, next);
default: return next(new HttpError(404, 'No such branding'));
}
}

View File

@@ -1,124 +0,0 @@
'use strict';
exports = module.exports = {
add: add,
get: get,
del: del,
getAll: getAll,
addToken: addToken,
getTokens: getTokens,
delTokens: delTokens,
delToken: delToken
};
var assert = require('assert'),
BoxError = require('../boxerror.js'),
clients = require('../clients.js'),
constants = require('../constants.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
validUrl = require('valid-url');
function add(req, res, next) {
var data = req.body;
if (!data) return next(new HttpError(400, 'Cannot parse data field'));
if (typeof data.appId !== 'string' || !data.appId) return next(new HttpError(400, 'appId is required'));
if (typeof data.redirectURI !== 'string' || !data.redirectURI) return next(new HttpError(400, 'redirectURI is required'));
if (typeof data.scope !== 'string' || !data.scope) return next(new HttpError(400, 'scope is required'));
if (!validUrl.isWebUri(data.redirectURI)) return next(new HttpError(400, 'redirectURI must be a valid uri'));
clients.add(data.appId, clients.TYPE_EXTERNAL, data.redirectURI, data.scope, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, result));
});
}
function get(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
clients.get(req.params.clientId, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, result));
});
}
function del(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
clients.get(req.params.clientId, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
// we do not allow to use the REST API to delete addon clients
if (result.type !== clients.TYPE_EXTERNAL) return next(new HttpError(405, 'Deleting app addon clients is not allowed.'));
clients.del(req.params.clientId, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204, result));
});
});
}
function getAll(req, res, next) {
clients.getAll(function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { clients: result }));
});
}
function addToken(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
assert.strictEqual(typeof req.body, 'object');
var data = req.body;
var expiresAt = data.expiresAt ? parseInt(data.expiresAt, 10) : Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
if (isNaN(expiresAt) || expiresAt <= Date.now()) return next(new HttpError(400, 'expiresAt must be a timestamp in the future'));
if ('name' in req.body && typeof req.body.name !== 'string') return next(new HttpError(400, 'name must be a string'));
clients.addTokenByUserId(req.params.clientId, req.user.id, expiresAt, { name: req.body.name || '' }, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { token: result }));
});
}
function getTokens(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.getTokensByUserId(req.params.clientId, req.user.id, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
result = result.map(clients.removeTokenPrivateFields);
next(new HttpSuccess(200, { tokens: result }));
});
}
function delTokens(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.delTokensByUserId(req.params.clientId, req.user.id, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
}
function delToken(req, res, next) {
assert.strictEqual(typeof req.params.clientId, 'string');
assert.strictEqual(typeof req.params.tokenId, 'string');
assert.strictEqual(typeof req.user, 'object');
clients.delToken(req.params.clientId, req.params.tokenId, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
}

View File

@@ -1,6 +1,11 @@
'use strict';
exports = module.exports = {
login: login,
logout: logout,
passwordResetRequest: passwordResetRequest,
passwordReset: passwordReset,
setupAccount: setupAccount,
reboot: reboot,
isRebootRequired: isRebootRequired,
getConfig: getConfig,
@@ -23,15 +28,135 @@ let assert = require('assert'),
auditSource = require('../auditsource.js'),
BoxError = require('../boxerror.js'),
cloudron = require('../cloudron.js'),
custom = require('../custom.js'),
constants = require('../constants.js'),
eventlog = require('../eventlog.js'),
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
sysinfo = require('../sysinfo.js'),
system = require('../system.js'),
tokendb = require('../tokendb.js'),
tokens = require('../tokens.js'),
updater = require('../updater.js'),
users = require('../users.js'),
updateChecker = require('../updatechecker.js');
function login(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if ('type' in req.body && typeof req.body.type !== 'string') return next(new HttpError(400, 'type must be a string'));
const type = req.body.type || tokens.ID_WEBADMIN;
const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
const auditSource = { authType: 'basic', ip: ip };
const error = tokens.validateTokenType(type);
if (error) return next(new HttpError(400, error.message));
tokens.add(type, req.user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: req.user.id, user: users.removePrivateFields(req.user) });
next(new HttpSuccess(200, result));
});
}
function logout(req, res) {
var token;
// this determines the priority
if (req.body && req.body.access_token) token = req.body.access_token;
if (req.query && req.query.access_token) token = req.query.access_token;
if (req.headers && req.headers.authorization) {
var parts = req.headers.authorization.split(' ');
if (parts.length == 2) {
var scheme = parts[0];
var credentials = parts[1];
if (/^Bearer$/i.test(scheme)) token = credentials;
}
}
if (!token) return res.redirect('/login.html');
tokendb.delByAccessToken(token, function () { res.redirect('/login.html'); });
}
function passwordResetRequest(req, res, next) {
if (!req.body.identifier || typeof req.body.identifier !== 'string') return next(new HttpError(401, 'A identifier must be non-empty string'));
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) console.error(error);
next(new HttpSuccess(202, {}));
});
}
function passwordReset(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid resetToken'));
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
if (!userObject.username) return next(new HttpError(409, 'No username set'));
// setPassword clears the resetToken
users.setPassword(userObject, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, { accessToken: result.accessToken }));
});
});
});
}
function setupAccount(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.email || typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a non-empty string'));
if (!req.body.resetToken || typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'resetToken must be a non-empty string'));
if (!req.body.password || typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a non-empty string'));
if (!req.body.username || typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a non-empty string'));
if (!req.body.displayName || typeof req.body.displayName !== 'string') return next(new HttpError(400, 'displayName must be a non-empty string'));
users.getByResetToken(req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid Reset Token'));
if (Date.now() - userObject.resetTokenCreationTime > 24 * 60 * 60 * 1000) return next(new HttpError(401, 'Token expired'));
users.update(userObject, { username: req.body.username, displayName: req.body.displayName }, auditSource.fromRequest(req), function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return next(new HttpError(409, 'Username already used'));
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error && error.reason === BoxError.NOT_FOUND) return next(new HttpError(404, 'No such user'));
if (error) return next(new HttpError(500, error));
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
// setPassword clears the resetToken
users.setPassword(userObject, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
tokens.add(tokens.ID_WEBADMIN, userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, { accessToken: result.accessToken }));
});
});
});
});
}
function reboot(req, res, next) {
// Finish the request, to let the appstore know we triggered the reboot
next(new HttpSuccess(202, {}));
@@ -165,8 +290,6 @@ function getLogStream(req, res, next) {
function setDashboardAndMailDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.setDashboardAndMailDomain(req.body.domain, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
@@ -177,8 +300,6 @@ function setDashboardAndMailDomain(req, res, next) {
function prepareDashboardDomain(req, res, next) {
if (!req.body.domain || typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
if (!custom.spec().domains.changeDashboardDomain) return next(new HttpError(405, 'feature disabled by admin'));
cloudron.prepareDashboardDomain(req.body.domain, auditSource.fromRequest(req), function (error, taskId) {
if (error) return next(BoxError.toHttpError(error));

View File

@@ -1,35 +0,0 @@
'use strict';
exports = module.exports = {
login: login
};
var clients = require('../clients.js'),
passport = require('passport'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
speakeasy = require('speakeasy');
function login(req, res, next) {
passport.authenticate('local', function (error, user) {
if (error) return next(new HttpError(500, error));
if (!user) return next(new HttpError(401, 'Invalid credentials'));
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
if (!user.ghost && user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) return next(new HttpError(401, 'A totpToken must be provided'));
let verified = speakeasy.totp.verify({ secret: user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
if (!verified) return next(new HttpError(401, 'Invalid totpToken'));
}
const auditSource = { authType: 'cli', ip: ip };
clients.issueDeveloperToken(user, auditSource, function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, result));
});
})(req, res, next);
}

View File

@@ -8,8 +8,6 @@ exports = module.exports = {
del: del,
checkDnsRecords: checkDnsRecords,
verifyDomainLock: verifyDomainLock
};
var assert = require('assert'),
@@ -19,18 +17,6 @@ var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
function verifyDomainLock(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
domains.get(req.params.domain, function (error, domain) {
if (error) return next(BoxError.toHttpError(error));
if (domain.locked) return next(new HttpError(423, 'This domain is locked'));
next();
});
}
function add(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
@@ -159,4 +145,4 @@ function checkDnsRecords(req, res, next) {
next(new HttpSuccess(200, { needsOverwrite: result.needsOverwrite }));
});
}
}

View File

@@ -2,18 +2,18 @@
exports = module.exports = {
accesscontrol: require('./accesscontrol.js'),
appPasswords: require('./apppasswords.js'),
apps: require('./apps.js'),
appstore: require('./appstore.js'),
backups: require('./backups.js'),
clients: require('./clients.js'),
branding: require('./branding.js'),
cloudron: require('./cloudron.js'),
developer: require('./developer.js'),
domains: require('./domains.js'),
eventlog: require('./eventlog.js'),
graphs: require('./graphs.js'),
groups: require('./groups.js'),
oauth2: require('./oauth2.js'),
mail: require('./mail.js'),
mailserver: require('./mailserver.js'),
notifications: require('./notifications.js'),
profile: require('./profile.js'),
provision: require('./provision.js'),
@@ -21,5 +21,6 @@ exports = module.exports = {
settings: require('./settings.js'),
support: require('./support.js'),
tasks: require('./tasks.js'),
tokens: require('./tokens.js'),
users: require('./users.js')
};

View File

@@ -2,9 +2,6 @@
exports = module.exports = {
getDomain: getDomain,
addDomain: addDomain,
getDomainStats: getDomainStats,
removeDomain: removeDomain,
setDnsRecords: setDnsRecords,
@@ -31,7 +28,7 @@ exports = module.exports = {
getList: getList,
addList: addList,
updateList: updateList,
removeList: removeList
removeList: removeList,
};
var assert = require('assert'),
@@ -39,11 +36,7 @@ var assert = require('assert'),
BoxError = require('../boxerror.js'),
mail = require('../mail.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
middleware = require('../middleware/index.js'),
url = require('url');
var mailProxy = middleware.proxy(url.parse('http://127.0.0.1:2020'));
HttpSuccess = require('connect-lastmile').HttpSuccess;
function getDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
@@ -55,31 +48,6 @@ function getDomain(req, res, next) {
});
}
function addDomain(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.domain !== 'string') return next(new HttpError(400, 'domain must be a string'));
mail.addDomain(req.body.domain, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(201, { domain: req.body.domain }));
});
}
function getDomainStats(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
var parsedUrl = url.parse(req.url, true /* parseQueryString */);
delete parsedUrl.query['access_token'];
delete req.headers['authorization'];
delete req.headers['cookies'];
req.url = url.format({ pathname: req.params.domain, query: parsedUrl.query });
mailProxy(req, res, next);
}
function setDnsRecords(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.params.domain, 'string');
@@ -95,16 +63,6 @@ function setDnsRecords(req, res, next) {
});
}
function removeDomain(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
mail.removeDomain(req.params.domain, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
});
}
function getStatus(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
@@ -197,10 +155,10 @@ function listMailboxes(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
mail.listMailboxes(req.params.domain, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));
@@ -261,10 +219,10 @@ function listAliases(req, res, next) {
assert.strictEqual(typeof req.params.domain, 'string');
var page = typeof req.query.page !== 'undefined' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a positive number'));
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a positive number'));
mail.listAliases(req.params.domain, page, perPage, function (error, result) {
if (error) return next(BoxError.toHttpError(error));

43
src/routes/mailserver.js Normal file
View File

@@ -0,0 +1,43 @@
'use strict';
exports = module.exports = {
proxy
};
var addons = require('../addons.js'),
assert = require('assert'),
BoxError = require('../boxerror.js'),
middleware = require('../middleware/index.js'),
HttpError = require('connect-lastmile').HttpError,
url = require('url');
function proxy(req, res, next) {
assert.strictEqual(typeof req.params.pathname, 'string');
let parsedUrl = url.parse(req.url, true /* parseQueryString */);
// do not proxy protected values
delete parsedUrl.query['access_token'];
delete req.headers['authorization'];
delete req.headers['cookies'];
addons.getServiceDetails('mail', 'CLOUDRON_MAIL_TOKEN', function (error, addonDetails) {
if (error) return next(BoxError.toHttpError(error));
parsedUrl.query['access_token'] = addonDetails.token;
req.url = url.format({ pathname: req.params.pathname, query: parsedUrl.query });
const proxyOptions = url.parse(`https://${addonDetails.ip}:3000`);
proxyOptions.rejectUnauthorized = false;
const mailserverProxy = middleware.proxy(proxyOptions);
mailserverProxy(req, res, function (error) {
if (!error) return next();
if (error.code === 'ECONNREFUSED') return next(new HttpError(424, 'Unable to connect to mail server'));
if (error.code === 'ECONNRESET') return next(new HttpError(424, 'Unable to query mail server'));
next(new HttpError(500, error));
});
});
}

View File

@@ -1,563 +0,0 @@
'use strict';
exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
loginForm: loginForm,
login: login,
logout: logout,
sessionCallback: sessionCallback,
passwordResetRequestSite: passwordResetRequestSite,
passwordResetRequest: passwordResetRequest,
passwordSentSite: passwordSentSite,
passwordResetSite: passwordResetSite,
passwordReset: passwordReset,
accountSetupSite: accountSetupSite,
accountSetup: accountSetup,
authorization: authorization,
token: token,
csrf: csrf
};
var apps = require('../apps.js'),
assert = require('assert'),
async = require('async'),
authcodedb = require('../authcodedb.js'),
BoxError = require('../boxerror.js'),
clients = require('../clients'),
constants = require('../constants.js'),
debug = require('debug')('box:routes/oauth2'),
eventlog = require('../eventlog.js'),
hat = require('../hat.js'),
HttpError = require('connect-lastmile').HttpError,
middleware = require('../middleware/index.js'),
oauth2orize = require('oauth2orize'),
passport = require('passport'),
querystring = require('querystring'),
session = require('connect-ensure-login'),
settings = require('../settings.js'),
speakeasy = require('speakeasy'),
url = require('url'),
users = require('../users.js'),
util = require('util'),
_ = require('underscore');
// appId is optional here
function auditSource(req, appId) {
var tmp = {
authType: 'oauth',
ip: req.headers['x-forwarded-for'] || req.connection.remoteAddress || null
};
if (appId) tmp.appId = appId;
return tmp;
}
var gServer = null;
function initialize() {
assert.strictEqual(gServer, null);
gServer = oauth2orize.createServer();
gServer.serializeClient(function (client, callback) {
return callback(null, client.id);
});
gServer.deserializeClient(function (id, callback) {
clients.get(id, callback);
});
// grant authorization code that can be exchanged for access tokens. this is used by external oauth clients
gServer.grant(oauth2orize.grant.code({ scopeSeparator: ',' }, function (client, redirectURI, user, ares, callback) {
debug('grant code:', client.id, redirectURI, user.id, ares);
var code = hat(256);
var expiresAt = Date.now() + 60 * 60000; // 1 hour
authcodedb.add(code, client.id, user.id, expiresAt, function (error) {
if (error) return callback(error);
debug('grant code: new auth code for client %s code %s', client.id, code);
callback(null, code);
});
}));
// exchange authorization codes for access tokens. this is used by external oauth clients
gServer.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, callback) {
authcodedb.get(code, function (error, authCode) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
if (client.id !== authCode.clientId) return callback(null, false);
authcodedb.del(code, function (error) {
if(error) return callback(error);
clients.addTokenByUserId(client.id, authCode.userId, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(error);
debug('exchange: new access token for client %s user %s token %s', client.id, authCode.userId, result.accessToken.slice(0, 6)); // partial token for security
callback(null, result.accessToken);
});
});
});
}));
// implicit token grant that skips issuing auth codes. this is used by our webadmin
gServer.grant(oauth2orize.grant.token({ scopeSeparator: ',' }, function (client, user, ares, callback) {
clients.addTokenByUserId(client.id, user.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return callback(error);
debug('grant token: new access token for client %s user %s token %s', client.id, user.id, result.accessToken.slice(0, 6)); // partial token for security
callback(null, result.accessToken);
});
}));
// overwrite the session.ensureLoggedIn to not use res.redirect() due to a chrome bug not sending cookies on redirects
session.ensureLoggedIn = function (redirectTo) {
assert.strictEqual(typeof redirectTo, 'string');
return function (req, res, next) {
if (!req.isAuthenticated || !req.isAuthenticated()) {
if (req.session) {
req.session.returnTo = req.originalUrl || req.url;
}
res.status(200).send(util.format('<script>window.location.href = "%s";</script>', redirectTo));
} else {
next();
}
};
};
}
function uninitialize() {
gServer = null;
}
function renderTemplate(res, template, data) {
assert.strictEqual(typeof res, 'object');
assert.strictEqual(typeof template, 'string');
assert.strictEqual(typeof data, 'object');
settings.getCloudronName(function (error, cloudronName) {
if (error) {
console.error(error);
cloudronName = 'Cloudron';
}
// amend template properties, for example used in the header
data.title = data.title || 'Cloudron';
data.adminOrigin = settings.adminOrigin();
data.cloudronName = cloudronName;
res.render(template, data);
});
}
function sendErrorPageOrRedirect(req, res, message) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof res, 'object');
assert.strictEqual(typeof message, 'string');
debug('sendErrorPageOrRedirect: returnTo %s.', req.query.returnTo, message);
if (typeof req.query.returnTo !== 'string') {
renderTemplate(res, 'error', {
message: message,
title: 'Cloudron Error'
});
} else {
var u = url.parse(req.query.returnTo);
if (!u.protocol || !u.host) {
return renderTemplate(res, 'error', {
message: 'Invalid request. returnTo query is not a valid URI. ' + message,
title: 'Cloudron Error'
});
}
res.redirect(util.format('%s//%s', u.protocol, u.host));
}
}
// use this instead of sendErrorPageOrRedirect(), in case we have a returnTo provided in the query, to avoid login loops
// This usually happens when the OAuth client ID is wrong
function sendError(req, res, message) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof res, 'object');
assert.strictEqual(typeof message, 'string');
renderTemplate(res, 'error', {
message: message,
title: 'Cloudron Error'
});
}
// -> GET /api/v1/session/login
function loginForm(req, res) {
if (typeof req.session.returnTo !== 'string') return sendErrorPageOrRedirect(req, res, 'Invalid login request. No returnTo provided.');
var u = url.parse(req.session.returnTo, true);
if (!u.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid login request. No client_id provided.');
function render(applicationName, applicationLogo) {
var error = req.query.error || null;
renderTemplate(res, 'login', {
csrf: req.csrfToken(),
applicationName: applicationName,
applicationLogo: applicationLogo,
error: error,
username: settings.isDemo() ? constants.DEMO_USERNAME : '',
password: settings.isDemo() ? 'cloudron' : '',
title: applicationName + ' Login'
});
}
function renderBuiltIn() {
settings.getCloudronName(function (error, cloudronName) {
if (error) {
console.error(error);
cloudronName = 'Cloudron';
}
render(cloudronName, '/api/v1/cloudron/avatar');
});
}
clients.get(u.query.client_id, function (error, result) {
if (error) return sendError(req, res, 'Unknown OAuth client');
switch (result.type) {
case clients.TYPE_BUILT_IN: return renderBuiltIn();
case clients.TYPE_EXTERNAL: return render(result.appId, '/api/v1/cloudron/avatar');
default: break;
}
apps.get(result.appId, function (error, result) {
if (error) return sendErrorPageOrRedirect(req, res, 'Unknown Application for those OAuth credentials');
var applicationName = result.fqdn;
render(applicationName, '/api/v1/apps/' + result.id + '/icon');
});
});
}
// -> POST /api/v1/session/login
function login(req, res) {
var returnTo = req.session.returnTo || req.query.returnTo;
var failureQuery = querystring.stringify({ error: 'Invalid username or password', returnTo: returnTo });
passport.authenticate('local', {
failureRedirect: '/api/v1/session/login?' + failureQuery
})(req, res, function (error) {
if (error) return res.redirect('/api/v1/session/login?' + failureQuery); // on some exception in the handlers
if (!req.user.ghost && req.user.twoFactorAuthenticationEnabled) {
if (!req.body.totpToken) {
let failureQuery = querystring.stringify({ error: 'A 2FA token is required', returnTo: returnTo });
return res.redirect('/api/v1/session/login?' + failureQuery);
}
let verified = speakeasy.totp.verify({ secret: req.user.twoFactorAuthenticationSecret, encoding: 'base32', token: req.body.totpToken, window: 2 });
if (!verified) {
let failureQuery = querystring.stringify({ error: 'The 2FA token is invalid', returnTo: returnTo });
return res.redirect('/api/v1/session/login?' + failureQuery);
}
}
res.redirect(returnTo);
});
}
// -> GET /api/v1/session/logout
function logout(req, res) {
function done() {
req.logout();
if (req.query && req.query.redirect) res.redirect(req.query.redirect);
else res.redirect('/');
}
if (!req.query.all) return done();
// find and destroy all login sessions by this user - this got rather complex quickly
req.sessionStore.list(function (error, result) {
if (error) {
console.error('Error listing sessions', error);
return done();
}
// WARNING fix this if we change the storage backend - Great stuff!
var sessionIds = result.map(function(s) { return s.replace('.json', ''); });
async.each(sessionIds, function (id, callback) {
req.sessionStore.get(id, function (error, result) {
if (error) {
console.error(`Error getting session ${id}`, error);
return callback();
}
// ignore empty or non passport sessions
if (!result || !result.passport || !result.passport.user) return callback();
// not this user
if (result.passport.user !== req.user.id) return callback();
req.sessionStore.destroy(id, function (error) {
if (error) console.error(`Unable to destroy session ${id}`, error);
callback();
});
});
}, done);
});
}
// Form to enter email address to send a password reset request mail
// -> GET /api/v1/session/password/resetRequest.html
function passwordResetRequestSite(req, res) {
var data = {
csrf: req.csrfToken(),
title: 'Password Reset'
};
renderTemplate(res, 'password_reset_request', data);
}
// This route is used for above form submission
// -> POST /api/v1/session/password/resetRequest
function passwordResetRequest(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.identifier !== 'string') return next(new HttpError(400, 'Missing identifier')); // email or username
debug('passwordResetRequest: email or username %s.', req.body.identifier);
users.resetPasswordByIdentifier(req.body.identifier, function (error) {
if (error && error.reason !== BoxError.NOT_FOUND) {
console.error(error);
return sendErrorPageOrRedirect(req, res, 'User not found');
}
res.redirect('/api/v1/session/password/sent.html');
});
}
// -> GET /api/v1/session/password/sent.html
function passwordSentSite(req, res) {
renderTemplate(res, 'password_reset_sent', { title: 'Cloudron Password Reset' });
}
function renderAccountSetupSite(res, req, userObject, error) {
renderTemplate(res, 'account_setup', {
user: userObject,
error: error,
csrf: req.csrfToken(),
resetToken: req.query.reset_token || req.body.resetToken,
email: req.query.email || req.body.email,
title: 'Account Setup'
});
}
// -> GET /api/v1/session/account/setup.html
function accountSetupSite(req, res) {
if (!req.query.reset_token) return sendError(req, res, 'Missing Reset Token');
if (!req.query.email) return sendError(req, res, 'Missing Email');
users.getByResetToken(req.query.email, req.query.reset_token, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Email or Reset Token');
renderAccountSetupSite(res, req, userObject, '');
});
}
// -> POST /api/v1/session/account/setup
function accountSetup(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'Missing username'));
if (typeof req.body.displayName !== 'string') return next(new HttpError(400, 'Missing displayName'));
debug(`acountSetup: for email ${req.body.email} with token ${req.body.resetToken}`);
users.getByResetToken(req.body.email, req.body.resetToken, function (error, userObject) {
if (error) return sendError(req, res, 'Invalid Reset Token');
var data = _.pick(req.body, 'username', 'displayName');
users.update(userObject.id, data, auditSource(req), function (error) {
if (error && error.reason === BoxError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
if (error && error.reason === BoxError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error && error.reason === BoxError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
if (error) return next(new HttpError(500, error));
userObject.username = req.body.username;
userObject.displayName = req.body.displayName;
// setPassword clears the resetToken
users.setPassword(userObject.id, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
if (error) return next(new HttpError(500, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${settings.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
});
}
// -> GET /api/v1/session/password/reset.html
function passwordResetSite(req, res, next) {
if (!req.query.email) return next(new HttpError(400, 'Missing email'));
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
users.getByResetToken(req.query.email, req.query.reset_token, function (error, user) {
if (error) return next(new HttpError(401, 'Invalid email or reset token'));
renderTemplate(res, 'password_reset', {
user: user,
csrf: req.csrfToken(),
resetToken: req.query.reset_token,
email: req.query.email,
title: 'Password Reset'
});
});
}
// -> POST /api/v1/session/password/reset
function passwordReset(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
debug(`passwordReset: for ${req.body.email} with token ${req.body.resetToken}`);
users.getByResetToken(req.body.email, req.body.resetToken, function (error, userObject) {
if (error) return next(new HttpError(401, 'Invalid email or resetToken'));
if (!userObject.username) return next(new HttpError(401, 'No username set'));
// setPassword clears the resetToken
users.setPassword(userObject.id, req.body.password, function (error) {
if (error && error.reason === BoxError.BAD_FIELD) return next(new HttpError(406, error.message));
if (error) return next(new HttpError(500, error));
clients.addTokenByUserId('cid-webadmin', userObject.id, Date.now() + constants.DEFAULT_TOKEN_EXPIRATION, {}, function (error, result) {
if (error) return next(new HttpError(500, error));
res.redirect(`${settings.adminOrigin()}?accessToken=${result.accessToken}&expiresAt=${result.expires}`);
});
});
});
}
// The callback page takes the redirectURI and the authCode and redirects the browser accordingly
//
// -> GET /api/v1/session/callback
function sessionCallback() {
return [
session.ensureLoggedIn('/api/v1/session/login'),
function (req, res) {
renderTemplate(res, 'callback', { callbackServer: req.query.redirectURI });
}
];
}
// The authorization endpoint is the entry point for an OAuth login.
//
// Each app would start OAuth by redirecting the user to:
//
// /api/v1/oauth/dialog/authorize?response_type=code&client_id=<clientId>&redirect_uri=<callbackURL>&scope=<ignored>
//
// - First, this will ensure the user is logged in.
// - Then it will redirect the browser to the given <callbackURL> containing the authcode in the query
//
// -> GET /api/v1/oauth/dialog/authorize
function authorization() {
return [
function (req, res, next) {
if (!req.query.redirect_uri) return sendErrorPageOrRedirect(req, res, 'Invalid request. redirect_uri query param is not set.');
if (!req.query.client_id) return sendErrorPageOrRedirect(req, res, 'Invalid request. client_id query param is not set.');
if (!req.query.response_type) return sendErrorPageOrRedirect(req, res, 'Invalid request. response_type query param is not set.');
if (req.query.response_type !== 'code' && req.query.response_type !== 'token') return sendErrorPageOrRedirect(req, res, 'Invalid request. Only token and code response types are supported.');
session.ensureLoggedIn('/api/v1/session/login?returnTo=' + req.query.redirect_uri)(req, res, next);
},
gServer.authorization({}, function (clientId, redirectURI, callback) {
debug('authorization: client %s with callback to %s.', clientId, redirectURI);
clients.get(clientId, function (error, client) {
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
// ignore the origin passed into form the client, but use the one from the clientdb
var redirectPath = url.parse(redirectURI).path;
var redirectOrigin = client.redirectURI;
callback(null, client, '/api/v1/session/callback?redirectURI=' + encodeURIComponent(url.resolve(redirectOrigin, redirectPath)));
});
}),
function (req, res, next) {
// Handle our different types of oauth clients
var type = req.oauth2.client.type;
if (type === clients.TYPE_EXTERNAL || type === clients.TYPE_BUILT_IN) {
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, req.oauth2.client.appId), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
return next();
}
apps.get(req.oauth2.client.appId, function (error, appObject) {
if (error) return sendErrorPageOrRedirect(req, res, 'Invalid request. Unknown app for this client_id.');
apps.hasAccessTo(appObject, req.oauth2.user, function (error, access) {
if (error) return sendError(req, res, 'Internal error');
if (!access) return sendErrorPageOrRedirect(req, res, 'No access to this app.');
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource(req, appObject.id), { userId: req.oauth2.user.id, user: users.removePrivateFields(req.oauth2.user) });
next();
});
});
},
gServer.decision({ loadTransaction: false })
];
}
// The token endpoint allows an OAuth client to exchange an authcode with an accesstoken.
//
// Authcodes are obtained using the authorization endpoint. The route is authenticated by
// providing a Basic auth with clientID as username and clientSecret as password.
// An authcode is only good for one such exchange to an accesstoken.
//
// -> POST /api/v1/oauth/token
function token() {
return [
passport.authenticate(['basic', 'oauth2-client-password'], { session: false }),
gServer.token(), // will call the token grant callback registered in initialize()
gServer.errorHandler()
];
}
// Cross-site request forgery protection middleware for login form
function csrf() {
return [
middleware.csrf(),
function (err, req, res, next) {
if (err.code !== 'EBADCSRFTOKEN') return next(err);
sendErrorPageOrRedirect(req, res, 'Form expired');
}
];
}

View File

@@ -37,7 +37,7 @@ function get(req, res, next) {
fallbackEmail: req.user.fallbackEmail,
displayName: req.user.displayName,
twoFactorAuthenticationEnabled: req.user.twoFactorAuthenticationEnabled,
admin: req.user.admin,
role: req.user.role,
source: req.user.source,
avatarUrl: fs.existsSync(path.join(paths.PROFILE_ICONS_DIR, req.user.id)) ? `${settings.adminOrigin()}/api/v1/profile/avatar/${req.user.id}` : `https://www.gravatar.com/avatar/${emailHash}.jpg`
}));
@@ -53,7 +53,7 @@ function update(req, res, next) {
var data = _.pick(req.body, 'email', 'fallbackEmail', 'displayName');
users.update(req.user.id, data, auditSource.fromRequest(req), function (error) {
users.update(req.user, data, auditSource.fromRequest(req), function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
@@ -88,7 +88,7 @@ function changePassword(req, res, next) {
if (typeof req.body.newPassword !== 'string') return next(new HttpError(400, 'newPassword must be a string'));
users.setPassword(req.user.id, req.body.newPassword, function (error) {
users.setPassword(req.user, req.body.newPassword, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));

View File

@@ -12,7 +12,6 @@ exports = module.exports = {
var addons = require('../addons.js'),
assert = require('assert'),
BoxError = require('../boxerror.js'),
debug = require('debug')('box:routes/addons'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -60,8 +59,6 @@ function getLogs(req, res, next) {
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number'));
debug(`Getting logs of service ${req.params.service}`);
var options = {
lines: lines,
follow: false,
@@ -85,8 +82,6 @@ function getLogs(req, res, next) {
function getLogStream(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
debug(`Getting logstream of service ${req.params.service}`);
var lines = 'lines' in req.query ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id
if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number'));
@@ -124,8 +119,6 @@ function getLogStream(req, res, next) {
function restart(req, res, next) {
assert.strictEqual(typeof req.params.service, 'string');
debug(`Restarting service ${req.params.service}`);
addons.restartService(req.params.service, function (error) {
if (error) return next(BoxError.toHttpError(error));

View File

@@ -1,21 +1,20 @@
'use strict';
exports = module.exports = {
set: set,
get: get,
set,
get,
getCloudronAvatar: getCloudronAvatar
// owner only settings
setBackupConfig
};
var assert = require('assert'),
backups = require('../backups.js'),
BoxError = require('../boxerror.js'),
custom = require('../custom.js'),
docker = require('../docker.js'),
externalLdap = require('../externalldap.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
safe = require('safetydance'),
settings = require('../settings.js');
function getAppAutoupdatePattern(req, res, next) {
@@ -58,26 +57,6 @@ function setBoxAutoupdatePattern(req, res, next) {
});
}
function setCloudronName(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
settings.setCloudronName(req.body.name, function (error) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getCloudronName(req, res, next) {
settings.getCloudronName(function (error, name) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { name: name }));
});
}
function getTimeZone(req, res, next) {
settings.getTimeZone(function (error, tz) {
if (error) return next(BoxError.toHttpError(error));
@@ -98,28 +77,11 @@ function setTimeZone(req, res, next) {
});
}
function setCloudronAvatar(req, res, next) {
assert.strictEqual(typeof req.files, 'object');
if (!req.files.avatar) return next(new HttpError(400, 'avatar must be provided'));
var avatar = safe.fs.readFileSync(req.files.avatar.path);
settings.setCloudronAvatar(avatar, function (error) {
function getSupportConfig(req, res, next) {
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
});
}
function getCloudronAvatar(req, res, next) {
settings.getCloudronAvatar(function (error, avatar) {
if (error) return next(BoxError.toHttpError(error));
// avoid caching the avatar on the client to see avatar changes immediately
res.set('Cache-Control', 'no-cache');
res.set('Content-Type', 'image/png');
res.status(200).send(avatar);
next(new HttpSuccess(200, supportConfig));
});
}
@@ -127,11 +89,6 @@ function getBackupConfig(req, res, next) {
settings.getBackupConfig(function (error, backupConfig) {
if (error) return next(BoxError.toHttpError(error));
// always send provider as it is used by the UI to figure if backups are disabled ('noop' backend)
if (!custom.spec().backups.configurable) {
return next(new HttpSuccess(200, { provider: backupConfig.provider }));
}
next(new HttpSuccess(200, backups.removePrivateFields(backupConfig)));
});
}
@@ -139,8 +96,6 @@ function getBackupConfig(req, res, next) {
function setBackupConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().backups.configurable) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.retentionSecs !== 'number') return next(new HttpError(400, 'retentionSecs is required'));
if (typeof req.body.intervalSecs !== 'number') return next(new HttpError(400, 'intervalSecs is required'));
@@ -225,8 +180,6 @@ function getDynamicDnsConfig(req, res, next) {
function setDynamicDnsConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().domains.dynamicDns) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled boolean is required'));
settings.setDynamicDnsConfig(req.body.enabled, function (error) {
@@ -314,9 +267,8 @@ function get(req, res, next) {
case settings.APP_AUTOUPDATE_PATTERN_KEY: return getAppAutoupdatePattern(req, res, next);
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return getBoxAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return getTimeZone(req, res, next);
case settings.CLOUDRON_NAME_KEY: return getCloudronName(req, res, next);
case settings.CLOUDRON_AVATAR_KEY: return getCloudronAvatar(req, res, next);
case settings.SUPPORT_CONFIG_KEY: return getSupportConfig(req, res, next);
default: return next(new HttpError(404, 'No such setting'));
}
@@ -327,7 +279,6 @@ function set(req, res, next) {
switch (req.params.setting) {
case settings.DYNAMIC_DNS_KEY: return setDynamicDnsConfig(req, res, next);
case settings.BACKUP_CONFIG_KEY: return setBackupConfig(req, res, next);
case settings.PLATFORM_CONFIG_KEY: return setPlatformConfig(req, res, next);
case settings.EXTERNAL_LDAP_KEY: return setExternalLdapConfig(req, res, next);
case settings.UNSTABLE_APPS_KEY: return setUnstableAppsConfig(req, res, next);
@@ -337,9 +288,6 @@ function set(req, res, next) {
case settings.APP_AUTOUPDATE_PATTERN_KEY: return setAppAutoupdatePattern(req, res, next);
case settings.BOX_AUTOUPDATE_PATTERN_KEY: return setBoxAutoupdatePattern(req, res, next);
case settings.TIME_ZONE_KEY: return setTimeZone(req, res, next);
case settings.CLOUDRON_NAME_KEY: return setCloudronName(req, res, next);
case settings.CLOUDRON_AVATAR_KEY: return setCloudronAvatar(req, res, next);
default: return next(new HttpError(404, 'No such setting'));
}

View File

@@ -4,23 +4,35 @@ exports = module.exports = {
createTicket: createTicket,
getRemoteSupport: getRemoteSupport,
enableRemoteSupport: enableRemoteSupport
enableRemoteSupport: enableRemoteSupport,
canCreateTicket: canCreateTicket,
canEnableRemoteSupport: canEnableRemoteSupport
};
var appstore = require('../appstore.js'),
assert = require('assert'),
auditSource = require('../auditsource.js'),
custom = require('../custom.js'),
constants = require('../constants.js'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
settings = require('../settings.js'),
support = require('../support.js'),
_ = require('underscore');
function canCreateTicket(req, res, next) {
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(new HttpError(503, error.message));
if (!supportConfig.submitTickets) return next(new HttpError(405, 'feature disabled by admin'));
next();
});
}
function createTicket(req, res, next) {
assert.strictEqual(typeof req.user, 'object');
if (!custom.spec().support.submitTickets) return next(new HttpError(405, 'feature disabled by admin'));
const VALID_TYPES = [ 'feedback', 'ticket', 'app_missing', 'app_error', 'upgrade_request', 'email_error' ];
if (typeof req.body.type !== 'string' || !req.body.type) return next(new HttpError(400, 'type must be string'));
@@ -30,18 +42,31 @@ function createTicket(req, res, next) {
if (req.body.appId && typeof req.body.appId !== 'string') return next(new HttpError(400, 'appId must be string'));
if (req.body.altEmail && typeof req.body.altEmail !== 'string') return next(new HttpError(400, 'altEmail must be string'));
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), auditSource.fromRequest(req), function (error) {
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${custom.spec().support.email}`));
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(new HttpError(503, `Error getting support config: ${error.message}`));
if (supportConfig.email !== constants.SUPPORT_EMAIL) return next(new HttpError(503, 'Sending to non-cloudron email not implemented yet'));
next(new HttpSuccess(201, { message: `An email for sent to ${custom.spec().support.email}. We will get back shortly!` }));
appstore.createTicket(_.extend({ }, req.body, { email: req.user.email, displayName: req.user.displayName }), auditSource.fromRequest(req), function (error, result) {
if (error) return next(new HttpError(503, `Error contacting cloudron.io: ${error.message}. Please email ${constants.SUPPORT_EMAIL}`));
next(new HttpSuccess(201, result));
});
});
}
function canEnableRemoteSupport(req, res, next) {
settings.getSupportConfig(function (error, supportConfig) {
if (error) return next(new HttpError(503, error.message));
if (!supportConfig.remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
next();
});
}
function enableRemoteSupport(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!custom.spec().support.remoteSupport) return next(new HttpError(405, 'feature disabled by admin'));
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enabled is required'));
support.enableRemoteSupport(req.body.enable, auditSource.fromRequest(req), function (error) {

View File

@@ -1,97 +1,27 @@
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var accesscontrol = require('../accesscontrol.js'),
expect = require('expect.js'),
HttpError = require('connect-lastmile').HttpError,
passport = require('passport');
HttpError = require('connect-lastmile').HttpError;
describe('scopes middleware', function () {
var passportAuthenticateSave = null;
before(function () {
passportAuthenticateSave = passport.authenticate;
passport.authenticate = function () {
return function (req, res, next) { next(); };
};
describe('access control middleware', function () {
describe('passwordAuth', function () {
// TBD
});
after(function () {
passport.authenticate = passportAuthenticateSave;
describe('tokenAuth', function () {
// TBD
});
it('fails due to empty scope in request', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: { authorizedScopes: [ ] } };
mw(req, null, function (error) {
expect(error).to.be.a(HttpError);
done();
});
describe('authorize', function () {
// TBD
});
it('fails due to wrong scope in request', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: { authorizedScopes: [ 'foobar', 'something' ] } };
mw(req, null, function (error) {
expect(error).to.be.a(HttpError);
done();
});
});
it('fails due to wrong scope in request', function (done) {
var mw = accesscontrol.scope('admin,users')[1];
var req = { authInfo: { authorizedScopes: [ 'foobar', 'admin' ] } };
mw(req, null, function (error) {
expect(error).to.be.a(HttpError);
done();
});
});
it('succeeds with one requested scope and one provided scope', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: { authorizedScopes: [ 'admin' ] } };
mw(req, null, function (error) {
expect(error).to.not.be.ok();
done();
});
});
it('succeeds with one requested scope and two provided scopes', function (done) {
var mw = accesscontrol.scope('admin')[1];
var req = { authInfo: { authorizedScopes: [ 'foobar', 'admin' ] } };
mw(req, null, function (error) {
expect(error).to.not.be.ok();
done();
});
});
it('succeeds with two requested scope and two provided scopes', function (done) {
var mw = accesscontrol.scope('admin,foobar')[1];
var req = { authInfo: { authorizedScopes: [ 'foobar', 'admin' ] } };
mw(req, null, function (error) {
expect(error).to.not.be.ok();
done();
});
});
it('succeeds with two requested scope and provided wildcard scope', function (done) {
var mw = accesscontrol.scope('admin,foobar')[1];
var req = { authInfo: { authorizedScopes: [ '*' ] } };
mw(req, null, function (error) {
expect(error).to.not.be.ok();
done();
});
describe('websocketAuth', function () {
// TBD
});
});

View File

@@ -7,7 +7,6 @@
let apps = require('../../apps.js'),
async = require('async'),
child_process = require('child_process'),
clients = require('../../clients.js'),
constants = require('../../constants.js'),
crypto = require('crypto'),
database = require('../../database.js'),
@@ -29,6 +28,7 @@ let apps = require('../../apps.js'),
settingsdb = require('../../settingsdb.js'),
superagent = require('superagent'),
tokendb = require('../../tokendb.js'),
tokens = require('../../tokens.js'),
url = require('url');
var SERVER_URL = 'http://localhost:' + constants.PORT;
@@ -36,8 +36,8 @@ var SERVER_URL = 'http://localhost:' + constants.PORT;
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '25.19.0';
var TEST_IMAGE_REPO = 'docker.io/cloudron/io.cloudron.testapp';
var TEST_IMAGE_TAG = '20200207-233155-725d9e015';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
const DOMAIN_0 = {
@@ -213,7 +213,7 @@ function startBox(done) {
token_1 = hat(8 * 32);
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: user_1_id, clientId: clients.ID_SDK, expires: Date.now() + 1000000, scope: 'apps', name: '' }, callback);
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: user_1_id, clientId: tokens.ID_SDK, expires: Date.now() + 1000000, scope: 'apps', name: '' }, callback);
});
},
@@ -633,22 +633,6 @@ describe('App API', function () {
});
});
it('oauth addon config', function (done) {
var appContainer = docker.getContainer(appEntry.containerId);
appContainer.inspect(function (error, data) {
expect(error).to.not.be.ok();
clients.getByAppIdAndType(APP_ID, clients.TYPE_OAUTH, function (error, client) {
expect(error).to.not.be.ok();
expect(client.id.length).to.be(40); // cid- + 32 hex chars (128 bits) + 4 hyphens
expect(client.clientSecret.length).to.be(256); // 32 hex chars (8 * 256 bits)
expect(data.Config.Env).to.contain('CLOUDRON_OAUTH_CLIENT_ID=' + client.id);
expect(data.Config.Env).to.contain('CLOUDRON_OAUTH_CLIENT_SECRET=' + client.clientSecret);
done();
});
});
});
it('installation - app can populate addons', function (done) {
superagent.get(`http://localhost:${appEntry.httpPort}/populate_addons`).end(function (error, res) {
expect(!error).to.be.ok();

View File

@@ -0,0 +1,238 @@
'use strict';
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
var async = require('async'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
fs = require('fs'),
paths = require('../../paths.js'),
server = require('../../server.js'),
superagent = require('superagent');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
async.series([
server.start.bind(null),
database._clear.bind(null),
function createAdmin(callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(!error).to.be.ok();
server.stop(done);
});
}
describe('Branding API', function () {
before(setup);
after(cleanup);
describe('cloudron_name', function () {
var name = 'foobar';
it('get default succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.name).to.be.ok();
done();
});
});
it('cannot set without name', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set empty name', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.send({ name: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.send({ name: name })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_name')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.name).to.eql(name);
done();
});
});
});
describe('cloudron_avatar', function () {
it('get default succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.a(Buffer);
done();
});
});
it('cannot set without data', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/cloudron_avatar')
.query({ access_token: token })
.attach('avatar', paths.CLOUDRON_DEFAULT_AVATAR_FILE)
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
it('get succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/cloudron_avatar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.toString()).to.eql(fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE, 'utf-8'));
done(err);
});
});
});
describe('appstore listing config', function () {
it('get default succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.whitelist).to.eql(null);
expect(res.body.blacklist).to.eql([]);
done();
});
});
it('cannot set with no bl or wl', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set bad bl', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.send({ blacklist: [ 1 ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('cannot set bad wl', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.send({ whitelist: 4 })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('set bl succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.send({ blacklist: [ 'id1', 'id2' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
it('get bl succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.blacklist).to.eql([ 'id1', 'id2' ]);
expect(res.body.whitelist).to.be(undefined);
done();
});
});
it('set wl succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.send({ whitelist: [ 'id1', 'id2' ] })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
});
it('get wl succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/branding/appstore_listing_config')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body.whitelist).to.eql([ 'id1', 'id2' ]);
expect(res.body.blacklist).to.be(undefined);
done();
});
});
});
});

View File

@@ -1,546 +0,0 @@
'use strict';
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
constants = require('../../constants.js'),
clients = require('../../clients.js'),
database = require('../../database.js'),
oauth2 = require('../oauth2.js'),
expect = require('expect.js'),
uuid = require('uuid'),
hat = require('../../hat.js'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var token = null;
function setup(done) {
async.series([
server.start,
database._clear,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.equal(201);
// stash token for further use
token = result.body.token;
callback();
});
}
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
server.stop(done);
});
}
describe('OAuth Clients API', function () {
describe('add', function () {
before(setup),
after(cleanup);
it('fails without token', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty appId', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: '', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without scope', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty scope', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: '', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with malformed redirectURI', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'foobar', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with invalid name', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: '$"$%^45asdfasdfadf.adf.', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('succeeds with dash', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'fo-1234-bar', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
done();
});
});
it('succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: 'someApp', redirectURI: 'http://foobar.com', scope: accesscontrol.SCOPE_PROFILE })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
expect(result.body.id).to.be.a('string');
expect(result.body.appId).to.be.a('string');
expect(result.body.redirectURI).to.be.a('string');
expect(result.body.clientSecret).to.be.a('string');
expect(result.body.scope).to.be.a('string');
expect(result.body.type).to.equal(clients.TYPE_EXTERNAL);
done();
});
});
});
describe('get', function () {
var CLIENT_0 = {
id: '',
appId: 'someAppId-0',
redirectURI: 'http://some.callback0',
scope: accesscontrol.SCOPE_PROFILE
};
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
CLIENT_0 = result.body;
callback();
});
}
], done);
});
after(cleanup);
it('fails without token', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown id', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body).to.eql(CLIENT_0);
done();
});
});
});
describe('del', function () {
var CLIENT_0 = {
id: '',
appId: 'someAppId-0',
redirectURI: 'http://some.callback0',
scope: accesscontrol.SCOPE_PROFILE
};
var CLIENT_1 = {
id: '',
appId: 'someAppId-1',
redirectURI: 'http://some.callback1',
scope: accesscontrol.SCOPE_PROFILE,
type: clients.TYPE_OAUTH
};
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.send({ appId: CLIENT_0.appId, redirectURI: CLIENT_0.redirectURI, scope: CLIENT_0.scope })
.end(function (error, result) {
expect(result.statusCode).to.equal(201);
CLIENT_0 = result.body;
callback();
});
}
], done);
});
after(cleanup);
it('fails without token', function (done) {
superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown id', function (done) {
superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id.toUpperCase())
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_0.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
});
it('fails for cid-webadmin', function (done) {
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
done();
});
});
});
it('fails for addon auth client', function (done) {
clients.add(CLIENT_1.appId, CLIENT_1.type, CLIENT_1.redirectURI, CLIENT_1.scope, function (error, result) {
expect(error).to.equal(null);
CLIENT_1.id = result.id;
superagent.del(SERVER_URL + '/api/v1/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(405);
superagent.get(SERVER_URL + '/api/v1/clients/' + CLIENT_1.id)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
done();
});
});
});
});
});
});
describe('Clients', function () {
var USER_0 = {
userId: uuid.v4(),
username: 'someusername',
password: 'Strong#$%2345',
email: 'some@email.com',
admin: true,
salt: 'somesalt',
createdAt: (new Date()).toISOString(),
modifiedAt: (new Date()).toISOString(),
resetToken: hat(256)
};
// make csrf always succeed for testing
oauth2.csrf = function () {
return function (req, res, next) {
req.csrfToken = function () { return hat(256); };
next();
};
};
function setup2(done) {
async.series([
setup,
function (callback) {
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result).to.be.ok();
expect(result.statusCode).to.eql(200);
USER_0.id = result.body.id;
callback();
});
}
], done);
}
describe('get', function () {
before(setup2);
after(cleanup);
it('fails due to missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to empty token', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.clients.length).to.eql(3);
done();
});
});
});
describe('get tokens by client', function () {
before(setup2);
after(cleanup);
it('fails due to missing token', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to empty token', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to unkown client', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/CID-WEBADMIN/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
done();
});
});
});
describe('delete tokens by client', function () {
before(setup2);
after(cleanup);
it('fails due to missing token', function (done) {
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to empty token', function (done) {
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to unkown client', function (done) {
superagent.del(SERVER_URL + '/api/v1/clients/CID-WEBADMIN/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.tokens.length).to.eql(1);
expect(result.body.tokens[0].identifier).to.eql(USER_0.id);
superagent.del(SERVER_URL + '/api/v1/clients/cid-webadmin/tokens')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(204);
// further calls with this token should not work
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
});
});
});
});

View File

@@ -13,8 +13,9 @@ let async = require('async'),
http = require('http'),
nock = require('nock'),
os = require('os'),
superagent = require('superagent'),
server = require('../../server.js'),
speakeasy = require('speakeasy'),
superagent = require('superagent'),
settings = require('../../settings.js'),
tokendb = require('../../tokendb.js');
@@ -192,16 +193,233 @@ describe('Cloudron', function () {
});
});
it('fails (non-admin)', function (done) {
it('succeeds (non-admin)', function (done) {
superagent.get(SERVER_URL + '/api/v1/config')
.query({ access_token: token_1 })
.end(function (error, result) {
expect(result.statusCode).to.equal(403);
expect(result.statusCode).to.equal(200);
expect(result.body.apiServerOrigin).to.eql('http://localhost:6060');
expect(result.body.webServerOrigin).to.eql('https://cloudron.io');
expect(result.body.adminFqdn).to.eql(settings.adminFqdn());
expect(result.body.version).to.eql(constants.VERSION);
expect(result.body.cloudronName).to.be.a('string');
done();
});
});
});
describe('login', function () {
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
callback();
});
},
], done);
});
after(cleanup);
it('fails without body', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: USERNAME })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: '', password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with empty password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: USERNAME, password: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unknown username', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: USERNAME + USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown email', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: USERNAME + EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: USERNAME, password: PASSWORD.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('with username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
it('with uppercase username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
it('with email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
it('with uppercase email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/cloudron/login')
.send({ username: EMAIL.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
});
describe('2fa login', function () {
var secret, accessToken;
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/activate`).query({ setupToken: 'somesetuptoken' }).send({ username: USERNAME, password: PASSWORD, email: EMAIL }).end(function (error) {
callback(error);
});
},
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/login`).send({ username: USERNAME, password: PASSWORD }).end(function (error, result) {
accessToken = result.body.accessToken;
callback(error);
});
},
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
secret = result.body.secret;
callback(error);
});
},
function (callback) {
var totpToken = speakeasy.totp({
secret: secret,
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error) {
callback(error);
});
}
], done);
});
after(function (done) {
async.series([
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error) {
callback(error);
});
},
cleanup
], done);
});
it('fails due to missing token', function (done) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/login`).send({ username: USERNAME, password: PASSWORD }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/login`).send({ username: USERNAME, password: PASSWORD }).send({ totpToken: 'wrongtoken' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
var totpToken = speakeasy.totp({
secret: secret,
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/cloudron/login`).send({ username: USERNAME, password: PASSWORD }).send({ totpToken: totpToken }).end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body).to.be.an(Object);
expect(result.body.accessToken).to.be.a('string');
done();
});
});
});
describe('logs', function () {
before(function (done) {
async.series([

View File

@@ -1,248 +0,0 @@
'use strict';
/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
var async = require('async'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
speakeasy = require('speakeasy'),
superagent = require('superagent'),
server = require('../../server.js');
var SERVER_URL = 'http://localhost:' + constants.PORT;
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
function setup(done) {
async.series([
server.start.bind(server),
database._clear
], done);
}
function cleanup(done) {
database._clear(function (error) {
expect(error).to.not.be.ok();
server.stop(done);
});
}
describe('Developer API', function () {
describe('login', function () {
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(result).to.be.ok();
callback();
});
},
], done);
});
after(cleanup);
it('fails without body', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails without password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with empty username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: '', password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with empty password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: '' })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown username', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME + USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with unknown email', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME + EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails with wrong password', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD.toUpperCase() })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('with username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
it('with uppercase username succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: USERNAME.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
it('with email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: EMAIL, password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
it('with uppercase email succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/developer/login')
.send({ username: EMAIL.toUpperCase(), password: PASSWORD })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(new Date(result.body.expires).toString()).to.not.be('Invalid Date');
expect(result.body.accessToken).to.be.a('string');
done();
});
});
});
describe('2fa login', function () {
var secret, accessToken;
before(function (done) {
async.series([
setup,
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/cloudron/activate`).query({ setupToken: 'somesetuptoken' }).send({ username: USERNAME, password: PASSWORD, email: EMAIL }).end(function (error) {
callback(error);
});
},
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/developer/login`).send({ username: USERNAME, password: PASSWORD }).end(function (error, result) {
accessToken = result.body.accessToken;
callback(error);
});
},
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication`).query({ access_token: accessToken }).end(function (error, result) {
secret = result.body.secret;
callback(error);
});
},
function (callback) {
var totpToken = speakeasy.totp({
secret: secret,
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/enable`).query({ access_token: accessToken }).send({ totpToken: totpToken }).end(function (error) {
callback(error);
});
}
], done);
});
after(function (done) {
async.series([
function (callback) {
superagent.post(`${SERVER_URL}/api/v1/profile/twofactorauthentication/disable`).query({ access_token: accessToken }).send({ password: PASSWORD }).end(function (error) {
callback(error);
});
},
cleanup
], done);
});
it('fails due to missing token', function (done) {
superagent.post(`${SERVER_URL}/api/v1/developer/login`).send({ username: USERNAME, password: PASSWORD }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails due to wrong token', function (done) {
superagent.post(`${SERVER_URL}/api/v1/developer/login`).send({ username: USERNAME, password: PASSWORD }).send({ totpToken: 'wrongtoken' }).end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
var totpToken = speakeasy.totp({
secret: secret,
encoding: 'base32'
});
superagent.post(`${SERVER_URL}/api/v1/developer/login`).send({ username: USERNAME, password: PASSWORD }).send({ totpToken: totpToken }).end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body).to.be.an(Object);
expect(result.body.accessToken).to.be.a('string');
done();
});
});
});
});

View File

@@ -213,49 +213,6 @@ describe('Domains API', function () {
});
});
describe('locked', function () {
before(function (done) {
domaindb.update(DOMAIN_0.domain, { locked: true }, done);
});
after(function (done) {
domaindb.update(DOMAIN_0.domain, { locked: false }, done);
});
it('can list the domains', function (done) {
superagent.get(SERVER_URL + '/api/v1/domains')
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.domains).to.be.an(Array);
expect(result.body.domains.length).to.equal(2);
expect(result.body.domains[0].domain).to.equal(DOMAIN_0.domain);
expect(result.body.domains[1].domain).to.equal(DOMAIN_1.domain);
done();
});
});
it('cannot get locked domain', function (done) {
superagent.get(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(423);
done();
});
});
it('cannot delete locked domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (error, result) {
expect(result.statusCode).to.equal(423);
done();
});
});
});
describe('delete', function () {
it('fails for non-existing domain', function (done) {
superagent.delete(SERVER_URL + '/api/v1/domains/' + DOMAIN_0.domain + DOMAIN_0.domain)

View File

@@ -6,8 +6,7 @@
'use strict';
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
var async = require('async'),
constants = require('../../constants.js'),
database = require('../../database.js'),
eventlogdb = require('../../eventlogdb.js'),
@@ -59,7 +58,7 @@ function setup(done) {
function (callback) {
superagent.post(SERVER_URL + '/api/v1/users')
.query({ access_token: token })
.send({ username: 'nonadmin', email: 'notadmin@server.test', invite: false })
.send({ username: 'nonadmin', email: 'notadmin@server.test' })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
@@ -73,7 +72,7 @@ function setup(done) {
token_1 = hat(8 * 32);
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add({ id: 'tid-0', accessToken: token_1, identifier: USER_1_ID, clientId: 'test-client-id', expires: Date.now() + 100000, scope: accesscontrol.SCOPE_PROFILE, name: '' }, callback);
tokendb.add({ id: 'tid-0', accessToken: token_1, identifier: USER_1_ID, clientId: 'test-client-id', expires: Date.now() + 100000, scope: 'unused', name: '' }, callback);
},
function (callback) {
@@ -200,7 +199,7 @@ describe('Eventlog API', function () {
.query({ access_token: token, page: 1, per_page: 10, search: EMAIL })
.end(function (error, result) {
expect(result.statusCode).to.equal(200);
expect(result.body.eventlogs.length).to.equal(2);
expect(result.body.eventlogs.length).to.equal(1);
done();
});

View File

@@ -6,8 +6,7 @@
'use strict';
var accesscontrol = require('../../accesscontrol.js'),
async = require('async'),
var async = require('async'),
constants = require('../../constants.js'),
database = require('../../database.js'),
expect = require('expect.js'),
@@ -67,7 +66,7 @@ function setup(done) {
userId_1 = result.body.id;
// HACK to get a token for second user (passwords are generated and the user should have gotten a password setup link...)
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: userId_1, clientId: 'test-client-id', expires: Date.now() + 100000, scope: accesscontrol.SCOPE_PROFILE, name: '' }, callback);
tokendb.add({ id: 'tid-1', accessToken: token_1, identifier: userId_1, clientId: 'test-client-id', expires: Date.now() + 100000, scope: 'unused', name: '' }, callback);
});
}
], done);

View File

@@ -124,46 +124,6 @@ describe('Mail API', function () {
after(cleanup);
describe('crud', function () {
it('cannot add non-existing domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: 'doesnotexist.com' })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('domain must be a string', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: ['doesnotexist.com'] })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('can add domain', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
it('cannot add domain twice', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('cannot get non-existing domain', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
@@ -188,33 +148,6 @@ describe('Mail API', function () {
done();
});
});
it('cannot delete non-existing domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/doesnotexist.com')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('cannot delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + ADMIN_DOMAIN.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(409);
done();
});
});
it('can delete admin mail domain', function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
});
describe('status', function () {
@@ -243,20 +176,13 @@ describe('Mail API', function () {
mxDomain = DOMAIN_0.domain;
dmarcDomain = '_dmarc.' + DOMAIN_0.domain;
superagent.post(SERVER_URL + '/api/v1/mail')
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.send({ enabled: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
expect(res.statusCode).to.equal(202);
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/enable')
.query({ access_token: token })
.send({ enabled: true })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
done();
});
done();
});
});
@@ -265,12 +191,7 @@ describe('Mail API', function () {
dns.resolve = resolve;
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
done();
});
it('does not fail when dns errors', function (done) {
@@ -503,25 +424,6 @@ describe('Mail API', function () {
});
describe('mail from validation', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get mail from validation succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -554,25 +456,6 @@ describe('Mail API', function () {
});
describe('catch_all', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get catch_all succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -624,25 +507,6 @@ describe('Mail API', function () {
});
describe('mail relay', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('get mail relay succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
@@ -701,25 +565,6 @@ describe('Mail API', function () {
});
describe('mailboxes', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
});
it('add succeeds', function (done) {
superagent.post(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain + '/mailboxes')
.send({ name: MAILBOX_NAME, userId: userId })
@@ -803,26 +648,10 @@ describe('Mail API', function () {
});
describe('aliases', function () {
before(function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
done();
});
});
@@ -924,30 +753,11 @@ describe('Mail API', function () {
});
describe('mailinglists', function () {
before(function (done) {
async.series([
function (done) {
superagent.post(SERVER_URL + '/api/v1/mail')
.query({ access_token: token })
.send({ domain: DOMAIN_0.domain })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
}
], done);
});
after(function (done) {
mail.removeMailboxes(DOMAIN_0.domain, function (error) {
if (error) return done(error);
superagent.del(SERVER_URL + '/api/v1/mail/' + DOMAIN_0.domain)
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(204);
done();
});
done();
});
});

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