Compare commits

..

278 Commits

Author SHA1 Message Date
Johannes Zellner a50409bdca Also show errors above input fields for password reset 2017-03-20 16:50:31 +01:00
Johannes Zellner 60a722e6cc Remove superflous quote in html 2017-03-20 16:43:36 +01:00
Johannes Zellner 4d6cafa589 Show form errors on the top during user activation 2017-03-20 15:57:02 +01:00
Johannes Zellner 63e557430b ng-href takes a template string 2017-03-20 15:26:31 +01:00
Johannes Zellner 04acb4423d Add open registration rest api tests 2017-03-20 14:27:47 +01:00
Johannes Zellner ea813acf4c Add open registration default value and test 2017-03-20 14:27:39 +01:00
Johannes Zellner b1198dfdbf Add settingsdb tests for open registration 2017-03-20 14:22:11 +01:00
Johannes Zellner 4342de3747 Show error response on signup 2017-03-20 14:19:52 +01:00
Johannes Zellner ef8bc7e7e9 username must be null or non-empty string 2017-03-20 14:01:12 +01:00
Johannes Zellner e18e401f6b Improve signup form 2017-03-20 14:00:56 +01:00
Johannes Zellner ab998c47e8 Show user signup link if registration is open 2017-03-20 13:52:31 +01:00
Johannes Zellner 9fb830b2e1 add section to toggle open registration in settings view 2017-03-20 12:55:48 +01:00
Johannes Zellner 415c3f90a1 Always send an object with properties 2017-03-20 12:53:21 +01:00
Johannes Zellner 60c8ff7fb1 Add open_registration settings routes 2017-03-20 12:31:53 +01:00
Johannes Zellner 037816313c Remove newline 2017-03-20 12:29:15 +01:00
Johannes Zellner 3d285d1ac6 Better signup styling 2017-03-20 12:04:23 +01:00
Johannes Zellner 135338786f Protect user creation if open registration is not allowed 2017-03-20 12:00:58 +01:00
Johannes Zellner 661f1fce31 Angular uses double curly brackets 2017-03-20 11:58:01 +01:00
Johannes Zellner 03664ef784 Add open registration setting 2017-03-20 11:56:58 +01:00
Johannes Zellner d2111ef2b6 Add user signup ui 2017-03-20 11:52:11 +01: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
Girish Ramakrishnan 7035b3c18a Fix issue where redis is unable to write on re-configure
The configure code path now ensures the volume which ends up
changing the ownership of the data directory. This means that the
redis container which is still running cannot write anymore
when it is re-created as part of setupAddons().

Just change ownership of top level directory. The subdirectores
like data/ redis/ are owned by containers which will chown
accordingly.
2017-02-20 13:32:05 -08:00
Girish Ramakrishnan 2108c61d97 Send machine info as part of alive status 2017-02-20 13:13:25 -08:00
Johannes Zellner 2bdbb47286 Fix crash when a user does not yet have an username 2017-02-20 21:59:16 +01:00
Johannes Zellner 333b8970b8 Add rosehosting afiliate link 2017-02-20 14:42:22 +01:00
Girish Ramakrishnan 5673cfe2be bump version 2017-02-19 21:48:33 -08:00
Girish Ramakrishnan 4429239dbc Fix debug 2017-02-19 20:30:35 -08:00
Girish Ramakrishnan b6ab9aa9f5 Add 0.102.0 changes 2017-02-19 17:22:52 -08:00
Girish Ramakrishnan 84bde6327f graphs: Better disk name matching
For some reason, docker devices are collected in collectd stats (despite
us collecting only ext4 and btrfs devices). They have the patter *docker*.

Fixes #222
2017-02-19 15:53:18 -08:00
Girish Ramakrishnan d6f49eb54f Remove _docker addon
this was a highly experimental code path from the past
2017-02-19 13:44:55 -08:00
Johannes Zellner 3c8c5e158b Send cloudron time zone with the backendSettings 2017-02-19 22:47:36 +01:00
Girish Ramakrishnan b3045b796f Configure dialog becomes 'repair' in errored state
Fixes #228
2017-02-18 13:17:46 -08:00
Girish Ramakrishnan c0febacc30 Remove configureLink
postInstallMessage already has this information
2017-02-18 12:52:47 -08:00
Johannes Zellner f8ada91dc5 Add rosehosting provider 2017-02-18 12:01:49 +01:00
Johannes Zellner d0e2ce9a9e Support Linode 1GB instance with cloudron-setup 2017-02-18 12:00:26 +01:00
Girish Ramakrishnan e157608992 doc: add note on memory limit 2017-02-17 13:47:19 -08:00
Girish Ramakrishnan 8dbe0ddaf3 doc: add note about incoming email 2017-02-17 13:01:23 -08:00
Girish Ramakrishnan b0cb18539c doc: add note to keep all ports open 2017-02-17 12:58:08 -08:00
Girish Ramakrishnan 97b6d76694 Explain certificate renewal error a bit more
Fixes #225
2017-02-17 10:41:33 -08:00
Girish Ramakrishnan 9de6c8ee2b doc: expand provider argument 2017-02-17 10:29:40 -08:00
Johannes Zellner cd28b1106b Only create app subvolume if it does not exist
Fixes #227
2017-02-17 15:15:50 +01:00
Johannes Zellner b3a5dafee0 Ensure we download docker images and have an app data volume on configure
Part of #227
2017-02-17 15:00:58 +01:00
Johannes Zellner eb4ab8defd Change cert CN from 'localhost' to 'cloudron'
Apparently localhost is special and triggerd a strange behavior in
firefox. Fixes #224
2017-02-17 14:05:21 +01:00
Girish Ramakrishnan 639744e9cb async.every usage has changed 2017-02-16 20:20:46 -08:00
Girish Ramakrishnan 6a942ab27a Use latest async for Inifinity retry to work 2017-02-16 19:22:07 -08:00
Girish Ramakrishnan 278f1d6d24 use curl with reconnect 2017-02-16 17:32:44 -08:00
Girish Ramakrishnan 563eeca1a9 Use the capture match length 2017-02-16 15:32:46 -08:00
Girish Ramakrishnan 7a9c954646 use ec2 sysinfo backend for lightsail 2017-02-16 14:47:16 -08:00
Johannes Zellner d768c36afb use generic sysinfo backend as fallback 2017-02-16 23:20:15 +01:00
Johannes Zellner 36ae3b267d Use better vultr referral link 2017-02-16 22:52:08 +01:00
Girish Ramakrishnan cd60f394d3 Use 1gb droplet (else docker ooms) 2017-02-16 11:52:25 -08:00
Johannes Zellner 9aba90a6f7 Show error message if subdomain was entered in setupdns view 2017-02-16 20:08:13 +01:00
Girish Ramakrishnan 68a8155f49 skip redis addon incremental update
Part of #223
2017-02-16 10:31:02 -08:00
Girish Ramakrishnan 16695fd4ec remove 0.9.0 2017-02-16 09:25:17 -08:00
Girish Ramakrishnan 9b6c6dc709 doc: base image 0.10.0 2017-02-16 09:20:27 -08:00
Girish Ramakrishnan 7923ed4f0d 0.101.0 changes 2017-02-15 23:31:28 -08:00
Girish Ramakrishnan 0b3d1c855c get the user before updating it
updating a non-existent user ends up creating a new mailbox
2017-02-15 23:19:56 -08:00
Girish Ramakrishnan d8273719d2 more robust detection and injection of SPF record
Fixes #210
2017-02-15 23:03:56 -08:00
Girish Ramakrishnan c6d2c39ff7 add azure 2017-02-15 20:10:33 -08:00
Girish Ramakrishnan 6960afdf0b Add more providers for stats 2017-02-15 15:49:00 -08:00
Girish Ramakrishnan 3a5000ab1d Detect loop support on linode correctly
We don't need any of the loop logic since it seems scaleway
also supports automatically this now
2017-02-15 15:40:19 -08:00
Girish Ramakrishnan 98951bab9e Store data args in file just in case installer fails 2017-02-15 14:27:51 -08:00
Girish Ramakrishnan 96fc3b8612 doc: pass --domain for old upgades to work 2017-02-15 11:59:55 -08:00
Girish Ramakrishnan 2b345b6c2d doc: Add note on <= 0.94.0 upgrades 2017-02-15 11:43:56 -08:00
Girish Ramakrishnan 504662b466 acme: link url is absolute in le-staging
Part of #217
2017-02-15 10:40:05 -08:00
Girish Ramakrishnan f56e6edbe4 use subdomains.waitForDns in mailer 2017-02-15 10:16:26 -08:00
Girish Ramakrishnan 191b84d389 Make value a regexp 2017-02-15 10:16:23 -08:00
Girish Ramakrishnan 8a4350d22e upsert already returns a SubdomainError 2017-02-14 22:29:33 -08:00
Girish Ramakrishnan cc6dae0f9e check if ns is set to DO nameservers 2017-02-14 22:27:27 -08:00
Girish Ramakrishnan 58528450e2 dns: Handle 422 errors in DO backend
Fixes #214
2017-02-14 20:51:44 -08:00
Girish Ramakrishnan ebf3559e60 Assume mailbox already exists 2017-02-14 15:42:38 -08:00
Girish Ramakrishnan 57d20b2b32 Assume mailbox already exists 2017-02-14 15:32:56 -08:00
Girish Ramakrishnan fd27240b26 delete user's old mailbox
we don't really support username change. this is only done for
completness.
2017-02-14 15:16:08 -08:00
Girish Ramakrishnan cad69d335c Fix result offset 2017-02-14 14:37:58 -08:00
Girish Ramakrishnan 1f08cca355 Add some newlines 2017-02-14 14:30:54 -08:00
Girish Ramakrishnan 7f4f525551 dhparams.pem must be part of backup 2017-02-14 14:12:03 -08:00
Girish Ramakrishnan b0037b6141 Update infra images to use latest base image 2017-02-14 12:23:17 -08:00
Girish Ramakrishnan 7956c8f58d use latest mysql 2017-02-14 11:27:48 -08:00
Girish Ramakrishnan 330c9054b4 add/del/update user mailbox as part of transaction 2017-02-14 10:42:32 -08:00
Girish Ramakrishnan d444d8552e add/del group mailbox as part of transaction 2017-02-14 09:54:52 -08:00
Girish Ramakrishnan 595bf583c7 delete mailbox as part of transaction 2017-02-13 15:19:17 -08:00
Girish Ramakrishnan 3386b99a29 fromEmail -> mailboxName 2017-02-13 15:15:07 -08:00
Girish Ramakrishnan 5fd667cdaf manual is not recommended 2017-02-13 11:20:07 -08:00
Johannes Zellner 4217db9e18 Ensure we don't crash if domain is not a string
Fixes #219
2017-02-13 13:21:25 +01:00
Johannes Zellner b4717e2edb Generating the dhparams.pem does not only apply to updates 2017-02-13 10:53:08 +01:00
Johannes Zellner 1d5465f21e Update the ssl ciphers and add dhparams.pem
Fixes #218
2017-02-13 00:28:22 +01:00
Johannes Zellner 2f1998fa67 Fix typo in cloudron-setup 2017-02-12 20:48:36 +01:00
Girish Ramakrishnan a7e998c030 Add new base image 2017-02-10 09:15:47 -08:00
Johannes Zellner 8cc15726ec Prevent the domain input from accepting trailing dots 2017-02-10 11:18:44 +01:00
Johannes Zellner 62e59868b4 Use the whole fontawesome package including fonts
The css file sources fonts relative to itself, so we need to include the
font files in our distribution as well.

Fixes #209
2017-02-10 10:53:32 +01:00
Girish Ramakrishnan a64027f4af Add PTR verification link 2017-02-09 19:17:24 -08:00
Girish Ramakrishnan f5a02930ec Use local bootstrap 3.3.7
We have 2 copies of bootstrap now. Just keeping both to be safe.
2017-02-09 14:37:44 -08:00
Girish Ramakrishnan 530ca20ee2 Use local font-awesome.min.css
Fixes #209
2017-02-09 14:37:44 -08:00
Girish Ramakrishnan f3b84ece3d doc: say what Encryption key is 2017-02-09 13:21:52 -08:00
Girish Ramakrishnan ca2d5957e4 Add canada and london to backup regions
Fixes #208
2017-02-09 12:59:13 -08:00
Girish Ramakrishnan 7837214276 Second Level -> Subdomain of public suffix list 2017-02-09 09:04:24 -08:00
Johannes Zellner 994202ca94 Use angular-tldjs for domain validation in certs view 2017-02-09 17:28:48 +01:00
Johannes Zellner ff7ceb1442 remove domain-validator directive
we will replace with angular-tldjs
2017-02-09 17:27:20 +01:00
Johannes Zellner 56545b7f41 Use check-tld directive for domain validation 2017-02-09 17:25:57 +01:00
Johannes Zellner 586e78dfea Apply manual fixes to angular-tld.js to support our use-case 2017-02-09 17:25:37 +01:00
Johannes Zellner 92ede4c242 Make the tld angular directive available in the main view and setupdns 2017-02-09 16:56:32 +01:00
Johannes Zellner 5ca2c2d564 Add angular-tld and its dependencies 2017-02-09 16:56:15 +01:00
Girish Ramakrishnan 9692aa3c08 Better error handling of unpurchase errors 2017-02-08 18:55:41 -08:00
Girish Ramakrishnan 10ad1028ae 0.100.1 changes 2017-02-08 15:14:14 -08:00
Johannes Zellner 7155856b08 Allocate the mailbox db record for apps in a transaction with appdb.add() 2017-02-08 23:52:14 +01:00
Girish Ramakrishnan 69aa771d44 Fix dkim dns crash
Fixes #207
2017-02-08 14:02:30 -08:00
Girish Ramakrishnan d164b5ae3a docs: add ptr links 2017-02-08 11:04:00 -08:00
Girish Ramakrishnan b34d09f547 ldap: Fix crash if displayName is empty and username is null 2017-02-08 10:13:50 -08:00
Girish Ramakrishnan 9e2850ffad setup: do not restart mysql unnecessarily 2017-02-08 07:53:55 -08:00
Johannes Zellner 480cface63 Do not crash if we did not receive a dmarc txt record 2017-02-08 16:55:08 +01:00
Girish Ramakrishnan 85aba589b8 Add hack to send heartbeat only after a minute of server running 2017-02-07 20:25:30 -08:00
Girish Ramakrishnan e890140aa9 Hold off sending heartbeat until the server is ready 2017-02-07 16:14:51 -08:00
Girish Ramakrishnan 53d56ef3a0 console.error -> debug 2017-02-07 10:48:51 -08:00
Girish Ramakrishnan b91674799b Create/destroy event listeners
mocha loads all the tests in same process. This means that when
we start a new test, the old state still persists. For event
listeners, this means that they get multiple duplicate event handlers.
2017-02-07 10:30:52 -08:00
Girish Ramakrishnan 4bb864e2ac use debug() instead
the tests are spewing out logs like crazy
2017-02-07 09:18:45 -08:00
Johannes Zellner 7db091525e Do not reboot the server on AMI creation
This will anyways happen once a new EC2 instance is created from the ami
and this ensures we do not encounter an SSH disconnect error when
running the cloudron-setup script during image creation
2017-02-07 12:49:28 +01:00
Johannes Zellner 695923ed75 cloudron-setup: support --skip-reboot for image creation 2017-02-07 12:49:28 +01:00
Johannes Zellner 1b43ccca6f Use new db-migrate which allows to specify the db backend
We use only mysql, so updating this means a lot of unused db backends
like sqlite do not need to be built with gyp anymore.
Note that this version is not yet released as stable, but works fine for
us. The outstanding issues are not related to our use-case from what I
can tell.

Fixes #82
2017-02-07 12:49:28 +01:00
Girish Ramakrishnan 96a0bad149 generate dkim keys in tests
move out dkim creation code that the tests require
2017-02-07 01:32:50 -08:00
Girish Ramakrishnan 243ade15e1 tests: restore aws.route53 mock 2017-02-07 01:32:34 -08:00
Girish Ramakrishnan 9d3cf990d1 Fix app test 2017-02-07 00:45:24 -08:00
Girish Ramakrishnan 02bcff2223 set isCustomDomain to true by default
CaaS is an exception and not the norm
2017-02-06 23:58:06 -08:00
Girish Ramakrishnan 8f388c86a6 do not unlink config file in test mode
this was incredibly hard to debug
2017-02-06 22:39:22 -08:00
Girish Ramakrishnan 8dc929f0ff Fix update checker test 2017-02-06 16:33:55 -08:00
Girish Ramakrishnan 509bd7e79b Make settings-test pass 2017-02-06 16:02:30 -08:00
Girish Ramakrishnan 19c665d747 docker daemon is deprecated 2017-02-06 11:33:10 -08:00
Girish Ramakrishnan cb09086ae8 Add dmsetup required by docker 2017-02-06 11:32:08 -08:00
Johannes Zellner fa915d0b23 Check for activated instead of fqdn to redirect to setup screens 2017-02-03 16:28:11 -08:00
Girish Ramakrishnan a383f01406 ami: wait for ssh as cloudron-setup reboots server 2017-02-03 11:26:56 -08:00
Girish Ramakrishnan 1a46e80403 remove .sh extension 2017-02-03 10:50:32 -08:00
Girish Ramakrishnan e8cd230c12 ami: remove ssh keys and fix subnet 2017-02-03 10:32:22 -08:00
Girish Ramakrishnan 0711dc2c5a createAMIImage: Add --region 2017-02-02 23:37:24 -08:00
Girish Ramakrishnan 486e72457d remove minlength for username 2017-02-02 16:09:07 -08:00
Girish Ramakrishnan 450e017bdb 0.100.0 changes 2017-02-02 15:43:16 -08:00
Girish Ramakrishnan c6d9cfc0d7 Passing IP never worked 2017-02-02 15:33:19 -08:00
Girish Ramakrishnan a0b073d881 Show DNS warning for manual/noop only 2017-02-02 14:43:34 -08:00
Girish Ramakrishnan 4dde16f987 Fix wording 2017-02-02 14:28:23 -08:00
Girish Ramakrishnan f7d2e262f4 Always fill spf record 2017-02-02 14:20:25 -08:00
Girish Ramakrishnan 34fedb5835 check spf and dkim all the time 2017-02-02 14:00:13 -08:00
Girish Ramakrishnan ff491be976 Display SPF/DKIM/PTR records for manual and noop backends 2017-02-02 13:58:49 -08:00
Girish Ramakrishnan 7635482191 Query via unbound
The timeout applies to each server. This resulted in us frequently
getting timeout out when we have more than one name server.
2017-02-02 13:58:49 -08:00
Girish Ramakrishnan b23001e43f better error log 2017-02-02 13:58:49 -08:00
Girish Ramakrishnan 06c8e8f0cb Query dns in parallel 2017-02-02 13:58:49 -08:00
Girish Ramakrishnan ce2cd00fbf Fix crash when no MX record is found 2017-02-02 13:58:49 -08:00
Johannes Zellner 651af185c8 Restart the droplet after installation for good measure
This is then in sync with selfhosting and ensures the init startup
sequence is in order, since the setup on its own creates unit files
which should run prior to services already running.

This hopefully fixes the mysql disconnect issues.
2017-02-02 11:24:49 -08:00
Girish Ramakrishnan 6951383ae0 Remove text-danger class in setupdns 2017-02-02 11:07:26 -08:00
Girish Ramakrishnan 37596e89b4 Fix display of info text in dns dialog 2017-02-02 11:05:56 -08:00
Girish Ramakrishnan 711fe37dad Add note on dnsRecordId 2017-02-02 10:43:04 -08:00
Girish Ramakrishnan 7fee3d0da0 Do not rely on dnsRecordId 2017-02-02 10:40:10 -08:00
Girish Ramakrishnan 45a61e9541 refreshDNS must always overwrite dns entries
dnsRecordId is mostly obsolete
2017-02-02 10:32:25 -08:00
Girish Ramakrishnan bd0be2affc No username implies no aliases 2017-02-02 01:22:44 -08:00
Girish Ramakrishnan 7812c0e5c2 Fix username and groupname min length 2017-02-02 01:16:32 -08:00
Girish Ramakrishnan 7efb6d60bc Allow usernames and groupnames of length 1
Fixes #204
2017-02-02 01:02:51 -08:00
Girish Ramakrishnan cd31e12bec Do not includeSubdomains in HSTS
This prevents one from redirecting to some http-only subdomain.
For example, surfer in naked domain redirects to www subdomain
(which is on github pages...)
2017-02-02 00:05:56 -08:00
Girish Ramakrishnan 87755c6097 ignore errors and continue 2017-02-01 23:38:06 -08:00
Girish Ramakrishnan 73f56efe2c Fix typo 2017-02-01 23:25:13 -08:00
Girish Ramakrishnan 20eaa60a97 clear timeout 2017-02-01 23:20:11 -08:00
Girish Ramakrishnan b80f0082e9 subdomain -> domain 2017-02-01 23:06:41 -08:00
Girish Ramakrishnan 1ff800a842 Display mx, dmarc, ptr status
Fixes #169
2017-02-01 22:59:06 -08:00
Girish Ramakrishnan 5b0abb1b17 re-setup DNS when the dns config changes 2017-02-01 14:34:57 -08:00
Girish Ramakrishnan 178aa4794a cloudron-setup: Add source-url 2017-02-01 14:11:50 -08:00
Girish Ramakrishnan 76583cb2fa createReleaseTarball: remove no-upload argument
We will put the upload in the release script
2017-02-01 13:41:27 -08:00
Girish Ramakrishnan aa484dc5b4 Add 0.99.1 changelog 2017-01-31 18:09:07 -08:00
188 changed files with 11230 additions and 4843 deletions
+69 -1
View File
@@ -736,4 +736,72 @@
* Cleanup graphs UI
* Polish webadmin UI
* Fix bug where hard disk size was detected incorrectly
* Use overlay2 as docker storage backend for scaleway
[0.99.1]
* Fix bug with duplicate nginx configs
[0.100.0]
* Improve DNS notifications for email
* Do not enable HSTS for subdomains
[0.100.1]
* Fix crash when fetching mail records
* Fix crash in LDAP server when username and displayName are empty
[0.101.0]
* New base image 0.10.0
* Better error handling of unpurchase errors
* Validate that cloudron domain name is a subdomain of public suffic list
* Add canada and london to S3 backup regions
* Bundle Font Awesome as part of webadmin
* Fix crash in custom certiicate validation
* Get A+ rating in SSL Check
* More robust detection and injection of SPF record
* Add azure, lightsail, linode, ovh, vultr to provider list
[0.102.0]
* Fix issue where SPF record check was only done 5 times (updated 'async')
* Make auto-generated self-signed cert load quickly on Firefox
* 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
+51 -23
View File
@@ -10,9 +10,7 @@ readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
IMAGE_ID="ami-5aee2235" # ubuntu 16.04 eu-central-1
INSTANCE_TYPE="t2.micro"
SECURITY_GROUP="sg-19f5a770" # everything open on eu-central-1
BLOCK_DEVICE="DeviceName=/dev/sda1,Ebs={VolumeSize=20,DeleteOnTermination=true,VolumeType=gp2}"
SSH_KEY_NAME="id_rsa_yellowtent"
@@ -22,8 +20,9 @@ server_id=""
server_ip=""
destroy_server="yes"
deploy_env="prod"
image_id=""
args=$(getopt -o "" -l "revision:,name:,no-destroy,env:" -n "$0" -- "$@")
args=$(getopt -o "" -l "revision:,name:,no-destroy,env:,region:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -32,19 +31,35 @@ while true; do
--revision) revision="$2"; shift 2;;
--name) ami_name="$2"; shift 2;;
--no-destroy) destroy_server="no"; shift 2;;
--region)
case "$2" in
"us-east-1")
image_id="ami-6edd3078"
security_group="sg-a5e17fd9"
subnet_id="subnet-b8fbc0f1"
;;
"eu-central-1")
image_id="ami-5aee2235"
security_group="sg-19f5a770" # everything open on eu-central-1
subnet_id=""
;;
*)
echo "Unknown aws region $2"
exit 1
;;
esac
export AWS_DEFAULT_REGION="$2" # used by the aws cli tool
shift 2
;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
done
export AWS_DEFAULT_REGION="eu-central-1" # we have to use us-east-1 to publish
# TODO fix this
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY}"
export AWS_SECRET_ACCESS_KEY="${AWS_ACCESS_SECRET}"
echo "=> Creating AMI"
readonly ssh_keys="${HOME}/.ssh/id_rsa_yellowtent"
readonly SSH="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
@@ -53,6 +68,11 @@ if [[ ! -f "${ssh_keys}" ]]; then
exit 1
fi
if [[ -z "${image_id}" ]]; then
echo "--region is required (us-east-1 or eu-central-1)"
exit 1
fi
function get_pretty_revision() {
local git_rev="$1"
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
@@ -60,21 +80,34 @@ function get_pretty_revision() {
echo "${sha1}"
}
function wait_for_ssh() {
echo "=> Waiting for ssh connection"
while true; do
echo -n "."
if $SSH ubuntu@${server_ip} echo "hello"; then
echo ""
break
fi
sleep 5
done
}
now=$(date "+%Y-%m-%d-%H%M%S")
pretty_revision=$(get_pretty_revision "${revision}")
if [[ -z "${ami_name}" ]]; then
# if you change this, change the regexp is appstore/janitor.js
ami_name="box-${deploy_env}-${pretty_revision}-${now}" # remove slashes
ami_name="box-${deploy_env}-${pretty_revision}-${now}"
fi
echo "=> Create EC2 instance"
id=$(aws ec2 run-instances --image-id "${IMAGE_ID}" --instance-type "${INSTANCE_TYPE}" --security-group-ids "${SECURITY_GROUP}" --block-device-mappings "${BLOCK_DEVICE}" --key-name "${SSH_KEY_NAME}"\
id=$(aws ec2 run-instances --image-id "${image_id}" --instance-type "${INSTANCE_TYPE}" --security-group-ids "${security_group}" --block-device-mappings "${BLOCK_DEVICE}" --key-name "${SSH_KEY_NAME}" --subnet-id "${subnet_id}" --associate-public-ip-address \
| $JSON Instances \
| $JSON 0.InstanceId)
[[ -z "$id" ]] && exit 1
echo "Instance created with ID $id"
echo "Instance created ID $id"
echo "=> Waiting for instance to get a public IP"
while true; do
@@ -93,17 +126,7 @@ done
echo "Got public IP ${server_ip}"
echo "=> Waiting for ssh connection"
while true; do
echo -n "."
if $SSH ubuntu@${server_ip} echo "hello"; then
echo ""
break
fi
sleep 5
done
wait_for_ssh
echo "=> Fetching cloudron-setup"
while true; do
@@ -118,7 +141,12 @@ while true; do
done
echo "=> Running cloudron-setup"
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ec2"
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ami" --skip-reboot
wait_for_ssh
echo "=> Removing ssh key"
$SSH ubuntu@${server_ip} sudo rm /home/ubuntu/.ssh/authorized_keys /root/.ssh/authorized_keys
echo "=> Creating AMI"
image_id=$(aws ec2 create-image --instance-id "${id}" --name "${ami_name}" | $JSON ImageId)
+1 -1
View File
@@ -31,7 +31,7 @@ function create_droplet() {
local image_region="sfo1"
local ubuntu_image_slug="ubuntu-16-04-x64"
local box_size="512mb"
local box_size="1gb"
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
+34 -2
View File
@@ -30,6 +30,7 @@ apt-get -y install \
build-essential \
cron \
curl \
dmsetup \
iptables \
logrotate \
mysql-server-5.7 \
@@ -51,13 +52,44 @@ 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
# create systemd drop-in file
mkdir -p /etc/systemd/system/docker.service.d
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper" > /etc/systemd/system/docker.service.d/cloudron.conf
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper" > /etc/systemd/system/docker.service.d/cloudron.conf
apt-get -y --allow-downgrades install docker-engine=1.12.5-0~ubuntu-xenial # apt-cache madison docker-engine
apt-mark hold docker-engine # do not update docker
+1 -5
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);
});
-64
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"
```
+46 -8
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: [ ]
@@ -449,7 +454,7 @@ POST `/api/v1/apps/:appId/configure` <scope>admin</scope>
Re-configures an existing app with id `appId`.
Configuring an app won't preserve existing data. Cloudron apps are written in a way to support reconfiguring
Configuring an app preserves existing data. Cloudron apps are written in a way to support reconfiguring
any of the parameters listed below without loss of data.
Request:
@@ -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.
@@ -1116,7 +1154,7 @@ POST `/api/v1/settings/mail_config` <scope>admin</scope> <scope>internal</scope>
Sets the email configuration. The Cloudron has a built-in email server for users.
This configuration can be used to enable or disable the email server. Note that
the Cloudron will always be able to send email on behalf of apps, regardless of
the Cloudron will always be able to send email on behalf of apps, regardless of
this setting.
Request:
-1
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
+8 -8
View File
@@ -1,6 +1,6 @@
# Overview
The application's Dockerfile must specify the FROM base image to be `cloudron/base:0.9.0`.
The application's Dockerfile must specify the FROM base image to be `cloudron/base:0.10.0`.
The base image already contains most popular software packages including node, nginx, apache,
ruby, PHP. Using the base image greatly reduces the size of app images.
@@ -17,16 +17,16 @@ install it yourself.
* Apache 2.4.18
* Composer 1.2.0
* Go 1.5.4, 1.6.3
* Go 1.6.4, 1.7.5 (install under `/usr/local/go-<version>`)
* Gunicorn 19.4.5
* Java 1.8
* Maven 3.3.9
* Mongo 2.6.10
* MySQL Client 5.7.13
* MySQL Client 5.7.17
* nginx 1.10.0
* Node 0.10.40, 0.12.7, 4.2.6, 4.4.7 (installed under `/usr/local/node-<version>`) [more information](#node-js)
* Node 0.10.48, 0.12.18, 4.7.3, 6.9.5 (installed under `/usr/local/node-<version>`) [more information](#node-js)
* Perl 5.22.1
* PHP 7.0.8
* PHP 7.0.13
* Postgresql client 9.5.4
* Python 2.7.12
* Redis 3.0.6
@@ -41,16 +41,16 @@ The base image can be inspected by installing [Docker](https://docs.docker.com/i
Once installed, pull down the base image locally using the following command:
```
docker pull cloudron/base:0.9.0
docker pull cloudron/base:0.10.0
```
To inspect the base image:
```
docker run -ti cloudron/base:0.9.0 /bin/bash
docker run -ti cloudron/base:0.10.0 /bin/bash
```
*Note:* Please use `docker 1.9.0` or above to pull the base image. Doing otherwise results in a base
image with an incorrect image id. The image id of `cloudron/base:0.9.0` is `d038af182821`.
image with an incorrect image id. The image id of `cloudron/base:0.10.0` is `5ec8ca8525be`.
# The `cloudron` user
+6 -20
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)
@@ -93,26 +95,6 @@ Example:
"changelog": "* Add support for IE8 \n* New logo"
```
## configurePath
Type: path string
Required: no
The `configurePath` can be used to specify the absolute path to the configuration / settings
page of the app. When this path is present, an absoluted URL is constructed from the app's
install location this path and presented to the user in the configuration dialog of the app.
This is useful for apps that have a main page which does not display a configuration / settings
url (i.e) it's hidden for aesthetic reasons. For example, a blogging app like wordpress might
keep the admin page url hidden in the main page. Setting the configurationPath makes the
configuration url discoverable by the user.
Example:
```
"configurePath": "/wp-admin"
```
## contactEmail
Type: email
@@ -298,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
+40 -14
View File
@@ -19,7 +19,7 @@ The Cloudron requires a domain name when it is installed. Apps are installed int
The `my` subdomain is special and is the location of the Cloudron web interface. For this to
work, the Cloudron requires a way to programmatically configure the DNS entries of the domain.
Note that the Cloudron will never overwrite _existing_ DNS entries and refuse to install
apps on existing subdomains.
apps on existing subdomains (so, it is safe to reuse an existing domain that runs other services).
# Cloud Server
@@ -34,9 +34,10 @@ In addition to those, the Cloudron community has successfully installed the plat
* [hosttech](https://www.hosttech.ch/?promocode=53619290)
* [Linode](https://www.linode.com/?r=f68d816692c49141e91dd4cef3305da457ac0f75)
* [OVH](https://www.ovh.com/)
* [Rosehosting](https://secure.rosehosting.com/clientarea/?affid=661)
* [Scaleway](https://www.scaleway.com/)
* [So you Start](https://www.soyoustart.com/)
* [Vultr](http://www.vultr.com/?ref=7063201)
* [Vultr](http://www.vultr.com/?ref=7110116-3B)
Please let us know if any of them requires tweaks or adjustments.
@@ -44,8 +45,13 @@ 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 and leave the traffic management to the Cloudron.
### Linode
@@ -64,7 +70,7 @@ SSH into your server and run the following commands:
```
wget https://cloudron.io/cloudron-setup
chmod +x cloudron-setup
./cloudron-setup --provider <digitalocean|ec2|generic|scaleway>
./cloudron-setup --provider <azure|digitalocean|ec2|lightsail|linode|ovh|rosehosting|scaleway|vultr|generic>
```
The setup will take around 10-15 minutes.
@@ -99,9 +105,9 @@ IP address (`https://ip`) to complete the installation.
The setup website will show a certificate warning. Accept the self-signed certificate
and proceed to the domain setup.
Currently, only Second Level Domains are supported. For example, `example.com`,
`example.co.uk` will work fine. Choosing a domain name at any other level like
`cloudron.example.com` will not work.
Currently, only subdomains of the [Public Suffix List](https://publicsuffix.org/) are supported.
For example, `example.com`, `example.co.uk` will work fine. Choosing other non-registrable
domain names like `cloudron.example.com` will not work.
### Route 53
@@ -194,6 +200,9 @@ for most use-cases.
}
```
The `Encryption key` is an arbitrary passphrase used to encrypt the backups. Keep the passphrase safe; it is
required to decrypt the backups when restoring the Cloudron.
## Minio S3
[Minio](https://minio.io/) is a distributed object storage server, providing the same API as Amazon S3.
@@ -219,6 +228,8 @@ The information to be copied to the Cloudron's backup settings form may look sim
<img src="/docs/img/minio_backup_config.png" class="shadow"><br/>
The `Encryption key` is an arbitrary passphrase used to encrypt the backups. Keep the passphrase safe; it is
required to decrypt the backups when restoring the Cloudron.
# Email
@@ -235,15 +246,27 @@ reputation should be easy to get back.
## Checklist
* Once your Cloudron is ready, setup a Reverse DNS PTR record to be setup for the `my` subdomain.
* If you are unable to receive mail, first thing to check is if your VPS provider lets you
receive mail on port 25.
* AWS/EC2 - Fill the PTR [request form](https://aws-portal.amazon.com/gp/aws/html-forms-controller/contactus/ec2-email-limit-rdns-request.
* Digital Ocean - New accounts frequently have port 25 blocked. Write to their support to
unblock your server.
* EC2, Lightsail & Scaleway - Edit your security group to allow email.
* Setup a Reverse DNS PTR record to be setup for the `my` subdomain.
**Note:** PTR records are a feature of your VPS provider and not your domain provider.
* You can verify the PTR record [https://mxtoolbox.com/ReverseLookup.aspx](here).
* AWS EC2 & Lightsail - Fill the [PTR request form](https://aws-portal.amazon.com/gp/aws/html-forms-controller/contactus/ec2-email-limit-rdns-request).
* Digital Ocean - Digital Ocean sets up a PTR record based on the droplet's name. So, simply rename
your droplet to `my.<domain>`. Note that some new Digital Ocean accounts have [port 25 blocked](https://www.digitalocean.com/community/questions/port-25-smtp-external-access).
* Scaleway - Edit your security group to allow email. You can also set a PTR record on the interface with your
`my.<domain>`.
* Linode - Follow this [guide](https://www.linode.com/docs/networking/dns/setting-reverse-dns).
* 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.
@@ -324,9 +347,12 @@ Similar to the initial installation, a Cloudron upgrade looks like:
$ ssh root@newserverip
> wget https://cloudron.io/cloudron-setup
> chmod +x cloudron-setup
> ./cloudron-setup --provider <digitalocean|ec2|generic|scaleway> --encryption-key <key> --restore-url <publicS3Url>
> ./cloudron-setup --provider <digitalocean|ec2|generic|scaleway> --domain <example.com> --encryption-key <key> --restore-url <publicS3Url>
```
Note: When upgrading an old version of Cloudron (<= 0.94.0), pass the `--version 0.94.1` flag and then continue updating
from that.
* Finally, once you see the newest version being displayed in your Cloudron webinterface, you can safely delete the old server instance.
# Restore
@@ -342,7 +368,7 @@ To restore a Cloudron from a specific backup:
* `File system` - When storing backups in `/var/backups`, you have to make the box and the app backups available to the new Cloudron instance's `/var/backups`. This can be achieved in a variety of ways depending on the situation: like scp'ing the backup files to the new machine before Cloudron installation OR mounting an external backup hard drive into the new Cloudron's `/var/backup` OR downloading a copy of the backup using `cloudron machine backup download` and uploading them to the new machine. After doing so, pass `file:///var/backups/<path to box backup>` as the `--restore-url` below.
* Create a new Cloudron by following the [installing](/references/selfhosting.html#installing) section.
When running the setup script, pass in the `version`, `encryption-key` and `restore-url` flags.
When running the setup script, pass in the `version`, `encryption-key`, `domain` and `restore-url` flags.
The `version` field is the version of the Cloudron that the backup corresponds to (it is embedded
in the backup file name).
+3 -3
View File
@@ -75,7 +75,7 @@ A Dockerfile contains commands to assemble an image.
Create a file named `tutorial/Dockerfile` with the following content:
```dockerfile
FROM cloudron/base:0.9.0
FROM cloudron/base:0.10.0
ADD server.js /app/code/server.js
@@ -171,7 +171,7 @@ Login successful.
Build scheduled with id 76cebfdd-7822-4f3d-af17-b3eb393ae604
Downloading source
Building
Step 0 : FROM cloudron/base:0.9.0
Step 0 : FROM cloudron/base:0.10.0
---> 97583855cc0c
Step 1 : ADD server.js /app/code
---> b09b97ecdfbc
@@ -333,7 +333,7 @@ and modify our Dockerfile to look like this:
File `tutorial/Dockerfile`
```dockerfile
FROM cloudron/base:0.9.0
FROM cloudron/base:0.10.0
ADD server.js /app/code/server.js
ADD package.json /app/code/package.json
+7 -5
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
@@ -79,7 +79,7 @@ console.log("Server running at port 8000");
The Dockerfile contains instructions on how to create an image for your application.
```Dockerfile
FROM cloudron/base:0.9.0
FROM cloudron/base:0.10.0
ADD server.js /app/code/server.js
@@ -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.
@@ -188,7 +188,7 @@ Build scheduled with id e7706847-f2e3-4ba2-9638-3f334a9453a5
Waiting for build to begin, this may take a bit...
Downloading source
Building
Step 1 : FROM cloudron/base:0.9.0
Step 1 : FROM cloudron/base:0.10.0
---> be9fc6312b2d
Step 2 : ADD server.js /app/code/server.js
---> 10513e428d7a
@@ -389,6 +389,8 @@ field in the manifest.
Design your application runtime for concurrent use by 50 users. The Cloudron is not designed for
concurrent access by 100s or 1000s of users.
An app can determine it's memory limit by reading `/sys/fs/cgroup/memory/memory.limit_in_bytes`.
## Authentication
Apps should integrate with one of the [authentication strategies](/references/authentication.html).
+2 -2
View File
@@ -1,5 +1,5 @@
var dbm = require('db-migrate');
var type = dbm.dataType;
'use strict';
var url = require('url');
exports.up = function(db, callback) {
+1 -2
View File
@@ -1,5 +1,4 @@
var dbm = require('db-migrate');
var type = dbm.dataType;
'use strict';
var fs = require('fs'),
async = require('async'),
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN resetToken VARCHAR(128) DEFAULT ""', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('DELETE FROM tokens', [], function (error) {
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE authcodes ADD COLUMN expiresAt BIGINT NOT NULL', function (error) {
@@ -13,4 +12,4 @@ exports.down = function(db, callback) {
if (error) console.error(error);
callback(error);
});
};
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE appPortBindings ADD COLUMN environmentVariable VARCHAR(128) NOT NULL', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE appPortBindings DROP COLUMN containerPort', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('DELETE FROM tokens', [], function (error) {
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN version', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN healthy, ADD COLUMN health VARCHAR(128)', [], function (error) {
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN lastBackupId VARCHAR(128)', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN createdAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
// everyday at 1am
@@ -8,5 +7,4 @@ exports.up = function(db, callback) {
exports.down = function(db, callback) {
db.runSql('DELETE * FROM settings WHERE name="autoupdate_pattern"', [ ], callback);
}
};
@@ -1,6 +1,6 @@
dbm = dbm || require('db-migrate');
'use strict';
var safe = require('safetydance');
var type = dbm.dataType;
exports.up = function(db, callback) {
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');
@@ -12,4 +12,3 @@ exports.up = function(db, callback) {
exports.down = function(db, callback) {
db.runSql('DELETE * FROM settings WHERE name="time_zone"', [ ], callback);
};
@@ -1,5 +1,5 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
var async = require('async');
exports.up = function(db, callback) {
@@ -1,5 +1,5 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
var async = require('async');
exports.up = function(db, callback) {
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN lastManifestJson VARCHAR(2048)', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE lastManifestJson lastBackupConfigJson VARCHAR(2048)', [], function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN oldConfigJson VARCHAR(2048)', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('DELETE FROM settings', [ ], callback);
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN oauthProxy BOOLEAN DEFAULT 0', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,5 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
var async = require('async');
exports.up = function(db, callback) {
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps CHANGE accessRestriction accessRestrictionJson VARCHAR(2048)', [], function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY manifestJson TEXT', [], function (error) {
@@ -1,5 +1,5 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
var async = require('async');
exports.up = function(db, callback) {
@@ -1,4 +1,4 @@
dbm = dbm || require('db-migrate');
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN displayName VARCHAR(512) DEFAULT ""', function (error) {
@@ -1,4 +1,4 @@
dbm = dbm || require('db-migrate');
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN memoryLimit BIGINT DEFAULT 0', function (error) {
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE groups(" +
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
@@ -1,6 +1,5 @@
'use strict';
var dbm = global.dbm || require('db-migrate');
var async = require('async');
var ADMIN_GROUP_ID = 'admin'; // see groups.js
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE backups(" +
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups ADD COLUMN configJson TEXT', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
var dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups DROP COLUMN configJson', function (error) {
@@ -14,4 +13,3 @@ exports.down = function(db, callback) {
callback(error);
});
};
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE backups CHANGE filename id VARCHAR(128)', [], function (error) {
@@ -1,4 +1,4 @@
dbm = dbm || require('db-migrate');
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users MODIFY username VARCHAR(254) UNIQUE', [], function (error) {
@@ -1,7 +1,5 @@
'use strict';
var dbm = dbm || require('db-migrate');
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', function (error) {
if (error) console.error(error);
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
var cmd = "CREATE TABLE eventlog(" +
@@ -1,4 +1,4 @@
dbm = dbm || require('db-migrate');
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users ADD COLUMN showTutorial BOOLEAN DEFAULT 0', function (error) {
@@ -1,8 +1,5 @@
'use strict';
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
exports.up = function(db, callback) {
var cmd = 'CREATE TABLE mailboxes(' +
'name VARCHAR(128) NOT NULL,' +
@@ -1,5 +1,6 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
var async = require('async');
// imports mailbox entries for existing users
exports.up = function(db, callback) {
@@ -1,5 +1,4 @@
dbm = dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN lastBackupConfigJson', function (error) {
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps MODIFY installationProgress TEXT', [], function (error) {
@@ -1,4 +1,4 @@
dbm = dbm || require('db-migrate');
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN xFrameOptions VARCHAR(512)', function (error) {
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.all('SELECT id FROM users', function (error, results) {
@@ -14,4 +13,3 @@ exports.up = function(db, callback) {
exports.down = function(db, callback) {
db.runSql('DELETE * FROM settings WHERE name="mail_config"', [ ], callback);
};
@@ -1,6 +1,6 @@
'use strict';
var dbm = dbm || require('db-migrate');
var async = require('async');
exports.up = function(db, callback) {
async.series([
@@ -71,4 +71,3 @@ exports.down = function(db, callback) {
});
});
};
+1 -2
View File
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN sso BOOLEAN DEFAULT 1', function (error) {
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps DROP COLUMN oauthProxy', function (error) {
@@ -1,5 +1,4 @@
var dbm = global.dbm || require('db-migrate');
var type = dbm.dataType;
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE users DROP COLUMN showTutorial', function (error) {
@@ -1,4 +1,4 @@
dbm = dbm || require('db-migrate');
'use strict';
exports.up = function(db, callback) {
db.runSql('ALTER TABLE apps ADD COLUMN debugModeJson TEXT', function (error) {
@@ -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);
});
};
+2 -2
View File
@@ -60,7 +60,7 @@ CREATE TABLE IF NOT EXISTS apps(
manifestJson TEXT,
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
location VARCHAR(128) NOT NULL UNIQUE,
dnsRecordId VARCHAR(512),
dnsRecordId VARCHAR(512), // tracks any id that we got back to track dns updates (unused)
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
memoryLimit BIGINT DEFAULT 0,
@@ -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));
+725 -1630
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -13,11 +13,11 @@
"node >=4.0.0 <=4.1.1"
],
"dependencies": {
"async": "^1.2.1",
"async": "^2.1.4",
"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",
@@ -25,7 +25,8 @@
"cookie-session": "^1.1.0",
"cron": "^1.0.9",
"csurf": "^1.6.6",
"db-migrate": "^0.9.2",
"db-migrate": "^0.10.0-beta.20",
"db-migrate-mysql": "^1.1.10",
"debug": "^2.2.0",
"dockerode": "^2.2.10",
"ejs": "^2.2.4",
@@ -66,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",
+46 -29
View File
@@ -14,13 +14,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="990" # this is mostly reported for 1GB main memory (DO 992, EC2 990)
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_bytes=$(LC_ALL=C fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }')
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024/1024))
# verify the system has minimum requirements met
@@ -46,9 +49,11 @@ versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
requestedVersion="latest"
apiServerOrigin="https://api.cloudron.io"
dataJson=""
prerelease=false
prerelease="false"
sourceTarballUrl=""
rebootServer="true"
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,provider:,encryption-key:,restore-url:,tls-provider:,version:,versions-url:,api-server:,dns-provider:,env:,prerelease" -n "$0" -- "$@")
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,provider:,encryption-key:,restore-url:,tls-provider:,version:,versions-url:,api-server:,dns-provider:,env:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -77,8 +82,10 @@ while true; do
--versions-url) versionsUrl="$2"; shift 2;;
--api-server) apiServerOrigin="$2"; shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--data) dataJson="$2"; shift 2;;
--prerelease) prerelease="true"; shift;;
--source-url) sourceTarballUrl="$2"; version="0.0.1+custom"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
@@ -87,15 +94,22 @@ done
# validate arguments in the absence of data
if [[ -z "${dataJson}" ]]; then
if [[ -z "${provider}" ]]; then
echo "--provider is required (generic, scaleway, ec2, digitalocean)"
echo "--provider is required (azure, digitalocean, ec2, lightsail, linode, ovh, scaleway, vultr or generic)"
exit 1
elif [[ \
"${provider}" != "generic" && \
"${provider}" != "scaleway" && \
"${provider}" != "ami" && \
"${provider}" != "azure" && \
"${provider}" != "digitalocean" && \
"${provider}" != "ec2" && \
"${provider}" != "digitalocean" \
"${provider}" != "lightsail" && \
"${provider}" != "linode" && \
"${provider}" != "ovh" && \
"${provider}" != "rosehosting" && \
"${provider}" != "scaleway" && \
"${provider}" != "vultr" && \
"${provider}" != "generic" \
]]; then
echo "--provider must be one of: generic, scaleway, ec2, digitalocean"
echo "--provider must be one of: azure, digitalocean, ec2, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
exit 1
fi
@@ -125,7 +139,7 @@ echo " Join us at https://chat.cloudron.io for any questions."
echo ""
if [[ "${initBaseImage}" == "true" ]]; then
echo "=> Updating apt and installing script dependancies"
echo "=> Updating apt and installing script dependencies"
if ! apt-get update &>> "${LOG_FILE}"; then
echo "Could not update package repositories"
exit 1
@@ -138,16 +152,18 @@ if [[ "${initBaseImage}" == "true" ]]; then
fi
echo "=> Checking version"
releaseJson=$(curl -s "${versionsUrl}")
if [[ "$requestedVersion" == "latest" ]]; then
pre=$([[ "${prerelease}" == "true" ]] && echo "null" || echo "-pre")
version=$(echo "${releaseJson}" | python3 -c "import json,sys,collections;obj=json.load(sys.stdin, object_pairs_hook=collections.OrderedDict);latest=list(v for v in obj if '${pre}' not in v)[-1];print(latest)")
else
version="${requestedVersion}"
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj[sys.argv[1]]["sourceTarballUrl"])' "${version}"); then
echo "No source code for version ${requestedVersion}"
exit 1
if [[ "${sourceTarballUrl}" == "" ]]; then
releaseJson=$($curl -s "${versionsUrl}")
if [[ "$requestedVersion" == "latest" ]]; then
pre=$([[ "${prerelease}" == "true" ]] && echo "null" || echo "-pre")
version=$(echo "${releaseJson}" | python3 -c "import json,sys,collections;obj=json.load(sys.stdin, object_pairs_hook=collections.OrderedDict);latest=list(v for v in obj if '${pre}' not in v)[-1];print(latest)")
else
version="${requestedVersion}"
fi
if ! sourceTarballUrl=$(echo "${releaseJson}" | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj[sys.argv[1]]["sourceTarballUrl"])' "${version}"); then
echo "No source code for version ${requestedVersion}"
exit 1
fi
fi
# Build data
@@ -200,7 +216,7 @@ fi
echo "=> Downloading version ${version} ..."
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
if ! curl -sL "${sourceTarballUrl}" | tar -zxf - -C "${box_src_tmp_dir}"; then
if ! $curl -sL "${sourceTarballUrl}" | tar -zxf - -C "${box_src_tmp_dir}"; then
echo "Could not download source tarball. See ${LOG_FILE} for details"
exit 1
fi
@@ -215,29 +231,30 @@ if [[ "${initBaseImage}" == "true" ]]; then
fi
echo "=> Installing version ${version} (this takes some time) ..."
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data "${data}" &>> "${LOG_FILE}"; then
echo "${data}" > "${DATA_FILE}"
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
rm "${DATA_FILE}"
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
while true; do
echo -n "."
if status=$(curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
if status=$($curl -q -f "http://localhost:3000/api/v1/cloudron/status" 2>/dev/null); then
[[ -z "$domain" ]] && break # with no domain, we are up and running
[[ "$status" == *"\"tls\": true"* ]] && break # with a domain, wait for the cert
fi
sleep 10
done
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
if [[ -n "${domain}" ]]; then
echo -e "Visit https://my.${domain} to finish setup once the server has rebooted.\n"
echo -e "\n\nVisit https://my.${domain} to finish setup once the server has rebooted.\n"
else
echo -e "Visit https://<IP> to finish setup once the server has rebooted.\n"
echo -e "\n\nVisit https://<IP> to finish setup once the server has rebooted.\n"
fi
if [[ "${initBaseImage}" == "true" ]]; then
if [[ "${rebootServer}" == "true" ]]; then
echo -e "\n\nRebooting this server now to let bootloader changes take effect.\n"
systemctl reboot
fi
+3 -26
View File
@@ -2,30 +2,23 @@
set -eu
assertNotEmpty() {
: "${!1:? "$1 is not set."}"
}
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
# brew install gnu-getopt to get the GNU getopt on OS X
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
readonly GNU_GETOPT
args=$(${GNU_GETOPT} -o "" -l "revision:,output:,no-upload" -n "$0" -- "$@")
args=$(${GNU_GETOPT} -o "" -l "revision:,output:" -n "$0" -- "$@")
eval set -- "${args}"
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
delete_bundle="yes"
commitish="HEAD"
upload="yes"
bundle_file=""
while true; do
case "$1" in
--revision) commitish="$2"; shift 2;;
--output) bundle_file="$2"; delete_bundle="no"; shift 2;;
--no-upload) upload="no"; shift;;
--output) bundle_file="$2"; shift 2;;
--) break;;
*) echo "Unknown option $1"; exit 1;;
esac
@@ -90,21 +83,5 @@ echo "Create final tarball"
echo "Cleaning up ${bundle_dir}"
rm -rf "${bundle_dir}"
if [[ "${upload}" == "yes" ]]; then
echo "Uploading bundle to S3"
echo "Tarball saved at ${bundle_file}"
assertNotEmpty AWS_DEV_ACCESS_KEY
assertNotEmpty AWS_DEV_SECRET_KEY
# That special header is needed to allow access with singed urls created with different aws credentials than the ones the file got uploaded
s3cmd --multipart-chunk-size-mb=5 --ssl --acl-public --access_key="${AWS_DEV_ACCESS_KEY}" --secret_key="${AWS_DEV_SECRET_KEY}" --no-mime-magic put "${bundle_file}" "s3://dev-cloudron-releases/box-${version}.tar.gz"
versions_file_url="https://dev-cloudron-releases.s3.amazonaws.com/box-${version}.tar.gz"
echo "The URL for the versions file is: ${versions_file_url}"
fi
if [[ "${delete_bundle}" == "no" ]]; then
echo "Tarball preserved at ${bundle_file}"
else
rm "${bundle_file}"
fi
+1 -1
View File
@@ -52,7 +52,7 @@ fi
if [[ "${is_update}" == "yes" ]]; then
echo "Setting up update splash screen"
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" # show splash from new code
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" || true # show splash from new code
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
fi
+5
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()
+21 -18
View File
@@ -71,7 +71,7 @@ systemctl restart apparmor
usermod ${USER} -a -G docker
temp_file=$(mktemp)
# create systemd drop-in. some apps do not work with aufs
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper --dns=172.18.0.1 --dns-search=." > "${temp_file}"
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=devicemapper --dns=172.18.0.1 --dns-search=." > "${temp_file}"
systemctl enable docker
# restart docker if options changed
@@ -97,12 +97,6 @@ if [[ "${arg_provider}" == "caas" ]]; then
fi
echo "==> Setup btrfs data"
if ! grep -q loop.ko /lib/modules/`uname -r`/modules.builtin; then
# on scaleway loop is not built-in
echo "loop" >> /etc/modules
modprobe loop
fi
if [[ ! -d "${DATA_DIR}" ]]; then
echo "==> Mounting loopback btrfs"
truncate -s "8192m" "${DATA_FILE}" # 8gb start (this will get resized dynamically by cloudron-resize-fs.service)
@@ -167,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/
@@ -200,7 +194,7 @@ 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
@@ -216,14 +210,18 @@ echo "==> Cleaning up snapshots"
find "${DATA_DIR}/snapshots" -mindepth 1 -maxdepth 1 | xargs --no-run-if-empty btrfs subvolume delete
# restart mysql to make sure it has latest config
# wait for all running mysql jobs
cp "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf
while true; do
if ! systemctl list-jobs | grep mysql; then break; fi
echo "Waiting for mysql jobs..."
sleep 1
done
systemctl restart mysql
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
# wait for all running mysql jobs
cp "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf
while true; do
if ! systemctl list-jobs | grep mysql; then break; fi
echo "Waiting for mysql jobs..."
sleep 1
done
systemctl restart mysql
else
systemctl start mysql
fi
readonly mysql_root_password="password"
mysqladmin -u root -ppassword password password # reset default root password
@@ -318,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"
+2 -2
View File
@@ -13,10 +13,10 @@ 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_bytes=$(LC_ALL=C 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 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
+12 -2
View File
@@ -25,12 +25,22 @@ server {
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
ssl_prefer_server_ciphers on;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
# ciphers according to https://weakdh.org/sysadmin.html
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_dhparam /home/yellowtent/boxdata/dhparams.pem;
add_header Strict-Transport-Security "max-age=15768000";
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
add_header X-Frame-Options "<%= xFrameOptions %>";
# 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";
add_header X-Download-Options "noopen";
add_header X-Content-Type-Options "nosniff";
add_header X-Permitted-Cross-Domain-Policies "none";
proxy_http_version 1.1;
proxy_intercept_errors on;
proxy_read_timeout 3500;
+3
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
+1 -59
View File
@@ -106,18 +106,6 @@ var KNOWN_ADDONS = {
teardown: NOOP,
backup: NOOP,
restore: NOOP
},
simpleauth: {
setup: setupSimpleAuth,
teardown: teardownSimpleAuth,
backup: NOOP,
restore: setupSimpleAuth
},
_docker: {
setup: NOOP,
teardown: NOOP,
backup: NOOP,
restore: NOOP
}
};
@@ -219,7 +207,6 @@ function getBindsSync(app, addons) {
for (var addon in addons) {
switch (addon) {
case '_docker': binds.push('/var/run/docker.sock:/var/run/docker.sock:rw'); break;
case 'localstorage': binds.push(path.join(paths.DATA_DIR, app.id, 'data') + ':/app/data:rw'); break;
default: break;
}
@@ -287,57 +274,12 @@ function teardownOauth(app, options, callback) {
debugApp(app, 'teardownOauth');
clients.delByAppIdAndType(app.id, clients.TYPE_OAUTH, function (error) {
if (error && error.reason !== ClientsError.NOT_FOUND) console.error(error);
if (error && error.reason !== ClientsError.NOT_FOUND) debug(error);
appdb.unsetAddonConfig(app.id, 'oauth', 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) console.error(error);
appdb.unsetAddonConfig(app.id, 'simpleauth', callback);
});
}
function setupEmail(app, options, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
+12 -2
View File
@@ -52,6 +52,7 @@ var assert = require('assert'),
async = require('async'),
database = require('./database.js'),
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js'),
safe = require('safetydance'),
util = require('util');
@@ -189,7 +190,7 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
var sso = 'sso' in data ? data.sso : null;
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
var queries = [ ];
var queries = [];
queries.push({
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
args: [ id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain, xFrameOptions, lastBackupId, sso, debugModeJson ]
@@ -202,6 +203,14 @@ function add(id, appStoreId, manifest, location, portBindings, data, callback) {
});
});
// only allocate a mailbox if mailboxName is set
if (data.mailboxName) {
queries.push({
query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)',
args: [ data.mailboxName, id, mailboxdb.TYPE_APP ]
});
}
database.transaction(queries, function (error) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
@@ -242,13 +251,14 @@ function del(id, callback) {
assert.strictEqual(typeof callback, 'function');
var queries = [
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
];
database.transaction(queries, function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (results[2].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
callback(null);
});
+2 -2
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');
@@ -138,7 +138,7 @@ function run() {
/*
OOM can be tested using stress tool like so:
docker run -ti -m 100M cloudron/base:0.9.0 /bin/bash
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
apt-get update && apt-get install stress
stress --vm 1 --vm-bytes 200M --vm-hang 0
*/
+39 -52
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) */
@@ -250,7 +249,7 @@ function getDuplicateErrorDetails(location, portBindings, error) {
var match = error.message.match(/ER_DUP_ENTRY: Duplicate entry '(.*)' for key/);
if (!match) {
console.error('Unexpected SQL error message.', error);
debug('Unexpected SQL error message.', error);
return new AppsError(AppsError.INTERNAL_ERROR);
}
@@ -296,11 +295,9 @@ function hasAccessTo(app, user, callback) {
if (!app.accessRestriction.groups) return callback(null, false);
async.some(app.accessRestriction.groups, function (groupId, iteratorDone) {
groups.isMember(groupId, user.id, function (error, member) {
iteratorDone(!error && member); // async.some does not take error argument in callback
});
}, function (result) {
callback(null, result);
groups.isMember(groupId, user.id, iteratorDone);
}, function (error, result) {
callback(null, !error && result);
});
}
@@ -314,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);
});
@@ -332,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);
});
@@ -347,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);
@@ -360,11 +360,9 @@ function getAllByUser(user, callback) {
getAll(function (error, result) {
if (error) return callback(error);
async.filter(result, function (app, callback) {
hasAccessTo(app, user, function (error, hasAccess) {
callback(hasAccess);
});
}, callback.bind(null, null)); // never error
async.filter(result, function (app, iteratorDone) {
hasAccessTo(app, user, iteratorDone);
}, callback);
});
}
@@ -428,10 +426,13 @@ function unpurchase(appId, appstoreId, callback) {
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppsError(AppsError.BILLING_REQUIRED));
if (result.statusCode === 404) return callback(null); // was never purchased
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
superagent.del(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error));
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppsError(AppsError.BILLING_REQUIRED));
if (result.statusCode !== 204) return callback(new AppsError(AppsError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
callback(null);
@@ -526,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'));
@@ -554,30 +555,25 @@ function install(data, auditSource, callback) {
altDomain: altDomain,
xFrameOptions: xFrameOptions,
sso: sso,
debugMode: debugMode
debugMode: debugMode,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
};
var from = (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
mailboxdb.add(from, appId, mailboxdb.TYPE_APP, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'Mailbox already exists'));
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.add(appId, appStoreId, manifest, location, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
// save cert to boxdata/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
// save cert to boxdata/certs
if (cert && key) {
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.cert'), cert)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving cert: ' + safe.error.message));
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, config.appFqdn(location) + '.user.key'), key)) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Error saving key: ' + safe.error.message));
}
taskmanager.restartAppTask(appId);
taskmanager.restartAppTask(appId);
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
eventlog.add(eventlog.ACTION_APP_INSTALL, auditSource, { appId: appId, location: location, manifest: manifest });
callback(null, { id : appId });
});
callback(null, { id : appId });
});
});
});
@@ -898,24 +894,19 @@ function clone(appId, data, auditSource, callback) {
accessRestriction: app.accessRestriction,
xFrameOptions: app.xFrameOptions,
lastBackupId: backupId,
sso: !!app.sso
sso: !!app.sso,
mailboxName: (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app'
};
var from = (location ? location : manifest.title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
mailboxdb.add(from, newAppId, mailboxdb.TYPE_APP, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new AppsError(AppsError.ALREADY_EXISTS, 'Mailbox already exists'));
appdb.add(newAppId, appStoreId, manifest, location, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
appdb.add(newAppId, appStoreId, manifest, location, portBindings, data, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(getDuplicateErrorDetails(location, portBindings, error));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.restartAppTask(newAppId);
taskmanager.restartAppTask(newAppId);
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: appId, backupId: backupId, location: location, manifest: manifest });
callback(null, { id : newAppId });
});
callback(null, { id : newAppId });
});
});
});
@@ -935,18 +926,14 @@ function uninstall(appId, auditSource, callback) {
unpurchase(appId, result.appStoreId, function (error) {
if (error) return callback(error);
mailboxdb.delByOwnerId(appId, function (error) {
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.stopAppTask(appId, function () {
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
taskmanager.stopAppTask(appId, function () {
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_UNINSTALL, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app'));
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
eventlog.add(eventlog.ACTION_APP_UNINSTALL, auditSource, { appId: appId });
taskmanager.startAppTask(appId, callback);
});
taskmanager.startAppTask(appId, callback);
});
});
});
+18 -10
View File
@@ -213,15 +213,14 @@ function downloadIcon(app, callback) {
}, callback);
}
function registerSubdomain(app, callback) {
function registerSubdomain(app, overwrite, callback) {
assert.strictEqual(typeof app, 'object');
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);
// even though the bare domain is already registered in the appstore, we still
// need to register it so that we have a dnsRecordId to wait for it to complete
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
debugApp(app, 'Registering subdomain location [%s]', app.location);
@@ -231,7 +230,7 @@ function registerSubdomain(app, callback) {
// refuse to update any existing DNS record for custom domains that we did not create
// note that the appstore sets up the naked domain for non-custom domains
if (config.isCustomDomain() && values.length !== 0 && !app.dnsRecordId) return retryCallback(null, new Error('DNS Record already exists'));
if (config.isCustomDomain() && values.length !== 0 && !overwrite) return retryCallback(null, new Error('DNS Record already exists'));
subdomains.upsert(app.location, 'A', [ ip ], function (error, changeId) {
if (error && (error.reason === SubdomainError.STILL_BUSY || error.reason === SubdomainError.EXTERNAL_ERROR)) return retryCallback(error); // try again
@@ -258,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) {
@@ -296,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);
@@ -363,7 +362,7 @@ function install(app, callback) {
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '30, Registering subdomain' }),
registerSubdomain.bind(null, app),
registerSubdomain.bind(null, app, false /* overwrite */),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
@@ -462,7 +461,7 @@ function restore(app, callback) {
downloadIcon.bind(null, app),
updateApp.bind(null, app, { installationProgress: '55, Registering subdomain' }), // ip might change during upgrades
registerSubdomain.bind(null, app),
registerSubdomain.bind(null, app, true /* overwrite */),
updateApp.bind(null, app, { installationProgress: '60, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
@@ -524,8 +523,17 @@ 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),
registerSubdomain.bind(null, app, true /* overwrite */),
updateApp.bind(null, app, { installationProgress: '40, Downloading image' }),
docker.downloadImage.bind(null, app.manifest),
updateApp.bind(null, app, { installationProgress: '45, Ensuring volume' }),
createVolume.bind(null, app),
// re-setup addons since they rely on the app's fqdn (e.g oauth)
updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }),
+6 -2
View File
@@ -352,12 +352,16 @@ Acme.prototype.createKeyAndCsr = function (domain, callback) {
Acme.prototype.downloadChain = function (linkHeader, callback) {
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
debug('downloadChain: linkHeader %s', linkHeader);
var linkInfo = parseLinks(linkHeader);
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
debug('downloadChain: downloading from %s', this.caOrigin + linkInfo.up);
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
superagent.get(this.caOrigin + linkInfo.up).buffer().parse(function (res, done) {
debug('downloadChain: downloading from %s', intermediateCertUrl);
superagent.get(intermediateCertUrl).buffer().parse(function (res, done) {
var data = [ ];
res.on('data', function(chunk) { data.push(chunk); });
res.on('end', function () { res.text = Buffer.concat(data); done(); });
+41 -15
View File
@@ -14,7 +14,11 @@ exports = module.exports = {
renewAll: renewAll,
events: new (require('events').EventEmitter)(),
initialize: initialize,
uninitialize: uninitialize,
events: null,
EVENT_CERT_CHANGED: 'cert_changed',
// exported for testing
@@ -39,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');
@@ -65,6 +68,20 @@ CertificatesError.INTERNAL_ERROR = 'Internal Error';
CertificatesError.INVALID_CERT = 'Invalid certificate';
CertificatesError.NOT_FOUND = 'Not Found';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = new (require('events').EventEmitter)();
callback();
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = null;
callback();
}
function getApi(app, callback) {
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof callback, 'function');
@@ -250,25 +267,30 @@ 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;
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
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
@@ -276,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;
}
-2
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;
+63 -37
View File
@@ -25,7 +25,8 @@ exports = module.exports = {
readDkimPublicKeySync: readDkimPublicKeySync,
refreshDNS: refreshDNS,
events: new (require('events').EventEmitter)(),
events: null,
EVENT_ACTIVATED: 'activated'
};
@@ -122,7 +123,16 @@ CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
function initialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = new (require('events').EventEmitter)();
gConfigState = { dns: false, tls: false, configured: false };
gUpdatingDns = false;
gBoxAndUserDetails = null;
async.series([
certificates.initialize,
settings.initialize,
platform.initialize,
installAppBundle,
checkConfigState,
configureDefaultServer
@@ -132,13 +142,17 @@ function initialize(callback) {
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = null;
platform.events.removeListener(platform.EVENT_READY, onPlatformReady);
async.series([
cron.uninitialize,
taskmanager.pauseTasks,
mailer.stop,
platform.uninitialize
platform.uninitialize,
certificates.uninitialize,
settings.uninitialize
], callback);
}
@@ -155,15 +169,17 @@ function onConfigured(callback) {
gConfigState.configured = true;
platform.events.on(platform.EVENT_READY, onPlatformReady);
settings.events.on(settings.DNS_CONFIG_KEY, function () { refreshDNS(); });
async.series([
clients.addDefaultClients,
cron.initialize,
certificates.ensureFallbackCertificate,
platform.initialize, // requires fallback certs for mail container
platform.start, // requires fallback certs for mail container
ensureDkimKey,
addDnsRecords,
configureAdmin,
mailer.start
mailer.start,
cron.initialize // do not send heartbeats until we are "ready"
], callback);
}
@@ -226,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, 'localhost');
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);
}
@@ -248,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) {
@@ -435,7 +452,7 @@ function sendHeartbeat() {
function sendAliveStatus(callback) {
if (typeof callback !== 'function') {
callback = function (error) {
if (error && error.reason !== CloudronError.INTERNAL_ERROR) console.error(error);
if (error && error.reason !== CloudronError.INTERNAL_ERROR) debug(error);
else if (error) debug(error);
};
}
@@ -451,7 +468,11 @@ function sendAliveStatus(callback) {
domain: config.fqdn(),
version: config.version(),
provider: config.provider(),
backendSettings: backendSettings
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
}
};
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
@@ -480,7 +501,8 @@ function sendAliveStatus(callback) {
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY]
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY]
};
// Caas Cloudrons do not store appstore credentials in their local database
@@ -507,12 +529,7 @@ function sendAliveStatus(callback) {
});
}
function readDkimPublicKeySync() {
if (!config.fqdn()) {
debug('Cannot read dkim public key without a domain.', safe.error);
return null;
}
function ensureDkimKey(callback) {
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
@@ -531,6 +548,18 @@ function readDkimPublicKeySync() {
debug('DKIM keys already present');
}
callback();
}
function readDkimPublicKeySync() {
if (!config.fqdn()) {
debug('Cannot read dkim public key without a domain.', safe.error);
return null;
}
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPublicKeyFile = path.join(dkimPath, 'public');
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
if (publicKey === null) {
@@ -545,6 +574,7 @@ function readDkimPublicKeySync() {
}
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
function txtRecordsWithSpf(callback) {
assert.strictEqual(typeof callback, 'function');
@@ -553,21 +583,25 @@ function txtRecordsWithSpf(callback) {
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
var i, validSpf;
var i, matches, validSpf;
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
matches = txtRecords[i].match(/^("?v=spf1) /); // DO backend may return without quotes
if (matches === null) continue;
validSpf = txtRecords[i].indexOf(' a:' + config.adminFqdn() + ' ') !== -1;
break;
// this won't work if the entry is arbitrarily "split" across quoted strings
validSpf = txtRecords[i].indexOf('a:' + config.adminFqdn()) !== -1;
break; // there can only be one SPF record
}
if (validSpf) return callback(null, null);
if (i == txtRecords.length) {
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ~all"';
} else {
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
if (!matches) { // no spf record was found, create one
txtRecords.push('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
debug('txtRecordsWithSpf: adding txt record');
} else { // just add ourself
txtRecords[i] = matches[1] + ' a:' + config.adminFqdn() + txtRecords[i].slice(matches[1].length);
debug('txtRecordsWithSpf: inserting txt record');
}
return callback(null, txtRecords);
@@ -588,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 ] };
@@ -661,7 +695,7 @@ function update(boxUpdateInfo, auditSource, callback) {
debug('Starting upgrade');
doUpgrade(boxUpdateInfo, function (error) {
if (error) {
console.error('Upgrade failed with error:', error);
debug('Upgrade failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
@@ -669,7 +703,7 @@ function update(boxUpdateInfo, auditSource, callback) {
debug('Starting update');
doUpdate(boxUpdateInfo, function (error) {
if (error) {
console.error('Update failed with error:', error);
debug('Update failed with error:', error);
locker.unlock(locker.OP_BOX_UPDATE);
}
});
@@ -924,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);
@@ -938,15 +972,7 @@ function refreshDNS(callback) {
if (error) return callback(error);
async.each(result, function (app, callback) {
// get the current record before updating it
subdomains.get(app.location, 'A', function (error, values) {
if (error) return callback(error);
// refuse to update any existing DNS record for custom domains that we did not create
if (values.length !== 0 && !app.dnsRecordId) return callback(null, new Error('DNS Record already exists'));
subdomains.upsert(app.location, 'A', [ ip ], callback);
});
subdomains.upsert(app.location, 'A', [ ip ], callback);
}, function (error) {
if (error) return callback(error);
+2 -6
View File
@@ -64,7 +64,7 @@ function saveSync() {
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
}
function _reset (callback) {
function _reset(callback) {
safe.fs.unlinkSync(cloudronConfigFileName);
initConfig();
@@ -79,12 +79,11 @@ function initConfig() {
data.token = null;
data.boxVersionsUrl = null;
data.version = null;
data.isCustomDomain = false;
data.isCustomDomain = true;
data.webServerOrigin = null;
data.smtpPort = 2525; // // this value comes from mail container
data.sysadminPort = 3001;
data.ldapPort = 3002;
data.simpleAuthPort = 3004;
data.provider = 'caas';
data.appBundle = [ ];
@@ -116,9 +115,6 @@ function initConfig() {
saveSync();
}
// cleanup any old config file we have for tests
if (exports.TEST) safe.fs.unlinkSync(cloudronConfigFileName);
initConfig();
// set(obj) or set(key, value)
+8 -2
View File
@@ -52,9 +52,15 @@ function initialize(callback) {
gHeartbeatJob = new CronJob({
cronTime: '00 */1 * * * *', // every minute
onTick: cloudron.sendHeartbeat,
start: true
start: false
});
cloudron.sendHeartbeat(); // latest unpublished version of CronJob has runOnInit
// hack: send the first heartbeat only after we are running for 60 seconds
// required as we end up sending a heartbeat and then cloudron-setup reboots the server
setTimeout(function () {
if (!gHeartbeatJob) return; // already uninitalized
gHeartbeatJob.start();
cloudron.sendHeartbeat();
}, 1000 * 60);
var randomHourMinute = Math.floor(60*Math.random());
gAliveJob = new CronJob({
+7
View File
@@ -85,6 +85,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
.end(function (error, result) {
if (error && !error.response) return callback(error);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, util.format('%s %j', result.statusCode, result.body)));
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null);
@@ -100,6 +101,7 @@ function upsert(dnsConfig, zoneName, subdomain, type, values, callback) {
if (error && !error.response) return callback(error);
if (result.statusCode === 403 || result.statusCode === 401) return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, util.format('%s %j', result.statusCode, result.body)));
if (result.statusCode === 422) return callback(new SubdomainError(SubdomainError.BAD_FIELD, result.body.message));
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
return callback(null);
@@ -190,6 +192,11 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
if (error && error.code === 'ENOTFOUND') return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, error ? error.message : 'Unable to get nameservers'));
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.digitalocean.com') === -1) {
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
}
upsert(credentials, domain, 'my', 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
+12 -16
View File
@@ -10,7 +10,7 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/noop'),
debug = require('debug')('box:dns/manual'),
dns = require('native-dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
util = require('util');
@@ -60,18 +60,14 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
dns.resolveNs(domain, function (error, nameservers) {
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to get nameservers'));
// async.every only reports bools
var stashedError = null;
async.every(nameservers, function (nameserver, callback) {
async.every(nameservers, function (nameserver, everyNsCallback) {
// ns records cannot have cname
dns.resolve4(nameserver, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) {
stashedError = new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain');
return callback(false);
return everyNsCallback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to resolve nameservers for this domain'));
}
async.every(nsIps, function (nsIp, callback) {
async.every(nsIps, function (nsIp, everyIpCallback) {
var req = dns.Request({
question: dns.Question({ name: adminDomain, type: 'A' }),
server: { address: nsIp },
@@ -80,20 +76,20 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
req.on('timeout', function () {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, adminDomain);
return callback(true); // should be ok if dns server is down
return everyIpCallback(null, true); // should be ok if dns server is down
});
req.on('message', function (error, message) {
if (error) {
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, adminDomain, error);
return callback(false);
return everyIpCallback(null, false);
}
var answer = message.answer;
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, adminDomain, 'A', message);
return callback(false);
return everyIpCallback(null, false);
}
debug('verifyDnsConfig: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, adminDomain, answer, ip);
@@ -102,16 +98,16 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
return a.address === ip;
});
if (match) return callback(true); // done!
if (match) return everyIpCallback(null, true); // done!
callback(false);
everyIpCallback(null, false);
});
req.send();
}, callback);
}, everyNsCallback);
});
}, function (success) {
if (stashedError) return callback(stashedError);
}, function (error, success) {
if (error) return callback(error);
if (!success) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'The domain ' + adminDomain + ' does not resolve to the server\'s IP ' + ip));
callback(null, { provider: dnsConfig.provider, wildcard: !!dnsConfig.wildcard });
+2 -2
View File
@@ -48,8 +48,8 @@ function del(dnsConfig, zoneName, subdomain, type, values, callback) {
function waitForDns(domain, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(type === 'A' || type === 'CNAME');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
+1 -1
View File
@@ -241,7 +241,7 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
}
upsert(credentials, domain, 'my', 'A', [ ip ], function (error, changeId) {
if (error) return callback(new SubdomainError(SubdomainError.INTERNAL_ERROR, error));
if (error) return callback(error);
debug('verifyDnsConfig: A record added with change id %s', changeId);
+18 -12
View File
@@ -7,12 +7,12 @@ var assert = require('assert'),
debug = require('debug')('box:dns/waitfordns'),
dns = require('native-dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
tld = require('tldjs');
tld = require('tldjs'),
util = require('util');
// the first arg to callback is not an error argument; this is required for async.every
function isChangeSynced(domain, value, type, nameserver, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(util.isRegExp(value));
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof nameserver, 'string');
assert.strictEqual(typeof callback, 'function');
@@ -33,31 +33,33 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
req.on('timeout', function () {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
return iteratorCallback(true); // should be ok if dns server is down
return iteratorCallback(null, true); // should be ok if dns server is down
});
req.on('message', function (error, message) {
if (error) {
debug('nameserver %s (%s) returned error trying to resolve %s: %s', nameserver, nsIp, domain, error);
return iteratorCallback(false);
return iteratorCallback(null, false);
}
var answer = message.answer;
if (!answer || answer.length === 0) {
debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, domain, type, message);
return iteratorCallback(false);
return iteratorCallback(null, false);
}
debug('isChangeSynced: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, domain, answer, value);
var match = answer.some(function (a) {
return ((type === 'A' && a.address === value) || (type === 'CNAME' && a.data === value));
return ((type === 'A' && value.test(a.address)) ||
(type === 'CNAME' && value.test(a.data)) ||
(type === 'TXT' && value.test(a.data.join(''))));
});
if (match) return iteratorCallback(true); // done!
if (match) return iteratorCallback(null, true); // done!
iteratorCallback(false);
iteratorCallback(null, false);
});
req.send();
@@ -68,12 +70,16 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
// check if IP change has propagated to every nameserver
function waitForDns(domain, value, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof value, 'string');
assert(type === 'A' || type === 'CNAME');
assert(typeof value === 'string' || util.isRegExp(value));
assert(type === 'A' || type === 'CNAME' || type === 'TXT');
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
assert.strictEqual(typeof callback, 'function');
var zoneName = tld.getDomain(domain);
if (typeof value === 'string') {
// http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
value = new RegExp('^' + value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$');
}
debug('waitForIp: domain %s to be %s in zone %s.', domain, value, zoneName);
var attempt = 1;
@@ -83,7 +89,7 @@ function waitForDns(domain, value, type, options, callback) {
dns.resolveNs(zoneName, function (error, nameservers) {
if (error || !nameservers) return retryCallback(error || new SubdomainError(SubdomainError.EXTERNAL_ERROR, 'Unable to get nameservers'));
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (synced) {
async.every(nameservers, isChangeSynced.bind(null, domain, value, type), function (error, synced) {
debug('waitForIp: %s %s ns: %j', domain, synced ? 'done' : 'not done', nameservers);
retryCallback(synced ? null : new SubdomainError(SubdomainError.EXTERNAL_ERROR, 'ETRYAGAIN'));
+2 -1
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',
+10 -4
View File
@@ -25,7 +25,8 @@ exports = module.exports = {
var assert = require('assert'),
constants = require('./constants.js'),
database = require('./database.js'),
DatabaseError = require('./databaseerror');
DatabaseError = require('./databaseerror'),
mailboxdb = require('./mailboxdb.js');
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
@@ -88,10 +89,14 @@ function add(id, name, callback) {
assert.strictEqual(typeof callback, 'function');
var data = [ id, name ];
database.query('INSERT INTO groups (id, name) VALUES (?, ?)',
data, function (error, result) {
var queries = [];
queries.push({ query: 'INSERT INTO mailboxes (name, ownerId, ownerType) VALUES (?, ?, ?)', args: [ name, id, mailboxdb.TYPE_GROUP ] });
queries.push({ query: 'INSERT INTO groups (id, name) VALUES (?, ?)', args: [ id, name ] });
database.transaction(queries, function (error, result) {
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error));
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (error || result[1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
@@ -105,6 +110,7 @@ function del(id, callback) {
var queries = [];
queries.push({ query: 'DELETE FROM groupMembers WHERE groupId = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM groups WHERE id = ?', args: [ id ] });
queries.push({ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] });
database.transaction(queries, function (error, result) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
+5 -16
View File
@@ -24,7 +24,6 @@ var assert = require('assert'),
constants = require('./constants.js'),
DatabaseError = require('./databaseerror.js'),
groupdb = require('./groupdb.js'),
mailboxdb = require('./mailboxdb.js'),
util = require('util'),
uuid = require('node-uuid');
@@ -60,7 +59,7 @@ GroupError.NOT_ALLOWED = 'Not Allowed';
function validateGroupname(name) {
assert.strictEqual(typeof name, 'string');
if (name.length < 2) return new GroupError(GroupError.BAD_FIELD, 'name must be atleast 2 chars');
if (name.length < 1) return new GroupError(GroupError.BAD_FIELD, 'name must be atleast 1 char');
if (name.length >= 200) return new GroupError(GroupError.BAD_FIELD, 'name too long');
if (constants.RESERVED_NAMES.indexOf(name) !== -1) return new GroupError(GroupError.BAD_FIELD, 'name is reserved');
@@ -85,16 +84,11 @@ function create(name, callback) {
if (error) return callback(error);
var id = 'gid-' + uuid.v4();
mailboxdb.add(name, id /* owner */, mailboxdb.TYPE_GROUP, function (error) {
groupdb.add(id, name, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new GroupError(GroupError.ALREADY_EXISTS));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
groupdb.add(id, name, function (error) {
if (error && error.reason === DatabaseError.ALREADY_EXISTS) return callback(new GroupError(GroupError.ALREADY_EXISTS));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null, { id: id, name: name });
});
callback(null, { id: id, name: name });
});
}
@@ -105,16 +99,11 @@ function remove(id, callback) {
// never allow admin group to be deleted
if (id === constants.ADMIN_GROUP_ID) return callback(new GroupError(GroupError.NOT_ALLOWED));
mailboxdb.delByOwnerId(id, function (error) {
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
groupdb.del(id, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new GroupError(GroupError.NOT_FOUND));
if (error) return callback(new GroupError(GroupError.INTERNAL_ERROR, error));
callback(null);
});
callback(null);
});
}
+9 -9
View File
@@ -5,20 +5,20 @@
// 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.9.0' ],
'baseImages': [ 'cloudron/base:0.10.0' ],
// Note that if any of the databases include an upgrade, bump the infra version above
// This is because we upgrade using dumps instead of mysql_upgrade, pg_upgrade etc
'images': {
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.13.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.15.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.11.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.10.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.29.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.10.0' }
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.14.0' },
'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.3' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
}
};
+34 -8
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,20 +41,42 @@ 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 ];
if (entry.admin) groups.push(GROUP_ADMINS_DN);
var displayName = entry.displayName || entry.username;
var displayName = entry.displayName || entry.username || ''; // displayName can be empty and username can be null
var nameParts = displayName.split(' ');
var firstName = nameParts[0];
var lastName = nameParts.length > 1 ? nameParts[nameParts.length - 1] : ''; // choose last part, if it exists
@@ -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();
}
@@ -4,6 +4,12 @@ Dear Cloudron Admin,
The certificate for <%= domain %> could not be renewed.
The Cloudron will attempt to renew the certificate every 12 hours
until the certificate expires (at which point it will switch to
using the fallback certificate).
The error was:
-------------------------------------
<%- message %>
+5 -3
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.
+1 -1
View File
@@ -208,7 +208,7 @@ function getAlias(name, callback) {
assert.strictEqual(typeof name, 'string');
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND aliasTarget IS NOT null', [ name ], function (error, results) {
database.query('SELECT ' + MAILBOX_FIELDS + ' FROM mailboxes WHERE name = ? AND aliasTarget IS NOT NULL', [ name ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
+20 -68
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'),
@@ -46,17 +45,17 @@ var assert = require('assert'),
settings = require('./settings.js'),
showdown = require('showdown'),
smtpTransport = require('nodemailer-smtp-transport'),
subdomains = require('./subdomains.js'),
users = require('./user.js'),
util = require('util'),
_ = require('underscore');
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
var MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates');
var gMailQueue = [ ],
gDnsReady = false,
gCheckDnsTimerId = null;
gDnsReady = false;
function splatchError(error) {
var result = { };
@@ -81,8 +80,6 @@ function stop(callback) {
assert.strictEqual(typeof callback, 'function');
// TODO: interrupt processQueue as well
clearTimeout(gCheckDnsTimerId);
gCheckDnsTimerId = null;
debug(gMailQueue.length + ' mail items dropped');
gMailQueue = [ ];
@@ -96,60 +93,13 @@ function mailConfig() {
};
}
function getTxtRecords(callback) {
dns.resolveNs(config.zoneName(), function (error, nameservers) {
if (error || !nameservers) return callback(error || new Error('Unable to get nameservers'));
var nameserver = nameservers[0];
dns.resolve4(nameserver, function (error, nsIps) {
if (error || !nsIps || nsIps.length === 0) return callback(error);
var req = dns.Request({
question: dns.Question({ name: config.fqdn(), type: 'TXT' }),
server: { address: nsIps[0] },
timeout: 5000
});
req.on('timeout', function () { return callback(new Error('ETIMEOUT')); });
req.on('message', function (error, message) {
if (error || !message.answer || message.answer.length === 0) return callback(null, null);
var records = message.answer.map(function (a) { return a.data[0]; });
callback(null, records);
});
req.send();
});
});
}
// keep this in sync with the cloudron.js dns changes
function checkDns() {
getTxtRecords(function (error, records) {
if (error || !records) {
debug('checkDns: DNS error or no records looking up TXT records for %s %s', config.fqdn(), error, records);
gCheckDnsTimerId = setTimeout(checkDns, 60000);
return;
}
var allowedToSendMail = false;
for (var i = 0; i < records.length; i++) {
if (records[i].indexOf('v=spf1 ') !== 0) continue; // not SPF
allowedToSendMail = records[i].indexOf('a:' + config.adminFqdn()) !== -1;
break; // only one SPF record can exist (https://support.google.com/a/answer/4568483?hl=en)
}
if (!allowedToSendMail) {
debug('checkDns: SPF records disallow sending email from cloudron. %j', records);
gCheckDnsTimerId = setTimeout(checkDns, 60000);
return;
}
subdomains.waitForDns(config.fqdn(), new RegExp('^v=spf1 .*a:' + config.adminFqdn().replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '.*'), 'TXT', { interval: 60000, times: Infinity }, function (error) {
if (error) return debug(error); // can never happen
debug('checkDns: SPF check passed. commencing mail processing');
gDnsReady = true;
processQueue();
});
@@ -183,7 +133,7 @@ function sendMails(queue, callback) {
async.mapSeries(queue, function iterator(mailOptions, callback) {
transport.sendMail(mailOptions, function (error) {
if (error) return console.error(error); // TODO: requeue?
if (error) return debug(error); // TODO: requeue?
debug('Email sent to ' + mailOptions.to);
});
callback(null);
@@ -198,8 +148,8 @@ function sendMails(queue, callback) {
function enqueue(mailOptions) {
assert.strictEqual(typeof mailOptions, 'object');
if (!mailOptions.from) console.error('sender address is missing');
if (!mailOptions.to) console.error('recipient address is missing');
if (!mailOptions.from) debug('sender address is missing');
if (!mailOptions.to) debug('recipient address is missing');
debug('Queued mail for ' + mailOptions.from + ' to ' + mailOptions.to);
gMailQueue.push(mailOptions);
@@ -232,7 +182,7 @@ function mailUserEventToAdmins(user, event) {
assert.strictEqual(typeof event, 'string');
getAdminEmails(function (error, adminEmails) {
if (error) return console.log('Error getting admins', error);
if (error) return debug('Error getting admins', error);
adminEmails = _.difference(adminEmails, [ user.email ]);
@@ -255,7 +205,7 @@ function sendInvite(user, invitor) {
settings.getCloudronName(function (error, cloudronName) {
if (error) {
console.error(error);
debug(error);
cloudronName = 'Cloudron';
}
@@ -300,7 +250,7 @@ function userAdded(user, inviteSent) {
settings.getCloudronName(function (error, cloudronName) {
if (error) {
console.error(error);
debug(error);
cloudronName = 'Cloudron';
}
@@ -355,7 +305,7 @@ function passwordReset(user) {
settings.getCloudronName(function (error, cloudronName) {
if (error) {
console.error(error);
debug(error);
cloudronName = 'Cloudron';
}
@@ -395,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' })
};
@@ -413,7 +363,7 @@ function boxUpdateAvailable(newBoxVersion, changelog) {
settings.getCloudronName(function (error, cloudronName) {
if (error) {
console.error(error);
debug(error);
cloudronName = 'Cloudron';
}
@@ -491,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' })
};
@@ -509,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' })
};
@@ -527,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' })
};
@@ -543,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',
+48
View File
@@ -0,0 +1,48 @@
<% include header %>
<!-- tester -->
<script>
'use strict';
// very basic angular app
var app = angular.module('Application', []);
app.controller('Controller', ['$scope', function ($scope) {
$scope.success = <%= success %>;
$scope.error = '<%= error %>';
}]);
</script>
<div class="container" ng-app="Application" ng-controller="Controller" ng-cloak>
<div class="row">
<div class="col-md-12 text-center">
<br/>
<h4 ng-hide="success">Hello there, welcome to <%= cloudronName %>.</h4>
<h2 ng-hide="success">Sign up with your email address.</h2>
<h3 ng-show="success">You have received an email invitation to this Cloudron to finish the signup.</h3>
<br/><br/>
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3" ng-show="!success">
<form action="/api/v1/session/account/create" method="post" name="createForm" autocomplete="off" role="form" novalidate>
<input type="password" style="display: none;">
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
<div class="form-group" ng-class="{ 'has-error': (createForm.email.$dirty && createForm.email.$invalid) || (!createForm.email.$dirty && error) }">
<label class="control-label" for="inputEmail">Email</label>
<input type="email" class="form-control" id="inputEmail" ng-model="email" name="email" autofocus required>
<div class="control-label" ng-show="(createForm.email.$dirty && createForm.email.$invalid) || (!createForm.email.$dirty && error)">
<small ng-show="createForm.email.$dirty && createForm.email.$invalid">Must be a valid email address</small>
<small ng-show="!createForm.email.$dirty && error">{{ error }}</small>
</div>
</div>
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="createForm.$invalid"/>
</form>
</div>
</div>
</div>
<% include footer %>

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