Compare commits

...

148 Commits

Author SHA1 Message Date
Girish Ramakrishnan
b65fee4b73 Pass ENABLE_MDA flag to mail addon 2017-03-22 20:42:28 -07:00
Girish Ramakrishnan
153dcc1826 Fix bug in example text 2017-03-22 18:23:24 -07:00
Girish Ramakrishnan
fa4725176c Group help text together 2017-03-22 16:44:18 -07:00
Girish Ramakrishnan
e42607fec6 Always show the password input 2017-03-22 16:13:18 -07:00
Girish Ramakrishnan
297c1ff266 Show error message only if the domain changed 2017-03-22 16:06:47 -07:00
Girish Ramakrishnan
5afe75f137 Bump mail container (for mx bypass fix) 2017-03-22 14:39:30 -07:00
Girish Ramakrishnan
4cfc85f6d3 Do not validate password length 2017-03-22 13:50:20 -07:00
Girish Ramakrishnan
b03f901bbf More 0.107.0 changes 2017-03-22 12:01:04 -07:00
Johannes Zellner
b9dfac94ed Revert "Add ldapjs-rate-limit module"
This reverts commit 3d60a04b36.
2017-03-22 19:35:06 +01:00
Johannes Zellner
c905adde1e Revert "Limit ldap queries per client to 60 per minute"
This reverts commit 466dfdf81f.
2017-03-22 19:35:06 +01:00
Girish Ramakrishnan
0e7efa77a5 Bump the mail container 2017-03-22 09:55:04 -07:00
Johannes Zellner
875ca0307f Fix the node tutorial to export the node PATH and use latest node release 2017-03-22 16:20:48 +01:00
Johannes Zellner
543c9843ba Use df instead of fdisk
some disk types do not contain proper partition tables like on time4vps
the type is simfs. On those fdisk fails to access the partition table,
thus being unable to determine the size of the volume.
df does only return the real usable disk space by the user, thus we
lower the 20GB threshold to 18

Fixes #275
2017-03-22 14:23:59 +01:00
Johannes Zellner
83254a16f9 Do not restrict CSP img-src as 3rd party apps might use other origins for medialinks 2017-03-21 20:20:16 +01:00
Johannes Zellner
466dfdf81f Limit ldap queries per client to 60 per minute
Part of #187
2017-03-21 16:43:22 +01:00
Johannes Zellner
3d60a04b36 Add ldapjs-rate-limit module 2017-03-21 16:43:02 +01:00
Johannes Zellner
103cb10cad Ignore upstream headers for security headers we set in nginx
Apps like nextcloud set their own security headers ending up with having
them set twice. I am not 100% sure if our headers should win or if we
should not inject headers with nginx if the upstream app sets them already.
This looks like the more permissive case where we simply enforce our
values, regardless what the apps sets.

This also fixes the nextcloud/owncloud security checks which were
failing because the header values were duplicated, which results in
string concatenation of values from same headers.
2017-03-21 14:18:39 +01:00
Johannes Zellner
29ef079a83 Do not let the invite link overflow the dialog 2017-03-21 13:36:36 +01:00
Johannes Zellner
a55645770e Add missing csp img-src policy for app icons 2017-03-21 13:25:29 +01:00
Johannes Zellner
132ddd2671 Add 0.107.0 changes 2017-03-21 11:15:51 +01:00
Johannes Zellner
fa5891b149 Also put csp meta tag in oauth views 2017-03-21 11:12:04 +01:00
Johannes Zellner
d01929debc Be more permissive with csp header values 2017-03-21 11:12:04 +01:00
Johannes Zellner
7c01ee58b5 Template the cloudron origin for csp to support local development 2017-03-21 11:12:04 +01:00
Johannes Zellner
ec89f8719c Add CSP meta tag for webadmin 2017-03-21 11:12:04 +01:00
Girish Ramakrishnan
9145022a2c Put scope in the end since it is pre-filled 2017-03-20 20:06:24 -07:00
Girish Ramakrishnan
9ae8ce3296 Change default oauth client scope to profile 2017-03-20 20:05:22 -07:00
Girish Ramakrishnan
eabf27f0c9 More OAuth wording changes 2017-03-20 19:55:27 -07:00
Girish Ramakrishnan
3102a15dff doc: add oauth note in user manual 2017-03-20 19:52:34 -07:00
Girish Ramakrishnan
7747c482d4 Fix oauth wording in the tokens UI 2017-03-20 19:12:32 -07:00
Girish Ramakrishnan
444ca1888b remove dead comment 2017-03-20 15:14:06 -07:00
Girish Ramakrishnan
86ccf5ea84 doc: add kimsufi note
Fixes #261
2017-03-20 14:32:35 -07:00
Girish Ramakrishnan
ef088293b6 Do not show repair and configure together 2017-03-20 08:48:50 -07:00
Johannes Zellner
e0df19c888 Remove unused api wrapper for getAppLogStream() 2017-03-20 10:46:27 +01:00
Girish Ramakrishnan
6a523606ca Revert "Bump version to Nginx IPv6 support."
This reverts commit 5555321cf5.
This reverts commit f087ebbee0.
This reverts commit d04f64d3d4.

Part of #264
2017-03-19 14:25:30 -07:00
Girish Ramakrishnan
b6cd40e63c Use latest manifestformat 2017-03-19 14:20:00 -07:00
Girish Ramakrishnan
b421866bf5 Remove simpleauth
Simple Auth used to provide auth over HTTP. The original motivation
behind this was this was a simple way to add Cloudron Auth integration.
Back in the day, Cloudron Auth was a requirement for apps but this is
not the case anymore.

This is currently not used by any app and having this might encourage
people to make Cloudron specific un-upstreamable changes.
2017-03-19 01:31:38 -07:00
Girish Ramakrishnan
fe06075816 more CHANGES 2017-03-17 13:49:47 -07:00
Girish Ramakrishnan
2b73eb90ec Merge branch 'ipv6' into 'master'
Add IPv6 Support

See merge request !3
2017-03-17 19:55:30 +00:00
Jonah Aragon
5555321cf5 Bump version to Nginx IPv6 support. 2017-03-17 19:43:54 +00:00
Jonah Aragon
f087ebbee0 Add listen [::]:80; for IPv6 redirects. 2017-03-17 19:13:18 +00:00
Jonah Aragon
d04f64d3d4 Add IPv6 listen directives 2017-03-17 19:12:25 +00:00
Girish Ramakrishnan
777a5a0929 Add 0.106.0 changes 2017-03-17 10:23:17 -07:00
Girish Ramakrishnan
6c297f890e Bump mail container 2017-03-17 10:23:17 -07:00
Johannes Zellner
3c8d0b1b37 Never hide the busy state on setup when it suceeded
In that case the whole page gets redirected and to avoid page flickering
we keep it at busy until the browser tears the whole page apart.
2017-03-16 09:58:21 +01:00
Johannes Zellner
74f2cd156f Only send setupToken on admin creation if it was actually specified 2017-03-16 09:37:28 +01:00
Girish Ramakrishnan
a9fdffa9af 0.105.1 changes 2017-03-15 21:15:15 -07:00
Girish Ramakrishnan
e6f8e8eb94 ami field is only required if shown 2017-03-15 21:10:22 -07:00
Girish Ramakrishnan
1bd89ca055 Wait for platform ready after box restarts
This is required for the case where the box restarts apptasks.
For example, the server can reboot mid-way when apptask is running
(as in cloudron-setup + appBundle case) and then when it comes back
up it doesn't wait for the platform to be ready. And the apps fail
to install (mysql takes a bit to startup)
2017-03-15 20:35:44 -07:00
Girish Ramakrishnan
0e226d0314 Download icon (for repair case) 2017-03-15 20:35:44 -07:00
Girish Ramakrishnan
e8d4e2c792 send more logs 2017-03-15 19:35:42 -07:00
Girish Ramakrishnan
4cfbed8273 Use inline docker pgp key
The one from keyserver keeps failing sporadically

https://github.com/docker/docker/issues/13555
https://github.com/docker/docker/issues/20022
http://askubuntu.com/questions/720517/key-server-times-out-while-installing-docker-on-ubuntu-14-04
2017-03-15 18:04:44 -07:00
Girish Ramakrishnan
0410ac9780 doc: activate api 2017-03-15 16:14:25 -07:00
Girish Ramakrishnan
82fcf6a770 setupToken is not required in activate 2017-03-15 15:55:31 -07:00
Girish Ramakrishnan
a1332865c0 Fix wording (should be prove otherwise) 2017-03-15 15:42:06 -07:00
Girish Ramakrishnan
ae0e4de93e No semicolons in bash code 2017-03-15 15:40:43 -07:00
Johannes Zellner
02a6525558 Add changes for 0.105.0 2017-03-15 14:56:35 +01:00
Girish Ramakrishnan
5afef14760 Actually send emails for responsive apps 2017-03-14 13:42:28 -07:00
Johannes Zellner
890d589a36 Do not show Route53 in dns setup for AMIs 2017-03-14 16:54:46 +01:00
Johannes Zellner
89a50c4b83 Use ami provider in ami creation script 2017-03-14 13:48:11 +01:00
Johannes Zellner
da5cd2b62c Show instance id input on cloudron setup for amis 2017-03-14 13:45:18 +01:00
Johannes Zellner
57321624aa Add ami setupToken verification in auth route 2017-03-14 13:45:04 +01:00
Johannes Zellner
876ae822b2 Skip splash setup if cloudron domain was not yet setup
This is based on the existence of admin.conf nginx file.
The splash would create/overwrite that file, but it will depend on the
host.cert to be already created, which is only the case after domain
setup.
2017-03-14 10:58:24 +01:00
Johannes Zellner
1ceb75868b Remove last remainder of apidocs 2017-03-14 10:12:17 +01:00
Johannes Zellner
98ad16f943 Remove unused requires 2017-03-14 10:10:59 +01:00
Johannes Zellner
9363746c1a Use ec2 sysinfo for ami provider 2017-03-14 09:34:39 +01:00
Johannes Zellner
7a1b9ab94c Support ami provider for ssh authorized_keys api 2017-03-14 09:34:11 +01:00
Johannes Zellner
46d6b5b81f Add hidden 'ami' provider for pre-built amis 2017-03-14 09:32:51 +01:00
Girish Ramakrishnan
7e8757a78c grep quietly 2017-03-13 13:52:16 -07:00
Girish Ramakrishnan
e508b25ecd Lower memory expectations 2017-03-13 13:05:59 -07:00
Girish Ramakrishnan
3fdc10c523 Parse free and fdisk output with C locale
some vps providers seem to set a different locale by default.
Settings LC_ALL overrides all the other LC_*
2017-03-13 10:36:05 -07:00
Johannes Zellner
717953c162 Half the backup progress polling 2017-03-13 13:28:14 +01:00
Johannes Zellner
daa34c3b4d add some asserts in the ldap code 2017-03-13 11:10:08 +01:00
Johannes Zellner
bf5c78d819 Refactor ldap user listing code to avoid pyramids 2017-03-13 11:09:12 +01:00
Johannes Zellner
1763144278 Only list users in ldap groups who have access to the app
Fixes #215
2017-03-13 11:06:29 +01:00
Johannes Zellner
2f598529fc Only list users who have access to the app in an ldap search
Part of #215
2017-03-13 11:02:45 +01:00
Girish Ramakrishnan
8264e69e2f remove unused require 2017-03-10 14:52:31 -08:00
Johannes Zellner
b0638df94e Only show the remote support for admins 2017-03-10 17:21:01 +01:00
Johannes Zellner
bb61eee557 Add missing quote in support view 2017-03-10 17:17:51 +01:00
Johannes Zellner
39c39b861d Require admins for authorized_keys route 2017-03-10 17:16:45 +01:00
Girish Ramakrishnan
e3deda4ef3 Always show port 25 status 2017-03-09 16:21:47 -08:00
Girish Ramakrishnan
7e44e7de82 Check outbound port 25
Fixes #243
2017-03-09 16:20:53 -08:00
Girish Ramakrishnan
9dd0518c00 Show email settings for non-caas
This is because people can use route53/DO now and we can show them
the RDNS settings as well.
2017-03-09 15:21:43 -08:00
Girish Ramakrishnan
81313d1c40 reduce nxdomain caching timeout
the other option is to use "/usr/sbin/unbound-control flush_negative"
on demand
2017-03-09 15:03:14 -08:00
Girish Ramakrishnan
2ceccc4557 Add note for caas users about enabling email 2017-03-09 14:25:03 -08:00
Girish Ramakrishnan
1c36918e92 Done -> Almost done 2017-03-09 10:21:52 -08:00
Girish Ramakrishnan
8d93df23c1 doc: cnameTarget 2017-03-09 10:00:42 -08:00
Johannes Zellner
0c06b34a2c Add more changes for 0.104.0 2017-03-09 15:38:09 +01:00
Johannes Zellner
fe980eab7f Show either cnameTarget or fqdn for CNAME setup hint
Fixes #101
2017-03-09 15:23:17 +01:00
Johannes Zellner
979b903bf2 Add cnameTarget for apps using an external domain
We have 4 properties related to the domain:
1) location, is the subdomain location without information how to craft
a fqdn on the client
2) fqdn, the intended domain to reach the app
3) altDomain, just the value for the external domain, merely a db record
value
4) cnameTarget, mostly for display purpose on the client, which
otherwise has no way to build the original cloudron local fqdn
2017-03-09 15:11:27 +01:00
Johannes Zellner
4b8ee0934a Add Cloudron cancel link to the settings view
Fixes #251
2017-03-09 13:36:29 +01:00
Girish Ramakrishnan
0439725790 Bump infra version 2017-03-08 22:27:41 -08:00
Girish Ramakrishnan
4b3ef33989 Add some basic secure headers
Part of #249
2017-03-08 22:14:44 -08:00
Girish Ramakrishnan
9e99d51853 do not remote support for caas 2017-03-08 16:15:13 -08:00
Girish Ramakrishnan
00a9fa8f34 fix wording a bit 2017-03-08 16:11:45 -08:00
Girish Ramakrishnan
84a35343d1 Display <sso> and <nosso> contents based on SSO 2017-03-08 15:16:15 -08:00
Girish Ramakrishnan
397bd17c55 update showdown to 1.6.4 2017-03-08 15:01:07 -08:00
Girish Ramakrishnan
c8e377a9bd doc: scaleway may need reboot for security group to take effect 2017-03-08 10:34:12 -08:00
Johannes Zellner
90e3138bae Show the correct postInstall message after app installation
Fixes #255
2017-03-08 15:42:29 +01:00
Girish Ramakrishnan
24b32a763b Add comments for CLI tool 2017-03-07 12:44:17 -08:00
Johannes Zellner
69a12d36ef Also give lightsail the special user treatment 2017-03-07 16:51:58 +01:00
Johannes Zellner
1485718fa6 Special treatment for ec2 and authorized_key user 2017-03-07 16:44:04 +01:00
Johannes Zellner
750f03d9de Add the public key of our support ssh key 2017-03-07 16:13:48 +01:00
Johannes Zellner
b5ddf1d24d Add ssh support toggle button in the support view 2017-03-07 16:12:00 +01:00
Johannes Zellner
043a35111d Remove unused requires in ssh test 2017-03-07 16:11:21 +01:00
Johannes Zellner
676457b589 Add authorized_key wrappers to client.js 2017-03-07 16:07:25 +01:00
Johannes Zellner
e61f11be81 Since we need root to save the authorized_key file we do it via sudo script 2017-03-07 15:16:41 +01:00
Johannes Zellner
101a44affd Add authorized_keys.sh 2017-03-07 15:16:18 +01:00
Johannes Zellner
7995c664ed Add shell.sudoSync() 2017-03-07 15:14:37 +01:00
Johannes Zellner
6023c0e5dc Ensure the authorized_file permissions are correct 2017-03-07 14:39:14 +01:00
Johannes Zellner
d49d76c1ee add ssh route tests and fixup the code accordingly 2017-03-07 14:12:25 +01:00
Johannes Zellner
77ef212daa Add SSH authorized_keys routes 2017-03-07 13:16:28 +01:00
Johannes Zellner
7aa80193c0 Add more changes 2017-03-06 10:47:36 +01:00
Johannes Zellner
5632c74556 Add isadmin ldap attribute
Fixes #241
2017-03-06 10:45:50 +01:00
Girish Ramakrishnan
7a08745af1 doc: add the 20gb requirement 2017-03-05 18:31:31 -08:00
Girish Ramakrishnan
d9ba0858c7 Add 0.103.1 changes 2017-03-03 09:42:31 -08:00
Johannes Zellner
617e51d294 Adjust the oom notification email 2017-03-03 11:04:48 +01:00
Johannes Zellner
c07d322fff Do not send ldap records for users without a username set
If an app relies on the attribute to be set, apps like owncloud would
fail internally.
2017-03-03 10:18:38 +01:00
Girish Ramakrishnan
9b8fa8a772 doc: sso 2017-03-02 15:15:15 -08:00
Girish Ramakrishnan
c351242af7 lie about the time if it is ahead of us
Fixes #247
2017-03-02 14:34:18 -08:00
Johannes Zellner
55245557f5 Use the new app login event in the webadmin
Part of #247
2017-03-02 17:15:01 +01:00
Johannes Zellner
ee1cef3ee8 Add new event type for app mailbox ldap login 2017-03-02 17:13:19 +01:00
Girish Ramakrishnan
5d51a7178f domain migrate: Add text that subdomains are not supported 2017-03-01 15:45:37 -08:00
Girish Ramakrishnan
9d52397bcc Move dhparam creation
Now that all cloudrons have the dhparams file, we can generate this
*after* restoring from backup and if required.
2017-03-01 15:25:20 -08:00
Girish Ramakrishnan
5098fbe061 Version 0.103.0 changes 2017-03-01 13:08:49 -08:00
Girish Ramakrishnan
7062aa4ac7 use test image 19.0.1 2017-02-28 20:21:02 -08:00
Girish Ramakrishnan
d6fec4f2b9 alertsTo must be an array 2017-02-28 18:17:17 -08:00
Girish Ramakrishnan
86ef462c76 doc: add email/recvmail to allowed addon keys 2017-02-27 06:51:54 -08:00
Girish Ramakrishnan
c76e7a3f63 randomize the cn in ip based cert
Fixes #224
2017-02-25 15:38:15 -08:00
Girish Ramakrishnan
2516a08659 remove reference to npm-demo from manifest 2017-02-25 13:36:27 -08:00
Girish Ramakrishnan
562fe30333 Update cloudron-manifestformat
adds the upcoming tls addon
2017-02-24 22:13:28 -08:00
Girish Ramakrishnan
4e0eed4bb2 make tests pass 2017-02-24 21:48:38 -08:00
Girish Ramakrishnan
b604caec72 Get rid of x509 module
This is the last of the "native" modules. These modules take forever
to rebuild in low memory machines
2017-02-24 21:01:48 -08:00
Girish Ramakrishnan
6b409e9089 Do not send crash logs to support in self-hosted case
Fixes #242
2017-02-24 10:40:51 -08:00
Girish Ramakrishnan
015d434358 remove unused require 2017-02-24 10:39:03 -08:00
Girish Ramakrishnan
c8e448cb84 Remove support@cloudron.io in app died mails
part of #242
2017-02-24 10:36:48 -08:00
Girish Ramakrishnan
03924be491 self-hosted: do not cc support for bounce mails from apps
part of #242
2017-02-24 10:34:07 -08:00
Girish Ramakrishnan
2729cecf4a self-hosting: remove support@cloudron.io frmo oom mails, cert renewal and backup failure mails
Part of #242
2017-02-24 10:25:20 -08:00
Girish Ramakrishnan
32e2377828 sysinfo: getIp -> getPublicIp 2017-02-23 22:03:48 -08:00
Girish Ramakrishnan
fdb8139b03 createAMI: better help text 2017-02-23 15:44:56 -08:00
Girish Ramakrishnan
4b25c8a5ad Add 0.102.1 changes 2017-02-23 11:08:15 -08:00
Girish Ramakrishnan
ae930a7fe8 dns setup: fix wording a bit 2017-02-23 10:57:05 -08:00
Johannes Zellner
3b9144ba4d Alter the backups.dependsOn field to store as TEXT
Fixes #239
2017-02-23 17:56:34 +01:00
Johannes Zellner
be6ea3d4c1 Add rosehosting to selfhosting docs 2017-02-22 16:02:03 +01:00
Girish Ramakrishnan
a2983e58b5 doc: typo 2017-02-21 11:24:25 -08:00
Girish Ramakrishnan
a99e86a5df doc: fix wording on base image a bit 2017-02-21 11:16:10 -08:00
Girish Ramakrishnan
906ad80069 Add terms link 2017-02-20 15:59:40 -08:00
Girish Ramakrishnan
ac65f765e5 doc: emphasize the traffic mgmt bit 2017-02-20 14:58:27 -08:00
Girish Ramakrishnan
c5bfe82315 more 0.102.0 changes 2017-02-20 14:13:53 -08:00
95 changed files with 1823 additions and 1875 deletions

43
CHANGES
View File

@@ -765,4 +765,47 @@
* Ensure we download docker images and have an app data volume on app re-configure
* Improve certificate renewal erorr message
* Fix disk usage graph
* Show Repair UI for errored apps
[0.102.1]
* Add terms link when signing up for Cloudron.io account
* Fix issue where Cloudrons with many apps (> 35) were unable to backup
* Improve wording of DNS Setup
[0.103.0]
* Do not send crash logs and other notifications to support@cloudron.io for self-hosted instances
* Make auto-generated self-signed cert load quickly on Firefox (take 2)
[0.104.0]
* (mail) Fix crash when sending mails to groups with just 1 user
* (ldap) Add isadmin attribute to better map users in apps
* (ldap) Hide users which have not yet set a username in ldap searches
* (core) Add SSH authorized_keys management
* (core) Add additional security related headers to the nginx reverse proxy
* (ui) Add remote SSH support option
* (ui) Fix eventlog display
* (ui) Fix CNAME setup information
[0.105.0]
* Always show email related checks
* Show outbound SMTP port 25 status
* Hide remote feature for normal users
* Only list users via ldap searches who have access to the app
* Fix installation issue on servers with a differente locale set
[0.105.1]
* Fix crash when setupToken is not provided in activate API
* Add inline Docker GPG key
* Re-download icon when repairing app
* Fix issue where pre-installed apps were not installed correctly
* Fix issue where new cloudrons could not be activated
[0.106.0]
* (mail) Fix email forwarding to external domains
* (mail) Set maximum email size to 25MB
* Remove SimpleAuth addon
[0.107.0]
* Support CSP for webinterface and OAuth views
* (mail) Fix issue where Cloudron is only used to send emails

View File

@@ -69,7 +69,7 @@ if [[ ! -f "${ssh_keys}" ]]; then
fi
if [[ -z "${image_id}" ]]; then
echo "--region is required"
echo "--region is required (us-east-1 or eu-central-1)"
exit 1
fi
@@ -141,7 +141,7 @@ while true; do
done
echo "=> Running cloudron-setup"
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ec2" --skip-reboot
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ami" --skip-reboot
wait_for_ssh

View File

@@ -52,7 +52,38 @@ apt-get install -y python # Install python which is required for npm rebuild
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
echo "==> Installing Docker"
apt-key adv --keyserver hkp://ha.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
docker_key="-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1
mQINBFWln24BEADrBl5p99uKh8+rpvqJ48u4eTtjeXAWbslJotmC/CakbNSqOb9o
ddfzRvGVeJVERt/Q/mlvEqgnyTQy+e6oEYN2Y2kqXceUhXagThnqCoxcEJ3+KM4R
mYdoe/BJ/J/6rHOjq7Omk24z2qB3RU1uAv57iY5VGw5p45uZB4C4pNNsBJXoCvPn
TGAs/7IrekFZDDgVraPx/hdiwopQ8NltSfZCyu/jPpWFK28TR8yfVlzYFwibj5WK
dHM7ZTqlA1tHIG+agyPf3Rae0jPMsHR6q+arXVwMccyOi+ULU0z8mHUJ3iEMIrpT
X+80KaN/ZjibfsBOCjcfiJSB/acn4nxQQgNZigna32velafhQivsNREFeJpzENiG
HOoyC6qVeOgKrRiKxzymj0FIMLru/iFF5pSWcBQB7PYlt8J0G80lAcPr6VCiN+4c
NKv03SdvA69dCOj79PuO9IIvQsJXsSq96HB+TeEmmL+xSdpGtGdCJHHM1fDeCqkZ
hT+RtBGQL2SEdWjxbF43oQopocT8cHvyX6Zaltn0svoGs+wX3Z/H6/8P5anog43U
65c0A+64Jj00rNDr8j31izhtQMRo892kGeQAaaxg4Pz6HnS7hRC+cOMHUU4HA7iM
zHrouAdYeTZeZEQOA7SxtCME9ZnGwe2grxPXh/U/80WJGkzLFNcTKdv+rwARAQAB
tDdEb2NrZXIgUmVsZWFzZSBUb29sIChyZWxlYXNlZG9ja2VyKSA8ZG9ja2VyQGRv
Y2tlci5jb20+iQI4BBMBAgAiBQJVpZ9uAhsvBgsJCAcDAgYVCAIJCgsEFgIDAQIe
AQIXgAAKCRD3YiFXLFJgnbRfEAC9Uai7Rv20QIDlDogRzd+Vebg4ahyoUdj0CH+n
Ak40RIoq6G26u1e+sdgjpCa8jF6vrx+smpgd1HeJdmpahUX0XN3X9f9qU9oj9A4I
1WDalRWJh+tP5WNv2ySy6AwcP9QnjuBMRTnTK27pk1sEMg9oJHK5p+ts8hlSC4Sl
uyMKH5NMVy9c+A9yqq9NF6M6d6/ehKfBFFLG9BX+XLBATvf1ZemGVHQusCQebTGv
0C0V9yqtdPdRWVIEhHxyNHATaVYOafTj/EF0lDxLl6zDT6trRV5n9F1VCEh4Aal8
L5MxVPcIZVO7NHT2EkQgn8CvWjV3oKl2GopZF8V4XdJRl90U/WDv/6cmfI08GkzD
YBHhS8ULWRFwGKobsSTyIvnbk4NtKdnTGyTJCQ8+6i52s+C54PiNgfj2ieNn6oOR
7d+bNCcG1CdOYY+ZXVOcsjl73UYvtJrO0Rl/NpYERkZ5d/tzw4jZ6FCXgggA/Zxc
jk6Y1ZvIm8Mt8wLRFH9Nww+FVsCtaCXJLP8DlJLASMD9rl5QS9Ku3u7ZNrr5HWXP
HXITX660jglyshch6CWeiUATqjIAzkEQom/kEnOrvJAtkypRJ59vYQOedZ1sFVEL
MXg2UCkD/FwojfnVtjzYaTCeGwFQeqzHmM241iuOmBYPeyTY5veF49aBJA1gEJOQ
TvBR8Q==
=Fm3p
-----END PGP PUBLIC KEY BLOCK-----
"
echo "$docker_key" | apt-key add -
echo "deb https://apt.dockerproject.org/repo ubuntu-xenial main" > /etc/apt/sources.list.d/docker.list
apt-get -y update

6
box.js
View File

@@ -13,8 +13,7 @@ var appHealthMonitor = require('./src/apphealthmonitor.js'),
async = require('async'),
config = require('./src/config.js'),
ldap = require('./src/ldap.js'),
server = require('./src/server.js'),
simpleauth = require('./src/simpleauth.js');
server = require('./src/server.js');
console.log();
console.log('==========================================');
@@ -33,7 +32,6 @@ console.log();
async.series([
server.start,
ldap.start,
simpleauth.start,
appHealthMonitor.start,
], function (error) {
if (error) {
@@ -48,13 +46,11 @@ var NOOP_CALLBACK = function () { };
process.on('SIGINT', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
simpleauth.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});
process.on('SIGTERM', function () {
server.stop(NOOP_CALLBACK);
ldap.stop(NOOP_CALLBACK);
simpleauth.stop(NOOP_CALLBACK);
setTimeout(process.exit.bind(process), 3000);
});

View File

@@ -318,67 +318,3 @@ cloudron exec
> swaks --server "${MAIL_SMTP_SERVER}" -p "${MAIL_SMTP_PORT}" --from "${MAIL_SMTP_USERNAME}@${MAIL_DOMAIN}" --body "Test mail from cloudron app at $(hostname -f)" --auth-user "${MAIL_SMTP_USERNAME}" --auth-password "${MAIL_SMTP_PASSWORD}"
```
## simpleauth
Simple Auth can be used for authenticating users with a HTTP request. This method of authentication is targeted
at applications, which for whatever reason can't use the ldap addon.
The response contains an `accessToken` which can then be used to access the [Cloudron API](/references/api.html).
Exported environment variables:
```
SIMPLE_AUTH_SERVER= # the simple auth HTTP server
SIMPLE_AUTH_PORT= # the simple auth server port
SIMPLE_AUTH_URL= # the simple auth server URL. same as "http://SIMPLE_AUTH_SERVER:SIMPLE_AUTH_PORT
SIMPLE_AUTH_CLIENT_ID # a client id for identifying the request originator with the auth server
```
This addons provides two REST APIs:
**POST /api/v1/login**
Request JSON body:
```
{
"username": "<username> or <email>",
"password": "<password>"
}
```
Response 200 with JSON body:
```
{
"accessToken": "<accessToken>",
"user": {
"id": "<userId>",
"username": "<username>",
"email": "<email>",
"admin": <admin boolean>,
"displayName": "<display name>"
}
}
```
**GET /api/v1/logout**
Request params:
```
?access_token=<accessToken>
```
Response 200 with JSON body:
```
{}
```
For debugging, [cloudron exec](https://www.npmjs.com/package/cloudron) can be used to run the `curl` tool within the context of the app:
```
cloudron exec
> USERNAME=<enter username>
> PASSWORD=<enter password>
> PAYLOAD="{\"clientId\":\"${SIMPLE_AUTH_CLIENT_ID}\", \"username\":\"${USERNAME}\", \"password\":\"${PASSWORD}\"}"
> curl -H "Content-Type: application/json" -X POST -d "${PAYLOAD}" "${SIMPLE_AUTH_ORIGIN}/api/v1/login"
```

View File

@@ -62,7 +62,7 @@ curl -H "Content-Type: application/json" -H "Authorization: Bearer <token>" http
## OAuth
OAuth authentication is meant to be used by apps. An app can get an OAuth token using the
[oauth](addons.html#oauth) or [simpleauth](addons.html#simpleauth) addon.
[oauth](addons.html#oauth) addon.
Tokens obtained via OAuth have a restricted scope wherein they can only access the user's profile.
This restriction is so that apps cannot make undesired changes to the user's Cloudron.
@@ -199,7 +199,8 @@ Response (200):
health: <enum>, // health of the application
location: <string>, // subdomain on which app is installed
fqdn: <string>, // the FQDN of this app
altDomain: <string> // alternate domain from which this app can be reached
altDomain: <string>, // alternate domain from which this app can be reached
cnameTarget: <string> || null, // If altDomain is set, this contains the CNAME location for the app
accessRestriction: null || { // list of users and groups who can access this application
users: [ ],
groups: [ ]
@@ -209,7 +210,8 @@ Response (200):
portBindings: { // mapping from application ports to public ports
},
iconUrl: <url>, // a relative url providing the icon
memoryLimit: <number> // memory constraint in bytes
memoryLimit: <number>, // memory constraint in bytes
sso: <boolean> // Enable single sign-on
}
```
@@ -257,6 +259,8 @@ is integrated with Cloudron Authentication.
`manifest` is the [application manifest](/references/manifest.html).
For apps that support optional single sign-on, the `sso` field can be used to disable Cloudron authentication. By default, single sign-on is enabled.
### List apps
GET `/api/v1/apps/:appId` <scope>admin</scope>
@@ -278,7 +282,8 @@ Response (200):
health: <enum>, // health of the application
location: <string>, // subdomain on which app is installed
fqdn: <string>, // the FQDN of this app
altDomain: <string> // alternate domain from which this app can be reached
altDomain: <string>, // alternate domain from which this app can be reached
cnameTarget: <string> || null, // If altDomain is set, this contains the CNAME location for the app
accessRestriction: null || { // list of users and groups who can access this application
users: [ ],
groups: [ ]
@@ -654,6 +659,38 @@ curl -L <url> | openssl aes-256-cbc -d -pass "pass:$<backupKey>" | tar -zxf -
## Cloudron
### Activate the Cloudron
POST `/api/v1/cloudron/activate`
Activates the Cloudron with an admin username and password.
Request:
```
{
username: <string>, // the admin username
password: <string>, // the admin password
email: <email> // the admin email
}
```
Response (201):
```
{
"token": "771ee724a66aa557f95af06b4e6c27992f9230f6b1d65d5fbaa34cae9318d453",
"expires": 1490224113353
}
```
The `token` parameter can be used to make further API calls.
Curl example to activate the cloudron:
```
curl -X POST -H "Content-Type: application/json" -d '{"username": "girish", "password":"MySecret123#", "email": "girish@cloudron.io" }' https://my.cloudron.info/api/v1/cloudron/activate
```
### Update the Cloudron
POST `/api/v1/cloudron/update` <scope>admin</scope>
@@ -682,8 +719,9 @@ Gets information about an in-progress Cloudron update or backup.
`update` or `backup` is `null` when there is no such activity in progress.
```
Response (200):
```
{
update: null || { percent: <number>, message: <string> },
backup: null || { percent: <number>, message: <string> }
@@ -806,7 +844,7 @@ Response (200):
* user.remove
* user.update
`source` contains information on the originator of the action. For example, for user.login, this contains the IP address, the appId and the authType (ldap or simpleauth or oauth).
`source` contains information on the originator of the action. For example, for user.login, this contains the IP address, the appId and the authType (ldap or oauth).
`data` contains information on the event itself. For example, for user.login, this contains the userId that logged in. For app.install, it contains the manifest and location of the app that was installed.

View File

@@ -29,7 +29,6 @@ Cloudron provides multiple authentication strategies.
* OAuth 2.0 provided by the [OAuth addon](/references/addons.html#oauth)
* LDAP provided by the [LDAP addon](/references/addons.html#ldap)
* Simple Auth provided by [Simple Auth addon](/references/addons.html#simpleauth)
# Choosing a strategy

View File

@@ -47,12 +47,14 @@ Type: object
Required: no
Allowed keys
* [email](addons.html#email)
* [ldap](addons.html#ldap)
* [localstorage](addons.html#localstorage)
* [mongodb](addons.html#mongodb)
* [mysql](addons.html#mysql)
* [oauth](addons.html#oauth)
* [postgresql](addons.html#postgresql)
* [recvmail](addons.html#recvmail)
* [redis](addons.html#redis)
* [sendmail](addons.html#sendmail)
@@ -278,6 +280,10 @@ The intended use of this field is to display some post installation steps that t
complete the installation. For example, displaying the default admin credentials and informing the user to
to change it.
The message can have the following special tags:
* `<sso> ... </sso>` - Content in `sso` blocks are shown if SSO enabled.
* `<nosso> ... </nosso>`- Content in `nosso` blocks are shows when SSO is disabled.
## optionalSso
Type: boolean

View File

@@ -45,12 +45,17 @@ Please let us know if any of them requires tweaks or adjustments.
## Create server
Create an `Ubuntu 16.04 (Xenial)` server with at-least `1gb` RAM. Do not make any changes
to vanilla ubuntu. Be sure to allocate a static IPv4 address for your server.
Create an `Ubuntu 16.04 (Xenial)` server with at-least `1gb` RAM and 20GB disk space.
Do not make any changes to vanilla ubuntu. Be sure to allocate a static IPv4 address
for your server.
Cloudron has a built-in firewall and ports are opened and closed dynamically, as and when
apps are installed, re-configured or removed. For this reason, be sure to open all TCP and
UDP traffic to the server.
UDP traffic to the server and leave the traffic management to the Cloudron.
### Kimsufi
Be sure to check the "use the distribution kernel" checkbox in the personalized installation mode.
### Linode
@@ -69,7 +74,7 @@ SSH into your server and run the following commands:
```
wget https://cloudron.io/cloudron-setup
chmod +x cloudron-setup
./cloudron-setup --provider <azure|digitalocean|ec2|lightsail|linode|ovh|scaleway|vultr|generic>
./cloudron-setup --provider <azure|digitalocean|ec2|lightsail|linode|ovh|rosehosting|scaleway|vultr|generic>
```
The setup will take around 10-15 minutes.
@@ -265,8 +270,7 @@ reputation should be easy to get back.
* Linode - Follow this [guide](https://www.linode.com/docs/networking/dns/setting-reverse-dns).
* Scaleway - Edit your security group to allow email. You can also set a PTR record on the interface with your
`my.<domain>`.
* Scaleway - Edit your security group to allow email and [reboot the server](https://community.online.net/t/security-group-not-working/2096) for the change to take effect. You can also set a PTR record on the interface with your `my.<domain>`.
* Check if your IP is listed in any DNSBL list [here](http://multirbl.valli.org/). In most cases,
you can apply for removal of your IP by filling out a form at the DNSBL manager site.

View File

@@ -341,6 +341,18 @@ beyond it's control, Cloudron admins will get a notification about it.
All the operations listed in this manual like installing app, configuring users and groups, are
completely programmable with a [REST API](/references/api.html).
# OAuth Provider
Cloudron is an OAuth 2.0 provider. To integrate Cloudron login into an external application, create
an OAuth application under `API Access`.
You can use the following OAuth URLs to add Cloudron in the external app:
```
authorizationURL: https://my.<domain>/api/v1/oauth/dialog/authorize
tokenURL: https://my.<domain>/api/v1/oauth/token
```
# Moving to a larger Cloudron
When using a Cloudron from cloudron.io, it is easy to migrate your apps and data to a bigger server.

View File

@@ -42,12 +42,12 @@ Creating an application for Cloudron can be summarized as follows:
1. Create a web application using any language/framework. This web application must run a HTTP server
and can optionally provide other services using custom protocols (like git, ssh, TCP etc).
2. Create a [Dockerfile](http://docs.docker.com/engine/reference/builder/) that specifies how to create
2. Create a [Dockerfile](http://docs.docker.com/engine/reference/builder/) that specifies how to create
an application ```image```. An ```image``` is essentially a bundle of the application source code
and it's dependencies.
3. Create a [CloudronManifest.json](/references/manifest.html) file that provides essential information
about the app. This includes information required for the Cloudron Store like title, version, icon and
about the app. This includes information required for the Cloudron Store like title, version, icon and
runtime requirements like `addons`.
## Simple Web application
@@ -79,7 +79,7 @@ FROM cloudron/base:0.10.0
ADD server.js /app/code/server.js
CMD [ "/usr/local/node-0.12.7/bin/node", "/app/code/server.js" ]
CMD [ "/usr/local/node-6.9.5/bin/node", "/app/code/server.js" ]
```
The `FROM` command specifies that we want to start off with Cloudron's [base image](/references/baseimage.html).
@@ -90,7 +90,7 @@ While this example only copies a single file, the ADD command can be used to cop
See the [Dockerfile](https://docs.docker.com/reference/builder/#add) documentation for more details.
The `CMD` command specifies how to run the server. There are multiple versions of node available under `/usr/local`. We
choose node v0.12.7 for our app.
choose node v6.9.5 for our app.
## CloudronManifest.json
@@ -176,7 +176,7 @@ Step 0 : FROM cloudron/base:0.10.0
Step 1 : ADD server.js /app/code
---> b09b97ecdfbc
Removing intermediate container 03c1e1f77acb
Step 2 : CMD /usr/local/node-0.12.7/bin/node /app/code/main.js
Step 2 : CMD /usr/local/node-6.9.5/bin/node /app/code/main.js
---> Running in 370f59d87ab2
---> 53b51eabcb89
Removing intermediate container 370f59d87ab2
@@ -335,13 +335,15 @@ File `tutorial/Dockerfile`
```dockerfile
FROM cloudron/base:0.10.0
ENV PATH /usr/local/node-6.9.5/bin:$PATH
ADD server.js /app/code/server.js
ADD package.json /app/code/package.json
WORKDIR /app/code
RUN npm install --production
CMD [ "/usr/local/node-0.12.7/bin/node", "/app/code/server.js" ]
CMD [ "node", "/app/code/server.js" ]
```
Notice the new `RUN` command which installs the node module dependencies in package.json using `npm install`.

View File

@@ -5,8 +5,8 @@ This tutorial outlines how to package an existing web application for the Cloudr
If you are aware of Docker and Heroku, you should feel at home packaging for the
Cloudron. Roughly, the steps involved are:
* Create a Dockerfile for your application. If your application already has
a Dockerfile, you should able to reuse most of it. By virtue of Docker, the Cloudron
* Create a Dockerfile for your application. If your application already has a Dockerfile, it
is a good starting point for packaging for the Cloudron. By virtue of Docker, the Cloudron
is able to run apps written in any language/framework.
* Create a CloudronManifest.json that provides information like title, author, description
@@ -88,7 +88,7 @@ CMD [ "/usr/local/node-4.4.7/bin/node", "/app/code/server.js" ]
The `FROM` command specifies that we want to start off with Cloudron's [base image](/references/baseimage.html).
All Cloudron apps **must** start from this base image. This approach conserves space on the Cloudron since
Docker images tend to be quiet large.
Docker images tend to be quite large and also helps us to do a security audit on apps more easily.
The `ADD` command copies the source code of the app into the directory `/app/code`. There is nothing special
about the `/app/code` directory and it is merely a convention we use to store the application code.

View File

@@ -2,17 +2,18 @@
'use strict';
var ejs = require('gulp-ejs'),
gulp = require('gulp'),
del = require('del'),
concat = require('gulp-concat'),
uglify = require('gulp-uglify'),
serve = require('gulp-serve'),
sass = require('gulp-sass'),
sourcemaps = require('gulp-sourcemaps'),
cssnano = require('gulp-cssnano'),
var argv = require('yargs').argv,
autoprefixer = require('gulp-autoprefixer'),
argv = require('yargs').argv;
concat = require('gulp-concat'),
cssnano = require('gulp-cssnano'),
del = require('del'),
ejs = require('gulp-ejs'),
gulp = require('gulp'),
sass = require('gulp-sass'),
serve = require('gulp-serve'),
sourcemaps = require('gulp-sourcemaps'),
uglify = require('gulp-uglify'),
url = require('url');
gulp.task('3rdparty', function () {
gulp.src([
@@ -54,14 +55,16 @@ gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-update'], function (
var oauth = {
clientId: argv.clientId || 'cid-webadmin',
clientSecret: argv.clientSecret || 'unused',
apiOrigin: argv.apiOrigin || ''
apiOrigin: argv.apiOrigin || '',
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
};
console.log();
console.log('Using OAuth credentials:');
console.log(' ClientId: %s', oauth.clientId);
console.log(' ClientSecret: %s', oauth.clientSecret);
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
console.log(' ClientId: %s', oauth.clientId);
console.log(' ClientSecret: %s', oauth.clientSecret);
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
console.log();
@@ -140,7 +143,7 @@ gulp.task('js-update', function () {
// --------------
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
return gulp.src('webadmin/src/*.html').pipe(gulp.dest('webadmin/dist'));
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
});
gulp.task('html-update', function () {

View File

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

View File

@@ -105,7 +105,7 @@ CREATE TABLE IF NOT EXISTS backups(
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
dependsOn VARCHAR(4096), /* comma separate list of objects this backup depends on */
dependsOn TEXT, /* comma separate list of objects this backup depends on */
state VARCHAR(16) NOT NULL,
PRIMARY KEY (filename));

1312
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@
"aws-sdk": "^2.1.46",
"body-parser": "^1.13.1",
"checksum": "^0.1.1",
"cloudron-manifestformat": "^2.6.0",
"cloudron-manifestformat": "^2.8.0",
"connect-ensure-login": "^0.1.1",
"connect-lastmile": "^0.1.0",
"connect-timeout": "^1.5.0",
@@ -67,8 +67,7 @@
"tldjs": "^1.6.2",
"underscore": "^1.7.0",
"valid-url": "^1.0.9",
"validator": "^4.9.0",
"x509": "^0.2.4"
"validator": "^4.9.0"
},
"devDependencies": {
"bootstrap-sass": "^3.3.3",

View File

@@ -15,16 +15,16 @@ fi
# change this to a hash when we make a upgrade release
readonly LOG_FILE="/var/log/cloudron-setup.log"
readonly DATA_FILE="/root/cloudron-install-data.json"
readonly MINIMUM_DISK_SIZE_GB="19" # this is the size of "/" and required to fit in docker images 19 is a safe bet for different reporting on 20GB min
readonly MINIMUM_MEMORY="980" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989)
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
# copied from cloudron-resize-fs.sh
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mountpoint -x $d)" ] && echo $d && break; done)"
readonly disk_size_bytes=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }')
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024/1024))
readonly disk_size_bytes=$(LC_ALL=C df | grep "${disk_device}" | awk '{ printf $2 }')
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
# verify the system has minimum requirements met
if [[ "${physical_memory}" -lt "${MINIMUM_MEMORY}" ]]; then
@@ -97,6 +97,7 @@ if [[ -z "${dataJson}" ]]; then
echo "--provider is required (azure, digitalocean, ec2, lightsail, linode, ovh, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \

View File

@@ -11,6 +11,11 @@ readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
echo "Setting up nginx update page"
if [[ ! -f "${DATA_DIR}/nginx/applications/admin.conf" ]]; then
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
exit
fi
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
# keep this is sync with config.js appFqdn()

View File

@@ -161,7 +161,7 @@ echo "==> Setting up unbound"
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
# We listen on 0.0.0.0 because there is no way control ordering of docker (which creates the 172.18.0.0/16) and unbound
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow" > /etc/unbound/unbound.conf.d/cloudron-network.conf
echo -e "server:\n\tinterface: 0.0.0.0\n\taccess-control: 127.0.0.1 allow\n\taccess-control: 172.18.0.1/16 allow\n\tcache-max-negative-ttl: 30" > /etc/unbound/unbound.conf.d/cloudron-network.conf
echo "==> Adding systemd services"
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
@@ -194,15 +194,11 @@ mkdir -p "${DATA_DIR}/nginx/applications"
mkdir -p "${DATA_DIR}/nginx/cert"
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
if ! grep "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
# default nginx service file does not restart on crash
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
systemctl daemon-reload
fi
# This is here, since the splash screen needs this file to be present :-(
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
fi
systemctl start nginx
# bookkeep the version as part of data
@@ -320,9 +316,14 @@ if [[ ! -z "${arg_tls_config}" ]]; then
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
fi
echo "==> Generating dhparams (takes forever)"
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
fi
set_progress "60" "Starting Cloudron"
systemctl start cloudron.target
sleep 2 # give systemd sometime to start the processes
set_progress "90" "Done"
set_progress "90" "Almost done"

View File

@@ -13,11 +13,11 @@ disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mou
existing_swap=$(cat /proc/meminfo | grep SwapTotal | awk '{ printf "%.0f", $2/1024 }')
# all sizes are in mb
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
readonly swap_size=$((${physical_memory} - ${existing_swap})) # if you change this, fix enoughResourcesAvailable() in client.js
readonly app_count=$((${physical_memory} / 200)) # estimated app count
readonly disk_size_bytes=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }') # can't rely on fdisk human readable units, using bytes instead
readonly disk_size=$((${disk_size_bytes}/1024/1024))
readonly disk_size_bytes=$(LC_ALL=C df | grep "${disk_device}" | awk '{ printf $2 }')
readonly disk_size=$((${disk_size_bytes}/1024))
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code, data and tmp
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1

View File

@@ -32,6 +32,19 @@ server {
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
add_header X-Frame-Options "<%= xFrameOptions %>";
proxy_hide_header X-Frame-Options;
# https://github.com/twitter/secureheaders
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
add_header X-XSS-Protection "1; mode=block";
proxy_hide_header X-XSS-Protection;
add_header X-Download-Options "noopen";
proxy_hide_header X-Download-Options;
add_header X-Content-Type-Options "nosniff";
proxy_hide_header X-Content-Type-Options;
add_header X-Permitted-Cross-Domain-Policies "none";
proxy_hide_header X-Permitted-Cross-Domain-Policies;
proxy_http_version 1.1;
proxy_intercept_errors on;
@@ -126,4 +139,3 @@ server {
<% } %>
}
}

View File

@@ -36,3 +36,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmbackup.sh
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh

View File

@@ -106,12 +106,6 @@ var KNOWN_ADDONS = {
teardown: NOOP,
backup: NOOP,
restore: NOOP
},
simpleauth: {
setup: setupSimpleAuth,
teardown: teardownSimpleAuth,
backup: NOOP,
restore: setupSimpleAuth
}
};
@@ -286,51 +280,6 @@ function teardownOauth(app, options, callback) {
});
}
function setupSimpleAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
if (!app.sso) return callback(null);
var appId = app.id;
var scope = 'profile';
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) { // remove existing creds
if (error && error.reason !== ClientsError.NOT_FOUND) return callback(error);
clients.add(appId, clients.TYPE_SIMPLE_AUTH, '', scope, function (error, result) {
if (error) return callback(error);
var env = [
'SIMPLE_AUTH_SERVER=172.18.0.1',
'SIMPLE_AUTH_PORT=' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_URL=http://172.18.0.1:' + config.get('simpleAuthPort'), // obsolete, remove
'SIMPLE_AUTH_ORIGIN=http://172.18.0.1:' + config.get('simpleAuthPort'),
'SIMPLE_AUTH_CLIENT_ID=' + result.id
];
debugApp(app, 'Setting simple auth addon config to %j', env);
appdb.setAddonConfig(appId, 'simpleauth', env, callback);
});
});
}
function teardownSimpleAuth(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
debugApp(app, 'teardownSimpleAuth');
clients.delByAppIdAndType(app.id, clients.TYPE_SIMPLE_AUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) debug(error);
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
});
}
function setupEmail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');

View File

@@ -50,7 +50,7 @@ function setHealth(app, health, callback) {
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
if (app.debugMode) mailer.appDied(app); // do not send mails for dev apps
if (!app.debugMode) mailer.appDied(app); // do not send mails for dev apps
gHealthInfo[app.id].emailSent = true;
} else {
debugApp(app, 'waiting for sometime to update the app health');

View File

@@ -152,7 +152,6 @@ function validatePortBindings(portBindings, tcpPorts) {
config.get('sysadminPort'), /* sysadmin app server (lo) */
config.get('smtpPort'), /* internal smtp port (lo) */
config.get('ldapPort'), /* ldap server (lo) */
config.get('simpleAuthPort'), /* simple auth server (lo) */
3306, /* mysql (lo) */
4190, /* managesieve */
8000 /* graphite (lo) */
@@ -312,6 +311,7 @@ function get(appId, callback) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
callback(null, app);
});
@@ -330,6 +330,7 @@ function getByIpAddress(ip, callback) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
callback(null, app);
});
@@ -345,6 +346,7 @@ function getAll(callback) {
apps.forEach(function (app) {
app.iconUrl = getIconUrlSync(app);
app.fqdn = app.altDomain || config.appFqdn(app.location);
app.cnameTarget = app.altDomain ? config.appFqdn(app.location) : null;
});
callback(null, apps);
@@ -525,7 +527,7 @@ function install(data, auditSource, callback) {
if ('sso' in data && !('optionalSso' in manifest)) return callback(new AppsError(AppsError.BAD_FIELD, 'sso can only be specified for apps with optionalSso'));
// if sso was unspecified, enable it by default if possible
if (sso === null) sso = !!manifest.addons['simpleauth'] || !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));

View File

@@ -218,7 +218,7 @@ function registerSubdomain(app, overwrite, callback) {
assert.strictEqual(typeof overwrite, 'boolean');
assert.strictEqual(typeof callback, 'function');
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
@@ -257,7 +257,7 @@ function unregisterSubdomain(app, location, callback) {
return callback(null);
}
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
@@ -295,7 +295,7 @@ function waitForDnsPropagation(app, callback) {
return callback(null);
}
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
subdomains.waitForDns(config.appFqdn(app.location), ip, 'A', { interval: 5000, times: 120 }, callback);
@@ -523,6 +523,9 @@ function configure(app, callback) {
reserveHttpPort.bind(null, app),
updateApp.bind(null, app, { installationProgress: '20, Downloading icon' }),
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '35, Registering subdomain' }),
registerSubdomain.bind(null, app, true /* overwrite */),

View File

@@ -43,8 +43,7 @@ var acme = require('./cert/acme.js'),
safe = require('safetydance'),
settings = require('./settings.js'),
user = require('./user.js'),
util = require('util'),
x509 = require('x509');
util = require('util');
function CertificatesError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -268,16 +267,6 @@ function validateCertificate(cert, key, fqdn) {
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
var content;
try {
content = x509.parseCert(cert);
} catch (e) {
return new Error('invalid cert: ' + e.message);
}
// check expiration
if (content.notAfter < new Date()) return new Error('cert expired');
function matchesDomain(domain) {
if (typeof domain !== 'string') return false;
if (domain === fqdn) return true;
@@ -286,8 +275,22 @@ function validateCertificate(cert, key, fqdn) {
return false;
}
// check domain
var domains = content.altNames.concat(content.subject.commonName);
// get commonName (http://stackoverflow.com/questions/17353122/parsing-strings-crt-files)
var result = safe.child_process.execSync('openssl x509 -noout -subject | sed -r "s|.*CN=(.*)|\\1|; s|/[^/]*=.*$||"', { encoding: 'utf8', input: cert });
if (!result) return new Error(util.format('could not get CN'));
var commonName = result.trim();
debug('validateCertificate: detected commonName as %s', commonName);
// https://github.com/drwetter/testssl.sh/pull/383
var cmd = `openssl x509 -noout -text | grep -A3 "Subject Alternative Name" | \
grep "DNS:" | \
sed -e "s/DNS://g" -e "s/ //g" -e "s/,/ /g" -e "s/othername:<unsupported>//g"`;
result = safe.child_process.execSync(cmd, { encoding: 'utf8', input: cert });
var altNames = result ? [ ] : result.trim().split(' '); // might fail if cert has no SAN
debug('validateCertificate: detected altNames as %j', altNames);
// check altNames
var domains = altNames.concat(commonName);
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
@@ -295,6 +298,10 @@ function validateCertificate(cert, key, fqdn) {
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
if (certModulus !== keyModulus) return new Error('key does not match the cert');
// check expiration
result = safe.child_process.execSync('openssl x509 -checkend 0', { encoding: 'utf8', input: cert });
if (!result) return new Error('cert expired');
return null;
}

View File

@@ -32,7 +32,6 @@ exports = module.exports = {
TYPE_EXTERNAL: 'external',
TYPE_BUILT_IN: 'built-in',
TYPE_OAUTH: 'addon-oauth',
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
TYPE_PROXY: 'addon-proxy'
};
@@ -192,7 +191,6 @@ function getAll(callback) {
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';
if (record.type === exports.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
record.location = result.location;

View File

@@ -242,7 +242,8 @@ function configureDefaultServer(callback) {
if (!fs.existsSync(certFilePath) || !fs.existsSync(keyFilePath)) {
debug('configureDefaultServer: create new cert');
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, 'cloudron');
var cn = 'cloudron-' + (new Date()).toISOString(); // randomize date a bit to keep firefox happy
var certCommand = util.format('openssl req -x509 -newkey rsa:2048 -keyout %s -out %s -days 3650 -subj /CN=%s -nodes', keyFilePath, certFilePath, cn);
safe.child_process.execSync(certCommand);
}
@@ -264,7 +265,7 @@ function configureAdmin(callback) {
debug('configureAdmin');
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
@@ -621,7 +622,7 @@ function addDnsRecords(callback) {
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
@@ -957,7 +958,7 @@ function migrate(options, callback) {
function refreshDNS(callback) {
callback = callback || NOOP_CALLBACK;
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
debug('refreshDNS: current ip %s', ip);

View File

@@ -84,7 +84,6 @@ function initConfig() {
data.smtpPort = 2525; // // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.simpleAuthPort = 3004;
data.provider = 'caas';
data.appBundle = [ ];

View File

@@ -8,7 +8,7 @@ exports = module.exports = {
getAllPaged: getAllPaged,
cleanup: cleanup,
// keep in sync with webadmin index.js filter
// keep in sync with webadmin index.js filter and CLI tool
ACTION_ACTIVATE: 'cloudron.activate',
ACTION_APP_CLONE: 'app.clone',
ACTION_APP_CONFIGURE: 'app.configure',
@@ -16,6 +16,7 @@ exports = module.exports = {
ACTION_APP_RESTORE: 'app.restore',
ACTION_APP_UNINSTALL: 'app.uninstall',
ACTION_APP_UPDATE: 'app.update',
ACTION_APP_LOGIN: 'app.login',
ACTION_BACKUP_FINISH: 'backup.finish',
ACTION_BACKUP_START: 'backup.start',
ACTION_CERTIFICATE_RENEWAL: 'certificate.renew',

View File

@@ -5,8 +5,8 @@
// Do not require anything here!
exports = module.exports = {
// a version bump means that all containers (apps and addons) are recreated
'version': 45,
// a version bump means that all app containers are recreated
'version': 46,
'baseImages': [ 'cloudron/base:0.10.0' ],
@@ -17,7 +17,7 @@ exports = module.exports = {
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.16.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.12.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.30.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.30.5' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
}
};

View File

@@ -7,6 +7,7 @@ exports = module.exports = {
var assert = require('assert'),
apps = require('./apps.js'),
async = require('async'),
config = require('./config.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:ldap'),
@@ -15,8 +16,7 @@ var assert = require('assert'),
UserError = user.UserError,
ldap = require('ldapjs'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
safe = require('safetydance');
var gServer = null;
@@ -26,6 +26,9 @@ var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
function getAppByRequest(req, callback) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof callback, 'function');
var sourceIp = req.connection.ldap.id.split(':')[0];
if (sourceIp.split('.').length !== 4) return callback(new ldap.InsufficientAccessRightsError('Missing source identifier'));
@@ -38,14 +41,36 @@ function getAppByRequest(req, callback) {
});
}
function getUsersWithAccessToApp(req, callback) {
assert.strictEqual(typeof req, 'object');
assert.strictEqual(typeof callback, 'function');
getAppByRequest(req, function (error, app) {
if (error) return callback(error);
user.list(function (error, result){
if (error) return callback(new ldap.OperationsError(error.toString()));
async.filter(result, apps.hasAccessTo.bind(null, app), function (error, result) {
if (error) return callback(new ldap.OperationsError(error.toString()));
callback(null, result);
});
});
});
}
function userSearch(req, res, next) {
debug('user search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
user.list(function (error, result) {
if (error) return next(new ldap.OperationsError(error.toString()));
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
// send user objects
result.forEach(function (entry) {
// skip entries with empty username. Some apps like owncloud can't deal with this
if (!entry.username) return;
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
var groups = [ GROUP_USERS_DN ];
@@ -69,6 +94,7 @@ function userSearch(req, res, next) {
givenName: firstName,
username: entry.username,
samaccountname: entry.username, // to support ActiveDirectory clients
isadmin: entry.admin ? 1 : 0,
memberof: groups
}
};
@@ -93,8 +119,8 @@ function userSearch(req, res, next) {
function groupSearch(req, res, next) {
debug('group search: dn %s, scope %s, filter %s (from %s)', req.dn.toString(), req.scope, req.filter.toString(), req.connection.ldap.id);
user.list(function (error, result){
if (error) return next(new ldap.OperationsError(error.toString()));
getUsersWithAccessToApp(req, function (error, result) {
if (error) return next(error);
var groups = [{
name: 'users',
@@ -293,7 +319,7 @@ function authenticateMailbox(req, res, next) {
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
if (req.credentials !== mailbox.ownerId) return next(new ldap.NoSuchObjectError(req.dn.toString()));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId });
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId });
return res.end();
}

View File

@@ -2,10 +2,12 @@
Dear Cloudron Admin,
The <%= program %> on <%= fqdn %> exited unexpectedly!
<%= program %> on <%= fqdn %> exited unexpectedly using too much memory!
The program has been restarted but should this message appear repeatedly,
you should give the program more memory.
The app has been restarted now. Should this message appear repeatedly or
undefined behavior is observed, give the app more memory.
This can be done in the advanced settings in the app configuration dialog
in your Cloudron's web interface.
Please see some excerpt of the logs below.

View File

@@ -6,7 +6,6 @@ exports = module.exports = {
listAliases: listAliases,
listMailboxes: listMailboxes,
// listGroups: listGroups, // this is beyond my SQL skillz
getMailbox: getMailbox,
getGroup: getGroup,

View File

@@ -37,7 +37,6 @@ var assert = require('assert'),
async = require('async'),
config = require('./config.js'),
debug = require('debug')('box:mailer'),
dns = require('native-dns'),
docker = require('./docker.js').connection,
ejs = require('ejs'),
nodemailer = require('nodemailer'),
@@ -346,7 +345,7 @@ function appDied(app) {
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.concat('support@cloudron.io').join(', '),
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] App %s is down', config.fqdn(), app.fqdn),
text: render('app_down.ejs', { fqdn: config.fqdn(), title: app.manifest.title, appFqdn: app.fqdn, format: 'text' })
};
@@ -442,7 +441,7 @@ function backupFailed(error) {
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.concat('support@cloudron.io').join(', '),
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Failed to backup', config.fqdn()),
text: render('backup_failed.ejs', { fqdn: config.fqdn(), message: message, format: 'text' })
};
@@ -460,7 +459,7 @@ function certificateRenewalError(domain, message) {
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.concat('support@cloudron.io').join(', '),
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] Certificate renewal error', domain),
text: render('certificate_renewal_error.ejs', { domain: domain, message: message, format: 'text' })
};
@@ -478,7 +477,7 @@ function oomEvent(program, context) {
var mailOptions = {
from: mailConfig().from,
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.concat('support@cloudron.io').join(', '),
to: config.provider() === 'caas' ? 'support@cloudron.io' : adminEmails.join(', '),
subject: util.format('[%s] %s exited unexpectedly', config.fqdn(), program),
text: render('oom_event.ejs', { fqdn: config.fqdn(), program: program, context: context, format: 'text' })
};
@@ -494,6 +493,8 @@ function unexpectedExit(program, context, callback) {
assert.strictEqual(typeof context, 'string');
assert.strictEqual(typeof callback, 'function');
if (config.provider() !== 'caas') return callback(); // no way to get admins without db access
var mailOptions = {
from: mailConfig().from,
to: 'support@cloudron.io',

View File

@@ -3,6 +3,7 @@
<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>

View File

@@ -67,7 +67,7 @@ function start(callback) {
// short-circuit for the restart case
if (_.isEqual(infra, existingInfra)) {
debug('platform is uptodate at version %s', infra.version);
process.nextTick(function () { exports.events.emit(exports.EVENT_READY); });
emitPlatformReady();
return callback();
}
@@ -82,14 +82,7 @@ function start(callback) {
], function (error) {
if (error) return callback(error);
// give 30 seconds for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
// TODO: make this smarter to not wait for 30secs for the crash-restart case
gPlatformReadyTimer = setTimeout(function () {
debug('emitting platform ready');
gPlatformReadyTimer = null;
exports.events.emit(exports.EVENT_READY);
}, 30000);
emitPlatformReady();
callback();
});
@@ -104,6 +97,17 @@ function uninitialize(callback) {
callback();
}
function emitPlatformReady() {
// give 30 seconds for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
// TODO: make this smarter to not wait for 30secs for the crash-restart case
gPlatformReadyTimer = setTimeout(function () {
debug('emitting platform ready');
gPlatformReadyTimer = null;
exports.events.emit(exports.EVENT_READY);
}, 30000);
}
function removeOldImages(callback) {
debug('removing old addon images');
@@ -241,7 +245,8 @@ function createMailConfig(callback) {
const alertsFrom = 'no-reply@' + config.fqdn();
user.getOwner(function (error, owner) {
var alertsTo = [ 'webmaster@cloudron.io' ].concat(error ? [] : owner.email).join(',');
var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
alertsTo.concat(error ? [] : owner.email).join(',');
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mail/mail.ini',
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}`, 'utf8')) {
@@ -284,6 +289,7 @@ function startMail(callback) {
--net-alias mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--env ENABLE_MDA=${mailConfig.enabled} \
-v "${dataDir}/mail:/app/data" \
-v "${dataDir}/addons/mail:/etc/mail" \
${ports} \
@@ -342,4 +348,3 @@ function startApps(existingInfra, callback) {
apps.configureInstalledApps(callback);
}
}

View File

@@ -54,6 +54,7 @@ function removeInternalAppFields(app) {
fqdn: app.fqdn,
memoryLimit: app.memoryLimit,
altDomain: app.altDomain,
cnameTarget: app.cnameTarget,
xFrameOptions: app.xFrameOptions,
sso: app.sso,
debugMode: app.debugMode

View File

@@ -24,8 +24,6 @@ var assert = require('assert'),
HttpSuccess = require('connect-lastmile').HttpSuccess,
progress = require('../progress.js'),
mailer = require('../mailer.js'),
settings = require('../settings.js'),
SettingsError = settings.SettingsError,
superagent = require('superagent'),
updateChecker = require('../updatechecker.js'),
_ = require('underscore');
@@ -35,18 +33,8 @@ function auditSource(req) {
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
}
/**
* Creating an admin user and activate the cloudron.
*
* @apiParam {string} username The administrator's user name
* @apiParam {string} password The administrator's password
* @apiParam {string} email The administrator's email address
*
* @apiSuccess (Created 201) {string} token A valid access token
*/
function activate(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
assert.strictEqual(typeof req.query.setupToken, 'string');
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be string'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be string'));
@@ -101,21 +89,33 @@ function dnsSetup(req, res, next) {
function setupTokenAuth(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
// skip setupToken auth for non caas case
if (config.provider() !== 'caas') return next();
if (config.provider() === 'caas') {
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
if (typeof req.query.setupToken !== 'string') return next(new HttpError(400, 'no setupToken provided'));
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/setup/verify').query({ setupToken:req.query.setupToken })
.timeout(30 * 1000)
.end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode === 403) return next(new HttpError(403, 'Invalid token'));
if (result.statusCode === 409) return next(new HttpError(409, 'Already setup'));
if (result.statusCode !== 200) return next(new HttpError(500, result.text || 'Internal error'));
next();
});
} else if (config.provider() === 'ami') {
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return next(new HttpError(500, error));
if (result.statusCode !== 200) return next(new HttpError(500, 'Unable to get meta data'));
if (result.text !== req.query.setupToken) return next(new HttpError(403, 'Invalid token'));
next();
});
} else {
next();
});
}
}
function getStatus(req, res, next) {

View File

@@ -13,5 +13,6 @@ exports = module.exports = {
profile: require('./profile.js'),
sysadmin: require('./sysadmin.js'),
settings: require('./settings.js'),
ssh: require('./ssh.js'),
user: require('./user.js')
};

View File

@@ -233,7 +233,6 @@ function loginForm(req, res) {
switch (result.type) {
case clients.TYPE_BUILT_IN: return renderBuiltIn();
case clients.TYPE_EXTERNAL: return render(result.appId, '/api/v1/cloudron/avatar');
case clients.TYPE_SIMPLE_AUTH: return sendError(req, res, 'Unknown OAuth client');
default: break;
}
@@ -450,8 +449,6 @@ var authorization = [
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 });
return next();
} else if (type === clients.TYPE_SIMPLE_AUTH) {
return sendError(req, res, 'Unknown OAuth client.');
}
appdb.get(req.oauth2.client.appId, function (error, appObject) {

View File

@@ -10,7 +10,7 @@ exports = module.exports = {
getCloudronAvatar: getCloudronAvatar,
setCloudronAvatar: setCloudronAvatar,
getEmailDnsRecords: getEmailDnsRecords,
getEmailStatus: getEmailStatus,
getDnsConfig: getDnsConfig,
setDnsConfig: setDnsConfig,
@@ -150,8 +150,8 @@ function getCloudronAvatar(req, res, next) {
});
}
function getEmailDnsRecords(req, res, next) {
settings.getEmailDnsRecords(function (error, records) {
function getEmailStatus(req, res, next) {
settings.getEmailStatus(function (error, records) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, records));

54
src/routes/ssh.js Normal file
View File

@@ -0,0 +1,54 @@
'use strict';
exports = module.exports = {
getAuthorizedKeys: getAuthorizedKeys,
getAuthorizedKey: getAuthorizedKey,
addAuthorizedKey: addAuthorizedKey,
delAuthorizedKey: delAuthorizedKey
};
var assert = require('assert'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
ssh = require('../ssh.js'),
SshError = ssh.SshError;
function getAuthorizedKeys(req, res, next) {
ssh.getAuthorizedKeys(function (error, result) {
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { keys: result }));
});
}
function getAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.params.identifier, 'string');
ssh.getAuthorizedKey(req.params.identifier, function (error, result) {
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, { identifier: result.identifier, key: result.key }));
});
}
function addAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.key !== 'string' || !req.body.key) return next(new HttpError(400, 'key must be a non empty'));
ssh.addAuthorizedKey(req.body.key, function (error) {
if (error && error.reason === SshError.INVALID_KEY) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(201, {}));
});
}
function delAuthorizedKey(req, res, next) {
assert.strictEqual(typeof req.params.identifier, 'string');
ssh.delAuthorizedKey(req.params.identifier, function (error) {
if (error && error.reason === SshError.NOT_FOUND) return next(new HttpError(404, error.message));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(202, {}));
});
}

View File

@@ -30,7 +30,6 @@ var appdb = require('../../appdb.js'),
safe = require('safetydance'),
server = require('../../server.js'),
settings = require('../../settings.js'),
simpleauth = require('../../simpleauth.js'),
superagent = require('superagent'),
taskmanager = require('../../taskmanager.js'),
tokendb = require('../../tokendb.js'),
@@ -42,7 +41,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '19.0.0';
var TEST_IMAGE_TAG = '20.0.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
@@ -174,7 +173,6 @@ function startBox(done) {
server.start.bind(server),
ldap.start,
simpleauth.start,
function (callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
@@ -257,7 +255,6 @@ function stopBox(done) {
appdb._clear,
server.stop,
ldap.stop,
simpleauth.stop,
config._reset
], done);
}

View File

@@ -273,16 +273,6 @@ describe('OAuth2', function () {
scope: 'profile'
};
// simple auth client
var CLIENT_8 = {
id: 'cid-client8',
appId: APP_2.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'secret8',
redirectURI: 'http://redirect8',
scope: 'profile'
};
// app with accessRestriction allowing group
var CLIENT_9 = {
id: 'cid-client9',
@@ -311,7 +301,6 @@ describe('OAuth2', function () {
clientdb.add.bind(null, CLIENT_5.id, CLIENT_5.appId, CLIENT_5.type, CLIENT_5.clientSecret, CLIENT_5.redirectURI, CLIENT_5.scope),
clientdb.add.bind(null, CLIENT_6.id, CLIENT_6.appId, CLIENT_6.type, CLIENT_6.clientSecret, CLIENT_6.redirectURI, CLIENT_6.scope),
clientdb.add.bind(null, CLIENT_7.id, CLIENT_7.appId, CLIENT_7.type, CLIENT_7.clientSecret, CLIENT_7.redirectURI, CLIENT_7.scope),
clientdb.add.bind(null, CLIENT_8.id, CLIENT_8.appId, CLIENT_8.type, CLIENT_8.clientSecret, CLIENT_8.redirectURI, CLIENT_8.scope),
clientdb.add.bind(null, CLIENT_9.id, CLIENT_9.appId, CLIENT_9.type, CLIENT_9.clientSecret, CLIENT_9.redirectURI, CLIENT_9.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1),
@@ -557,24 +546,6 @@ describe('OAuth2', function () {
});
});
});
it('fails when using simple auth credentials', function (done) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_8.redirectURI + '&client_id=' + CLIENT_8.id + '&response_type=code';
request.get(url, { jar: true }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body).to.eql('<script>window.location.href = "/api/v1/session/login?returnTo=' + CLIENT_8.redirectURI + '";</script>');
request.get(SERVER_URL + '/api/v1/session/login?returnTo=' + CLIENT_8.redirectURI, { jar: true, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client')).to.not.equal(-1);
done();
});
});
});
});
describe('loginForm submit', function () {
@@ -796,21 +767,6 @@ describe('OAuth2', function () {
});
});
it('fails for grant type code due to simple auth credentials', function (done) {
startAuthorizationFlow(CLIENT_7, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_8.redirectURI + '&client_id=' + CLIENT_8.id + '&response_type=code';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
done();
});
});
});
it('succeeds for grant type code with accessRestriction', function (done) {
startAuthorizationFlow(CLIENT_7, 'code', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_7.redirectURI + '&client_id=' + CLIENT_7.id + '&response_type=code';
@@ -858,21 +814,6 @@ describe('OAuth2', function () {
});
});
it('fails for grant type token due to simple auth credentials', function (done) {
startAuthorizationFlow(CLIENT_7, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_8.redirectURI + '&client_id=' + CLIENT_8.id + '&response_type=token';
request.get(url, { jar: jar, followRedirect: false }, function (error, response, body) {
expect(error).to.not.be.ok();
expect(response.statusCode).to.eql(200);
expect(body.indexOf('<!-- error tester -->')).to.not.equal(-1);
expect(body.indexOf('Unknown OAuth client.')).to.not.equal(-1);
done();
});
});
});
it('succeeds for grant type token', function (done) {
startAuthorizationFlow(CLIENT_7, 'token', function (jar) {
var url = SERVER_URL + '/api/v1/oauth/dialog/authorize?redirect_uri=' + CLIENT_7.redirectURI + '&client_id=' + CLIENT_7.id + '&response_type=token';

View File

@@ -1,512 +0,0 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var appdb = require('../../appdb.js'),
async = require('async'),
clientdb = require('../../clientdb.js'),
clients = require('../../clients.js'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js'),
simpleauth = require('../../simpleauth.js'),
nock = require('nock'),
settings = require('../../settings.js');
describe('SimpleAuth API', function () {
var SERVER_URL = 'http://localhost:' + config.get('port');
var SIMPLE_AUTH_ORIGIN = 'http://localhost:' + config.get('simpleAuthPort');
var USERNAME = 'superaDMin', PASSWORD = 'Foobar?1337', EMAIL ='silly@ME.com';
var APP_0 = {
id: 'app0',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test0',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone'] },
memoryLimit: 0,
altDomain: null
};
var APP_1 = {
id: 'app1',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test1',
portBindings: {},
accessRestriction: { users: [ 'foobar', 'someone' ] },
memoryLimit: 0,
altDomain: null
};
var APP_2 = {
id: 'app2',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test2',
portBindings: {},
accessRestriction: null,
memoryLimit: 0,
altDomain: null
};
var APP_3 = {
id: 'app3',
appStoreId: '',
manifest: { version: '0.1.0', addons: { } },
location: 'test3',
portBindings: {},
accessRestriction: { groups: [ 'someothergroup', 'admin', 'anothergroup' ] },
memoryLimit: 0,
altDomain: null
};
var CLIENT_0 = {
id: 'someclientid',
appId: 'someappid',
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_1 = {
id: 'someclientid1',
appId: APP_0.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret1',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_2 = {
id: 'someclientid2',
appId: APP_1.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret2',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_3 = {
id: 'someclientid3',
appId: APP_2.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret3',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_4 = {
id: 'someclientid4',
appId: APP_2.id,
type: clients.TYPE_OAUTH,
clientSecret: 'someclientsecret4',
redirectURI: '',
scope: 'user,profile'
};
var CLIENT_5 = {
id: 'someclientid5',
appId: APP_3.id,
type: clients.TYPE_SIMPLE_AUTH,
clientSecret: 'someclientsecret5',
redirectURI: '',
scope: 'user,profile'
};
before(function (done) {
async.series([
server.start.bind(server),
simpleauth.start.bind(simpleauth),
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
superagent.post(SERVER_URL + '/api/v1/cloudron/activate')
.query({ setupToken: 'somesetuptoken' })
.send({ username: USERNAME, password: PASSWORD, email: EMAIL })
.end(function (error, result) {
expect(error).to.not.be.ok();
expect(result).to.be.ok();
expect(result.statusCode).to.eql(201);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
superagent.get(SERVER_URL + '/api/v1/profile').query({ access_token: result.body.token}).end(function (error, result) {
expect(error).to.not.be.ok();
expect(result.statusCode).to.eql(200);
APP_1.accessRestriction.users.push(result.body.id);
callback();
});
});
},
clientdb.add.bind(null, CLIENT_0.id, CLIENT_0.appId, CLIENT_0.type, CLIENT_0.clientSecret, CLIENT_0.redirectURI, CLIENT_0.scope),
clientdb.add.bind(null, CLIENT_1.id, CLIENT_1.appId, CLIENT_1.type, CLIENT_1.clientSecret, CLIENT_1.redirectURI, CLIENT_1.scope),
clientdb.add.bind(null, CLIENT_2.id, CLIENT_2.appId, CLIENT_2.type, CLIENT_2.clientSecret, CLIENT_2.redirectURI, CLIENT_2.scope),
clientdb.add.bind(null, CLIENT_3.id, CLIENT_3.appId, CLIENT_3.type, CLIENT_3.clientSecret, CLIENT_3.redirectURI, CLIENT_3.scope),
clientdb.add.bind(null, CLIENT_4.id, CLIENT_4.appId, CLIENT_4.type, CLIENT_4.clientSecret, CLIENT_4.redirectURI, CLIENT_4.scope),
clientdb.add.bind(null, CLIENT_5.id, CLIENT_5.appId, CLIENT_5.type, CLIENT_5.clientSecret, CLIENT_5.redirectURI, CLIENT_5.scope),
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
appdb.add.bind(null, APP_1.id, APP_1.appStoreId, APP_1.manifest, APP_1.location, APP_1.portBindings, APP_1),
appdb.add.bind(null, APP_2.id, APP_2.appStoreId, APP_2.manifest, APP_2.location, APP_2.portBindings, APP_2),
appdb.add.bind(null, APP_3.id, APP_3.appStoreId, APP_3.manifest, APP_3.location, APP_3.portBindings, APP_3),
settings.setBackupConfig.bind(null, { provider: 'caas', token: 'BACKUP_TOKEN', bucket: 'Bucket', prefix: 'Prefix' })
], done);
});
after(function (done) {
async.series([
database._clear,
simpleauth.stop.bind(simpleauth),
server.stop.bind(server)
], done);
});
describe('login', function () {
it('cannot login without clientId', function (done) {
var body = {};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login without username', function (done) {
var body = {
clientId: 'someclientid'
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login without password', function (done) {
var body = {
clientId: 'someclientid',
username: USERNAME
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('cannot login with unkown clientId', function (done) {
var body = {
clientId: CLIENT_0.id+CLIENT_0.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with unkown user', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME+USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with empty password', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: ''
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('cannot login with wrong password', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: PASSWORD+PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for unkown app', function (done) {
var body = {
clientId: CLIENT_0.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('fails for disallowed app', function (done) {
var body = {
clientId: CLIENT_1.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds for allowed app', function (done) {
var body = {
clientId: CLIENT_2.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('succeeds for allowed app with email', function (done) {
var body = {
clientId: CLIENT_2.id,
username: EMAIL,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('succeeds for app without accessRestriction', function (done) {
var body = {
clientId: CLIENT_3.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('succeeds for app with group accessRestriction', function (done) {
var body = {
clientId: CLIENT_5.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
expect(result.body.accessToken).to.be.a('string');
expect(result.body.user).to.be.an('object');
expect(result.body.user.id).to.be.a('string');
expect(result.body.user.username).to.be.a('string');
expect(result.body.user.email).to.be.a('string');
expect(result.body.user.displayName).to.be.a('string');
expect(result.body.user.admin).to.be.a('boolean');
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: result.body.accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.body).to.be.an('object');
expect(result.body.username).to.eql(USERNAME.toLowerCase());
expect(result.body.email).to.eql(EMAIL.toLowerCase());
done();
});
});
});
it('fails for wrong client credentials', function (done) {
var body = {
clientId: CLIENT_4.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
});
describe('logout', function () {
var accessToken;
before(function (done) {
var body = {
clientId: CLIENT_3.id,
username: USERNAME,
password: PASSWORD
};
superagent.post(SIMPLE_AUTH_ORIGIN + '/api/v1/login')
.send(body)
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
accessToken = result.body.accessToken;
done();
});
});
it('fails without access_token', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.end(function (error, result) {
expect(result.statusCode).to.equal(400);
done();
});
});
it('fails with unkonwn access_token', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken+accessToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
it('succeeds', function (done) {
superagent.get(SIMPLE_AUTH_ORIGIN + '/api/v1/logout')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(error).to.be(null);
expect(result.statusCode).to.equal(200);
superagent.get(SERVER_URL + '/api/v1/profile')
.query({ access_token: accessToken })
.end(function (error, result) {
expect(result.statusCode).to.equal(401);
done();
});
});
});
});
});

240
src/routes/test/ssh-test.js Normal file
View File

@@ -0,0 +1,240 @@
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var ssh = require('../../ssh.js'),
async = require('async'),
config = require('../../config.js'),
database = require('../../database.js'),
expect = require('expect.js'),
superagent = require('superagent'),
server = require('../../server.js'),
nock = require('nock');
var SERVER_URL = 'http://localhost:' + config.get('port');
var USERNAME = 'superadmin', PASSWORD = 'Foobar?1337', EMAIL ='silly@me.com';
var INVALID_KEY_TYPE = 'ssh-foobar AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
var INVALID_KEY_VALUE = 'ssh-rsa foobar nebulon@nebulon';
var INVALID_KEY_IDENTIFIER = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N';
var VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N nebulon@nebulon';
var VALID_KEY_1 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCibC8G04mZy3o3AVMxjUMQoEQj0HSsl6AMVZDQK9A0e8qVRWft4HaRZdw0dW3iFDsEdny7s1zSAc5Kp5y38kJdSyEHGKxvcR8TaghUa8jpmu0sEVOTn+X4UtoonkNuJ0Jnl2tjPYsq5BtJmAeYUa1bKH5CjomCYi5OSfXRtnuZV6SiYX0A1OnZPXFWa/iFwwOUJQvDLGbFkgtJxqhxpc7yvzwFK5B9MNs7LJxA8+kRibJ9LTN1OKWNxb0oSk/PE6PFo9M2Q/SL9uj2IRXRipGj2XcOtZlqcAK5i+aq3UjjAGekztK2srQPcBkWbnI3Oim2N8l2purCfe0AoCCQHK7N muchmore';
var token = null;
var server;
function setup(done) {
config.set('fqdn', 'foobar.com');
async.series([
server.start.bind(server),
ssh._clear,
database._clear,
function createAdmin(callback) {
var scope1 = nock(config.apiServerOrigin()).get('/api/v1/boxes/' + config.fqdn() + '/setup/verify?setupToken=somesetuptoken').reply(200, {});
var scope2 = nock(config.apiServerOrigin()).post('/api/v1/boxes/' + config.fqdn() + '/setup/done?setupToken=somesetuptoken').reply(201, {});
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);
expect(scope1.isDone()).to.be.ok();
expect(scope2.isDone()).to.be.ok();
// 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('SSH API', function () {
this.timeout(10000);
before(setup);
after(cleanup);
describe('add authorized_keys', function () {
it('fails due to missing key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to empty key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: '' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: 'foobar' })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key type', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_TYPE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key value', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_VALUE })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('fails due to invalid key identifier', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: INVALID_KEY_IDENTIFIER })
.end(function (err, res) {
expect(res.statusCode).to.equal(400);
done();
});
});
it('succeeds', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
done();
});
});
});
describe('get authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.identifier).to.be.a('string');
expect(res.body.identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.key).to.equal(VALID_KEY);
done();
});
});
});
describe('list authorized_keys', function () {
it('succeeds', function (done) {
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(1);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY);
done();
});
});
it('succeeds with two keys', function (done) {
superagent.put(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.send({ key: VALID_KEY_1 })
.end(function (err, res) {
expect(res.statusCode).to.equal(201);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(200);
expect(res.body).to.be.an('object');
expect(res.body.keys).to.be.an('array');
expect(res.body.keys.length).to.equal(2);
expect(res.body.keys[0]).to.be.an('object');
expect(res.body.keys[0].identifier).to.be.a('string');
expect(res.body.keys[0].identifier).to.equal(VALID_KEY_1.split(' ')[2]);
expect(res.body.keys[0].key).to.equal(VALID_KEY_1);
expect(res.body.keys[1]).to.be.an('object');
expect(res.body.keys[1].identifier).to.be.a('string');
expect(res.body.keys[1].identifier).to.equal(VALID_KEY.split(' ')[2]);
expect(res.body.keys[1].key).to.equal(VALID_KEY);
done();
});
});
});
});
describe('delete authorized_keys', function () {
it('fails for non existing key', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/foobar')
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
it('succeeds', function (done) {
superagent.del(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(202);
superagent.get(SERVER_URL + '/api/v1/cloudron/ssh/authorized_keys/' + VALID_KEY.split(' ')[2])
.query({ access_token: token })
.end(function (err, res) {
expect(res.statusCode).to.equal(404);
done();
});
});
});
});
});

29
src/scripts/authorized_keys.sh Executable file
View File

@@ -0,0 +1,29 @@
#!/bin/bash
set -eu -o pipefail
if [[ ${EUID} -ne 0 ]]; then
echo "This script should be run as root." > /dev/stderr
exit 1
fi
if [[ $# -eq 0 ]]; then
echo "No arguments supplied"
exit 1
fi
if [[ "$1" == "--check" ]]; then
echo "OK"
exit 0
fi
# verify argument count
if [[ $# -lt 3 ]]; then
echo "Usage: authorized_keys.sh <user> <source> <destination>"
exit 1
fi
if [[ -f "$2" ]]; then
cp "$2" "$3"
chown "$1":"$1" "$3"
fi

View File

@@ -21,7 +21,7 @@ readonly program_name=$1
echo "${program_name}.log"
echo "-------------------"
journalctl --all --no-pager -u ${program_name} -n 300
journalctl --all --no-pager -u ${program_name} -n 800
echo
echo
echo "dmesg"

View File

@@ -105,6 +105,10 @@ function initializeExpressSync() {
router.post('/api/v1/cloudron/reboot', cloudronScope, routes.cloudron.reboot);
router.post('/api/v1/cloudron/migrate', cloudronScope, routes.user.requireAdmin, routes.user.verifyPassword, routes.cloudron.migrate);
router.get ('/api/v1/cloudron/graphs', cloudronScope, routes.graphs.getGraphs);
router.get ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKeys);
router.put ('/api/v1/cloudron/ssh/authorized_keys', cloudronScope, routes.user.requireAdmin, routes.ssh.addAuthorizedKey);
router.get ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.getAuthorizedKey);
router.del ('/api/v1/cloudron/ssh/authorized_keys/:identifier', cloudronScope, routes.user.requireAdmin, routes.ssh.delAuthorizedKey);
// feedback
router.post('/api/v1/cloudron/feedback', usersScope, routes.cloudron.feedback);
@@ -184,7 +188,7 @@ function initializeExpressSync() {
router.post('/api/v1/settings/cloudron_name', settingsScope, routes.user.requireAdmin, routes.settings.setCloudronName);
router.get ('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, routes.settings.getCloudronAvatar);
router.post('/api/v1/settings/cloudron_avatar', settingsScope, routes.user.requireAdmin, multipart, routes.settings.setCloudronAvatar);
router.get ('/api/v1/settings/email_dns_records', settingsScope, routes.user.requireAdmin, routes.settings.getEmailDnsRecords);
router.get ('/api/v1/settings/email_status', settingsScope, routes.user.requireAdmin, routes.settings.getEmailStatus);
router.get ('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.getDnsConfig);
router.post('/api/v1/settings/dns_config', settingsScope, routes.user.requireAdmin, routes.settings.setDnsConfig);
router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig);

View File

@@ -6,7 +6,7 @@ exports = module.exports = {
initialize: initialize,
uninitialize: uninitialize,
getEmailDnsRecords: getEmailDnsRecords,
getEmailStatus: getEmailStatus,
getAutoupdatePattern: getAutoupdatePattern,
setAutoupdatePattern: setAutoupdatePattern,
@@ -74,6 +74,7 @@ var assert = require('assert'),
cloudron = require('./cloudron.js'),
CloudronError = cloudron.CloudronError,
moment = require('moment-timezone'),
net = require('net'),
paths = require('./paths.js'),
safe = require('safetydance'),
settingsdb = require('./settingsdb.js'),
@@ -143,10 +144,10 @@ function uninitialize(callback) {
callback();
}
function getEmailDnsRecords(callback) {
function getEmailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
var records = {};
var records = {}, outboundPort25 = {};
var dkimKey = cloudron.readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
@@ -260,7 +261,7 @@ function getEmailDnsRecords(callback) {
status: false
};
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
records.ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
@@ -279,6 +280,44 @@ function getEmailDnsRecords(callback) {
});
}
function checkOutbound25(callback) {
var smtpServer = _.sample([
'smtp.gmail.com',
'smtp.live.com',
'smtp.mail.yahoo.com',
'smtp.o2.ie',
'smtp.comcast.net',
'outgoing.verizon.net'
]);
outboundPort25 = {
value: 'OK',
status: false
};
var client = new net.Socket();
client.setTimeout(5000);
client.connect(25, smtpServer);
client.on('connect', function () {
outboundPort25.status = true;
outboundPort25.value = 'OK';
client.end();
callback();
});
client.on('timeout', function () {
outboundPort25.status = false;
outboundPort25.value = 'Connect to ' + smtpServer + ' timed out';
client.destroy();
callback(new Error('Timeout'));
});
client.on('error', function (error) {
outboundPort25.status = false;
outboundPort25.value = 'Connect to ' + smtpServer + ' failed: ' + error.message;
client.destroy();
callback(error);
});
}
function ignoreError(what, func) {
return function (callback) {
func(function (error) {
@@ -298,9 +337,10 @@ function getEmailDnsRecords(callback) {
ignoreError('spf', checkSpf),
ignoreError('dmarc', checkDmarc),
ignoreError('dkim', checkDkim),
ignoreError('ptr', checkPtr)
ignoreError('ptr', checkPtr),
ignoreError('port25', checkOutbound25)
], function () {
callback(null, records);
callback(null, { dns: records, outboundPort25: outboundPort25 } );
});
}
@@ -453,7 +493,7 @@ function setDnsConfig(dnsConfig, domain, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof callback, 'function');
sysinfo.getIp(function (error, ip) {
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, 'Error getting IP:' + error.message));
subdomains.verifyDnsConfig(dnsConfig, domain, ip, function (error, result) {

View File

@@ -3,7 +3,8 @@
exports = module.exports = {
exec: exec,
execSync: execSync,
sudo: sudo
sudo: sudo,
sudoSync: sudoSync
};
var assert = require('assert'),
@@ -70,3 +71,20 @@ function sudo(tag, args, callback) {
var cp = exec(tag, SUDO, [ '-S' ].concat(args), callback);
cp.stdin.end();
}
function sudoSync(tag, cmd, callback) {
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof cmd, 'string');
// -S makes sudo read stdin for password
cmd = 'sudo -S ' + cmd;
debug(cmd);
try {
child_process.execSync(cmd, { stdio: 'inherit' });
} catch (e) {
if (callback) return callback(e);
throw e;
}
if (callback) callback();
}

View File

@@ -1,163 +0,0 @@
'use strict';
exports = module.exports = {
start: start,
stop: stop
};
var apps = require('./apps.js'),
AppsError = apps.AppsError,
assert = require('assert'),
clients = require('./clients.js'),
ClientsError = clients.ClientsError,
config = require('./config.js'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:src/simpleauth'),
eventlog = require('./eventlog.js'),
express = require('express'),
http = require('http'),
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess,
middleware = require('./middleware'),
tokendb = require('./tokendb.js'),
user = require('./user.js'),
UserError = require('./user.js').UserError;
var gHttpServer = null;
function loginLogic(clientId, username, password, callback) {
assert.strictEqual(typeof clientId, 'string');
assert.strictEqual(typeof username, 'string');
assert.strictEqual(typeof password, 'string');
assert.strictEqual(typeof callback, 'function');
debug('login: client %s and user %s', clientId, username);
clients.get(clientId, function (error, clientObject) {
if (error) return callback(error);
// only allow simple auth clients
if (clientObject.type !== clients.TYPE_SIMPLE_AUTH) return callback(new ClientsError(ClientsError.INVALID_CLIENT));
var authFunction = (username.indexOf('@') === -1) ? user.verifyWithUsername : user.verifyWithEmail;
authFunction(username, password, function (error, userObject) {
if (error) return callback(error);
apps.get(clientObject.appId, function (error, appObject) {
if (error) return callback(error);
apps.hasAccessTo(appObject, userObject, function (error, access) {
if (error) return callback(error);
if (!access) return callback(new AppsError(AppsError.ACCESS_DENIED));
var accessToken = tokendb.generateToken();
var expires = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
tokendb.add(accessToken, userObject.id, clientId, expires, clientObject.scope, function (error) {
if (error) return callback(error);
debug('login: new access token for client %s and user %s: %s', clientId, username, accessToken);
callback(null, { accessToken: accessToken, user: userObject });
});
});
});
});
});
}
function logoutLogic(accessToken, callback) {
assert.strictEqual(typeof accessToken, 'string');
assert.strictEqual(typeof callback, 'function');
debug('logout: %s', accessToken);
tokendb.del(accessToken, function (error) {
if (error) return callback(error);
callback(null);
});
}
function login(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.clientId !== 'string') return next(new HttpError(400, 'clientId is required'));
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required'));
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required'));
loginLogic(req.body.clientId, req.body.username, req.body.password, function (error, result) {
if (error && error.reason === ClientsError.NOT_FOUND) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === ClientsError.INVALID_CLIENT) return next(new HttpError(401, 'Unknown client'));
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === AppsError.NOT_FOUND) return next(new HttpError(401, 'Unknown app'));
if (error && error.reason === UserError.WRONG_PASSWORD) return next(new HttpError(401, 'Forbidden'));
if (error && error.reason === AppsError.ACCESS_DENIED) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'simpleauth', clientId: req.body.clientId }, { userId: result.user.id });
var tmp = {
accessToken: result.accessToken,
user: {
id: result.user.id,
username: result.user.username,
email: result.user.email,
admin: !!result.user.admin,
displayName: result.user.displayName
}
};
next(new HttpSuccess(200, tmp));
});
}
function logout(req, res, next) {
assert.strictEqual(typeof req.query, 'object');
if (typeof req.query.access_token !== 'string') return next(new HttpError(400, 'access_token in query required'));
logoutLogic(req.query.access_token, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return next(new HttpError(401, 'Forbidden'));
if (error) return next(new HttpError(500, error));
next(new HttpSuccess(200, {}));
});
}
function initializeExpressSync() {
var app = express();
var httpServer = http.createServer(app);
httpServer.on('error', console.error);
var json = middleware.json({ strict: true, limit: '100kb' });
var router = new express.Router();
// basic auth
router.post('/api/v1/login', login);
router.get ('/api/v1/logout', logout);
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('SimpleAuth :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
app
.use(middleware.timeout(10000))
.use(json)
.use(router)
.use(middleware.lastMile());
return httpServer;
}
function start(callback) {
assert.strictEqual(typeof callback, 'function');
gHttpServer = initializeExpressSync();
gHttpServer.listen(config.get('simpleAuthPort'), '0.0.0.0', callback);
}
function stop(callback) {
assert.strictEqual(typeof callback, 'function');
if (gHttpServer) gHttpServer.close(callback);
}

149
src/ssh.js Normal file
View File

@@ -0,0 +1,149 @@
'use strict';
exports = module.exports = {
SshError: SshError,
getAuthorizedKeys: getAuthorizedKeys,
getAuthorizedKey: getAuthorizedKey,
addAuthorizedKey: addAuthorizedKey,
delAuthorizedKey: delAuthorizedKey,
_clear: clear
};
var assert = require('assert'),
config = require('./config.js'),
fs = require('fs'),
path = require('path'),
safe = require('safetydance'),
shell = require('./shell.js'),
util = require('util');
var AUTHORIZED_KEYS_FILEPATH = config.TEST ? path.join(config.baseDir(), 'authorized_keys') : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? '/home/ubuntu/.ssh/authorized_keys' : '/root/.ssh/authorized_keys');
var AUTHORIZED_KEYS_TMP_FILEPATH = '/tmp/.authorized_keys';
var AUTHORIZED_KEYS_CMD = path.join(__dirname, 'scripts/authorized_keys.sh');
var VALID_KEY_TYPES = ['ssh-rsa']; // TODO add all supported ones
var VALID_MIN_KEY_LENGTH = 370; // TODO verify this length requirement
function SshError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
Error.call(this);
Error.captureStackTrace(this, this.constructor);
this.name = this.constructor.name;
this.reason = reason;
if (typeof errorOrMessage === 'undefined') {
this.message = reason;
} else if (typeof errorOrMessage === 'string') {
this.message = errorOrMessage;
} else {
this.message = 'Internal error';
this.nestedError = errorOrMessage;
}
}
util.inherits(SshError, Error);
SshError.NOT_FOUND = 'Not found';
SshError.INVALID_KEY = 'Invalid key';
SshError.INTERNAL_ERROR = 'Internal Error';
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
fs.unlink(AUTHORIZED_KEYS_FILEPATH, function (error) {
if (error && error.code !== 'ENOENT') return callback(error);
callback();
});
}
function saveKeys(keys) {
assert(Array.isArray(keys));
if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) {
console.error(safe.error);
return false;
}
try {
// 600 = rw-------
fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600');
} catch (e) {
console.error('Failed to adjust permissions of %s', AUTHORIZED_KEYS_TMP_FILEPATH, e);
return false;
}
var user = config.TEST ? process.env.USER : ((config.provider() === 'ec2' || config.provider() === 'lightsail' || config.provider() === 'ami') ? 'ubuntu' : 'root');
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, user, AUTHORIZED_KEYS_TMP_FILEPATH, AUTHORIZED_KEYS_FILEPATH));
return true;
}
function getKeys() {
shell.sudoSync('authorized_keys', util.format('%s %s %s %s', AUTHORIZED_KEYS_CMD, process.env.USER, AUTHORIZED_KEYS_FILEPATH, AUTHORIZED_KEYS_TMP_FILEPATH));
var content = safe.fs.readFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, 'utf8');
if (!content) return [];
var keys = content.split('\n')
.filter(function (k) { return !!k.trim(); })
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
.filter(function (k) { return k.identifier && k.key; });
return keys;
}
function getAuthorizedKeys(callback) {
assert.strictEqual(typeof callback, 'function');
return callback(null, getKeys().sort(function (a, b) { return a.identifier.localeCompare(b.identifier); }));
}
function getAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
var keys = getKeys();
if (keys.length === 0) return callback(new SshError(SshError.NOT_FOUND));
var key = keys.find(function (k) { return k.identifier === identifier; });
if (!key) return callback(new SshError(SshError.NOT_FOUND));
callback(null, key);
}
function addAuthorizedKey(key, callback) {
assert.strictEqual(typeof key, 'string');
assert.strictEqual(typeof callback, 'function');
var tmp = key.split(' ');
if (tmp.length !== 3) return callback(new SshError(SshError.INVALID_KEY));
if (!VALID_KEY_TYPES.some(function (t) { return tmp[0] === t; })) return callback(new SshError(SshError.INVALID_KEY, 'Invalid key type'));
if (tmp[1].length < VALID_MIN_KEY_LENGTH) return callback(new SshError(SshError.INVALID_KEY));
var identifier = tmp[2];
var keys = getKeys();
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index !== -1) keys[index] = { identifier: identifier, key: key };
else keys.push({ identifier: identifier, key: key });
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
callback();
}
function delAuthorizedKey(identifier, callback) {
assert.strictEqual(typeof identifier, 'string');
assert.strictEqual(typeof callback, 'function');
var keys = getKeys();
var index = keys.findIndex(function (k) { return k.identifier === identifier; });
if (index === -1) return callback(new SshError(SshError.NOT_FOUND));
// now remove the key
keys.splice(index, 1);
if (!saveKeys(keys)) return callback(new SshError(SshError.INTERNAL_ERROR));
callback();
}

View File

@@ -3,7 +3,7 @@
exports = module.exports = {
SysInfoError: SysInfoError,
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
@@ -44,17 +44,18 @@ function getApi(callback) {
case 'digitalocean': return callback(null, generic);
case 'ec2': return callback(null, ec2);
case 'lightsail': return callback(null, ec2);
case 'ami': return callback(null, ec2);
case 'scaleway': return callback(null, scaleway);
default: return callback(null, generic);
}
}
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
getApi(function (error, api) {
if (error) return callback(error);
api.getIp(callback);
api.getPublicIp(callback);
});
}

View File

@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
@@ -9,7 +9,7 @@ var assert = require('assert'),
safe = require('safetydance'),
SysInfoError = require('../sysinfo.js').SysInfoError;
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
if (process.env.BOX_ENV === 'test') return callback(null, '127.0.0.1');

View File

@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
@@ -9,7 +9,7 @@ var assert = require('assert'),
SysInfoError = require('../sysinfo.js').SysInfoError,
util = require('util');
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get('http://169.254.169.254/latest/meta-data/public-ipv4').timeout(30 * 1000).end(function (error, result) {

View File

@@ -1,7 +1,7 @@
'use strict';
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
@@ -10,7 +10,7 @@ var assert = require('assert'),
superagent = require('superagent'),
SysInfoError = require('../sysinfo.js').SysInfoError;
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
async.retry({ times: 10, interval: 5000 }, function (callback) {

View File

@@ -7,12 +7,12 @@
// -------------------------------------------
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert');
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
callback(new Error('not implemented'));

View File

@@ -1,13 +1,13 @@
'use strict';
exports = module.exports = {
getIp: getIp
getPublicIp: getPublicIp
};
var assert = require('assert'),
superagent = require('superagent');
function getIp(callback) {
function getPublicIp(callback) {
assert.strictEqual(typeof callback, 'function');
superagent.get('http://169.254.42.42/conf').timeout(30 * 1000).end(function (error, result) {

View File

@@ -15,6 +15,8 @@ var async = require('async'),
function setup(done) {
async.series([
database.initialize,
settings.initialize,
certificates.initialize,
database._clear
], done);
}

View File

@@ -19,7 +19,8 @@ scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \
"${SOURCE_DIR}/src/scripts/reboot.sh" \
"${SOURCE_DIR}/src/scripts/update.sh" \
"${SOURCE_DIR}/src/scripts/collectlogs.sh" \
"${SOURCE_DIR}/src/scripts/reloadcollectd.sh")
"${SOURCE_DIR}/src/scripts/reloadcollectd.sh" \
"${SOURCE_DIR}/src/scripts/authorized_keys.sh")
for script in "${scripts[@]}"; do
if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then
@@ -49,4 +50,3 @@ if [[ "${image_missing}" == "true" ]]; then
echo "Pull above images before running tests"
exit 1
fi

View File

@@ -20,6 +20,7 @@ describe('dns provider', function () {
before(function (done) {
async.series([
database.initialize,
settings.initialize,
config._reset
], done);
});

View File

@@ -38,6 +38,12 @@ var USER_1 = {
email: 'USER1@email.com',
displayName: 'User 1'
};
var USER_2 = {
username: 'Username2',
password: 'Username2pass?12345',
email: 'USER2@email.com',
displayName: 'User 2'
};
var GROUP_ID, GROUP_NAME = 'developers';
@@ -98,6 +104,15 @@ function setup(done) {
callback(null);
});
},
function (callback) {
user.create(USER_2.username, USER_2.password, USER_2.email, USER_0.displayName, AUDIT_SOURCE, { invitor: USER_0 }, function (error, result) {
if (error) return callback(error);
USER_2.id = result.id;
callback(null);
});
},
function (callback) {
groups.create(GROUP_NAME, function (error, result) {
if (error) return callback(error);
@@ -409,6 +424,66 @@ describe('Ldap', function () {
});
});
});
it ('does not list users who have no access', function (done) {
appdb.update(APP_0.id, { accessRestriction: { users: [], groups: [] } }, function (error) {
expect(error).to.be(null);
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: 'objectcategory=person'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(0);
appdb.update(APP_0.id, { accessRestriction: null }, done);
});
});
});
});
it ('does only list users who have access', function (done) {
appdb.update(APP_0.id, { accessRestriction: { users: [], groups: [ GROUP_ID ] } }, function (error) {
expect(error).to.be(null);
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: 'objectcategory=person'
};
client.search('ou=users,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
entries.sort(function (a, b) { return a.username > b.username; });
expect(entries[0].username).to.equal(USER_0.username.toLowerCase());
expect(entries[1].username).to.equal(USER_1.username.toLowerCase());
appdb.update(APP_0.id, { accessRestriction: null }, done);
});
});
});
});
});
describe('search groups', function () {
@@ -435,9 +510,10 @@ describe('Ldap', function () {
entries.sort(function (a, b) { return a.username < b.username; });
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid.length).to.equal(3);
expect(entries[0].memberuid[0]).to.equal(USER_0.id);
expect(entries[0].memberuid[1]).to.equal(USER_1.id);
expect(entries[0].memberuid[2]).to.equal(USER_2.id);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.id);
@@ -465,9 +541,10 @@ describe('Ldap', function () {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid.length).to.equal(3);
expect(entries[0].memberuid[0]).to.equal(USER_0.id);
expect(entries[0].memberuid[1]).to.equal(USER_1.id);
expect(entries[0].memberuid[2]).to.equal(USER_2.id);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.id);
@@ -495,11 +572,46 @@ describe('Ldap', function () {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(1);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid.length).to.equal(3);
done();
});
});
});
it ('does only list users who have access', function (done) {
appdb.update(APP_0.id, { accessRestriction: { users: [], groups: [ GROUP_ID ] } }, function (error) {
expect(error).to.be(null);
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
var opts = {
filter: '&(objectclass=group)(cn=*)'
};
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
expect(error).to.be(null);
expect(result).to.be.an(EventEmitter);
var entries = [];
result.on('searchEntry', function (entry) { entries.push(entry.object); });
result.on('error', done);
result.on('end', function (result) {
expect(result.status).to.equal(0);
expect(entries.length).to.equal(2);
expect(entries[0].cn).to.equal('users');
expect(entries[0].memberuid.length).to.equal(2);
expect(entries[0].memberuid[0]).to.equal(USER_0.id);
expect(entries[0].memberuid[1]).to.equal(USER_1.id);
expect(entries[1].cn).to.equal('admins');
// if only one entry, the array becomes a string :-/
expect(entries[1].memberuid).to.equal(USER_0.id);
appdb.update(APP_0.id, { accessRestriction: null }, done);
});
});
});
});
});
function ldapSearch(dn, filter, callback) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -3,6 +3,7 @@
<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' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
<title> Cloudron App Error </title>

View File

@@ -3,6 +3,7 @@
<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' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
<title> Cloudron Error </title>

View File

@@ -3,6 +3,7 @@
<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' *.cloudron.io <%= apiOriginHostname %>; img-src *;" />
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
<title> Cloudron </title>
@@ -52,7 +53,7 @@
<script src="3rdparty/js/ansi_up.js"></script>
<!-- Showdown (markdown converter) -->
<script src="3rdparty/js/showdown-1.1.0.min.js"></script>
<script src="3rdparty/js/showdown-1.6.4.min.js"></script>
<script src="3rdparty/js/showdown-target-blank.min.js"></script>
<!-- Bootstrap slider -->

View File

@@ -1,7 +1,6 @@
'use strict';
/* global angular */
/* global EventSource */
angular.module('Application').service('Client', ['$http', 'md5', 'Notification', function ($http, md5, Notification) {
var client = null;
@@ -440,8 +439,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.getExpectedDnsRecords = function (callback) {
get('/api/v1/settings/email_dns_records').success(function(data, status) {
Client.prototype.getEmailStatus = function (callback) {
get('/api/v1/settings/email_status').success(function(data, status) {
if (status !== 200) return callback(new ClientError(status, data));
callback(null, data);
}).error(defaultErrorHandler(callback));
@@ -477,6 +476,27 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
}).error(defaultErrorHandler(callback));
};
Client.prototype.addAuthorizedKey = function (key, callback) {
put('/api/v1/cloudron/ssh/authorized_keys', { key: key }).success(function (data, status) {
if (status !== 201) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.delAuthorizedKey = function (identifier, callback) {
del('/api/v1/cloudron/ssh/authorized_keys/' + identifier).success(function (data, status) {
if (status !== 202) return callback(new ClientError(status, data));
callback(null);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getAuthorizedKeys = function (callback) {
get('/api/v1/cloudron/ssh/authorized_keys').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
callback(null, data.keys);
}).error(defaultErrorHandler(callback));
};
Client.prototype.getBackups = function (callback) {
get('/api/v1/backups').success(function (data, status) {
if (status !== 200 || typeof data !== 'object') return callback(new ClientError(status, data));
@@ -592,11 +612,6 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
else return callback(new Error('App not found'));
};
Client.prototype.getAppLogStream = function (appId) {
var source = new EventSource(client.apiOrigin + '/api/v1/apps/' + appId + '/logstream');
return source;
};
Client.prototype.getAppIconUrls = function (app) {
return {
cloudron: app.iconUrl ? (this.apiOrigin + app.iconUrl + '?access_token=' + token) : null,
@@ -628,7 +643,10 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
displayName: displayName
};
post('/api/v1/cloudron/activate?setupToken=' + setupToken, data).success(function(data, status) {
var query = '';
if (setupToken) query = '?setupToken=' + setupToken;
post('/api/v1/cloudron/activate' + query, data).success(function(data, status) {
if (status !== 201 || typeof data !== 'object') return callback(new ClientError(status, data));
that.setToken(data.token);

View File

@@ -216,7 +216,7 @@ app.filter('prettyDate', function () {
day_diff = Math.floor(diff / 86400);
if (isNaN(day_diff) || day_diff < 0)
return;
return 'just now';
return day_diff === 0 && (
diff < 60 && 'just now' ||
@@ -253,7 +253,11 @@ app.filter('postInstallMessage', function () {
if (!app) return text;
var parts = text.split(SSO_MARKER);
if (parts.length === 1) return text;
if (parts.length === 1) {
// [^] matches even newlines. '?' makes it non-greedy
if (app.sso) return text.replace(/\<nosso\>[^]*?\<\/nosso\>/g, '');
else return text.replace(/\<sso\>[^]*?\<\/sso\>/g, '');
}
if (app.sso) return parts[1];
else return parts[0];
@@ -261,7 +265,7 @@ app.filter('postInstallMessage', function () {
});
// keep this in sync with eventlog.js
// keep this in sync with eventlog.js and CLI tool
var ACTION_ACTIVATE = 'cloudron.activate';
var ACTION_APP_CONFIGURE = 'app.configure';
var ACTION_APP_INSTALL = 'app.install';
@@ -269,6 +273,7 @@ var ACTION_APP_RESTORE = 'app.restore';
var ACTION_APP_UNINSTALL = 'app.uninstall';
var ACTION_APP_UPDATE = 'app.update';
var ACTION_APP_UPDATE = 'app.update';
var ACTION_APP_LOGIN = 'app.login';
var ACTION_BACKUP_FINISH = 'backup.finish';
var ACTION_BACKUP_START = 'backup.start';
var ACTION_CERTIFICATE_RENEWAL = 'certificate.renew';
@@ -281,6 +286,7 @@ var ACTION_USER_REMOVE = 'user.remove';
var ACTION_USER_UPDATE = 'user.update';
app.filter('eventLogDetails', function() {
// NOTE: if you change this, the CLI tool (cloudron machine eventlog) probably needs fixing as well
return function(eventLog) {
var source = eventLog.source;
var data = eventLog.data;
@@ -293,6 +299,7 @@ app.filter('eventLogDetails', function() {
case ACTION_APP_RESTORE: return 'App ' + data.appId + ' restored';
case ACTION_APP_UNINSTALL: return 'App ' + data.appId + ' uninstalled';
case ACTION_APP_UPDATE: return 'App ' + data.appId + ' updated to version ' + data.toManifest.id + '@' + data.toManifest.version;
case ACTION_APP_LOGIN: return 'App ' + data.appId + ' logged in';
case ACTION_BACKUP_START: return 'Backup started';
case ACTION_BACKUP_FINISH: return 'Backup finished. ' + (errorMessage ? ('error: ' + errorMessage) : ('id: ' + data.filename));
case ACTION_CERTIFICATE_RENEWAL: return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : 'succeeded');

View File

@@ -74,12 +74,12 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
Client.getDnsConfig(function (error, result) {
if (error) return console.error(error);
if (result.provider !== 'manual' && result.provider !== 'noop') return;
if (result.provider === 'caas') return;
Client.getExpectedDnsRecords(function (error, result) {
Client.getEmailStatus(function (error, result) {
if (error) return console.error(error);
if (!result.spf.status || !result.dkim.status) {
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.outboundPort25.status) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/settings';

View File

@@ -20,15 +20,20 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
$scope.provider = '';
$scope.apiServerOrigin = '';
$scope.setupToken = '';
$scope.instanceId = '';
$scope.activateCloudron = function () {
$scope.busy = true;
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken, function (error) {
if (error) {
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken || $scope.instanceId, function (error) {
if (error && error.statusCode === 403) {
$scope.busy = false;
$scope.error = $scope.provider === 'ami' ? 'Wrong instance id' : 'Wrong setup token';
return;
} else if (error) {
$scope.busy = false;
console.error('Internal error', error);
$scope.error = error;
$scope.busy = false;
return;
}
@@ -77,6 +82,7 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
$scope.account.displayName = search.displayName || $scope.account.displayName;
$scope.account.requireEmail = !search.email;
$scope.provider = status.provider;
$scope.instanceId = search.instanceId;
$scope.apiServerOrigin = status.apiServerOrigin;
$scope.initialized = true;

View File

@@ -78,6 +78,11 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', func
if (status.adminFqdn) return waitForDnsSetup();
if (status.provider === 'digitalocean') $scope.dnsCredentials.provider = 'digitalocean';
if (status.provider === 'ami') {
// remove route53 on ami
$scope.dnsProvider.shift();
$scope.dnsCredentials.provider = 'wildcard';
}
$scope.provider = status.provider;
$scope.initialized = true;

View File

@@ -1,6 +1,7 @@
<html>
<head>
<title> Cloudron OAuth Callback </title>
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>" />
<script>

View File

@@ -3,6 +3,7 @@
<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' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
<title> Cloudron </title>

View File

@@ -3,6 +3,7 @@
<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' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
<title> Cloudron Admin Setup </title>
@@ -77,6 +78,11 @@
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
</div>
</div>
<div class="form-group" ng-class="{ 'has-error': setupForm.instanceId.$dirty && (setupForm.instanceId.$invalid || error) }" ng-show="provider === 'ami'">
<p>Provide the EC2 instance id to verify you are the owner</p>
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="AWS EC2 instance id" ng-maxlength="20" ng-minlength="10" ng-required="provider === 'ami'" autocomplete="off">
<p ng-show="error" class="has-error">{{ error }}</p>
</div>
</div>
</div>
<div class="row">

View File

@@ -3,6 +3,7 @@
<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' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
<title> Cloudron Setup </title>
@@ -53,7 +54,7 @@
<div class="row">
<div class="col-md-10 col-md-offset-1 text-center">
<h1>Cloudron Setup</h1>
<h3>Provide the domain for your Cloudron</h3>
<h3>Provide a domain for your Cloudron</h3>
<p>Apps will be installed on subdomains of that domain.</p>
<div class="form-group" style="margin-bottom: 0;" ng-class="{ 'has-error': dnsCredentialsForm.domain.$dirty && dnsCredentialsForm.domain.$invalid }">
<input type="text" class="form-control" ng-model="dnsCredentials.domain" name="domain" check-tld placeholder="example.com" required autofocus ng-disabled="dnsCredentials.busy">
@@ -63,7 +64,7 @@
</div>
<div class="row">
<div class="col-md-10 col-md-offset-1 text-center">
<h3>DNS Configuration</h3>
<h3>Choose how the domain is managed</h3>
<p class="has-error text-center" ng-show="dnsCredentials.error">{{ dnsCredentials.error }}</p>
<div class="form-group">
@@ -77,14 +78,14 @@
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.secretAccessKey.$dirty && dnsCredentialsForm.secretAccessKey.$invalid }" ng-show="dnsCredentials.provider === 'route53'">
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" name="secretAccessKey" placeholder="Secret Access Key" ng-required="dnsCredentials.provider === 'route53'" ng-disabled="dnsCredentials.busy">
<br/>
<span>This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</span>
<span>{{ dnsCredentials.domain || 'The domain' }} must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</span>
</div>
<!-- DigitalOcean -->
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.digitalOceanToken.$dirty && dnsCredentialsForm.digitalOceanToken.$invalid }" ng-show="dnsCredentials.provider === 'digitalocean'">
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" name="digitalOceanToken" placeholder="API Token" ng-required="dnsCredentials.provider === 'digitalocean'" ng-disabled="dnsCredentials.busy">
<br/>
<span>This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</span>
<span>{{ dnsCredentials.domain || 'The domain' }} must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</span>
</div>
<!-- Wildcard -->

View File

@@ -3,6 +3,7 @@
<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' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' *.cloudron.io <%= apiOriginHostname %>" />
<title> Cloudron </title>

View File

@@ -14,6 +14,7 @@ angular.module('Application').controller('ActivityController', ['$scope', '$loca
{ name: 'app.restore', value: 'app.restore' },
{ name: 'app.uninstall', value: 'app.uninstall' },
{ name: 'app.update', value: 'app.update' },
{ name: 'app.login', value: 'app.login' },
{ name: 'backup.finish', value: 'backup.finish' },
{ name: 'backup.start', value: 'backup.start' },
{ name: 'certificate.renew', value: 'certificate.renew' },

View File

@@ -51,7 +51,7 @@
</div>
<p class="text-center" ng-show="appConfigure.usingAltDomain && appConfigure.location && appConfigure.isAltDomainValid()">
Add a CNAME record for {{ appConfigure.location }} to {{ appConfigure.app.fqdn }}
Add a CNAME record for <b>{{ appConfigure.location }}</b> to <b>{{ appConfigure.app.cnameTarget || appConfigure.app.fqdn }}</b>
<br>
</p>
@@ -318,7 +318,7 @@
</div>
<div class="form-group" ng-class="{ 'has-error': (!appUpdateForm.password.$dirty && appUpdate.error.password) || (appUpdateForm.password.$dirty && appUpdateForm.password.$invalid) }">
<label class="control-label" for="inputUpdatePassword">Provide your password to confirm this action</label>
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" ng-maxlength="30" ng-minlength="8" required autofocus>
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" required autofocus>
</div>
<input class="ng-hide" type="submit" ng-disabled="appUpdateForm.$invalid || busy"/>
</form>
@@ -412,7 +412,7 @@
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
</div>
<div ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure'">
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-pencil scale"></i></a>
</div>

View File

@@ -188,7 +188,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
$scope.appConfigure.accessRestriction = app.accessRestriction || { users: [], groups: [] };
$scope.appConfigure.memoryLimit = app.memoryLimit || app.manifest.memoryLimit || (256 * 1024 * 1024);
$scope.appConfigure.xFrameOptions = app.xFrameOptions.indexOf('ALLOW-FROM') === 0 ? app.xFrameOptions.split(' ')[1] : '';
$scope.appConfigure.customAuth = !(app.manifest.addons['simpleauth'] || app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
$scope.appConfigure.customAuth = !(app.manifest.addons['ldap'] || app.manifest.addons['oauth']);
// create ticks starting from manifest memory limit
$scope.appConfigure.memoryTicks = [

View File

@@ -225,10 +225,18 @@
</div>
</div>
<div class="checkbox" ng-show="appstoreLogin.register">
<label>
<input type="checkbox" ng-model="appstoreLogin.termsAccepted">I agree to Cloudron <a href="https://cloudron.io/legal/terms.html" target="_blank">terms</a>
</label>
</div>
<br/>
<a class="pull-left" href="" ng-click="appstoreLogin.register = true" ng-hide="appstoreLogin.register">Don't have an Account yet?</a>
<a class="pull-left" href="" ng-click="appstoreLogin.register = false" ng-show="appstoreLogin.register">Already have an Account?</a>
<button type="submit" class="btn btn-success pull-right" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy">
<button type="submit" class="btn btn-success pull-right" ng-disabled="appstoreLoginForm.$invalid || appstoreLogin.busy || (appstoreLogin.register && !appstoreLogin.termsAccepted)">
<i class="fa fa-circle-o-notch fa-spin" ng-show="appstoreLogin.busy"></i> <span ng-hide="appstoreLogin.register">Login</span><span ng-show="appstoreLogin.register">Sign up</span>
</button>
</form>

View File

@@ -118,7 +118,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
var manifest = app.manifest;
$scope.appInstall.optionalSso = !!manifest.optionalSso;
$scope.appInstall.customAuth = !(manifest.addons['simpleauth'] || manifest.addons['ldap'] || manifest.addons['oauth']);
$scope.appInstall.customAuth = !(manifest.addons['ldap'] || manifest.addons['oauth']);
$scope.appInstall.accessRestrictionOption = 'any';
// set default ports
@@ -156,6 +156,9 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
sso: !$scope.appInstall.optionalSso ? undefined : ($scope.appInstall.accessRestrictionOption !== 'nosso')
};
// add sso property for the postInstall message to be shown correctly
$scope.appInstall.app.sso = data.sso;
Client.installApp($scope.appInstall.app.id, $scope.appInstall.app.manifest, $scope.appInstall.app.title, data, function (error) {
if (error) {
if (error.statusCode === 409 && (error.message.indexOf('is reserved') !== -1 || error.message.indexOf('is already in use') !== -1)) {
@@ -258,6 +261,7 @@ angular.module('Application').controller('AppStoreController', ['$scope', '$loca
email: '',
password: '',
register: true,
termsAccepted: false,
submit: function () {
$scope.appstoreLogin.error = {};

View File

@@ -12,6 +12,7 @@
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.customDomainId.$invalid }" uib-tooltip="{{ config.provider === 'caas' ? '' : 'Changing the domain is not yet supported' }}">
<label class="control-label" for="customDomainId">Domain name</label>
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" check-tld placeholder="example.com" required autofocus>
<p>&nbsp;<span ng-show="dnsCredentialsForm.customDomainId.$error.invalidSubdomain && dnsCredentials.customDomain !== config.fqdn" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
</div>
<div class="form-group">
@@ -27,19 +28,15 @@
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'route53'">
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret access key</label>
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'route53'">
<br/>
<p>This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</p>
</div>
<!-- DigitalOcean -->
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'digitalocean'">
<label class="control-label" for="dnsCredentialsDigitalOceanToken">DigitalOcean token</label>
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" id="dnsCredentialsDigitalOceanToken" name="digitalOceanToken" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'digitalocean'">
<br/>
<p>This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</p>
</div>
<div class="form-group" ng-class="{ 'has-error': false }" ng-if="config.fqdn !== dnsCredentials.customDomain && !dnsCredentialsForm.customDomainId.$invalid">
<div class="form-group" ng-class="{ 'has-error': false }">
<label class="control-label" for="dnsCredentialsPassword">Provide your password to confirm this action</label>
<input type="password" class="form-control" ng-model="dnsCredentials.password" id="dnsCredentialsPassword" name="password" ng-disabled="dnsCredentials.busy" required>
</div>
@@ -48,14 +45,20 @@
</fieldset>
</form>
<!-- Wildcard -->
<p ng-show="dnsCredentials.provider === 'wildcard'">
Setup <i>A</i> records for <b>*.{{ dnsCredentials.domain || 'example.com' }}</b> and <b>{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.
<p ng-show="dnsCredentials.provider === 'route53'">
This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.
</p>
<p ng-show="dnsCredentials.provider === 'digitalocean'">
This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.
</p>
<p ng-show="dnsCredentials.provider === 'wildcard'">
Setup <i>A</i> records for <b>*.{{ dnsCredentials.customDomain || 'example.com' }}</b> and <b>{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP.
</p>
<!-- Manual -->
<p ng-show="dnsCredentials.provider === 'manual'">
Setup an <i>A</i> record for <b>my.{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
Setup an <i>A</i> record for <b>my.{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
</p>
</div>
<div class="modal-footer ">

View File

@@ -184,7 +184,8 @@ angular.module('Application').controller('CertsController', ['$scope', '$locatio
$scope.showChangeDnsCredentials = function () {
dnsCredentialsReset();
$scope.dnsCredentials.customDomain = $scope.config.fqdn;
// clear the input box for non-custom domain
$scope.dnsCredentials.customDomain = $scope.config.isCustomDomain ? $scope.config.fqdn : '';
$scope.dnsCredentials.accessKeyId = $scope.dnsConfig.accessKeyId;
$scope.dnsCredentials.secretAccessKey = $scope.dnsConfig.secretAccessKey;
$scope.dnsCredentials.digitalOceanToken = $scope.dnsConfig.provider === 'digitalocean' ? $scope.dnsConfig.token : '';

View File

@@ -61,16 +61,16 @@
<div class="modal-header">
<h4 class="modal-title">Cloudron Email Server</h4>
</div>
<div class="modal-body" ng-show="dnsConfig.provider === 'noop'">
No DNS provider is setup. Required DNS records will be displayed and have to be manually setup.<br/>
<br/>
Contact us for help on how to configure DNS manually by <a href="mailto:support@cloudron.io">Email</a> or <a href="https://chat.cloudron.io" target="_blank">Chat</a>
<div class="modal-body" ng-show="dnsConfig.provider === 'noop' || dnsConfig.provider === 'manual'">
No DNS provider is setup. Displayed DNS records will have to be setup manually.<br/>
</div>
<div class="modal-body" ng-show="dnsConfig.provider !== 'noop'">
<div class="modal-body" ng-show="dnsConfig.provider === 'route53' || dnsConfig.provider === 'digitalocean'">
The Cloudron will setup Email related DNS records automatically.
If this domain is already configured to handle email with some other provider, it will <b>overwrite</b> those records.
<br/><br/>
Disabling Cloudron Email later will <b>not</b> put the old records back.
<br/>
<br/><br/>
Status of DNS Records will show an error when DNS is propagating (~5 minutes).
<br/>
</div>
<div class="modal-footer">
@@ -258,6 +258,8 @@
<div class="row">
<div class="col-xs-12 text-right">
<a href="{{ config.webServerOrigin }}/console.html#/userprofile" target="_blank">Change payment method</a>
or
<a href="{{ config.webServerOrigin }}/console.html" target="_blank">Cancel this Cloudron</a>
</div>
</div>
<div class="row">
@@ -291,15 +293,12 @@
<div class="col-md-12">
Cloudron has a built-in email server that allows users to send and receive email for your domain.
The <a href="https://cloudron.io/references/usermanual.html#email" target="_blank">User manual</a> has information on how to setup email clients.
<br/>
<br/>
Apps can send email regardless of this setting. Enable this option to allow apps to receive emails.
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12" ng-show="(dnsConfig.provider === 'noop' || dnsConfig.provider === 'manual')">
Set the following DNS records to guarantee email functionality.
<div class="col-md-12" ng-show="(dnsConfig.provider !== 'caas')">
Set the following DNS records to guarantee email delivery.
<div ng-repeat="record in expectedDnsRecordsTypes">
<div class="row" ng-if="mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX')">
@@ -320,6 +319,21 @@
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<h4 class="text-muted">
Outbound SMTP (Port 25) <i ng-class="outboundPort25.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'" aria-hidden="true"></i>
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!outboundPort25.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
</h4>
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">Advanced</a>
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
<div class="panel-body">
<p><b> {{ outboundPort25.value }} </b> </p>
</div>
</div>
</div>
</div>
</div>
</div>
<br/>
@@ -327,6 +341,9 @@
<div class="col-md-12" ng-show="dnsConfig.provider !== 'caas'">
<button ng-class="mailConfig.enabled ? 'btn btn-danger pull-right' : 'btn btn-primary pull-right'" ng-click="email.toggle()" ng-enabled="mailConfig">{{ mailConfig.enabled ? "Disable Email" : "Enable Email" }}</button>
</div>
<div class="col-md-12" ng-show="dnsConfig.provider === 'caas'">
<span class="text-danger text-bold">This feature requires the Cloudron to be on <a href="https://cloudron.io/references/usermanual.html#entire-cloudron-on-a-custom-domain" target="_blank">custom domain</a>.</span>
</div>
</div>
</div>

View File

@@ -8,6 +8,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.config = Client.getConfig();
$scope.backupConfig = {};
$scope.dnsConfig = {};
$scope.outboundPort25 = {};
$scope.expectedDnsRecords = {};
$scope.expectedDnsRecordsTypes = [
{ name: 'MX', value: 'mx' },
@@ -147,7 +148,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.createBackup.percent = data.backup.percent;
$scope.createBackup.message = data.backup.message;
window.setTimeout(checkIfDone, 250);
window.setTimeout(checkIfDone, 500);
});
}
@@ -509,10 +510,11 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
function showExpectedDnsRecords(callback) {
callback = callback || function (error) { if (error) console.error(error); };
Client.getExpectedDnsRecords(function (error, dnsRecords) {
Client.getEmailStatus(function (error, result) {
if (error) return callback(error);
$scope.expectedDnsRecords = dnsRecords;
$scope.expectedDnsRecords = result.dns;
$scope.outboundPort25 = result.outboundPort25;
callback(null);
});

View File

@@ -11,24 +11,12 @@
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
<h3>Community</h3>
Chat with us live at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
</div>
</div>
</div>
</div>
<br/>
<div class="card card-large">
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
<h3>Docs</h3>
For user manuals and developer related questions, please refer to our <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank"> documentation</a>.
<br/>
<br/>
Cloudron is open source. To report issues, <a href="https://git.cloudron.io/cloudron/box/issues" target="_blank">open a ticket</a>.
<h3>Documentation and Chat</h3>
For user manuals and app development related questions, please refer to our <a href="{{ config.webServerOrigin }}/documentation.html" target="_blank"> documentation</a>.
Cloudron is <a href="https://git.cloudron.io" target="_blank">open source</a> - use the <a href="https://git.cloudron.io/cloudron/box/issues" target="_blank">issue tracker</a>
to report bugs and raise feature requests.
<br/><br/>
For any other questions, chat with us live at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
</div>
</div>
</div>
@@ -67,6 +55,25 @@
</div>
</div>
</div>
<br/>
<div class="card card-large" ng-show="config.provider !== 'caas' && user.admin">
<div class="grid-item-top">
<div class="row animateMeOpacity">
<div class="col-lg-12">
<h3>Remote Support</h3>
Enable this option to allow Cloudron engineers to connect to this server via SSH.
<br/>
<br/>
Do not enable this option before contacting us first at <a href="https://chat.cloudron.io/" target="_blank">chat.cloudron.io</a>.
<br/>
<br/>
<button class="btn" ng-class="{ 'btn-danger': !sshSupportEnabled, 'btn-primary': sshSupportEnabled }" ng-click="toggleSshSupport()">{{ sshSupportEnabled ? 'Disable SSH support access' : 'Enable SSH support access' }}</button>
</div>
</div>
</div>
</div>
</div>
<!-- Offset the footer -->

View File

@@ -2,6 +2,7 @@
angular.module('Application').controller('SupportController', ['$scope', '$location', 'Client', function ($scope, $location, Client) {
$scope.config = Client.getConfig();
$scope.user = Client.getUserInfo();
$scope.feedback = {
error: null,
@@ -12,6 +13,8 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
description: ''
};
$scope.sshSupportEnabled = false;
function resetFeedback() {
$scope.feedback.subject = '';
$scope.feedback.description = '';
@@ -38,5 +41,30 @@ angular.module('Application').controller('SupportController', ['$scope', '$locat
});
};
var CLOUDRON_SUPPORT_PUBLIC_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io';
var CLOUDRON_SUPPORT_PUBLIC_KEY_IDENTIFIER = 'support@cloudron.io';
$scope.toggleSshSupport = function () {
if ($scope.sshSupportEnabled) {
Client.delAuthorizedKey(CLOUDRON_SUPPORT_PUBLIC_KEY_IDENTIFIER, function (error) {
if (error) return console.error(error);
$scope.sshSupportEnabled = false;
});
} else {
Client.addAuthorizedKey(CLOUDRON_SUPPORT_PUBLIC_KEY, function (error) {
if (error) return console.error(error);
$scope.sshSupportEnabled = true;
});
}
};
Client.onReady(function () {
Client.getAuthorizedKeys(function (error, keys) {
if (error) return console.error(error);
$scope.sshSupportEnabled = keys.some(function (k) { return k.key === CLOUDRON_SUPPORT_PUBLIC_KEY; });
});
});
$('.modal-backdrop').remove();
}]);

View File

@@ -4,18 +4,25 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Add API Client</h4>
<h4 class="modal-title">Add OAuth Client</h4>
</div>
<div class="modal-body">
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.name.$dirty && clientAddForm.name.$invalid) || (!clientAddForm.name.$dirty && clientAdd.error.name) }">
<label class="control-label">Name</label>
<label class="control-label">Application name</label>
<div class="control-label" ng-show="(!clientAddForm.name.$dirty && clientAdd.error.name) || (clientAddForm.name.$dirty && clientAddForm.name.$invalid)">
<small ng-show="clientAddForm.name.$error.required">A name is required</small>
<small ng-show="!clientAddForm.name.$dirty && clientAdd.error.name">{{ clientAdd.error.name }}</small>
</div>
<input type="text" class="form-control" ng-model="clientAdd.name" name="name" id="clientAddName" required autofocus>
</div>
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.redirectURI.$dirty && clientAddForm.redirectURI.$invalid) || (!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI) }">
<label class="control-label">Authorization callback URL</label>
<div class="control-label" ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">
<small ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">{{ clientAdd.error.redirectURI }}</small>
</div>
<input type="text" class="form-control" ng-model="clientAdd.redirectURI" name="redirectURI" id="clientAddRedirectURI" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.scope.$dirty && clientAddForm.scope.$invalid) || (!clientAddForm.scope.$dirty && clientAdd.error.scope) }">
<label class="control-label">Scope</label>
<div class="control-label" ng-show="(!clientAddForm.scope.$dirty && clientAdd.error.scope) || (clientAddForm.scope.$dirty && clientAddForm.scope.$invalid)">
@@ -24,19 +31,12 @@
</div>
<input type="text" class="form-control" ng-model="clientAdd.scope" name="scope" id="clientAddScope" placeholder="Specify any number of scope separated by a comma ','" required>
</div>
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.redirectURI.$dirty && clientAddForm.redirectURI.$invalid) || (!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI) }">
<label class="control-label">Redirect URI</label>
<div class="control-label" ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">
<small ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">{{ clientAdd.error.redirectURI }}</small>
</div>
<input type="text" class="form-control" ng-model="clientAdd.redirectURI" name="redirectURI" id="clientAddRedirectURI" placeholder="Only required if OAuth logins are used">
</div>
<input class="hide" type="submit" ng-disabled="clientAddForm.$invalid || clientAdd.busy"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientAdd.busy"></i> Add API Client</button>
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientAdd.busy"></i> Add OAuth Client</button>
</div>
</div>
</div>
@@ -47,7 +47,7 @@
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Remove API Client</h4>
<h4 class="modal-title">Remove OAuth Client</h4>
</div>
<div class="modal-body">
<p>
@@ -57,7 +57,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientRemove.busy"></i> Remove API Client</button>
<button type="button" class="btn btn-danger" ng-click="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientRemove.busy"></i> Remove OAuth Client</button>
</div>
</div>
</div>
@@ -116,10 +116,11 @@
<div class="row">
<div class="col-xs-12">
<p>These tokens can be used to access the <a href="https://cloudron.io/references/api.html" target="_blank">Cloudron API</a>. They have the <b>admin</b> <a href="https://cloudron.io/references/api.html#scopes" target="_blank">scope</a> and do not expire.</p>
<br/>
<h4 class="text-muted">Active Tokens</h4>
<hr/>
<p ng-repeat="token in apiClient.activeTokens">
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
<span ng-click-select>{{ token.accessToken }}</span> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
</p>
</div>
</div>
@@ -130,7 +131,7 @@
<div class="section-header">
<div class="text-left">
<h3>Apps<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New API Client</button></h3>
<h3>OAuth Apps<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New OAuth Client</button></h3>
</div>
</div>
@@ -155,7 +156,7 @@
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse{{client.id}}">Advanced</a>
<div id="collapse{{client.id}}" class="panel-collapse collapse">
<div class="panel-body">
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove API Client" ng-show="client.type === 'external'">Remove API Client</button></h4>
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove OAuth Client" ng-show="client.type === 'external'">Remove OAuth Client</button></h4>
<hr/>
<p>Scope: <b ng-click-select>{{ client.scope }}</b></p>
<p>RedirectURI: <b ng-click-select>{{ client.redirectURI }}</b></p>

View File

@@ -19,7 +19,7 @@ angular.module('Application').controller('TokensController', ['$scope', 'Client'
$scope.clientAdd.error = {};
$scope.clientAdd.name = '';
$scope.clientAdd.scope = '*';
$scope.clientAdd.scope = 'profile';
$scope.clientAdd.redirectURI = '';
$scope.clientAddForm.$setUntouched();

View File

@@ -221,8 +221,8 @@
</div>
<div class="modal-body">
<p>An email has been sent to <b>{{ inviteSent.email }}</b>.</p>
<p>You can also share this invite link directly.</p>
<p ng-click-select>{{ inviteSent.setupLink }}</p>
<p>You can also share this invite link directly:</p>
<p style="overflow: auto; white-space: nowrap;" eng-click-select>{{ inviteSent.setupLink }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>