Compare commits

..

137 Commits

Author SHA1 Message Date
Johannes Zellner 50c344033c Add 0.160.1 changes 2017-07-22 11:12:51 +02:00
Girish Ramakrishnan 0c269925c2 Add the dialog text to the email 2017-07-21 12:40:04 -07:00
Johannes Zellner dcd84a9636 send lastLogin event timestamp with alive status 2017-07-19 23:05:14 +02:00
Johannes Zellner fa7273025b Send update notification every 4 days regardless of update pattern 2017-07-18 14:50:53 +02:00
Johannes Zellner eb3ae2c34f Give better feedback when a plan was selected 2017-06-09 14:26:28 +02:00
Johannes Zellner eba79cd859 Open all outlinks in a new tab 2017-06-09 14:10:42 +02:00
Girish Ramakrishnan d7d8cf97ed update dialog text 2017-06-08 14:19:32 -07:00
Girish Ramakrishnan 089f7301b8 set webServerOrigin in cloudron.conf
also remove the hardly used --api-server
2017-06-08 10:51:28 -07:00
Johannes Zellner fb4f13eb13 Fixes to the update dialog logic 2017-06-08 17:44:35 +02:00
Johannes Zellner 89878ff9ad Also preset the login email for account details 2017-06-08 14:09:01 +02:00
Johannes Zellner ba62f577fa Show the correct navbar badge for managed cloudron users 2017-06-08 11:17:32 +02:00
Johannes Zellner 4c5bd2d318 Specifically redirect the managed cloudron user to the cc setup 2017-06-08 11:07:05 +02:00
Johannes Zellner 3c318a72f7 Add email query param name 2017-06-08 10:14:09 +02:00
Girish Ramakrishnan 23532eafea Fix path to version 2017-06-07 20:31:18 -07:00
Girish Ramakrishnan 5b7a080d98 Add email when redirecting to cloudron.io 2017-06-07 17:47:03 -07:00
Girish Ramakrishnan 0a44b8c23b Change badge text based on 1.0.0 or not 2017-06-07 15:15:14 -07:00
Girish Ramakrishnan c0c07c2839 ensure .ssh dir exists
Fixes #349
2017-06-07 09:50:31 -07:00
Girish Ramakrishnan 96d2b32a9f doc: scaleway does not require boot script anymore 2017-06-07 09:18:02 -07:00
Girish Ramakrishnan 795c2ad91c typo 2017-06-07 09:04:11 -07:00
Johannes Zellner fc9a9c3f87 Add new changes to changelog 2017-06-07 16:22:56 +02:00
Johannes Zellner d141d6ba21 Do not poll for subscription so often 2017-06-07 13:41:18 +02:00
Johannes Zellner 479da5393a Reword the version 1.0 update dialog 2017-06-07 13:40:56 +02:00
Johannes Zellner 307334ef81 Also test for parent object in case it does not exist 2017-06-07 12:46:01 +02:00
Johannes Zellner c1ec7a06bf If we don't have a dockerImage, we can't proceed with the update 2017-06-07 12:46:01 +02:00
Johannes Zellner 1126a0fc1e Use the app manifest from the box updater 2017-06-07 12:46:01 +02:00
Johannes Zellner b5f678613b Add version 1.0 welcome dialog 2017-06-07 12:46:01 +02:00
Johannes Zellner b7e3447a46 Show subscription dialog on app update 2017-06-07 12:46:01 +02:00
Johannes Zellner 32fa3b8a51 Show subscription indicator in navbar 2017-06-07 12:46:01 +02:00
Johannes Zellner fe0e4000a6 Fix link to subscription page 2017-06-07 12:46:01 +02:00
Johannes Zellner 9ceeb70fc2 No need to pull in unused AppStore dependency 2017-06-07 12:46:01 +02:00
Johannes Zellner aa8b4f1fba show cloudron account in the settings view 2017-06-07 12:46:01 +02:00
Johannes Zellner 95ba51dfb2 Add wrapper to get current subscription 2017-06-07 12:46:01 +02:00
Girish Ramakrishnan c74fb07ff7 Replace all / with _ when querying graphite
Part of #348
2017-06-06 21:25:20 -07:00
Johannes Zellner 03f1326073 Tweak the architecture doc page 2017-06-05 18:10:50 +02:00
Johannes Zellner daa4c66e7f Do not perform automatic updates for major platform version 2017-06-05 18:06:00 +02:00
Johannes Zellner 571abc56fe Fix email view flickering while not eveything has loaded yet 2017-06-05 14:22:34 +02:00
Johannes Zellner 4aaeccecbd Hide DNS record listing for caas dnsprovider 2017-06-02 10:48:00 +02:00
Johannes Zellner 4287d69397 Correctly show dns recrods on view load 2017-06-02 10:47:56 +02:00
Johannes Zellner de328e34d8 Ensure menu is sorted 2017-06-02 10:47:53 +02:00
Johannes Zellner 8d45ce6971 Move email related things into separate view 2017-06-02 10:47:46 +02:00
Johannes Zellner fa3f173e8a Reduce app grid item size a bit to avoid too early overflow 2017-06-02 09:28:22 +02:00
Girish Ramakrishnan 414e9bdf05 Do not use lastBackupId in cleanup logic
lastBackupId is only used as a "message" passing field for apptask restore.

Theoretically, this code somehow protects a race between the cleanup logic
and the restore apptask. this is unlikely to happen and adds unnecessary
complexity.
2017-06-01 14:47:57 -07:00
Girish Ramakrishnan c342e52e7d Record copyLastBackup in the backupdb 2017-06-01 14:08:55 -07:00
Girish Ramakrishnan 78aa9c66f7 Add a note why we do not cleanup more aggressively yet 2017-06-01 10:33:49 -07:00
Girish Ramakrishnan 986ec02ac6 Add debug on what backup is preserved 2017-06-01 09:38:39 -07:00
Girish Ramakrishnan 4e0bb9187a lower case domain in migrate code path 2017-06-01 09:26:03 -07:00
Johannes Zellner 9c8a8571b4 Ensure we lowercase the domain name before consuming it in dns setup
Finally fixes #335
2017-06-01 17:29:46 +02:00
Johannes Zellner 7f30b8de9d Ensure we test domains with lowercase
Fixes #335
2017-06-01 16:42:16 +02:00
Johannes Zellner d1bfa4875a Give the domain name a bit more space
Fixes #340
2017-06-01 15:31:58 +02:00
Johannes Zellner 0250e1ea59 Improve the domain name fitting 2017-06-01 15:31:58 +02:00
Johannes Zellner 924fb337e8 Ensure long domain names are visible in the app grid
Part of #340
2017-06-01 15:31:58 +02:00
Girish Ramakrishnan 0c9dce0c9f redis: set memoryLimit to 600 because only half is RAM 2017-05-31 23:09:47 -07:00
Girish Ramakrishnan 9e9470c6af Fix link to managed hosting 2017-05-31 21:49:28 -07:00
Girish Ramakrishnan 471539d64b CNAME output from dig has trailing dot 2017-05-30 21:14:28 -07:00
Girish Ramakrishnan 95127a868d 0.150.0 changes 2017-05-30 16:23:06 -07:00
Girish Ramakrishnan f34d429052 kill the backup process if it runs for too long 2017-05-30 16:11:12 -07:00
Girish Ramakrishnan 82e53bce36 ensure backups and clean them every 6 hours
also, make sure they don't run at the same time.
2017-05-30 16:04:32 -07:00
Girish Ramakrishnan b04a417cfc Cleanup errored and creating backups
Fixes #330
2017-05-30 15:16:08 -07:00
Girish Ramakrishnan 77641f4b51 Add backupdb.getByState and backupdb.getByTypeAndState
part of #330
2017-05-30 14:30:06 -07:00
Girish Ramakrishnan 765d20c8be Add backup states to track unfinished backups
part of #330
2017-05-30 13:43:30 -07:00
Girish Ramakrishnan d2420de594 refactor backup cleanup logic 2017-05-30 13:43:30 -07:00
Girish Ramakrishnan 8e9da38451 update schema file 2017-05-26 22:23:24 -07:00
Girish Ramakrishnan ddb69eb25c remove native-dns and use dig directly
native-dns module is unmaintained and we keep getting sporadic
errors from that module

Fixes #220
2017-05-26 16:51:05 -07:00
Girish Ramakrishnan 11697f11cf use constants for admin location 2017-05-24 15:41:37 -07:00
Girish Ramakrishnan 35a2a656d3 doc: fix path to node 2017-05-22 12:25:18 -07:00
Girish Ramakrishnan 6fc69c05ca Add noop storage backend
This is sometimes useful when an update gets stuck because of some
bug in backup logic.

Note that you cannot restore from this backend because nothing is
saved.
2017-05-22 10:45:01 -07:00
Girish Ramakrishnan 65cff35be6 Do not dump certs in the log files 2017-05-19 14:39:08 -07:00
Girish Ramakrishnan 7467907c09 Do not dump data in update script since it might have the cert 2017-05-19 14:34:20 -07:00
Girish Ramakrishnan d6c32a2632 tweak redis memory limit based on app's memory 2017-05-18 15:39:38 -07:00
Johannes Zellner 7dc277a80c Give the backup task more memory 150 is often too close to the limit 2017-05-17 14:17:54 +02:00
Girish Ramakrishnan 4881d090f0 disable dnsmasq on ovh 2017-05-16 16:33:43 -07:00
Girish Ramakrishnan 48330423c6 Add 0.140.0 changes 2017-05-15 14:27:24 -07:00
Girish Ramakrishnan 88e844b545 Bump infra version to reconfigure apps for http2 support 2017-05-12 16:25:14 -07:00
Girish Ramakrishnan f45da2efc4 Merge branch 'http2' into 'master'
Add HTTP/2 support to NGINX configs

See merge request !9
2017-05-12 23:23:41 +00:00
Girish Ramakrishnan f422614e7b doc: new app store submission guidelines
Fixes #292
2017-05-11 15:58:02 -07:00
Johannes Zellner d164f881ca Bring back code for alt domain match
There are no actual tests for this yet. Should be added.
2017-05-11 21:55:29 +02:00
Johannes Zellner 4994a5da49 Use -checkhost openssl subcommand 2017-05-11 21:31:01 +02:00
Johannes Zellner 393317d114 Automatically expand the failing dns records 2017-05-11 16:44:18 +02:00
Johannes Zellner 8de940ae36 Condense the dns checks in the settings view 2017-05-11 16:34:15 +02:00
Johannes Zellner 374130d12a Only set local dns server if run on a cloudron 2017-05-11 15:37:44 +02:00
Johannes Zellner 05fcdb0a67 Extract CN from cert with JS
unlike the sed script, this does not rely on the order openssl reports the subject entities
2017-05-11 15:19:02 +02:00
Johannes Zellner 23827974d8 Fix certificate validation to work with new openssl version as well 2017-05-11 14:58:29 +02:00
Girish Ramakrishnan ae2c0f3503 Use new mail container (fix for exec) 2017-05-10 21:58:39 -07:00
Girish Ramakrishnan cbb93ef7ad For low end cloudrons, give a delay between addon starts
Starting them all at once, sometimes hogs cpu/memory too much
and makes the startup scripts of the addons error.

The new addons setup a .setup file to confirm initialization.
In a future commit, we can use those .setup files to check if
the addon has started up instead of a timeout
2017-05-10 15:43:02 -07:00
Girish Ramakrishnan 4d3c6f7caa better error message 2017-05-09 11:24:47 -07:00
Girish Ramakrishnan 4f3c846e2b Add 0.130.3 changes 2017-05-09 09:22:07 -07:00
Girish Ramakrishnan 6ef2f974ae fs: Use key to determine backup extension 2017-05-08 16:03:29 -07:00
Girish Ramakrishnan 180cafad0c Fix restore of unencrypted backups 2017-05-08 15:48:32 -07:00
Girish Ramakrishnan f707f59765 Only ext4 supports as data dir
Fixes #325
2017-05-08 15:25:16 -07:00
Girish Ramakrishnan 969ef3fb11 doc: ensure the data directory exists 2017-05-08 15:16:58 -07:00
Girish Ramakrishnan 7af3f85d7c cloudron-setup: pass --data-dir for all non 0.10x.x versions 2017-05-08 12:04:00 -07:00
Johannes Zellner ffc0a75545 user.get() returns UserErrors 2017-05-08 13:51:19 +02:00
Johannes Zellner d5b5bdb104 Replace old cloud logo with cloudron logo in error and no app pages 2017-05-08 13:51:19 +02:00
Girish Ramakrishnan 8ae65661dd redact the password so it is never displayed in logs 2017-05-05 15:36:47 -07:00
Johannes Zellner 423c4446de Show description if setup fails due to reserved username 2017-05-05 11:54:47 +02:00
Girish Ramakrishnan 53cffd5133 doc: Add note on A record for external domain 2017-05-04 20:49:53 -07:00
Johannes Zellner 15ff1fb093 Add changes for 0.130.2 2017-05-04 21:52:17 +02:00
Johannes Zellner 195d388990 Bring back tldExists() for the dns setup screen 2017-05-04 21:49:27 +02:00
Johannes Zellner d008e871da Add changes for 0.130.1 2017-05-04 14:34:48 +02:00
Johannes Zellner 3e6295de92 Fix form validation for external domains 2017-05-03 15:25:24 +02:00
Ian Fijolek 788004245a Add HTTP/2 support to NGINX configs
This easy fix should improve performance with newer browsers especially
for applications that require many files to be sent over the wire
*cough*Nextcloud11*cough*

NGINX blog post about HTTP/2 support: https://www.nginx.com/blog/nginx-1-9-5/
2017-05-02 22:00:55 +00:00
Girish Ramakrishnan be5221d5b8 bash gymnastics for password with spaces 2017-05-01 11:40:08 -07:00
Girish Ramakrishnan dacc66bb35 Ignore fifo files during backup
Fixes #318
2017-05-01 10:11:41 -07:00
Girish Ramakrishnan 5f26c3a2c1 bump test image version 2017-05-01 09:46:20 -07:00
Girish Ramakrishnan 228af62c39 Add more changes to 0.130.0 2017-05-01 08:03:40 -07:00
Girish Ramakrishnan b531922175 do not quote the argument 2017-04-30 22:17:23 -07:00
Girish Ramakrishnan dad58efc94 Version 0.130.0 changes 2017-04-30 19:30:03 -07:00
Girish Ramakrishnan 7a3d3a3c74 Fix usage of tar.gz API 2017-04-30 17:42:55 -07:00
Girish Ramakrishnan e5c42f2b90 Do a multipart download for slow internet connections
Fixes #317
2017-04-28 17:28:40 -07:00
Girish Ramakrishnan 6cbf64b88e use openssl password only when restore key is non-empty or backup ends with .enc 2017-04-28 15:00:17 -07:00
Girish Ramakrishnan 9635f9aa24 Use key to determine if we should encrypt or not
When encrypting we use the .enc extension. When not encrypting, we
use the plain .tar.gz extension.

Fixes #315
2017-04-28 14:50:20 -07:00
Girish Ramakrishnan 893f9d87bc make s3 upload use queueSize of 1 2017-04-28 14:50:08 -07:00
Girish Ramakrishnan bfda0d4891 drop support for old format backups 2017-04-28 14:45:44 -07:00
Girish Ramakrishnan 65a62f9fbf allow backup prefix to be an empty string 2017-04-26 22:28:52 -07:00
Girish Ramakrishnan 6d74f7e26f doc: fix link for blacklist testing 2017-04-26 21:20:30 -07:00
Girish Ramakrishnan 14ca0c1623 Support naked domains as external location
Let the user add an A record for naked domains

Fixes #272
2017-04-26 15:56:39 -07:00
Girish Ramakrishnan 3f6e8273a7 remove hack to update docker 2017-04-26 15:50:01 -07:00
Girish Ramakrishnan 287b96925a Check if dns flag is in some intermediate state 2017-04-26 12:36:33 -07:00
Girish Ramakrishnan 608cc1e036 remove notification that can never trigger
this code comes from 0601ea2f39
2017-04-25 17:31:14 -07:00
Girish Ramakrishnan 5fa27c4954 show warning if domain config is not working
fixes #302
2017-04-25 17:31:09 -07:00
Girish Ramakrishnan 8deadece05 handle null tlsCert and tlsKey 2017-04-25 17:29:26 -07:00
Girish Ramakrishnan 797dc26f47 ip_based_setup.conf is long gone 2017-04-25 17:29:26 -07:00
Girish Ramakrishnan ddf7823b19 Make box come up regardless of dns config
Part of #302
2017-04-25 16:53:14 -07:00
Girish Ramakrishnan 923e1d0524 Kill more event based logic 2017-04-25 16:36:38 -07:00
Girish Ramakrishnan 339bc71435 Rename onConfigured to onDomainConfigured 2017-04-25 14:09:13 -07:00
Girish Ramakrishnan 863612356d refactor addDnsRecords to take IP as argument 2017-04-25 14:06:13 -07:00
Girish Ramakrishnan 56cdaefecc configureAdmin on dns key change
This allows the user to re-get an admin certificate by updating
the DNS config.

Part of #302
2017-04-25 14:04:27 -07:00
Girish Ramakrishnan 9e611b6ae3 Run scheduler containers in cloudron network as well
This results in:
  box:scheduler Unhandled error:  { Error: (HTTP code 409) unexpected - Conflicting options: dns and the network mode

Part of #307
2017-04-25 12:25:21 -07:00
Girish Ramakrishnan 7e26b4091b use ":" in security-opt is deprecated 2017-04-25 11:41:05 -07:00
Girish Ramakrishnan d7702b96e5 Also set dns args for redis addon
part of #307
2017-04-25 10:13:52 -07:00
Girish Ramakrishnan 41edd3778d Merge branch 'dns-fixes' into 'master'
Set DNS per container rather than the daemon

Closes #307

See merge request !6
2017-04-25 17:06:31 +00:00
Ian Fijolek 0ac69cc6c9 Add DNS args to platform containers 2017-04-25 15:21:23 +00:00
Johannes Zellner fbb01b1ce7 Add 0.120.1 changes 2017-04-25 13:59:41 +02:00
Johannes Zellner a723203b28 Fix typo of missing data argument 2017-04-25 13:48:12 +02:00
Ian Fijolek 851e70be6e Bump version to force creation of new containers 2017-04-20 21:34:31 +00:00
Ian Fijolek f0ba126156 Move dns-search from daemon to client as well
Verified no regression of #130
2017-04-20 21:33:16 +00:00
Ian Fijolek 9dd51575ab Set DNS per container rather than the daemon
All Cloudron containers need to have the nameserver 172.18.0.1. This was
being done at the daemon level, however since there are also iptables
rules restricting access to the nameserver from containers that aren't
on the Cloudron Docker network, this broke DNS for non-Cloudron
containers.

Since the DNS is only required for Cloudron containers in the first
place, this patch specifies 172.18.0.1 as the nameserver when Cloudron
creates a container and reverts the change at the daemon level
2017-04-20 19:02:10 +00:00
77 changed files with 1452 additions and 923 deletions
+44
View File
@@ -842,4 +842,48 @@
* Fix issue where Cloudron's with errored apps won't backup when using fs backend
* Fix DNS check issue where PTR records was read from hosts file
[0.120.1]
* Fix managed Cloudron backup cleanup
[0.130.0]
* Use Cloudron DNS server only for containers created by Cloudron
* Make Cloudron always start even if DNS credentials are invalid
* Show warning if DNS configuration is not valid
* Drop the '.enc' extension for non-encrypted backups
* Do not encrypt backups when the backup key is empty
* Do a multipart S3 download for slow internet connections
* Support naked domains as external location
[0.130.1]
* Fix app configure dialog regression
[0.130.2]
* Fix app configure dialog regression and dns setup screen
[0.130.3]
* Show error message if setup fails due to reserved username
* (security) Do not print password in the logs in the configure route
* Fix restore of unencrypted backups
* Fix bug where FS backups have incorrect extension for unencrypted backups
[0.140.0]
* HTTP2 support
* Condense the dns checks in the settings view
* Document new app store submission guidelines
[0.150.0]
* Disable dnsmasq on OVH
* Scale redis memory based on the app's memory limit
* (security) Do not print the ssl cert in debug logs
* Add noop storage backend to temporarily disable backups
* Replace native-dns module with dig to prevent spurious crashes
* Cleanup unfinished and errored backups
* Set a timelimit of 4 hours for backup to finish
[0.160.0]
* Fix disk graphs when using device mapper
* Prevent email view from flickering
* Prepare for 1.0
[0.160.1]
* Improved update notification
+7 -3
View File
@@ -46,10 +46,14 @@ Try our demo at https://my-demo.cloudron.me (username: cloudron password: cloudr
## Installing
You can install the Cloudron platform on your own server or get a managed server
from cloudron.io.
from cloudron.io. In either case, the Cloudron platform will keep your server and
apps up-to-date and secure.
* [Selfhosting](https://cloudron.io/references/selfhosting.html)
* [Managed Hosting](https://cloudron.io/pricing.html)
* [Selfhosting](https://cloudron.io/references/selfhosting.html) - [Pricing](https://cloudron.io/pricing.html)
* [Managed Hosting](https://cloudron.io/managed.html)
The wiki has instructions on how you can install and update the Cloudron and the
apps from source.
## Documentation
+5
View File
@@ -95,3 +95,8 @@ fi
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
systemctl stop bind9 || true
systemctl disable bind9 || true
# on ovh images dnsmasq seems to run by default
systemctl stop dnsmasq || true
systemctl disable dnsmasq || true
+11 -12
View File
@@ -1,25 +1,25 @@
# Introduction
The Cloudron platform is designed to easily install and run web applications.
The application architecture is designed to let the Cloudron take care of system
The application architecture is designed to let the Cloudron take care of system
operations like updates, backups, firewalls, domain management, certificate management
etc. This allows app developers to focus on their application logic instead of deployment.
At a high level, an application provides an `image` and a `manifest`. The image is simply
a docker image that is a bundle of the application code and it's dependencies. The manifest
a docker image that is a bundle of the application code and it's dependencies. The manifest
file specifies application runtime requirements like database type and authentication scheme.
It also provides meta information for display purposes in the [Cloudron Store](/appstore.html)
It also provides meta information for display purposes in the [Cloudron Store](/appstore.html)
like the title, icon and pricing.
Web applications like blogs, wikis, password managers, code hosting, document editing,
file syncers, notes, email, forums are a natural fit for the Cloudron. Decentralized "social"
Web applications like blogs, wikis, password managers, code hosting, document editing,
file syncers, notes, email, forums are a natural fit for the Cloudron. Decentralized "social"
networks are also good app candidates for the Cloudron.
# Image
Application images are created using [Docker](https://www.docker.io). Docker provides a way
to package (and containerize) the application as a filesystem which contains it's code, system libraries
and just about anything the app requires. This flexible approach allows the application to use just
to package (and containerize) the application as a filesystem which contains it's code, system libraries
and just about anything the app requires. This flexible approach allows the application to use just
about any language or framework.
Application images are instantiated as `containers`. Cloudron can run one or more isolated instances
@@ -77,12 +77,11 @@ Authentication strategies include OAuth 2.0, LDAP or Simple Auth. See the
Authorizing users is application specific and it is only authentication that is delegated to the
Cloudron.
# Cloudron Store
# Cloudron App Library
Cloudron Store provides a market place to publish and optionally monetize your app. Submitting to the
Cloudron Store enables any Cloudron user to discover, purchase and install your application with
a few clicks.
Cloudron App Library provides a market place to publish your app.
Submitting to the app library enables any Cloudron user to discover and install your application with a few clicks.
# What next?
* [Package an existing app for the Cloudron](/tutorials/packaging.html)
* [Package an existing app for the Cloudron](/tutorials/packaging.html)
+6 -7
View File
@@ -62,11 +62,6 @@ Be sure to check the "use the distribution kernel" checkbox in the personalized
Since Linode does not manage SSH keys, be sure to add the public key to
`/root/.ssh/authorized_keys`.
### Scaleway
Use the [boot script](https://github.com/scaleway-community/scaleway-docker/issues/2) to
enable memory accouting.
## Run setup
SSH into your server and run the following commands:
@@ -95,7 +90,8 @@ Initially a self-signed one is provided, which can be overwritten later in the a
This may be useful for non-public installations.
* `--data-dir` is the path where Cloudron will store platform and application data.
* `--data-dir` is the path where Cloudron will store platform and application data. Note: data
directory must be an `ext4` filesystem.
Optional arguments used for update and restore:
@@ -275,7 +271,7 @@ reputation should be easy to get back.
* 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/) and [here](www.blk.mx).
* Check if your IP is listed in any DNSBL list [here](http://multirbl.valli.org/) and [here](http://www.blk.mx).
In most cases, you can apply for removal of your IP by filling out a form at the DNSBL manager site.
* When using wildcard or manual DNS backends, you have to setup the DMARC, MX records manually.
@@ -471,6 +467,8 @@ The goal of rate limits is to prevent password brute force attacks.
If you are installing a brand new Cloudron, you can configure the data directory
that Cloudron uses by passing the `--data-dir` option to `cloudron-setup`.
Note: data directory must be an `ext4` filesystem.
```
./cloudron-setup --provider <digitalocean|ec2|generic|scaleway> --data-dir /var/cloudrondata
```
@@ -482,6 +480,7 @@ to a new location as follows (`DATA_DIR` is the location to move your data):
systemctl stop cloudron.target
systemctl stop docker
DATA_DIR="/var/data"
mkdir -p "${DATA_DIR}"
mv /home/yellowtent/appsdata "${DATA_DIR}"
ln -s "${DATA_DIR}/appsdata" /home/yellowtent/appsdata
mv /home/yellowtent/platformdata "${DATA_DIR}"
+2 -2
View File
@@ -160,8 +160,8 @@ domain. For this, open the app's configure dialog and choose `External Domain` i
<img src="/docs/img/app_external_domain.png" class="shadow">
This dialog will suggest you to add a `CNAME` record. Once you setup a CNAME record with your DNS provider,
the app will be accessible from that external domain.
This dialog will suggest you to add a `CNAME` record (for subdomains) or an `A` record (for naked domains).
Once you setup a record with your DNS provider, the app will be accessible from that external domain.
## Entire Cloudron on a custom domain
+24 -12
View File
@@ -83,7 +83,7 @@ FROM cloudron/base:0.10.0
ADD server.js /app/code/server.js
CMD [ "/usr/local/node-4.4.7/bin/node", "/app/code/server.js" ]
CMD [ "/usr/local/node-4.7.3/bin/node", "/app/code/server.js" ]
```
The `FROM` command specifies that we want to start off with Cloudron's [base image](/references/baseimage.html).
@@ -94,7 +94,7 @@ The `ADD` command copies the source code of the app into the directory `/app/cod
about the `/app/code` directory and it is merely a convention we use to store the application code.
The `CMD` command specifies how to run the server. The base image already contains many different versions of
node.js. We use Node 4.4.7 here.
node.js. We use Node 4.7.3 here.
This Dockerfile can be built and run locally as:
```
@@ -179,7 +179,7 @@ Initiate a build using ```cloudron build```:
$ cloudron build
Building io.cloudron.tutorial@0.0.1
Appstore login:
cloudron.io login:
Email: ramakrishnan.girish@gmail.com # cloudron.io account
Password: # Enter password
Login successful.
@@ -627,14 +627,28 @@ export JAVA_OPTS="-XX:MaxRAM=${LIMIT}M"
java ${JAVA_OPTS} -jar ...
```
# Beta Testing
# App Store
## Metadata
## Requirements
Publishing to the Cloudron Store requires apps to have meta data specified in the `CloudronManifest.json`.
The Cloudron Store is a mechanism to share your app with others who use Cloudron. Currently, to ensure that
apps are maintained, secure and well supported there are some restrictions imposed on apps submitted to
the Cloudron Store. See [#292](https://git.cloudron.io/cloudron/box/issues/292) and [#327](https://git.cloudron.io/cloudron/box/issues/327) for an in-depth discussion.
The `cloudron` tool will notify if any such information is missing, prior to uploading.
See more information for each field [here](/references/manifest.html).
The following criteria must be met before submitting an app for review:
* You must be willing to relocate your app packaging code to the [Cloudron Git Repo](https://git.cloudron.io/cloudron/).
* Contributed apps must have browser tests. You can see the various [app repos](https://git.cloudron.io/cloudron/) to get an idea on how to write these tests. The Cloudron team can help you write the tests.
* For all practical purposes, you are the maintainer of the app and Cloudron team will not commit to the repo
directly. Any changes will be submitted as Merge Requests.
* You agree that the Cloudron team can take over the responsibility of progressing the app further if you become unresponsive (48 hours), lose interest, lack time etc. Please send us an email if your priorities change.
* You must sign the [Cloudron CLA](https://cla.cloudron.io/).
As a token of our appreciation, 3rd party app authors can use the Cloudron for personal or business use for free.
## Upload for Testing
@@ -651,7 +665,7 @@ Cloudron to check if the icon, description and other details appear correctly.
Other Cloudron users can install your app on their Cloudron's using
`cloudron install --appstore-id <appid@version>`.
# Publishing
## Publishing
Once you are satisfied with the beta testing, you can submit it for review.
@@ -661,9 +675,7 @@ Once you are satisfied with the beta testing, you can submit it for review.
The cloudron.io team will review the app and publish the app to the store.
# Updating the app
## Versioning
## Versioning and Updates
To create an update for an app, simply bump up the [semver version](/references/manifest.html#version) field in
the manifest and publish a new version to the store.
+2 -2
View File
@@ -102,7 +102,7 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
FOREIGN KEY(appId) REFERENCES apps(id));
CREATE TABLE IF NOT EXISTS backups(
filename VARCHAR(128) NOT NULL,
id VARCHAR(128) NOT NULL,
creationTime TIMESTAMP,
version VARCHAR(128) NOT NULL, /* app version or box version */
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
@@ -110,7 +110,7 @@ CREATE TABLE IF NOT EXISTS backups(
state VARCHAR(16) NOT NULL,
restoreConfigJson TEXT, /* JSON including the manifest of the backed up app */
PRIMARY KEY (filename));
PRIMARY KEY (id));
CREATE TABLE IF NOT EXISTS eventlog(
id VARCHAR(128) NOT NULL,
+5 -49
View File
@@ -260,11 +260,6 @@
"from": "bignumber.js@3.1.2",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-3.1.2.tgz"
},
"binaryheap": {
"version": "0.0.3",
"from": "binaryheap@>=0.0.3",
"resolved": "http://registry.npmjs.org/binaryheap/-/binaryheap-0.0.3.tgz"
},
"bl": {
"version": "1.2.0",
"from": "bl@>=1.0.0 <2.0.0",
@@ -336,28 +331,6 @@
"from": "buffer-shims@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz"
},
"buffercursor": {
"version": "0.0.12",
"from": "buffercursor@>=0.0.12",
"resolved": "http://registry.npmjs.org/buffercursor/-/buffercursor-0.0.12.tgz",
"dependencies": {
"assert-plus": {
"version": "1.0.0",
"from": "assert-plus@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz"
},
"extsprintf": {
"version": "1.3.0",
"from": "extsprintf@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz"
},
"verror": {
"version": "1.9.0",
"from": "verror@>=1.4.0 <2.0.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.9.0.tgz"
}
}
},
"buildmail": {
"version": "2.0.0",
"from": "buildmail@>=2.0.0 <3.0.0",
@@ -2876,28 +2849,6 @@
"from": "nan@>=2.3.2 <3.0.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz"
},
"native-dns": {
"version": "0.7.0",
"from": "native-dns@>=0.7.0 <0.8.0",
"resolved": "https://registry.npmjs.org/native-dns/-/native-dns-0.7.0.tgz",
"dependencies": {
"ipaddr.js": {
"version": "0.1.9",
"from": "ipaddr.js@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-0.1.9.tgz"
}
}
},
"native-dns-cache": {
"version": "0.0.2",
"from": "native-dns-cache@>=0.0.2 <0.1.0",
"resolved": "http://registry.npmjs.org/native-dns-cache/-/native-dns-cache-0.0.2.tgz"
},
"native-dns-packet": {
"version": "0.1.1",
"from": "native-dns-packet@>=0.1.1 <0.2.0",
"resolved": "http://registry.npmjs.org/native-dns-packet/-/native-dns-packet-0.1.1.tgz"
},
"natives": {
"version": "1.1.0",
"from": "natives@>=1.1.0 <2.0.0",
@@ -4129,6 +4080,11 @@
"from": "rndm@1.2.0",
"resolved": "https://registry.npmjs.org/rndm/-/rndm-1.2.0.tgz"
},
"s3-block-read-stream": {
"version": "0.2.0",
"from": "s3-block-read-stream@latest",
"resolved": "https://registry.npmjs.org/s3-block-read-stream/-/s3-block-read-stream-0.2.0.tgz"
},
"safe-buffer": {
"version": "5.0.1",
"from": "safe-buffer@>=5.0.1 <6.0.0",
+1 -1
View File
@@ -43,7 +43,6 @@
"morgan": "^1.7.0",
"multiparty": "^4.1.2",
"mysql": "^2.7.0",
"native-dns": "^0.7.0",
"node-uuid": "^1.4.3",
"nodemailer": "^1.3.0",
"nodemailer-smtp-transport": "^1.0.3",
@@ -58,6 +57,7 @@
"password-generator": "^2.0.2",
"progress-stream": "^2.0.0",
"proxy-middleware": "^0.13.0",
"s3-block-read-stream": "^0.2.0",
"safetydance": "^0.2.0",
"semver": "^4.3.6",
"showdown": "^1.6.0",
+9 -5
View File
@@ -46,6 +46,7 @@ dnsProvider="manual"
tlsProvider="le-prod"
requestedVersion=""
apiServerOrigin="https://api.cloudron.io"
webServerOrigin="https://cloudron.io"
dataJson=""
prerelease="false"
sourceTarballUrl=""
@@ -55,7 +56,7 @@ baseDataDir=""
# TODO this is still there for the restore case, see other occasions below
versionsUrl="https://s3.amazonaws.com/prod-cloudron-releases/versions.json"
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,api-server:,dns-provider:,env:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
args=$(getopt -o "" -l "domain:,help,skip-baseimage-init,data:,data-dir:,provider:,encryption-key:,restore-url:,tls-provider:,version:,dns-provider:,env:,prerelease,skip-reboot,source-url:" -n "$0" -- "$@")
eval set -- "${args}"
while true; do
@@ -72,16 +73,17 @@ while true; do
if [[ "$2" == "dev" ]]; then
versionsUrl="https://s3.amazonaws.com/dev-cloudron-releases/versions.json"
apiServerOrigin="https://api.dev.cloudron.io"
webServerOrigin="https://dev.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
elif [[ "$2" == "staging" ]]; then
versionsUrl="https://s3.amazonaws.com/staging-cloudron-releases/versions.json"
apiServerOrigin="https://api.staging.cloudron.io"
webServerOrigin="https://staging.cloudron.io"
tlsProvider="le-staging"
prerelease="true"
fi
shift 2;;
--api-server) apiServerOrigin="$2"; shift 2;;
--skip-baseimage-init) initBaseImage="false"; shift;;
--skip-reboot) rebootServer="false"; shift;;
--data) dataJson="$2"; shift 2;;
@@ -187,6 +189,7 @@ if [[ -z "${dataJson}" ]]; then
"fqdn": "${domain}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"tlsConfig": {
"provider": "${tlsProvider}"
},
@@ -213,6 +216,7 @@ EOF
"fqdn": "${domain}",
"provider": "${provider}",
"apiServerOrigin": "${apiServerOrigin}",
"webServerOrigin": "${webServerOrigin}",
"restore": {
"url": "${restoreUrl}",
"key": "${encryptionKey}"
@@ -246,13 +250,13 @@ fi
echo "=> Installing version ${version} (this takes some time) ..."
echo "${data}" > "${DATA_FILE}"
# poor mans semver
if [[ ${version} == "0.11"* ]]; then
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
if [[ ${version} == "0.10"* ]]; then
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
else
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" --data-dir "${baseDataDir}" &>> "${LOG_FILE}"; then
echo "Failed to install cloudron. See ${LOG_FILE} for details"
exit 1
fi
+1 -32
View File
@@ -73,38 +73,7 @@ fi
cd /root
echo "==> installer: updating packages"
if [[ $(docker version --format {{.Client.Version}}) != "17.03.1-ce" ]]; then
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_17.03.1~ce-0~ubuntu-xenial_amd64.deb -o /tmp/docker.deb
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
if [[ $(md5sum /tmp/docker.deb | cut -d' ' -f1) != "d6d175900edd243abbdb253990b2fe59" ]]; then
echo "docker binary download is corrupt"
exit 5
fi
echo "Waiting for all dpkg tasks to finish..."
while fuser /var/lib/dpkg/lock; do
sleep 1
done
while ! dpkg --force-confold --configure -a; do
echo "Failed to fix packages. Retry"
sleep 1
done
if dpkg --status docker-engine; then
while ! apt-get remove -y --allow-change-held-packages docker-engine; do
echo "Failed to remove outdated docker-engine. Retry"
sleep 1
done
fi
while ! apt install -y /tmp/docker.deb; do
echo "Failed to install docker. Retry"
sleep 1
done
rm /tmp/docker.deb
fi
# add logic to update apt packages here
echo "==> installer: switching the box code"
rm -rf "${BOX_SRC_DIR}"
+2
View File
@@ -61,7 +61,9 @@ while true; do
[[ "${arg_is_demo}" == "" ]] && arg_is_demo="false"
arg_tls_cert=$(echo "$2" | $json tlsCert)
[[ "${arg_tls_cert}" == "null" ]] && arg_tls_cert=""
arg_tls_key=$(echo "$2" | $json tlsKey)
[[ "${arg_tls_key}" == "null" ]] && arg_tls_key=""
arg_token=$(echo "$2" | $json token)
arg_provider=$(echo "$2" | $json provider)
+10 -3
View File
@@ -42,7 +42,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/dockerd -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" > "${temp_file}"
systemctl enable docker
# restart docker if options changed
@@ -208,10 +208,17 @@ mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
if [[ -n "${arg_restore_url}" ]]; then
set_progress "30" "Downloading restore data"
echo "==> Downloading backup: ${arg_restore_url} and key: ${arg_restore_key}"
decrypt=""
if [[ "${arg_restore_url}" == *.tar.gz.enc || -n "${arg_restore_key}" ]]; then
echo "==> Downloading encrypted backup: ${arg_restore_url} and key: ${arg_restore_key}"
decrypt=(openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}")
else
echo "==> Downloading backup: ${arg_restore_url}"
decrypt=(cat -)
fi
while true; do
if $curl -L "${arg_restore_url}" | openssl aes-256-cbc -d -nosalt -pass "pass:${arg_restore_key}" \
if $curl -L "${arg_restore_url}" | "${decrypt[@]}" \
| tar -zxf - --overwrite --transform="s,^box/\?,boxdata/," --transform="s,^mail/\?,platformdata/mail/," --show-transformed-names -C "${HOME_DIR}"; then break; fi
echo "Failed to download data, trying again"
done
+2 -2
View File
@@ -6,10 +6,10 @@ map $http_upgrade $connection_upgrade {
server {
<% if (vhost) { %>
listen 443;
listen 443 http2;
server_name <%= vhost %>;
<% } else { %>
listen 443 default_server;
listen 443 http2 default_server;
<% } %>
ssl on;
+16 -2
View File
@@ -20,6 +20,7 @@ var appdb = require('./appdb.js'),
async = require('async'),
clients = require('./clients.js'),
config = require('./config.js'),
constants = require('./constants.js'),
ClientsError = clients.ClientsError,
debug = require('debug')('box:addons'),
docker = require('./docker.js'),
@@ -632,12 +633,25 @@ function setupRedis(app, options, callback) {
if (!safe.fs.mkdirSync(redisDataDir) && safe.error.code !== 'EEXIST') return callback(new Error('Error creating redis data dir:' + safe.error));
// Compute redis memory limit based on app's memory limit (this is arbitrary)
var memoryLimit = app.memoryLimit || app.manifest.memoryLimit || 0;
if (memoryLimit === -1) { // unrestricted (debug mode)
memoryLimit = 0;
} else if (memoryLimit === 0 || memoryLimit <= (2 * 1024 * 1024 * 1024)) { // less than 2G (ram+swap)
memoryLimit = 150 * 1024 * 1024; // 150m
} else {
memoryLimit = 600 * 1024 * 1024; // 600m
}
const tag = infra.images.redis.tag, redisName = 'redis-' + app.id;
const cmd = `docker run --restart=always -d --name=${redisName} \
--net cloudron \
--net-alias ${redisName} \
-m 100m \
--memory-swap 150m \
-m ${memoryLimit/2} \
--memory-swap ${memoryLimit} \
--dns 172.18.0.1 \
--dns-search=. \
-v ${redisVarsFile}:/etc/redis/redis_vars.sh:ro \
-v ${redisDataDir}:/var/lib/redis:rw \
--read-only -v /tmp -v /run ${tag}`;
+43 -35
View File
@@ -15,6 +15,7 @@ exports = module.exports = {
var assert = require('assert'),
config = require('./config.js'),
debug = require('debug')('box:appstore'),
eventlog = require('./eventlog.js'),
os = require('os'),
settings = require('./settings.js'),
superagent = require('superagent'),
@@ -127,45 +128,52 @@ function sendAliveStatus(data, callback) {
settings.getAll(function (error, result) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var backendSettings = {
dnsConfig: {
provider: result[settings.DNS_CONFIG_KEY].provider,
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
},
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY]
};
eventlog.getAllPaged(eventlog.ACTION_USER_LOGIN, null, 1, 1, function (error, loginEvents) {
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
var data = {
domain: config.fqdn(),
version: config.version(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
}
};
var backendSettings = {
dnsConfig: {
provider: result[settings.DNS_CONFIG_KEY].provider,
wildcard: result[settings.DNS_CONFIG_KEY].provider === 'manual' ? result[settings.DNS_CONFIG_KEY].wildcard : undefined
},
tlsConfig: {
provider: result[settings.TLS_CONFIG_KEY].provider
},
backupConfig: {
provider: result[settings.BACKUP_CONFIG_KEY].provider
},
mailConfig: {
enabled: result[settings.MAIL_CONFIG_KEY].enabled
},
autoupdatePattern: result[settings.AUTOUPDATE_PATTERN_KEY],
timeZone: result[settings.TIME_ZONE_KEY],
};
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
var data = {
domain: config.fqdn(),
version: config.version(),
provider: config.provider(),
backendSettings: backendSettings,
machine: {
cpus: os.cpus(),
totalmem: os.totalmem()
},
events: {
lastLogin: loginEvents[0] ? (new Date(loginEvents[0].creationTime).getTime()) : 0
}
};
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
getAppstoreConfig(function (error, appstoreConfig) {
if (error) return callback(error);
callback(null);
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
callback(null);
});
});
});
});
+13 -3
View File
@@ -50,6 +50,7 @@ var addons = require('./addons.js'),
subdomains = require('./subdomains.js'),
superagent = require('superagent'),
sysinfo = require('./sysinfo.js'),
tld = require('tldjs'),
util = require('util'),
_ = require('underscore');
@@ -307,7 +308,16 @@ function waitForAltDomainDnsPropagation(app, callback) {
// try for 10 minutes before giving up. this allows the user to "reconfigure" the app in the case where
// an app has an external domain and cloudron is migrated to custom domain.
subdomains.waitForDns(app.altDomain, config.appFqdn(app.location), 'CNAME', { interval: 10000, times: 60 }, callback);
var isNakedDomain = tld.getDomain(app.altDomain) === app.altDomain;
if (isNakedDomain) { // check naked domains with A record since CNAME records don't work there
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
subdomains.waitForDns(app.altDomain, ip, 'A', { interval: 10000, times: 60 }, callback);
});
} else {
subdomains.waitForDns(app.altDomain, config.appFqdn(app.location) + '.', 'CNAME', { interval: 10000, times: 60 }, callback);
}
}
// updates the app object and the database
@@ -404,7 +414,7 @@ function install(app, callback) {
updateApp.bind(null, app, { installationProgress: '85, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain CNAME setup' }),
updateApp.bind(null, app, { installationProgress: '90, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app), // required when restoring and !lastBackupId
updateApp.bind(null, app, { installationProgress: '95, Configure nginx' }),
@@ -492,7 +502,7 @@ function configure(app, callback) {
updateApp.bind(null, app, { installationProgress: '80, Waiting for DNS propagation' }),
exports._waitForDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain CNAME setup' }),
updateApp.bind(null, app, { installationProgress: '85, Waiting for External Domain setup' }),
exports._waitForAltDomainDnsPropagation.bind(null, app),
updateApp.bind(null, app, { installationProgress: '90, Configuring Nginx' }),
+1 -1
View File
@@ -100,7 +100,7 @@ function initialize(callback) {
var info = { scope: token.scope };
user.get(token.identifier, function (error, user) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
if (error) return callback(error);
callback(null, user, info);
+46 -3
View File
@@ -10,9 +10,13 @@ var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'st
exports = module.exports = {
add: add,
getPaged: getPaged,
getByTypeAndStatePaged: getByTypeAndStatePaged,
getByTypePaged: getByTypePaged,
get: get,
del: del,
update: update,
getByAppIdPaged: getByAppIdPaged,
_clear: clear,
@@ -21,6 +25,8 @@ exports = module.exports = {
BACKUP_TYPE_BOX: 'box',
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
BACKUP_STATE_CREATING: 'creating',
BACKUP_STATE_ERROR: 'error'
};
function postProcess(result) {
@@ -32,14 +38,31 @@ function postProcess(result) {
delete result.restoreConfigJson;
}
function getPaged(type, page, perPage, callback) {
function getByTypeAndStatePaged(type, state, page, perPage, callback) {
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
assert.strictEqual(typeof state, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, exports.BACKUP_STATE_NORMAL, (page-1)*perPage, perPage ], function (error, results) {
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
callback(null, results);
});
}
function getByTypePaged(type, page, perPage, callback) {
assert(type === exports.BACKUP_TYPE_APP || type === exports.BACKUP_TYPE_BOX);
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,?',
[ type, (page-1)*perPage, perPage ], function (error, results) {
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
results.forEach(function (result) { postProcess(result); });
@@ -102,6 +125,26 @@ function add(backup, callback) {
});
}
function update(id, backup, callback) {
assert.strictEqual(typeof id, 'string');
assert.strictEqual(typeof backup, 'object');
assert.strictEqual(typeof callback, 'function');
var fields = [ ], values = [ ];
for (var p in backup) {
fields.push(p + ' = ?');
values.push(backup[p]);
}
values.push(id);
database.query('UPDATE backups SET ' + fields.join(', ') + ' WHERE id = ?', values, function (error) {
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
callback(null);
});
}
function clear(callback) {
assert.strictEqual(typeof callback, 'function');
+158 -94
View File
@@ -5,7 +5,7 @@ exports = module.exports = {
testConfig: testConfig,
getPaged: getPaged,
getByStatePaged: getByStatePaged,
getByAppIdPaged: getByAppIdPaged,
getRestoreConfig: getRestoreConfig,
@@ -35,6 +35,7 @@ var addons = require('./addons.js'),
filesystem = require('./storage/filesystem.js'),
locker = require('./locker.js'),
mailer = require('./mailer.js'),
noop = require('./storage/noop.js'),
path = require('path'),
paths = require('./paths.js'),
progress = require('./progress.js'),
@@ -90,6 +91,7 @@ function api(provider) {
case 's3': return s3;
case 'filesystem': return filesystem;
case 'minio': return s3;
case 'noop': return noop;
default: return null;
}
}
@@ -104,12 +106,13 @@ function testConfig(backupConfig, callback) {
api(backupConfig.provider).testConfig(backupConfig, callback);
}
function getPaged(page, perPage, callback) {
function getByStatePaged(state, page, perPage, callback) {
assert.strictEqual(typeof state, 'string');
assert(typeof page === 'number' && page > 0);
assert(typeof perPage === 'number' && perPage > 0);
assert.strictEqual(typeof callback, 'function');
backupdb.getPaged(backupdb.BACKUP_TYPE_BOX, page, perPage, function (error, results) {
backupdb.getByTypeAndStatePaged(backupdb.BACKUP_TYPE_BOX, state, page, perPage, function (error, results) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, results);
@@ -152,15 +155,29 @@ function copyLastBackup(app, manifest, prefix, callback) {
var timestamp = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
var newBackupId = util.format('%s/app_%s_%s_v%s', prefix, app.id, timestamp, manifest.version);
var restoreConfig = apps.getAppConfig(app);
restoreConfig.manifest = manifest;
settings.getBackupConfig(function (error, backupConfig) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('copyLastBackup: copying backup %s to %s', app.lastBackupId, newBackupId);
api(backupConfig.provider).copyBackup(backupConfig, app.lastBackupId, newBackupId, function (error) {
if (error) return callback(error);
backupdb.add({ id: newBackupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, newBackupId);
api(backupConfig.provider).copyBackup(backupConfig, app.lastBackupId, newBackupId, function (copyBackupError) {
const state = copyBackupError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
debugApp(app, 'copyLastBackup: %s done with state %s', newBackupId, state);
backupdb.update(newBackupId, { state: state }, function (error) {
if (copyBackupError) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, copyBackupError.message));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, newBackupId);
});
});
});
});
}
@@ -170,7 +187,13 @@ function runBackupTask(backupId, appId, callback) {
assert(appId === null || typeof backupId === 'string');
assert.strictEqual(typeof callback, 'function');
shell.sudo('backup' + (appId ? 'App' : 'Box'), [ NODE_CMD, BACKUPTASK_CMD, backupId ].concat(appId ? [ appId ] : [ ]), function (error) {
var killTimerId = null;
var cp = shell.sudo('backup' + (appId ? 'App' : 'Box'), [ NODE_CMD, BACKUPTASK_CMD, backupId ].concat(appId ? [ appId ] : [ ]), function (error) {
clearTimeout(killTimerId);
cp = null;
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'backuptask crashed'));
} else if (error && error.code === 50) { // exited with error
@@ -180,6 +203,11 @@ function runBackupTask(backupId, appId, callback) {
callback();
});
killTimerId = setTimeout(function () {
debug('runBackupTask: backup task taking too long. killing');
cp.kill();
}, 4 * 60 * 60 * 1000); // 4 hours
}
function backupBoxWithAppBackupIds(appBackupIds, prefix, callback) {
@@ -201,18 +229,22 @@ function backupBoxWithAppBackupIds(appBackupIds, prefix, callback) {
shell.exec('backupBox', '/bin/bash', mysqlDumpArgs, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
runBackupTask(backupId, null /* appId */, function (error) {
if (error) return callback(error);
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, restoreConfig: null }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debug('backupBoxWithAppBackupIds: success');
runBackupTask(backupId, null /* appId */, function (backupTaskError) {
const state = backupTaskError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
debug('backupBoxWithAppBackupIds: %s', state);
backupdb.add({ id: backupId, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds, restoreConfig: null }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
backupdb.update(backupId, { state: state }, function (error) {
if (backupTaskError) return callback(backupTaskError);
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
// FIXME this is only needed for caas, hopefully we can remove that in the future
api(backupConfig.provider).backupDone(backupId, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, backupId);
// FIXME this is only needed for caas, hopefully we can remove that in the future
api(backupConfig.provider).backupDone(backupId, appBackupIds, function (error) {
if (error) return callback(error);
callback(null, backupId);
});
});
});
});
@@ -248,15 +280,20 @@ function createNewAppBackup(app, manifest, prefix, callback) {
addons.backupAddons(app, manifest.addons, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
runBackupTask(backupId, app.id, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debugApp(app, 'createNewAppBackup: %s done', backupId);
runBackupTask(backupId, app.id, function (backupTaskError) {
const state = backupTaskError ? backupdb.BACKUP_STATE_ERROR : backupdb.BACKUP_STATE_NORMAL;
backupdb.add({ id: backupId, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ], restoreConfig: restoreConfig }, function (error) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
debugApp(app, 'createNewAppBackup: %s done with state %s', backupId, state);
callback(null, backupId);
backupdb.update(backupId, { state: state }, function (error) {
if (backupTaskError) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, backupTaskError.message));
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
callback(null, backupId);
});
});
});
});
@@ -389,7 +426,7 @@ function ensureBackup(auditSource, callback) {
debug('ensureBackup: %j', auditSource);
getPaged(1, 1, function (error, backups) {
getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, backups) {
if (error) {
debug('Unable to list backups', error);
return callback(error); // no point trying to backup if appstore is down
@@ -421,6 +458,101 @@ function restoreApp(app, addonsToRestore, backupId, callback) {
});
}
function cleanupAppBackups(backupConfig, referencedAppBackups, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert(Array.isArray(referencedAppBackups));
assert.strictEqual(typeof callback, 'function');
const now = new Date();
// we clean app backups of any state because the ones to keep are determined by the box cleanup code
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, appBackups) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.eachSeries(appBackups, function iterator(backup, iteratorDone) {
if (referencedAppBackups.indexOf(backup.id) !== -1) return iteratorDone();
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
debug('cleanup: removing %s', backup.id);
api(backupConfig.provider).removeBackups(backupConfig, [ backup.id ], function (error) {
if (error) {
debug('cleanup: error removing backup %j : %s', backup, error.message);
iteratorDone();
}
backupdb.del(backup.id, function (error) {
if (error) debug('cleanup: error removing from database', error);
else debug('cleanup: removed %s', backup.id);
iteratorDone();
});
});
}, function () {
debug('cleanup: done cleaning app backups');
callback();
});
});
}
function cleanupBoxBackups(backupConfig, callback) {
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof callback, 'function');
const now = new Date();
var referencedAppBackups = [];
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, boxBackups) {
if (error) return callback(error);
if (boxBackups.length === 0) return callback(null, []);
// search for the first valid backup
var i;
for (i = 0; i < boxBackups.length; i++) {
if (boxBackups[i].state === backupdb.BACKUP_STATE_NORMAL) break;
}
// keep the first valid backup
if (i !== boxBackups.length) {
debug('cleanup: preserving box backup %j', boxBackups[i]);
referencedAppBackups = boxBackups[i].dependsOn;
boxBackups.splice(i, 1);
} else {
debug('cleanup: no box backup to preserve');
}
async.eachSeries(boxBackups, function iterator(backup, iteratorDone) {
referencedAppBackups = referencedAppBackups.concat(backup.dependsOn);
// TODO: errored backups should probably be cleaned up before retention time, but we will
// have to be careful not to remove any backup currently being created
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
debug('cleanup: removing %s', backup.id);
var backupIds = [].concat(backup.id, backup.dependsOn);
api(backupConfig.provider).removeBackups(backupConfig, backupIds, function (error) {
if (error) {
debug('cleanup: error removing backup %j : %s', backup, error.message);
iteratorDone();
}
backupdb.del(backup.id, function (error) {
if (error) debug('cleanup: error removing from database', error);
else debug('cleanup: removed %j', backupIds);
iteratorDone();
});
});
}, function () {
return callback(null, referencedAppBackups);
});
});
}
function cleanup(callback) {
assert(!callback || typeof callback === 'function'); // callback is null when called from cronjob
@@ -434,80 +566,12 @@ function cleanup(callback) {
return callback();
}
getPaged(1, 1000, function (error, result) {
cleanupBoxBackups(backupConfig, function (error, referencedAppBackups) {
if (error) return callback(error);
if (result.length === 0) return callback();
debug('cleanup: done cleaning box backups');
// ensure we keep at least the last backup to ensure we have one if backup creation failed for some reason
var referencedAppBackups = result[0].dependsOn;
result = result.slice(1);
var now = new Date();
async.eachSeries(result, function iterator(backup, iteratorDone) {
referencedAppBackups = referencedAppBackups.concat(backup.dependsOn);
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
debug('cleanup: removing %s', backup.id);
var backupIds = [].concat(backup.id, backup.dependsOn);
api(backupConfig.provider).removeBackups(backupConfig, backupIds, function (error) {
if (error) {
debug('cleanup: error removing backup %j : %s', backup, error.message);
iteratorDone();
}
backupdb.del(backup.id, function (error) {
if (error) debug('cleanup: error removing from database', error);
else debug('cleanup: removed %j', backupIds);
iteratorDone();
});
});
}, function () {
debug('cleanup: done cleaning box backups');
apps.getAll(function (error, result) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
result.forEach(function (app) {
if (!app.lastBackupId) return;
referencedAppBackups.push(app.lastBackupId);
});
backupdb.getPaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, result) {
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
async.eachSeries(result, function iterator(backup, iteratorDone) {
if (referencedAppBackups.indexOf(backup.id) !== -1) return iteratorDone();
if ((now - backup.creationTime) < (backupConfig.retentionSecs * 1000)) return iteratorDone();
debug('cleanup: removing %s', backup.id);
api(backupConfig.provider).removeBackups(backupConfig, [ backup.id ], function (error) {
if (error) {
debug('cleanup: error removing backup %j : %s', backup, error.message);
iteratorDone();
}
backupdb.del(backup.id, function (error) {
if (error) debug('cleanup: error removing from database', error);
else debug('cleanup: removed %s', backup.id);
iteratorDone();
});
});
}, function () {
debug('cleanup: done cleaning app backups');
callback();
});
});
});
});
cleanupAppBackups(backupConfig, referencedAppBackups, callback);
});
});
}
+2
View File
@@ -15,6 +15,7 @@ var assert = require('assert'),
database = require('./database.js'),
debug = require('debug')('box:backuptask'),
filesystem = require('./storage/filesystem.js'),
noop = require('./storage/noop.js'),
path = require('path'),
paths = require('./paths.js'),
s3 = require('./storage/s3.js'),
@@ -27,6 +28,7 @@ function api(provider) {
case 's3': return s3;
case 'filesystem': return filesystem;
case 'minio': return s3;
case 'noop': return noop;
default: return null;
}
}
+18 -19
View File
@@ -263,10 +263,6 @@ function validateCertificate(cert, key, fqdn) {
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof fqdn, 'string');
if (cert === null && key === null) return null;
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
function matchesDomain(domain) {
if (typeof domain !== 'string') return false;
if (domain === fqdn) return true;
@@ -275,23 +271,26 @@ function validateCertificate(cert, key, fqdn) {
return false;
}
// 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);
if (cert === null && key === null) return null;
if (!cert && key) return new Error('missing cert');
if (cert && !key) return new Error('missing key');
// 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);
var result = safe.child_process.execSync('openssl x509 -noout -checkhost "' + fqdn + '"', { encoding: 'utf8', input: cert });
if (!result) return new Error(util.format('could not get cert subject'));
// 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));
// if no match, check alt names
if (result.indexOf('does match certificate') === -1) {
// 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
if (!altNames.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, altNames));
}
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
+83 -115
View File
@@ -19,16 +19,11 @@ exports = module.exports = {
retire: retire,
migrate: migrate,
getConfigStateSync: getConfigStateSync,
checkDiskSpace: checkDiskSpace,
readDkimPublicKeySync: readDkimPublicKeySync,
refreshDNS: refreshDNS,
events: null,
EVENT_ACTIVATED: 'activated'
configureWebadmin: configureWebadmin
};
var appdb = require('./appdb.js'),
@@ -90,9 +85,8 @@ const BOX_AND_USER_TEMPLATE = {
}
};
var gUpdatingDns = false, // flag for dns update reentrancy
gBoxAndUserDetails = null, // cached cloudron details like region,size...
gConfigState = { dns: false, tls: false, configured: false };
var gBoxAndUserDetails = null, // cached cloudron details like region,size...
gWebadminStatus = { dns: false, tls: false, configuring: false };
function CloudronError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
@@ -126,26 +120,27 @@ 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;
gWebadminStatus = { dns: false, tls: false, configuring: false };
gBoxAndUserDetails = null;
async.series([
certificates.initialize,
settings.initialize,
installAppBundle,
checkConfigState,
configureDefaultServer
], callback);
configureDefaultServer,
onDomainConfigured
], function (error) {
if (error) return callback(error);
configureWebadmin(NOOP_CALLBACK); // for restore() and caas initial setup. do not block
callback();
});
}
function uninitialize(callback) {
assert.strictEqual(typeof callback, 'function');
exports.events = null;
async.series([
cron.uninitialize,
mailer.stop,
@@ -155,49 +150,21 @@ function uninitialize(callback) {
], callback);
}
function onConfigured(callback) {
function onDomainConfigured(callback) {
callback = callback || NOOP_CALLBACK;
// if we hit here, the domain has to be set, this is a logic issue if it isn't
assert(config.fqdn());
debug('onConfigured: current state: %j', gConfigState);
if (gConfigState.configured) return callback(); // re-entracy flag
gConfigState.configured = true;
settings.events.on(settings.DNS_CONFIG_KEY, function () { addDnsRecords(); });
if (!config.fqdn()) return callback();
async.series([
clients.addDefaultClients,
certificates.ensureFallbackCertificate,
platform.start, // requires fallback certs for mail container
ensureDkimKey,
addDnsRecords,
configureAdmin,
mailer.start,
cron.initialize // do not send heartbeats until we are "ready"
platform.start, // requires fallback certs for mail container
mailer.start, // this requires the "mail" container to be running
cron.initialize
], callback);
}
function getConfigStateSync() {
return gConfigState;
}
function checkConfigState(callback) {
callback = callback || NOOP_CALLBACK;
if (!config.fqdn()) {
settings.events.once(settings.DNS_CONFIG_KEY, function () { checkConfigState(); }); // check again later
return callback(null);
}
debug('checkConfigState: configured');
onConfigured(callback);
}
function dnsSetup(dnsConfig, domain, callback) {
assert.strictEqual(typeof dnsConfig, 'object');
assert.strictEqual(typeof domain, 'string');
@@ -211,7 +178,10 @@ function dnsSetup(dnsConfig, domain, callback) {
config.set('fqdn', domain); // set fqdn only after dns config is valid, otherwise cannot re-setup if we failed
onConfigured(); // do not block
async.series([ // do not block
onDomainConfigured,
configureWebadmin
], NOOP_CALLBACK);
callback();
});
@@ -235,8 +205,6 @@ function configureDefaultServer(callback) {
safe.child_process.execSync(certCommand);
}
safe.fs.unlinkSync(path.join(paths.NGINX_APPCONFIG_DIR,'ip_based_setup.conf'));
nginx.configureAdmin(certFilePath, keyFilePath, 'default.conf', '', function (error) {
if (error) return callback(error);
@@ -246,30 +214,39 @@ function configureDefaultServer(callback) {
});
}
function configureAdmin(callback) {
function configureWebadmin(callback) {
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
debug('configureWebadmin: fqdn:%s status:%j', config.fqdn(), gWebadminStatus);
debug('configureAdmin');
if (process.env.BOX_ENV === 'test' || !config.fqdn() || gWebadminStatus.configuring) return callback();
gWebadminStatus.configuring = true; // re-entracy guard
function done(error) {
gWebadminStatus.configuring = false;
debug('configureWebadmin: done error:%j', error);
callback(error);
}
sysinfo.getPublicIp(function (error, ip) {
if (error) return callback(error);
if (error) return done(error);
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return callback(error);
addDnsRecords(ip, function (error) {
if (error) return done(error);
gConfigState.dns = true;
subdomains.waitForDns(config.adminFqdn(), ip, 'A', { interval: 30000, times: 50000 }, function (error) {
if (error) return done(error);
certificates.ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
if (error) { // currently, this can never happen
debug('Error obtaining certificate. Proceed anyway', error);
return callback();
}
gWebadminStatus.dns = true;
gConfigState.tls = true;
certificates.ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
if (error) return done(error);
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), callback);
gWebadminStatus.tls = true;
nginx.configureAdmin(certFilePath, keyFilePath, constants.NGINX_ADMIN_CONFIG_FILE_NAME, config.adminFqdn(), done);
});
});
});
});
@@ -330,7 +307,7 @@ function activate(username, password, email, displayName, ip, auditSource, callb
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
exports.events.emit(exports.EVENT_ACTIVATED);
platform.createMailConfig(NOOP_CALLBACK); // bounces can now be sent to the cloudron owner
callback(null, { token: token, expires: expires });
});
@@ -354,7 +331,7 @@ function getStatus(callback) {
provider: config.provider(),
cloudronName: cloudronName,
adminFqdn: config.fqdn() ? config.adminFqdn() : null,
configState: gConfigState
webadminStatus: gWebadminStatus
});
});
});
@@ -458,6 +435,8 @@ function sendHeartbeat() {
}
function ensureDkimKey(callback) {
assert(config.fqdn(), 'fqdn is not set');
var dkimPath = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn());
var dkimPrivateKeyFile = path.join(dkimPath, 'private');
var dkimPublicKeyFile = path.join(dkimPath, 'public');
@@ -536,66 +515,55 @@ function txtRecordsWithSpf(callback) {
});
}
function addDnsRecords(callback) {
function addDnsRecords(ip, callback) {
assert.strictEqual(typeof ip, 'string');
callback = callback || NOOP_CALLBACK;
if (process.env.BOX_ENV === 'test') return callback();
if (gUpdatingDns) {
debug('addDnsRecords: dns update already in progress');
return callback();
}
gUpdatingDns = true;
var dkimKey = readDkimPublicKeySync();
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
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 ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: constants.DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
// t=s limits the domainkey to this domain and not it's subdomains
var dkimRecord = { subdomain: constants.DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
// for non-custom domains, we show a noapp.html page
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
var records = [ ];
if (config.isCustomDomain()) {
records.push(webadminRecord);
records.push(dkimRecord);
} else {
// for non-custom domains, we show a noapp.html page
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
}
records.push(nakedDomainRecord);
records.push(webadminRecord);
records.push(dkimRecord);
}
debug('addDnsRecords: %j', records);
debug('addDnsRecords: %j', records);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
txtRecordsWithSpf(function (error, txtRecords) {
if (error) return retryCallback(error);
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
debug('addDnsRecords: will update %j', records);
debug('addDnsRecords: will update %j', records);
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.upsert(record.subdomain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
async.mapSeries(records, function (record, iteratorCallback) {
subdomains.upsert(record.subdomain, record.type, record.values, iteratorCallback);
}, function (error, changeIds) {
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
retryCallback(error);
});
retryCallback(error);
});
}, function (error) {
gUpdatingDns = false;
debug('addDnsRecords: done updating records with error:', error);
callback(error);
});
}, function (error) {
debug('addDnsRecords: done updating records with error:', error);
callback(error);
});
}
@@ -740,7 +708,7 @@ function doUpdate(boxUpdateInfo, callback) {
version: boxUpdateInfo.version
};
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, data);
debug('updating box %s %j', boxUpdateInfo.sourceTarballUrl, _.omit(data, 'tlsCert', 'tlsKey', 'token', 'appstore', 'caas'));
progress.set(progress.UPDATE, 5, 'Downloading and extracting new version');
@@ -906,7 +874,7 @@ function refreshDNS(callback) {
debug('refreshDNS: current ip %s', ip);
addDnsRecords(function (error) {
addDnsRecords(ip, function (error) {
if (error) return callback(error);
debug('refreshDNS: done for system records');
+9 -4
View File
@@ -19,6 +19,7 @@ var apps = require('./apps.js'),
janitor = require('./janitor.js'),
scheduler = require('./scheduler.js'),
settings = require('./settings.js'),
semver = require('semver'),
updateChecker = require('./updatechecker.js');
var gAutoupdaterJob = null,
@@ -92,7 +93,7 @@ function recreateJobs(tz) {
if (gBackupJob) gBackupJob.stop();
gBackupJob = new CronJob({
cronTime: '00 00 */4 * * *', // every 4 hours. backups.ensureBackup() will only trigger a backup once per day
cronTime: '00 00 */6 * * *', // every 6 hours. backups.ensureBackup() will only trigger a backup once per day
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
start: true,
timeZone: tz
@@ -135,7 +136,7 @@ function recreateJobs(tz) {
if (gCleanupBackupsJob) gCleanupBackupsJob.stop();
gCleanupBackupsJob = new CronJob({
cronTime: '00 00 */4 * * *', // every 4 hours
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
onTick: backups.cleanup,
start: true,
timeZone: tz
@@ -189,8 +190,12 @@ function autoupdatePatternChanged(pattern) {
onTick: function() {
var updateInfo = updateChecker.getUpdateInfo();
if (updateInfo.box) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
if (semver.major(updateInfo.box.version) === semver.major(config.version())) {
debug('Starting autoupdate to %j', updateInfo.box);
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
} else {
debug('Block automatic update for major version');
}
} else if (updateInfo.apps) {
debug('Starting app update to %j', updateInfo.apps);
apps.updateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
+46
View File
@@ -0,0 +1,46 @@
'use strict';
exports = module.exports = {
resolve: resolve
};
var assert = require('assert'),
child_process = require('child_process'),
debug = require('debug')('box:dig');
function resolve(domain, type, options, callback) {
assert.strictEqual(typeof domain, 'string');
assert.strictEqual(typeof type, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof callback, 'function');
// dig @server cloudron.io TXT +short
var args = [ ];
if (options.server) args.push('@' + options.server);
if (type === 'PTR') {
args.push('-x', domain);
} else {
args.push(domain, type);
}
args.push('+short');
child_process.execFile('/usr/bin/dig', args, { encoding: 'utf8', killSignal: 'SIGKILL', timeout: options.timeout || 0 }, function (error, stdout, stderr) {
if (error && error.killed) error.code = 'ETIMEDOUT';
if (error || stderr) debug('resolve error (%j): %j %s %s', args, error, stdout, stderr);
if (error) return callback(error);
debug('resolve (%j): %s', args, stdout);
if (!stdout) return callback(); // timeout or no result
var lines = stdout.trim().split('\n');
if (type === 'MX') {
lines = lines.map(function (line) {
var parts = line.split(' ');
return { priority: parts[0], exchange: parts[1] };
});
}
return callback(null, lines);
});
}
+3 -2
View File
@@ -10,8 +10,9 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/digitalocean'),
dns = require('native-dns'),
dns = require('dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
superagent = require('superagent'),
util = require('util');
@@ -201,7 +202,7 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Digital Ocean'));
}
upsert(credentials, domain, 'my', 'A', [ ip ], function (error, changeId) {
upsert(credentials, domain, constants.ADMIN_LOCATION, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: A record added with change id %s', changeId);
+11 -21
View File
@@ -10,8 +10,10 @@ exports = module.exports = {
var assert = require('assert'),
async = require('async'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/manual'),
dns = require('native-dns'),
dig = require('../dig.js'),
dns = require('dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
util = require('util');
@@ -55,7 +57,7 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
assert.strictEqual(typeof ip, 'string');
assert.strictEqual(typeof callback, 'function');
var adminDomain = 'my.' + domain;
var adminDomain = constants.ADMIN_LOCATION + '.' + domain;
dns.resolveNs(domain, function (error, nameservers) {
if (error || !nameservers) return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Unable to get nameservers'));
@@ -68,42 +70,30 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
}
async.every(nsIps, function (nsIp, everyIpCallback) {
var req = dns.Request({
question: dns.Question({ name: adminDomain, type: 'A' }),
server: { address: nsIp },
timeout: 5000
});
dig.resolve(adminDomain, 'A', { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'ETIMEDOUT') {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, adminDomain);
return everyIpCallback(null, true); // should be ok if dns server is down
}
req.on('timeout', function () {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, adminDomain);
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 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);
debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, adminDomain, 'A', answer);
return everyIpCallback(null, false);
}
debug('verifyDnsConfig: ns: %s (%s), name:%s Actual:%j Expecting:%s', nameserver, nsIp, adminDomain, answer, ip);
var match = answer.some(function (a) {
return a.address === ip;
});
var match = answer.some(function (a) { return a === ip; });
if (match) return everyIpCallback(null, true); // done!
everyIpCallback(null, false);
});
req.send();
}, everyNsCallback);
});
}, function (error, success) {
+3 -2
View File
@@ -13,8 +13,9 @@ exports = module.exports = {
var assert = require('assert'),
AWS = require('aws-sdk'),
constants = require('../constants.js'),
debug = require('debug')('box:dns/route53'),
dns = require('native-dns'),
dns = require('dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
util = require('util'),
_ = require('underscore');
@@ -245,7 +246,7 @@ function verifyDnsConfig(dnsConfig, domain, ip, callback) {
return callback(new SubdomainError(SubdomainError.BAD_FIELD, 'Domain nameservers are not set to Route53'));
}
upsert(credentials, domain, 'my', 'A', [ ip ], function (error, changeId) {
upsert(credentials, domain, constants.ADMIN_LOCATION, 'A', [ ip ], function (error, changeId) {
if (error) return callback(error);
debug('verifyDnsConfig: A record added with change id %s', changeId);
+12 -20
View File
@@ -5,7 +5,8 @@ exports = module.exports = waitForDns;
var assert = require('assert'),
async = require('async'),
debug = require('debug')('box:dns/waitfordns'),
dns = require('native-dns'),
dig = require('../dig.js'),
dns = require('dns'),
SubdomainError = require('../subdomains.js').SubdomainError,
tld = require('tldjs'),
util = require('util');
@@ -25,45 +26,36 @@ function isChangeSynced(domain, value, type, nameserver, callback) {
}
async.every(nsIps, function (nsIp, iteratorCallback) {
var req = dns.Request({
question: dns.Question({ name: domain, type: type }),
server: { address: nsIp },
timeout: 5000
});
dig.resolve(domain, type, { server: nsIp, timeout: 5000 }, function (error, answer) {
if (error && error.code === 'ETIMEDOUT') {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
return iteratorCallback(null, true); // should be ok if dns server is down
}
req.on('timeout', function () {
debug('nameserver %s (%s) timed out when trying to resolve %s', nameserver, nsIp, domain);
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(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);
debug('bad answer from nameserver %s (%s) resolving %s (%s): %j', nameserver, nsIp, domain, type, answer);
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' && value.test(a.address)) ||
(type === 'CNAME' && value.test(a.data)) ||
(type === 'TXT' && value.test(a.data.join(''))));
return ((type === 'A' && value.test(a)) ||
(type === 'CNAME' && value.test(a)) ||
(type === 'TXT' && value.test(a)));
});
if (match) return iteratorCallback(null, true); // done!
iteratorCallback(null, false);
});
req.send();
}, callback);
});
}
+4 -2
View File
@@ -202,8 +202,10 @@ function createSubcontainer(app, name, cmd, options, callback) {
},
CpuShares: 512, // relative to 1024 for system processes
VolumesFrom: isAppContainer ? null : [ app.containerId + ":rw" ],
NetworkMode: isAppContainer ? 'cloudron' : ('container:' + app.containerId), // share network namespace with parent
SecurityOpt: enableSecurityOpt ? [ "apparmor:docker-cloudron-app" ] : null // profile available only on cloudron
NetworkMode: 'cloudron',
Dns: ['172.18.0.1'], // use internal dns
DnsSearch: ['.'], // use internal dns
SecurityOpt: enableSecurityOpt ? [ "apparmor=docker-cloudron-app" ] : null // profile available only on cloudron
}
};
containerOptions = _.extend(containerOptions, options);
+5 -5
View File
@@ -7,18 +7,18 @@
exports = module.exports = {
// a major version makes all apps restore from backup
// a minor version makes all apps re-configure themselves
'version': '48.1.0',
'version': '48.3.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.16.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.16.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.12.0' },
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.17.0' },
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.17.0' },
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.13.0' },
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.31.0' },
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.32.0' },
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
}
};
+22 -17
View File
@@ -2,17 +2,21 @@
Dear <%= cloudronName %> Admin,
Version <%= newBoxVersion %> for Cloudron <%= fqdn %> is now available!
Cloudron Version <%= newBoxVersion %> is now available!
Your Cloudron will update automatically tonight. Alternately, update immediately at <%= webadminUrl %>.
Your Cloudron will update to 1.0 once you have selected a plan (https://cloudron.io/pricing.html).
Changelog:
<% for (var i = 0; i < changelog.length; i++) { %>
* <%- changelog[i] %>
<% } %>
With a paid plan, you get continuous updates for the Cloudron and apps just like you had until now.
This ensures you are running the latest versions of apps and keeps your server secure. All paid
plans come with support via email (support@cloudron.io) and live chat (https://chat.cloudron.io).
You can read more about our pricing changes in our blog at https://cloudron.io/blog/2017-06-06-pricing.html.
Visit our pricing page https://cloudron.io/pricing.html for pricing information.
Visit your Cloudron at <%= webadminUrl %> to perform the update.
Thank you,
your Cloudron
Cloudron.io team
<% } else { %>
@@ -24,20 +28,22 @@ your Cloudron
<div style="width: 650px; text-align: left;">
<p>
Version <b><%= newBoxVersion %></b> for Cloudron <%= fqdn %> is now available!
Cloudron Version <b><%= newBoxVersion %></b> is now available!
</p>
<p>
Your Cloudron will update automatically tonight.<br/>
Alternately, update immediately <a href="<%= webadminUrl %>">here</a>.
Your Cloudron will update to 1.0 once you have selected a <a href="https://cloudron.io/pricing.html">plan</a>.
</p>
<p>
With a paid plan, you get continuous updates for the Cloudron and apps just like you had until now. This ensures you are running the latest versions of apps and keeps your server secure. All paid plans come with support via <a href="mailto:support@cloudron.io">email</a> and <a target="_blank" href="https://chat.cloudron.io">live chat</a>.
</p>
<p>
You can read more about our pricing changes in our <a href="https://cloudron.io/blog/2017-06-06-pricing.html" target="_blank">blog</a>.
</p>
<h5>Changelog:</h5>
<ul>
<% for (var i = 0; i < changelogHTML.length; i++) { %>
<li><%- changelogHTML[i] %></li>
<% } %>
</ul>
<p>
Visit your Cloudron <a href="<%= webadminUrl %>">here</a> to perform the update.
</p>
<br/>
<br/>
@@ -52,4 +58,3 @@ your Cloudron
<img src="https://analytics.cloudron.io/piwik.php?idsite=2&rec=1&e_c=CloudronEmail&e_a=update" style="border:0" alt="" />
<% } %>
+1 -1
View File
@@ -97,7 +97,7 @@ function mailConfig() {
function checkDns() {
if (process.env.BOX_ENV === 'test') return;
subdomains.waitForDns(config.fqdn(), new RegExp('^v=spf1 .*a:' + config.adminFqdn().replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '.*'), 'TXT', { interval: 60000, times: Infinity }, function (error) {
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');
+19 -10
View File
@@ -2,13 +2,14 @@
exports = module.exports = {
start: start,
stop: stop
stop: stop,
createMailConfig: createMailConfig
};
var apps = require('./apps.js'),
assert = require('assert'),
async = require('async'),
cloudron = require('./cloudron.js'),
config = require('./config.js'),
certificates = require('./certificates.js'),
debug = require('debug')('box:platform'),
@@ -45,8 +46,6 @@ function start(callback) {
if (domain === '*.' + config.fqdn() || domain === config.adminFqdn()) startMail(NOOP_CALLBACK);
});
cloudron.events.on(cloudron.EVENT_ACTIVATED, function () { createMailConfig(NOOP_CALLBACK); });
var existingInfra = { version: 'none' };
if (fs.existsSync(paths.INFRA_VERSION_FILE)) {
existingInfra = safe.JSON.parse(fs.readFileSync(paths.INFRA_VERSION_FILE, 'utf8'));
@@ -85,14 +84,14 @@ function stop(callback) {
}
function emitPlatformReady() {
// give 30 seconds for the platform to "settle". For example, mysql might still be initing the
// give some time for the platform to "settle". For example, mysql might still be initing the
// database dir and we cannot call service scripts until that's done.
// TODO: make this smarter to not wait for 30secs for the crash-restart case
// TODO: make this smarter to not wait for 15secs for the crash-restart case
gPlatformReadyTimer = setTimeout(function () {
debug('emitting platform ready');
gPlatformReadyTimer = null;
taskmanager.resumeTasks();
}, 30000);
}, 15000);
}
function removeOldImages(callback) {
@@ -141,6 +140,8 @@ function startGraphite(callback) {
--net-alias graphite \
-m 75m \
--memory-swap 150m \
--dns 172.18.0.1 \
--dns-search=. \
-p 127.0.0.1:2003:2003 \
-p 127.0.0.1:2004:2004 \
-p 127.0.0.1:8000:8000 \
@@ -168,13 +169,15 @@ function startMysql(callback) {
--net-alias mysql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${dataDir}/mysql:/var/lib/mysql" \
-v "${dataDir}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMysql', cmd);
callback();
setTimeout(callback, 5000);
}
function startPostgresql(callback) {
@@ -192,13 +195,15 @@ function startPostgresql(callback) {
--net-alias postgresql \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${dataDir}/postgresql:/var/lib/postgresql" \
-v "${dataDir}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startPostgresql', cmd);
callback();
setTimeout(callback, 5000);
}
function startMongodb(callback) {
@@ -216,13 +221,15 @@ function startMongodb(callback) {
--net-alias mongodb \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
-v "${dataDir}/mongodb:/var/lib/mongodb" \
-v "${dataDir}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
--read-only -v /tmp -v /run "${tag}"`;
shell.execSync('startMongodb', cmd);
callback();
setTimeout(callback, 5000);
}
function createMailConfig(callback) {
@@ -277,6 +284,8 @@ function startMail(callback) {
--net-alias mail \
-m ${memoryLimit}m \
--memory-swap ${memoryLimit * 2}m \
--dns 172.18.0.1 \
--dns-search=. \
--env ENABLE_MDA=${mailConfig.enabled} \
-v "${dataDir}/mail:/app/data" \
-v "${dataDir}/addons/mail:/etc/mail" \
+3 -2
View File
@@ -5,7 +5,8 @@ exports = module.exports = {
create: create
};
var backups = require('../backups.js'),
var backupdb = require('../backupdb.js'),
backups = require('../backups.js'),
BackupsError = require('../backups.js').BackupsError,
HttpError = require('connect-lastmile').HttpError,
HttpSuccess = require('connect-lastmile').HttpSuccess;
@@ -22,7 +23,7 @@ function get(req, res, next) {
var perPage = typeof req.query.per_page !== 'undefined'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
backups.getPaged(page, perPage, function (error, result) {
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, page, perPage, function (error, result) {
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return next(new HttpError(503, error.message));
if (error) return next(new HttpError(500, error));
+3 -1
View File
@@ -80,7 +80,7 @@ function dnsSetup(req, res, next) {
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.domain !== 'string' || !req.body.domain) return next(new HttpError(400, 'domain is required'));
cloudron.dnsSetup(req.body, req.body.domain, function (error) {
cloudron.dnsSetup(req.body, req.body.domain.toLowerCase(), function (error) {
if (error && error.reason === CloudronError.ALREADY_SETUP) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
if (error) return next(new HttpError(500, error));
@@ -164,6 +164,8 @@ function migrate(req, res, next) {
var options = _.pick(req.body, 'domain', 'size', 'region');
if (Object.keys(options).length === 0) return next(new HttpError(400, 'no migrate option provided'));
if (options.domain) options.domain = options.domain.toLowerCase();
cloudron.migrate(req.body, function (error) { // pass req.body because 'domain' can have arbitrary options
if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message));
if (error && error.reason === CloudronError.BAD_FIELD) return next(new HttpError(400, error.message));
+1 -1
View File
@@ -41,7 +41,7 @@ var SERVER_URL = 'http://localhost:' + config.get('port');
// Test image information
var TEST_IMAGE_REPO = 'cloudron/test';
var TEST_IMAGE_TAG = '22.0.0';
var TEST_IMAGE_TAG = '23.0.0';
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
+43 -43
View File
@@ -536,11 +536,11 @@ describe('Settings API', function () {
this.timeout(10000);
before(function (done) {
var dns = require('native-dns');
var dig = require('../../dig.js');
// replace dns resolveTxt()
resolve = dns.resolve;
dns.resolve = function (hostname, type, callback) {
resolve = dig.resolve;
dig.resolve = function (hostname, type, options, callback) {
expect(hostname).to.be.a('string');
expect(callback).to.be.a('function');
@@ -558,9 +558,9 @@ describe('Settings API', function () {
});
after(function (done) {
var dns = require('native-dns');
var dig = require('../../dig.js');
dns.resolve = resolve;
dig.resolve = resolve;
done();
});
@@ -594,32 +594,32 @@ describe('Settings API', function () {
expect(res.body.dns.dkim.domain).to.eql(dkimDomain);
expect(res.body.dns.dkim.type).to.eql('TXT');
expect(res.body.dns.dkim.value).to.eql(null);
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync() + '"');
expect(res.body.dns.dkim.status).to.eql(false);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql(null);
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.type).to.eql('TXT');
expect(res.body.dns.dmarc.value).to.eql(null);
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.type).to.eql('MX');
expect(res.body.dns.mx.value).to.eql(null);
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn());
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.type).to.eql('PTR');
// expect(res.body.ptr.value).to.eql(null); this will be anything random
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
done();
@@ -640,27 +640,27 @@ describe('Settings API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql(null);
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync() + '"');
expect(res.body.dns.dkim.status).to.eql(false);
expect(res.body.dns.dkim.value).to.eql(null);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.dmarc.value).to.eql(null);
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn());
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql(null);
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -671,10 +671,10 @@ describe('Settings API', function () {
it('succeeds with all different spf, dkim, dmarc, mx, ptr records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() }, { priority: '30', exchange: config.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC2; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM2; t=s; p=' + cloudron.readDkimPublicKeySync()]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:random.com ~all']];
dnsAnswerQueue[mxDomain].MX = [ { priority: '20', exchange: config.mailFqdn() + '.' }, { priority: '30', exchange: config.mailFqdn() + '.'} ];
dnsAnswerQueue[dmarcDomain].TXT = ['"v=DMARC2; p=reject; pct=100"'];
dnsAnswerQueue[dkimDomain].TXT = ['"v=DKIM2; t=s; p=' + cloudron.readDkimPublicKeySync() + '"'];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:random.com ~all"'];
superagent.get(SERVER_URL + '/api/v1/settings/email_status')
.query({ access_token: token })
@@ -682,27 +682,27 @@ describe('Settings API', function () {
expect(res.statusCode).to.equal(200);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all"');
expect(res.body.dns.spf.status).to.eql(false);
expect(res.body.dns.spf.value).to.eql('v=spf1 a:random.com ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:random.com ~all"');
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync() + '"');
expect(res.body.dns.dkim.status).to.eql(false);
expect(res.body.dns.dkim.value).to.eql('v=DKIM2; t=s; p=' + cloudron.readDkimPublicKeySync());
expect(res.body.dns.dkim.value).to.eql('"v=DKIM2; t=s; p=' + cloudron.readDkimPublicKeySync() + '"');
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(false);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC2; p=reject; pct=100');
expect(res.body.dns.dmarc.value).to.eql('"v=DMARC2; p=reject; pct=100"');
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(false);
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn());
expect(res.body.dns.mx.value).to.eql('20 ' + config.mailFqdn() + ' 30 ' + config.mailFqdn());
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('20 ' + config.mailFqdn() + '. 30 ' + config.mailFqdn() + '.');
expect(res.body.dns.ptr).to.be.an('object');
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn());
expect(res.body.dns.ptr.expected).to.eql(config.mailFqdn() + '.');
expect(res.body.dns.ptr.status).to.eql(false);
// expect(res.body.ptr.value).to.eql(null); this will be anything random
@@ -715,7 +715,7 @@ describe('Settings API', function () {
it('succeeds with existing embedded spf', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all']];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"'];
superagent.get(SERVER_URL + '/api/v1/settings/email_status')
.query({ access_token: token })
@@ -725,8 +725,8 @@ describe('Settings API', function () {
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(true);
done();
@@ -736,10 +736,10 @@ describe('Settings API', function () {
it('succeeds with all correct records', function (done) {
clearDnsAnswerQueue();
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() } ];
dnsAnswerQueue[dmarcDomain].TXT = [['v=DMARC1; p=reject; pct=100']];
dnsAnswerQueue[dkimDomain].TXT = [['v=DKIM1;', 't=s;', 'p=' + cloudron.readDkimPublicKeySync()]];
dnsAnswerQueue[spfDomain].TXT = [['v=spf1', ' a:' + config.adminFqdn(), ' ~all']];
dnsAnswerQueue[mxDomain].MX = [ { priority: '10', exchange: config.mailFqdn() + '.' } ];
dnsAnswerQueue[dmarcDomain].TXT = ['"v=DMARC1; p=reject; pct=100"'];
dnsAnswerQueue[dkimDomain].TXT = ['"v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync() + '"'];
dnsAnswerQueue[spfDomain].TXT = ['"v=spf1 a:' + config.adminFqdn() + ' ~all"'];
superagent.get(SERVER_URL + '/api/v1/settings/email_status')
.query({ access_token: token })
@@ -749,26 +749,26 @@ describe('Settings API', function () {
expect(res.body.dns.dkim).to.be.an('object');
expect(res.body.dns.dkim.domain).to.eql(dkimDomain);
expect(res.body.dns.dkim.type).to.eql('TXT');
expect(res.body.dns.dkim.value).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
expect(res.body.dns.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
expect(res.body.dns.dkim.value).to.eql('"v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync() + '"');
expect(res.body.dns.dkim.expected).to.eql('"v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync() + '"');
expect(res.body.dns.dkim.status).to.eql(true);
expect(res.body.dns.spf).to.be.an('object');
expect(res.body.dns.spf.domain).to.eql(spfDomain);
expect(res.body.dns.spf.type).to.eql('TXT');
expect(res.body.dns.spf.value).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
expect(res.body.dns.spf.value).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.expected).to.eql('"v=spf1 a:' + config.adminFqdn() + ' ~all"');
expect(res.body.dns.spf.status).to.eql(true);
expect(res.body.dns.dmarc).to.be.an('object');
expect(res.body.dns.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.expected).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.dmarc.status).to.eql(true);
expect(res.body.dns.dmarc.value).to.eql('v=DMARC1; p=reject; pct=100');
expect(res.body.dns.dmarc.value).to.eql('"v=DMARC1; p=reject; pct=100"');
expect(res.body.dns.mx).to.be.an('object');
expect(res.body.dns.mx.status).to.eql(true);
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn());
expect(res.body.dns.mx.value).to.eql('10 ' + config.mailFqdn());
expect(res.body.dns.mx.expected).to.eql('10 ' + config.mailFqdn() + '.');
expect(res.body.dns.mx.value).to.eql('10 ' + config.mailFqdn() + '.');
done();
});
+2
View File
@@ -153,6 +153,8 @@ function verifyPassword(req, res, next) {
if (error && error.reason === UserError.NOT_FOUND) return next(new HttpError(403, 'Password incorrect'));
if (error) return next(new HttpError(500, error));
req.body.password = '<redacted>'; // this will prevent logs from displaying plain text password
next();
});
}
+3
View File
@@ -24,6 +24,9 @@ if [[ $# -lt 3 ]]; then
fi
if [[ -f "$2" ]]; then
# on some vanilla ubuntu installs, the .ssh directory does not exist
mkdir -p "$(dirname $3)"
cp "$2" "$3"
chown "$1":"$1" "$3"
fi
+1 -2
View File
@@ -20,5 +20,4 @@ fi
echo "Running node with memory constraints"
# note BOX_ENV and NODE_ENV are derived from parent process
exec env "DEBUG=box*,connect-lastmile" /usr/bin/node --max_old_space_size=150 "$@"
exec env "DEBUG=box*,connect-lastmile" /usr/bin/node --max_old_space_size=200 "$@"
-1
View File
@@ -25,7 +25,6 @@ readonly sourceTarballUrl="${1}"
readonly data="${2}"
echo "Updating Cloudron with ${sourceTarballUrl}"
echo "${data}"
# TODO: pre-download tarball
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
+24 -23
View File
@@ -71,7 +71,7 @@ var assert = require('assert'),
CronJob = require('cron').CronJob,
DatabaseError = require('./databaseerror.js'),
debug = require('debug')('box:settings'),
dns = require('native-dns'),
dig = require('./dig.js'),
cloudron = require('./cloudron.js'),
CloudronError = cloudron.CloudronError,
moment = require('moment-timezone'),
@@ -108,6 +108,8 @@ var gDefaults = (function () {
return result;
})();
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function SettingsError(reason, errorOrMessage) {
assert.strictEqual(typeof reason, 'string');
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
@@ -149,6 +151,8 @@ function uninitialize(callback) {
function getEmailStatus(callback) {
assert.strictEqual(typeof callback, 'function');
var digOptions = { server: '127.0.0.1', port: 53, timeout: 5000 };
var records = {}, outboundPort25 = {};
var dkimKey = cloudron.readDkimPublicKeySync();
@@ -158,17 +162,17 @@ function getEmailStatus(callback) {
records.dkim = {
domain: constants.DKIM_SELECTOR + '._domainkey.' + config.fqdn(),
type: 'TXT',
expected: 'v=DKIM1; t=s; p=' + dkimKey,
expected: '"v=DKIM1; t=s; p=' + dkimKey + '"',
value: null,
status: false
};
dns.resolve(records.dkim.domain, records.dkim.type, function (error, txtRecords) {
dig.resolve(records.dkim.domain, records.dkim.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
if (error) return callback(error);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
records.dkim.value = txtRecords[0].join(' ');
records.dkim.value = txtRecords[0];
records.dkim.status = (records.dkim.value === records.dkim.expected);
}
@@ -181,12 +185,12 @@ function getEmailStatus(callback) {
domain: config.fqdn(),
type: 'TXT',
value: null,
expected: 'v=spf1 a:' + config.adminFqdn() + ' ~all',
expected: '"v=spf1 a:' + config.adminFqdn() + ' ~all"',
status: false
};
// https://agari.zendesk.com/hc/en-us/articles/202952749-How-long-can-my-SPF-record-be-
dns.resolve(records.spf.domain, records.spf.type, function (error, txtRecords) {
dig.resolve(records.spf.domain, records.spf.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
if (error) return callback(error);
@@ -194,8 +198,8 @@ function getEmailStatus(callback) {
var i;
for (i = 0; i < txtRecords.length; i++) {
if (txtRecords[i].join('').indexOf('v=spf1 ') !== 0) continue; // not SPF
records.spf.value = txtRecords[i].join('');
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
records.spf.value = txtRecords[i];
records.spf.status = records.spf.value.indexOf(' a:' + config.adminFqdn()) !== -1;
break;
}
@@ -203,7 +207,7 @@ function getEmailStatus(callback) {
if (records.spf.status) {
records.spf.expected = records.spf.value;
} else if (i !== txtRecords.length) {
records.spf.expected = 'v=spf1 a:' + config.adminFqdn() + ' ' + records.spf.value.slice('v=spf1 '.length);
records.spf.expected = '"v=spf1 a:' + config.adminFqdn() + ' ' + records.spf.value.slice('"v=spf1 '.length);
}
callback();
@@ -215,16 +219,16 @@ function getEmailStatus(callback) {
domain: config.fqdn(),
type: 'MX',
value: null,
expected: '10 ' + config.mailFqdn(),
expected: '10 ' + config.mailFqdn() + '.',
status: false
};
dns.resolve(records.mx.domain, records.mx.type, function (error, mxRecords) {
dig.resolve(records.mx.domain, records.mx.type, digOptions, function (error, mxRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
if (error) return callback(error);
if (Array.isArray(mxRecords) && mxRecords.length !== 0) {
records.mx.status = mxRecords.length == 1 && mxRecords[0].exchange === config.mailFqdn();
records.mx.status = mxRecords.length == 1 && mxRecords[0].exchange === (config.mailFqdn() + '.');
records.mx.value = mxRecords.map(function (r) { return r.priority + ' ' + r.exchange; }).join(' ');
}
@@ -237,16 +241,16 @@ function getEmailStatus(callback) {
domain: '_dmarc.' + config.fqdn(),
type: 'TXT',
value: null,
expected: 'v=DMARC1; p=reject; pct=100',
expected: '"v=DMARC1; p=reject; pct=100"',
status: false
};
dns.resolve(records.dmarc.domain, records.dmarc.type, function (error, txtRecords) {
dig.resolve(records.dmarc.domain, records.dmarc.type, digOptions, function (error, txtRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
if (error) return callback(error);
if (Array.isArray(txtRecords) && txtRecords.length !== 0) {
records.dmarc.value = txtRecords[0].join(' ');
records.dmarc.value = txtRecords[0];
records.dmarc.status = (records.dmarc.value === records.dmarc.expected);
}
@@ -259,7 +263,7 @@ function getEmailStatus(callback) {
domain: null,
type: 'PTR',
value: null,
expected: config.mailFqdn(),
expected: config.mailFqdn() + '.',
status: false
};
@@ -268,13 +272,13 @@ function getEmailStatus(callback) {
records.ptr.domain = ip.split('.').reverse().join('.') + '.in-addr.arpa';
dns.reverse(ip, function (error, ptrRecords) {
dig.resolve(ip, 'PTR', digOptions, function (error, ptrRecords) {
if (error && error.code === 'ENOTFOUND') return callback(null); // not setup
if (error) return callback(error);
if (Array.isArray(ptrRecords) && ptrRecords.length !== 0) {
records.ptr.value = ptrRecords.join(' ');
records.ptr.status = ptrRecords.some(function (v) { return v === config.mailFqdn(); });
records.ptr.status = ptrRecords.some(function (v) { return v === records.ptr.expected; });
}
return callback();
@@ -332,11 +336,6 @@ function getEmailStatus(callback) {
};
}
dns.platform.timeout = 5000; // hack so that each query finish in 5 seconds. this applies to _each_ ns
dns.platform.name_servers = [ { address: '127.0.0.1', port: 53 } ];
dns.platform.attempts = 1;
dns.platform.hosts.purge(); // otherwise, reverse() uses /etc/hosts
async.parallel([
ignoreError('mx', checkMx),
ignoreError('spf', checkSpf),
@@ -514,6 +513,8 @@ function setDnsConfig(dnsConfig, domain, callback) {
exports.events.emit(exports.DNS_CONFIG_KEY, dnsConfig);
cloudron.configureWebadmin(NOOP_CALLBACK); // do not block
callback(null);
});
});
+1 -1
View File
@@ -37,7 +37,7 @@ function exec(tag, file, args, callback) {
callback = once(callback); // exit may or may not be called after an 'error'
debug(tag + ' execFile: %s %s', file, args.join(' '));
debug(tag + ' execFile: %s', file); // do not dump args as it might have sensitive info
var cp = child_process.spawn(file, args);
cp.stdout.on('data', function (data) {
+11 -8
View File
@@ -19,6 +19,7 @@ var assert = require('assert'),
once = require('once'),
PassThrough = require('stream').PassThrough,
path = require('path'),
S3BlockReadStream = require('s3-block-read-stream'),
superagent = require('superagent'),
targz = require('./targz.js');
@@ -54,6 +55,8 @@ function getBackupFilePath(apiConfig, backupId) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
const FILE_TYPE = apiConfig.key ? '.tar.gz.enc' : '.tar.gz';
return path.join(apiConfig.prefix, backupId.endsWith(FILE_TYPE) ? backupId : backupId+FILE_TYPE);
}
@@ -82,7 +85,8 @@ function backup(apiConfig, backupId, sourceDirectories, callback) {
};
var s3 = new AWS.S3(credentials);
s3.upload(params, function (error) {
// s3.upload automatically does a multi-part upload. we set queueSize to 1 to reduce memory usage
s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, function (error) {
if (error) {
debug('[%s] backup: s3 upload error.', backupId, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
@@ -91,7 +95,7 @@ function backup(apiConfig, backupId, sourceDirectories, callback) {
callback(null);
});
targz.create(sourceDirectories, apiConfig.key || '', passThrough, callback);
targz.create(sourceDirectories, apiConfig.key || null, passThrough, callback);
});
}
@@ -103,8 +107,7 @@ function restore(apiConfig, backupId, destination, callback) {
callback = once(callback);
var isOldFormat = backupId.endsWith('.tar.gz');
var backupFilePath = isOldFormat ? path.join(apiConfig.prefix, backupId) : getBackupFilePath(apiConfig, backupId);
var backupFilePath = getBackupFilePath(apiConfig, backupId);
debug('[%s] restore: %s -> %s', backupId, backupFilePath, destination);
@@ -117,9 +120,9 @@ function restore(apiConfig, backupId, destination, callback) {
};
var s3 = new AWS.S3(credentials);
var s3get = s3.getObject(params).createReadStream();
var multipartDownload = new S3BlockReadStream(s3, params, { blockSize: 64 * 1024 * 1024, logCallback: debug });
s3get.on('error', function (error) {
multipartDownload.on('error', function (error) {
// TODO ENOENT for the mock, fix upstream!
if (error.code === 'NoSuchKey' || error.code === 'ENOENT') return callback(new BackupsError(BackupsError.NOT_FOUND));
@@ -127,7 +130,7 @@ function restore(apiConfig, backupId, destination, callback) {
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
targz.extract(s3get, isOldFormat, destination, apiConfig.key || '', callback);
targz.extract(multipartDownload, destination, apiConfig.key || null, callback);
});
}
@@ -179,7 +182,7 @@ function removeBackups(apiConfig, backupIds, callback) {
});
var s3 = new AWS.S3(credentials);
s3.deleteObjects(params, function (error) {
s3.deleteObjects(params, function (error, data) {
if (error) debug('Unable to remove %s. Not fatal.', params.Key, error);
else debug('removeBackups: Deleted: %j Errors: %j', data.Deleted, data.Errors);
+5 -5
View File
@@ -24,7 +24,6 @@ var assert = require('assert'),
targz = require('./targz.js');
var FALLBACK_BACKUP_FOLDER = '/var/backups';
var FILE_TYPE = '.tar.gz.enc';
var BACKUP_USER = config.TEST ? process.env.USER : 'yellowtent';
// internal only
@@ -32,6 +31,8 @@ function getBackupFilePath(apiConfig, backupId) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
const FILE_TYPE = apiConfig.key ? '.tar.gz.enc' : '.tar.gz';
return path.join(apiConfig.backupFolder || FALLBACK_BACKUP_FOLDER, backupId.endsWith(FILE_TYPE) ? backupId : backupId+FILE_TYPE);
}
@@ -68,7 +69,7 @@ function backup(apiConfig, backupId, sourceDirectories, callback) {
callback(null);
});
targz.create(sourceDirectories, apiConfig.key || '', fileStream, callback);
targz.create(sourceDirectories, apiConfig.key || null, fileStream, callback);
});
}
@@ -80,8 +81,7 @@ function restore(apiConfig, backupId, destination, callback) {
callback = once(callback);
var isOldFormat = backupId.endsWith('.tar.gz');
var sourceFilePath = isOldFormat ? path.join(apiConfig.backupFolder || FALLBACK_BACKUP_FOLDER, backupId) : getBackupFilePath(apiConfig, backupId);
var sourceFilePath = getBackupFilePath(apiConfig, backupId);
debug('[%s] restore: %s -> %s', backupId, sourceFilePath, destination);
@@ -94,7 +94,7 @@ function restore(apiConfig, backupId, destination, callback) {
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
targz.extract(fileStream, isOldFormat, destination, apiConfig.key || '', callback);
targz.extract(fileStream, destination, apiConfig.key || null, callback);
}
function copyBackup(apiConfig, oldBackupId, newBackupId, callback) {
+73
View File
@@ -0,0 +1,73 @@
'use strict';
exports = module.exports = {
backup: backup,
restore: restore,
copyBackup: copyBackup,
removeBackups: removeBackups,
backupDone: backupDone,
testConfig: testConfig
};
var assert = require('assert'),
debug = require('debug')('box:storage/noop');
function backup(apiConfig, backupId, sourceDirectories, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert(Array.isArray(sourceDirectories));
assert.strictEqual(typeof callback, 'function');
debug('backup: %s %j', backupId, sourceDirectories);
callback();
}
function restore(apiConfig, backupId, destination, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
assert.strictEqual(typeof destination, 'string');
assert.strictEqual(typeof callback, 'function');
debug('restore: %s %s', backupId, destination);
callback(new Error('Cannot restore from noop backend'));
}
function copyBackup(apiConfig, oldBackupId, newBackupId, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof oldBackupId, 'string');
assert.strictEqual(typeof newBackupId, 'string');
assert.strictEqual(typeof callback, 'function');
debug('copyBackup: %s -> %s', oldBackupId, newBackupId);
callback();
}
function removeBackups(apiConfig, backupIds, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert(Array.isArray(backupIds));
assert.strictEqual(typeof callback, 'function');
debug('removeBackups: %j', backupIds);
callback();
}
function testConfig(apiConfig, callback) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof callback, 'function');
callback();
}
function backupDone(backupId, appBackupIds, callback) {
assert.strictEqual(typeof backupId, 'string');
assert(Array.isArray(appBackupIds));
assert.strictEqual(typeof callback, 'function');
callback();
}
+10 -9
View File
@@ -22,10 +22,9 @@ var assert = require('assert'),
once = require('once'),
PassThrough = require('stream').PassThrough,
path = require('path'),
S3BlockReadStream = require('s3-block-read-stream'),
targz = require('./targz.js');
var FILE_TYPE = '.tar.gz.enc';
// test only
var originalAWS;
function mockInject(mock) {
@@ -61,6 +60,8 @@ function getBackupFilePath(apiConfig, backupId) {
assert.strictEqual(typeof apiConfig, 'object');
assert.strictEqual(typeof backupId, 'string');
const FILE_TYPE = apiConfig.key ? '.tar.gz.enc' : '.tar.gz';
return path.join(apiConfig.prefix, backupId.endsWith(FILE_TYPE) ? backupId : backupId+FILE_TYPE);
}
@@ -89,7 +90,8 @@ function backup(apiConfig, backupId, sourceDirectories, callback) {
};
var s3 = new AWS.S3(credentials);
s3.upload(params, function (error) {
// s3.upload automatically does a multi-part upload. we set queueSize to 1 to reduce memory usage
s3.upload(params, { partSize: 10 * 1024 * 1024, queueSize: 1 }, function (error) {
if (error) {
debug('[%s] backup: s3 upload error.', backupId, error);
return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
@@ -98,7 +100,7 @@ function backup(apiConfig, backupId, sourceDirectories, callback) {
callback(null);
});
targz.create(sourceDirectories, apiConfig.key || '', passThrough, callback);
targz.create(sourceDirectories, apiConfig.key || null, passThrough, callback);
});
}
@@ -110,8 +112,7 @@ function restore(apiConfig, backupId, destination, callback) {
callback = once(callback);
var isOldFormat = backupId.endsWith('.tar.gz');
var backupFilePath = isOldFormat ? path.join(apiConfig.prefix, backupId) : getBackupFilePath(apiConfig, backupId);
var backupFilePath = getBackupFilePath(apiConfig, backupId);
debug('[%s] restore: %s -> %s', backupId, backupFilePath, destination);
@@ -125,9 +126,9 @@ function restore(apiConfig, backupId, destination, callback) {
var s3 = new AWS.S3(credentials);
var s3get = s3.getObject(params).createReadStream();
var multipartDownload = new S3BlockReadStream(s3, params, { blockSize: 64 * 1024 * 1024, logCallback: debug });
s3get.on('error', function (error) {
multipartDownload.on('error', function (error) {
// TODO ENOENT for the mock, fix upstream!
if (error.code === 'NoSuchKey' || error.code === 'ENOENT') return callback(new BackupsError(BackupsError.NOT_FOUND));
@@ -135,7 +136,7 @@ function restore(apiConfig, backupId, destination, callback) {
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
targz.extract(s3get, isOldFormat, destination, apiConfig.key || '', callback);
targz.extract(multipartDownload, destination, apiConfig.key || null, callback);
});
}
+24 -31
View File
@@ -11,27 +11,27 @@ var assert = require('assert'),
debug = require('debug')('box:storage/targz'),
mkdirp = require('mkdirp'),
progress = require('progress-stream'),
spawn = require('child_process').spawn,
tar = require('tar-fs'),
zlib = require('zlib');
function create(sourceDirectories, key, outStream, callback) {
assert(Array.isArray(sourceDirectories));
assert.strictEqual(typeof key, 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
var pack = tar.pack('/', {
dereference: false, // pack the symlink and not what it points to
entries: sourceDirectories.map(function (m) { return m.source; }),
map: function(header) {
sourceDirectories.forEach(function (m) {
header.name = header.name.replace(new RegExp('^' + m.source + '(/?)'), m.destination + '$1');
});
return header;
}
},
strict: false // do not error for unknown types (skip fifo, char/block devices)
});
var gzip = zlib.createGzip({});
var encrypt = crypto.createCipher('aes-256-cbc', key);
var progressStream = progress({ time: 10000 }); // display a progress every 10 seconds
pack.on('error', function (error) {
@@ -44,36 +44,30 @@ function create(sourceDirectories, key, outStream, callback) {
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
encrypt.on('error', function (error) {
debug('backup: encrypt stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
progressStream.on('progress', function(progress) {
debug('backup: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
});
pack.pipe(gzip).pipe(encrypt).pipe(progressStream).pipe(outStream);
if (key !== null) {
var encrypt = crypto.createCipher('aes-256-cbc', key);
encrypt.on('error', function (error) {
debug('backup: encrypt stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
pack.pipe(gzip).pipe(encrypt).pipe(progressStream).pipe(outStream);
} else {
pack.pipe(gzip).pipe(progressStream).pipe(outStream);
}
}
function extract(inStream, isOldFormat, destination, key, callback) {
assert.strictEqual(typeof isOldFormat, 'boolean');
function extract(inStream, destination, key, callback) {
assert.strictEqual(typeof destination, 'string');
assert.strictEqual(typeof key, 'string');
assert(key === null || typeof key === 'string');
assert.strictEqual(typeof callback, 'function');
mkdirp(destination, function (error) {
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
var decrypt;
if (isOldFormat) {
let args = ['aes-256-cbc', '-d', '-pass', 'pass:' + key];
decrypt = spawn('openssl', args, { stdio: [ 'pipe', 'pipe', process.stderr ]});
} else {
decrypt = crypto.createDecipher('aes-256-cbc', key);
}
var gunzip = zlib.createGunzip({});
var progressStream = progress({ time: 10000 }); // display a progress every 10 seconds
var extract = tar.extract(destination);
@@ -82,11 +76,6 @@ function extract(inStream, isOldFormat, destination, key, callback) {
debug('restore: %s@%s', Math.round(progress.transferred/1024/1024) + 'M', Math.round(progress.speed/1024/1024) + 'Mbps');
});
decrypt.on('error', function (error) {
debug('restore: decrypt stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
gunzip.on('error', function (error) {
debug('restore: gunzip stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
@@ -102,11 +91,15 @@ function extract(inStream, isOldFormat, destination, key, callback) {
callback(null);
});
if (isOldFormat) {
inStream.pipe(progressStream).pipe(decrypt.stdin);
decrypt.stdout.pipe(gunzip).pipe(extract);
} else {
if (key !== null) {
var decrypt = crypto.createDecipher('aes-256-cbc', key);
decrypt.on('error', function (error) {
debug('restore: decrypt stream error.', error);
callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
});
inStream.pipe(progressStream).pipe(decrypt).pipe(gunzip).pipe(extract);
} else {
inStream.pipe(progressStream).pipe(gunzip).pipe(extract);
}
});
}
+2 -2
View File
@@ -17,14 +17,14 @@ function getPublicIp(callback) {
superagent.get('http://169.254.169.254/metadata/v1.json').timeout(30 * 1000).end(function (error, result) {
if (error || result.statusCode !== 200) {
console.error('Error getting metadata', error);
return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'No IP found'));
return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'Could not detect public IP from metadata'));
}
// Note that we do not use a floating IP for 3 reasons:
// The PTR record is not set to floating IP, the outbound interface is not changeable to floating IP
// and there are reports that port 25 on floating IP is blocked.
var ip = safe.query(result.body, 'interfaces.public[0].ipv4.ip_address');
if (!ip) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'No IP found'));
if (!ip) return callback(new SysInfoError(SysInfoError.INTERNAL_ERROR, 'Could not detect public IP from interface'));
callback(null, ip);
});
+1 -1
View File
@@ -30,7 +30,7 @@ var MANIFEST = {
"contactEmail": "support@cloudron.io",
"version": "0.1.0",
"manifestVersion": 1,
"dockerImage": "cloudron/test:18.0.0",
"dockerImage": "cloudron/test:23.0.0",
"healthCheckPath": "/",
"httpPort": 7777,
"tcpPorts": {
+3 -53
View File
@@ -103,7 +103,7 @@ describe('backups', function () {
backups.cleanup(function (error) {
expect(error).to.not.be.ok();
backups.getPaged(1, 1000, function (error, result) {
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, result) {
expect(error).to.not.be.ok();
expect(result.length).to.equal(1);
expect(result[0].id).to.equal(BACKUP_1.id);
@@ -124,7 +124,7 @@ describe('backups', function () {
backups.cleanup(function (error) {
expect(error).to.not.be.ok();
backups.getPaged(1, 1000, function (error, result) {
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, result) {
expect(error).to.not.be.ok();
expect(result.length).to.equal(1);
expect(result[0].id).to.equal(BACKUP_1.id);
@@ -149,7 +149,7 @@ describe('backups', function () {
backups.cleanup(function (error) {
expect(error).to.not.be.ok();
backupdb.getPaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, result) {
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, result) {
expect(error).to.not.be.ok();
expect(result.length).to.equal(2);
@@ -159,55 +159,5 @@ describe('backups', function () {
}, 2000);
});
});
it('succeeds for app backups not referenced by a box backup or app', function (done) {
var APP_0 = {
id: 'appid-0',
appStoreId: 'appStoreId-0',
dnsRecordId: null,
installationState: appdb.ISTATE_PENDING_INSTALL,
installationProgress: null,
runState: null,
location: 'some-location-0',
manifest: { version: '0.1', dockerImage: 'docker/app0', healthCheckPath: '/', httpPort: 80, title: 'app0' },
httpPort: null,
containerId: null,
portBindings: { port: 5678 },
health: null,
accessRestriction: null,
lastBackupId: null,
oldConfig: null,
memoryLimit: 4294967296,
altDomain: null,
xFrameOptions: 'DENY',
sso: true,
debugMode: null
};
async.eachSeries([BACKUP_0_APP_0, BACKUP_0_APP_1], backupdb.add, function (error) {
expect(error).to.not.be.ok();
// wait for expiration
setTimeout(function () {
// now reference one backup
APP_0.lastBackupId = BACKUP_0_APP_0.id;
appdb.add(APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0, function (error) {
expect(error).to.not.be.ok();
backups.cleanup(function (error) {
expect(error).to.not.be.ok();
backupdb.getPaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, result) {
expect(error).to.not.be.ok();
expect(result.length).to.equal(3);
done();
});
});
});
}, 2000);
});
});
});
});
+1 -1
View File
@@ -3,7 +3,7 @@
set -eu
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
readonly TEST_IMAGE="cloudron/test:22.0.0"
readonly TEST_IMAGE="cloudron/test:23.0.0"
# reset sudo timestamp to avoid wrong success
sudo -k || sudo --reset-timestamp
+2 -2
View File
@@ -1051,8 +1051,8 @@ describe('database', function () {
});
});
it('getPaged succeeds', function (done) {
backupdb.getPaged(backupdb.BACKUP_TYPE_BOX, 1, 5, function (error, results) {
it('getByTypePaged succeeds', function (done) {
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 5, function (error, results) {
expect(error).to.be(null);
expect(results).to.be.an(Array);
expect(results.length).to.be(1);
+13 -12
View File
@@ -12,7 +12,6 @@ exports = module.exports = {
var apps = require('./apps.js'),
appstore = require('./appstore.js'),
async = require('async'),
config = require('./config.js'),
constants = require('./constants.js'),
debug = require('debug')('box:updatechecker'),
mailer = require('./mailer.js'),
@@ -28,7 +27,7 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
function loadState() {
var state = safe.JSON.parse(safe.fs.readFileSync(paths.UPDATE_CHECKER_FILE, 'utf8'));
return state || { };
return state || {};
}
function saveState(mailedUser) {
@@ -111,7 +110,10 @@ function checkAppUpdates(callback) {
iteratorDone();
});
}, function () {
newState.box = loadState().box; // preserve the latest box state information
// preserve the latest box state information
newState.box = loadState().box;
newState.boxTimestamp = loadState().boxTimestamp;
saveState(newState);
callback();
});
@@ -143,22 +145,21 @@ function checkBoxUpdates(callback) {
// decide whether to send email
var state = loadState();
if (state.box === gBoxUpdateInfo.version) {
debug('Skipping notification of box update as user was already notified');
const NOTIFICATION_OFFSET = 1000 * 60 * 60 * 24 * 5; // 5 days
if (state.box === gBoxUpdateInfo.version && state.boxTimestamp > Date.now() - NOTIFICATION_OFFSET) {
debug('Skipping notification of box update as user was already notified within the last 5 days');
return callback();
}
state.boxTimestamp = Date.now();
state.box = updateInfo.version;
mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
saveState(state);
// only send notifications if update pattern is 'never'
settings.getAutoupdatePattern(function (error, result) {
if (error) debug(error);
else if (result === constants.AUTOUPDATE_PATTERN_NEVER) mailer.boxUpdateAvailable(updateInfo.version, updateInfo.changelog);
callback();
});
callback();
});
});
}
+1
View File
@@ -0,0 +1 @@
!function(t,e,i,n){"use strict";i.module("ngFitText",[]).value("fitTextDefaultConfig",{debounce:!1,delay:250,loadDelay:10,compressor:1,min:0,max:Number.POSITIVE_INFINITY}).directive("fittext",["$timeout","fitTextDefaultConfig","fitTextConfig",function(e,n,o){return{restrict:"A",scope:!0,link:function(f,a,l){function r(){var t=T*h/s.offsetWidth/h;return Math.max(Math.min((c[0].offsetWidth-6)*t*p,parseFloat(y)),parseFloat(m))}function u(){s.offsetHeight*s.offsetWidth!==0&&(d.fontSize=T+"px",d.lineHeight="1",d.display="inline-block",d.fontSize=r()+"px",d.lineHeight=b,d.display=v)}i.extend(n,o.config);var c=a.parent(),s=a[0],d=s.style,x=t.getComputedStyle(a[0],null),h=a.children().length||1,g=l.fittextLoadDelay||n.loadDelay,p=l.fittext||n.compressor,m=("inherit"===l.fittextMin?x["font-size"]:l.fittextMin)||n.min,y=("inherit"===l.fittextMax?x["font-size"]:l.fittextMax)||n.max,b=x["line-height"],v=x.display,T=10;e(function(){u()},g),f.$watch(l.ngBind,function(){u()}),n.debounce?i.element(t).bind("resize",n.debounce(function(){f.$apply(u)},n.delay)):i.element(t).bind("resize",function(){f.$apply(u)})}}}]).provider("fitTextConfig",function(){var t=this;return this.config={},this.$get=function(){var e={};return e.config=t.config,e},this})}(window,document,angular);
+15 -5
View File
@@ -1,14 +1,19 @@
// !!!
// This module is manually patched by us to not only report valid domains, but verify that subdomains are not accepted
// !!!
'use strict';
angular.module('ngTld', [])
.factory('ngTld', ngTld)
.directive('checkTld', checkTld);
function ngTld() {
function tldExists(path) {
function isValid(path) {
// https://github.com/oncletom/tld.js/issues/58
return (path.slice(-1) !== '.') && tld.isValid(path);
}
function tldExists(path) {
return (path.slice(-1) !== '.') && path === tld.getDomain(path);
}
@@ -16,9 +21,15 @@ function ngTld() {
return (path.slice(-1) !== '.') && !!tld.getDomain(path) && path !== tld.getDomain(path);
}
function isNakedDomain(path) {
return (path.slice(-1) !== '.') && !!tld.getDomain(path) && path === tld.getDomain(path);
}
return {
isValid: isValid,
tldExists: tldExists,
isSubdomain: isSubdomain
isSubdomain: isSubdomain,
isNakedDomain: isNakedDomain
};
}
@@ -28,13 +39,12 @@ function checkTld(ngTld) {
require: 'ngModel',
link: function(scope, element, attr, ngModel) {
ngModel.$validators.invalidTld = function(modelValue, viewValue) {
return ngTld.tldExists(ngModel.$viewValue);
return ngTld.tldExists(ngModel.$viewValue.toLowerCase());
};
ngModel.$validators.invalidSubdomain = function(modelValue, viewValue) {
return !ngTld.isSubdomain(ngModel.$viewValue);
return !ngTld.isSubdomain(ngModel.$viewValue.toLowerCase());
};
}
};
}
+1 -1
View File
@@ -63,7 +63,7 @@
<div class="wrapper">
<div class="content">
<img ng-src="avatarUrl" onerror="this.src = '/img/logo_inverted_192.png'"/>
<img ng-src="avatarUrl" width="128" height="128" onerror="this.src = '/img/logo.png'"/>
<h1> Cloudron </h1>
<div ng-show="errorCode == 0">
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

+52
View File
@@ -40,6 +40,7 @@
<script src="3rdparty/js/angular-sanitize.min.js"></script>
<script src="3rdparty/js/angular-slick.min.js"></script>
<script src="3rdparty/js/angular-ui-notification.min.js"></script>
<script src="3rdparty/js/angular-fittext.min.js"></script>
<script src="3rdparty/js/autofill-event.js"></script>
<!-- Angular directives for tldjs -->
@@ -138,6 +139,56 @@
</div>
</div>
<!-- Modal setup subscription -->
<div class="modal fade" id="setupSubscriptionModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title" id="updateModalLabel">Setup Subscription</h4>
</div>
<div class="modal-body">
<p>
You can update to the next version once you have selected a <a ng-href="{{ config.webServerOrigin + '/pricing.html' }}" target="_blank">plan</a>.
</p>
<p>
With a paid plan, you get continuous updates for the Cloudron and apps. This ensures you are running the latest versions of apps and keeps your server secure. All paid plans come with support via <a href="mailto:support@cloudron.io">email</a> and <a target="_blank" href="https://chat.cloudron.io">live chat</a>.
</p>
</div>
<div class="modal-footer">
<a class="btn btn-success" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email }}" target="_blank">Setup Subscription</a>
</div>
</div>
</div>
</div>
<!-- Modal version 1.0 -->
<div class="modal fade" id="version1Modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<h3>Cloudron 1.0 is here!</h3>
<p>
Your Cloudron will update to 1.0 once you have selected a <a ng-href="{{ config.webServerOrigin + '/pricing.html' }}" target="_blank">plan</a>.
</p>
<p>
With a paid plan, you get continuous updates for the Cloudron and apps just like you had until now. This ensures you are running the latest versions of apps and keeps your server secure. All paid plans come with support via <a href="mailto:support@cloudron.io">email</a> and <a target="_blank" href="https://chat.cloudron.io">live chat</a>.
</p>
<p>
With the free plan, you can keep the Cloudron and Apps updated on your own
following the instructions in our <a href="https://git.cloudron.io/cloudron/box/wikis/home" target="_blank">wiki</a>.
</p>
<p>
You can read more about our pricing changes in our <a href="https://cloudron.io/blog/2017-06-06-pricing.html" target="_blank">blog</a>.
</p>
</div>
<div class="modal-footer">
<a class="btn btn-success" ng-click="waitForPlanSelection()" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank"><i class="fa fa-circle-o-notch fa-spin" ng-show="waitingForPlanSelection"></i> Setup Subscription</a>
</div>
</div>
</div>
</div>
<div class="animateMe ng-hide" ng-show="initialized">
<!-- Navigation -->
@@ -178,6 +229,7 @@
<li ng-show="user.admin"><a href="#/activity"><i class="fa fa-list-alt fa-fw"></i> Activity</a></li>
<li ng-show="user.admin"><a href="#/tokens"><i class="fa fa-key fa-fw"></i> API Access</a></li>
<li ng-show="user.admin"><a href="#/certs"><i class="fa fa-certificate fa-fw"></i> Domain & Certs</a></li>
<li ng-show="user.admin"><a href="#/email"><i class="fa fa-envelope fa-fw"></i> Email</a></li>
<li ng-show="user.admin"><a href="#/graphs"><i class="fa fa-bar-chart fa-fw"></i> Graphs</a></li>
<li ng-show="user.admin"><a href="#/settings"><i class="fa fa-wrench fa-fw"></i> Settings</a></li>
<li ng-show="user.admin" class="divider"></li>
+11
View File
@@ -180,5 +180,16 @@ angular.module('Application').service('AppStore', ['$http', '$base64', 'Client',
});
};
AppStore.prototype.getSubscription = function (appstoreConfig, callback) {
if (Client.getConfig().apiServerOrigin === null) return callback(new AppStoreError(420, 'Enhance Your Calm'));
$http.get(Client.getConfig().apiServerOrigin + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/subscription', { params: { accessToken: appstoreConfig.token }}).success(function (data, status) {
if (status !== 200) return callback(new AppStoreError(status, data));
return callback(null, data.subscription);
}).error(function (data, status) {
return callback(new AppStoreError(status, data));
});
};
return new AppStore();
}]);
+13 -1
View File
@@ -9,7 +9,7 @@ if (search.accessToken) localStorage.token = search.accessToken;
// create main application module
var app = angular.module('Application', ['ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld']);
var app = angular.module('Application', ['ngFitText', 'ngRoute', 'ngAnimate', 'ngSanitize', 'angular-md5', 'base64', 'slick', 'ui-notification', 'ui.bootstrap', 'ui.bootstrap-slider', 'ngTld']);
app.config(['NotificationProvider', function (NotificationProvider) {
NotificationProvider.setOptions({
@@ -46,6 +46,9 @@ app.config(['$routeProvider', function ($routeProvider) {
}).when('/certs', {
controller: 'CertsController',
templateUrl: 'views/certs.html'
}).when('/email', {
controller: 'EmailController',
templateUrl: 'views/email.html'
}).when('/settings', {
controller: 'SettingsController',
templateUrl: 'views/settings.html'
@@ -467,3 +470,12 @@ app.directive('tagInput', function () {
'</div>'
};
});
app.config(['fitTextConfigProvider', function (fitTextConfigProvider) {
fitTextConfigProvider.config = {
loadDelay: 250,
compressor: 0.9,
min: 8,
max: 24
};
}]);
+90 -13
View File
@@ -1,11 +1,14 @@
'use strict';
angular.module('Application').controller('MainController', ['$scope', '$route', '$interval', 'Client', function ($scope, $route, $interval, Client) {
angular.module('Application').controller('MainController', ['$scope', '$route', '$interval', '$timeout', 'Client', 'AppStore', function ($scope, $route, $interval, $timeout, Client, AppStore) {
$scope.initialized = false;
$scope.user = Client.getUserInfo();
$scope.installedApps = Client.getInstalledApps();
$scope.config = {};
$scope.status = {};
$scope.client = Client;
$scope.currentSubscription = null;
$scope.appstoreConfig = {};
$scope.update = {
busy: false,
@@ -29,6 +32,31 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
window.location.href = '/error.html';
};
$scope.waitingForPlanSelection = false;
$('#version1Modal').on('hide.bs.modal', function () {
$scope.waitingForPlanSelection = false;
});
$scope.waitForPlanSelection = function () {
if ($scope.waitingForPlanSelection) return;
$scope.waitingForPlanSelection = true;
function checkPlan() {
if (!$scope.waitingForPlanSelection) return;
if ($scope.currentSubscription.plan.id !== 'undecided') {
$scope.waitingForPlanSelection = false;
$('#version1Modal').modal('hide');
$('#updateModal').modal('show');
} else {
$timeout(checkPlan, 1000);
}
}
checkPlan();
};
$scope.showUpdateModal = function (form) {
$scope.update.error.generic = null;
$scope.update.error.password = null;
@@ -37,7 +65,25 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
form.$setPristine();
form.$setUntouched();
$('#updateModal').modal('show');
if (!$scope.config.update.box.sourceTarballUrl) {
// no sourceTarballUrl means we can't update here this is only from 1.0 on
// this will also handle the 'undecided' and 'free' plan, since the server does not send the url in this case
$('#setupSubscriptionModal').modal('show');
} else if ($scope.config.provider === 'caas') {
$('#updateModal').modal('show');
} else if (!$scope.currentSubscription || !$scope.currentSubscription.plan) {
// do nothing as we were not able to get a subscription, yet
} else if ($scope.config.update.box.version === '1.0.0') {
// special case for updating to 1.0
if ($scope.currentSubscription.plan.id === 'undecided') {
$('#version1Modal').modal('show');
} else {
// user selected a plan already, let him update
$('#updateModal').modal('show');
}
} else {
$('#updateModal').modal('show');
}
};
$scope.doUpdate = function () {
@@ -73,13 +119,13 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
Client.getDnsConfig(function (error, result) {
if (error) return console.error(error);
// check if we have aws credentials if selfhosting
if ($scope.config.isCustomDomain) {
if (result.provider === 'route53' && (!result.accessKeyId || !result.secretAccessKey)) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/certs';
Client.notify('Missing AWS credentials', 'Please provide AWS credentials, click here to add them.', true, 'error', actionScope);
}
var actionScope;
// warn user if dns config is not working (the 'configuring' flag detects if configureWebadmin is 'active')
if (!$scope.status.webadminStatus.configuring && !$scope.status.webadminStatus.dns) {
actionScope = $scope.$new(true);
actionScope.action = '/#/certs';
Client.notify('Invalid Domain Config', 'Unable to update DNS. Click here to update it.', true, 'error', actionScope);
}
if (result.provider === 'caas') return;
@@ -90,7 +136,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
if (!result.dns.spf.status || !result.dns.dkim.status || !result.dns.ptr.status || !result.outboundPort25.status) {
var actionScope = $scope.$new(true);
actionScope.action = '/#/settings';
actionScope.action = '/#/email';
Client.notify('DNS Configuration', 'Please setup all required DNS records to guarantee correct mail delivery', false, 'info', actionScope);
}
@@ -98,6 +144,31 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
});
}
function getSubscription() {
Client.getAppstoreConfig(function (error, result) {
if (error) return console.error(error);
if (result.token) {
$scope.appstoreConfig = result;
AppStore.getProfile(result.token, function (error, result) {
if (error) return console.error(error);
$scope.appstoreConfig.profile = result;
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return console.error(error);
$scope.currentSubscription = result;
// check again to give more immediate feedback once a subscription was setup
if (result.plan.id === 'undecided') $timeout(getSubscription, 5000);
});
});
}
});
}
Client.getStatus(function (error, status) {
if (error) return $scope.error(error);
@@ -109,8 +180,8 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
// 4. local development with gulp develop
if (!status.activated) {
console.log('You have on domain, redirecting', status.configState.configured);
window.location.href = status.configState.configured ? '/setup.html' : '/setupdns.html';
console.log('Not activated yet, redirecting', status);
window.location.href = status.adminFqdn ? '/setup.html' : '/setupdns.html';
return;
}
@@ -120,6 +191,8 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
return;
}
$scope.status = status;
Client.refreshConfig(function (error) {
if (error) return $scope.error(error);
@@ -156,7 +229,11 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
$scope.initialized = true;
if ($scope.user.admin) runConfigurationChecks();
if ($scope.user.admin) {
runConfigurationChecks();
if ($scope.config.provider !== 'caas') getSubscription();
}
});
});
});
+7 -3
View File
@@ -23,16 +23,20 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
$scope.activateCloudron = function () {
$scope.busy = true;
$scope.error = null;
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken, function (error) {
if (error && error.statusCode === 403) {
if (error && error.statusCode === 400) {
$scope.busy = false;
$scope.error = $scope.provider === 'ami' ? 'Wrong instance id' : 'Wrong setup token';
$scope.error = { username: error.message };
$scope.account.username = '';
$scope.setupForm.username.$setPristine();
setTimeout(function () { $('#inputUsername').focus(); }, 200);
return;
} else if (error) {
$scope.busy = false;
console.error('Internal error', error);
$scope.error = error;
$scope.error = { generic: error.message };
return;
}
+1 -1
View File
@@ -70,7 +70,7 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', func
$scope.busy = true;
Client.getStatus(function (error, status) {
if (!error && status.adminFqdn && status.configState.dns && status.configState.tls) {
if (!error && status.adminFqdn && status.webadminStatus.dns && status.webadminStatus.tls) {
window.location.href = 'https://' + status.adminFqdn + '/setup.html';
}
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="wrapper">
<div class="content">
<h1>
<img id="avatar" width="48" height="48" src="/api/v1/cloudron/avatar" onerror="this.src = '/img/logo_inverted_192.png'"/>
<img id="avatar" width="48" height="48" src="/api/v1/cloudron/avatar" onerror="this.src = '/img/logo.png'"/>
<span style="padding-left:10px">Cloudron</span>
</h1>
<br/>
+2 -1
View File
@@ -72,7 +72,8 @@
<div ng-show="account.requireEmail" class="form-group" ng-class="{ 'has-error': setupForm.email.$dirty && setupForm.email.$invalid }">
<input type="email" class="form-control" ng-model="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off" tooltip-trigger="focus" uib-tooltip="This email address is local to your Cloudron and used for notifications and password reset.">
</div>
<div class="form-group" ng-class="{ 'has-error': setupForm.username.$dirty && setupForm.username.$invalid }">
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) || (!setupForm.username.$dirty && error.username) }">
<p ng-show="!setupForm.username.$dirty && error.username">{{ error.username }}</p>
<input type="text" class="form-control" ng-model="account.username" id="inputUsername" name="username" placeholder="Username" ng-maxlength="512" ng-minlength="3" required autocomplete="off">
</div>
<div class="form-group" ng-class="{ 'has-error': setupForm.password.$dirty && setupForm.password.$invalid }">
+11 -2
View File
@@ -170,7 +170,17 @@ h1, h2, h3 {
.grid-item {
padding: 10px;
min-width: 200px;
min-width: 205px;
.col-xs-12 {
padding-left: 5px;
padding-right: 5px;
.status, .status {
padding-left: 5px;
padding-right: 5px;
}
}
}
.grid-item:hover .grid-item-bottom {
@@ -191,7 +201,6 @@ h1, h2, h3 {
}
.grid-item-top-title {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-size: 24px;
+12 -7
View File
@@ -50,11 +50,16 @@
</div>
</div>
<p class="text-center" ng-show="appConfigure.usingAltDomain && appConfigure.location && appConfigure.isAltDomainValid()">
<p class="text-center" ng-show="appConfigure.usingAltDomain && appConfigure.location && appConfigure.isAltDomainSubdomain()">
Add a CNAME record for <b>{{ appConfigure.location }}</b> to <b>{{ appConfigure.app.cnameTarget || appConfigure.app.fqdn }}</b>
<br>
</p>
<p class="text-center" ng-show="appConfigure.usingAltDomain && appConfigure.location && appConfigure.isAltDomainNaked()">
Add an A record for <b>{{ appConfigure.location }}</b> to this Cloudron's public IP</b>
<br>
</p>
<p class="text-center" ng-show="appConfigure.location && dnsConfig.provider === 'manual' && !dnsConfig.wildcard">
<b>Do not forget, to add an A record for {{ appConfigure.location }}.{{ config.fqdn }}</b>
<br>
@@ -173,14 +178,14 @@
</div>
<input type="password" class="form-control" ng-model="appConfigure.password" id="appConfigurePasswordInput" name="password" required>
</div>
<input class="ng-hide" type="submit" ng-disabled="appConfigureForm.$invalid || busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid())"/>
<input class="ng-hide" type="submit" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()) || (appConfigure.usingAltDomain && !appConfigure.isAltDomainValid())"/>
</form>
</fieldset>
</div>
<div class="modal-footer ">
<button type="button" class="btn btn-default pull-left" ng-click="restartApp(appConfigure.app)" ng-disabled="restartAppBusy"><i class="fa fa-circle-o-notch fa-spin" ng-show="restartAppBusy"></i> Restart</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()) || !appConfigure.isAltDomainValid()"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Save</button>
<button type="button" class="btn btn-success" ng-click="doConfigure()" ng-disabled="appConfigureForm.$invalid || appConfigure.busy || (appConfigure.accessRestrictionOption === 'groups' && !appConfigure.isAccessRestrictionValid()) || (appConfigure.usingAltDomain && !appConfigure.isAltDomainValid())"><i class="fa fa-circle-o-notch fa-spin" ng-show="appConfigure.busy"></i> Save</button>
</div>
</div>
</div>
@@ -384,11 +389,11 @@
<br/>
<div class="row">
<div class="col-xs-12 text-center">
<div class="grid-item-top-title">{{ app.altDomain || app.location || app.fqdn }}</div>
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
<div class="grid-item-top-title" data-fittext>{{ app.altDomain || app.location || app.fqdn }}</div>
<div class="text-muted status" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
{{ app | installationStateLabel }}
</div>
<div ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
<div class="status" ng-style="{ 'visibility': (app | installationActive) ? 'visible' : 'hidden' }">
<div class="progress progress-striped active">
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
</div>
@@ -440,7 +445,7 @@
<!-- we check the version here because the box updater does not know when an app gets updated -->
<div class="app-update-badge" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
<a href="" ng-click="showUpdate(app)" title="Update Available"><i class="fa fa-asterisk fa-2x text-success scale"></i></a>
<a href="" ng-click="showUpdate(app, config.update.apps[app.id].manifest)" title="Update Available"><i class="fa fa-asterisk fa-2x text-success scale"></i></a>
</div>
</a>
</div>
+72 -65
View File
@@ -1,7 +1,7 @@
'use strict';
angular.module('Application').controller('AppsController', ['$scope', '$location', '$timeout', 'Client', 'AppStore', function ($scope, $location, $timeout, Client, AppStore) {
angular.module('Application').controller('AppsController', ['$scope', '$location', '$timeout', 'Client', 'ngTld', 'AppStore', function ($scope, $location, $timeout, Client, ngTld, AppStore) {
$scope.HOST_PORT_MIN = 1024;
$scope.HOST_PORT_MAX = 65535;
$scope.installedApps = Client.getInstalledApps();
@@ -42,8 +42,15 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
},
isAltDomainValid: function () {
if (!$scope.appConfigure.usingAltDomain) return true;
return /.+\..+\..+/.test($scope.appConfigure.location); // 2 dots
return ngTld.isValid($scope.appConfigure.location);
},
isAltDomainSubdomain: function () {
return ngTld.isSubdomain($scope.appConfigure.location);
},
isAltDomainNaked: function () {
return ngTld.isNakedDomain($scope.appConfigure.location);
}
};
@@ -322,7 +329,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
Client.getAppBackups(app.id, function (error, backups) {
if (error) {
Client.error(error)
Client.error(error);
} else {
$scope.appRestore.backups = backups;
if (backups.length) $scope.appRestore.selectedBackup = backups[0]; // pre-select first backup
@@ -385,79 +392,79 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
});
};
$scope.showUpdate = function (app) {
$scope.showUpdate = function (app, updateManifest) {
if (!updateManifest.dockerImage) {
$('#setupSubscriptionModal').modal('show');
return;
}
$scope.reset();
$scope.appUpdate.app = app;
$scope.appUpdate.manifest = angular.copy(updateManifest);
AppStore.getManifest(app.appStoreId, function (error, manifest) {
if (error) return console.error(error);
// ensure we always operate on objects here
app.portBindings = app.portBindings || {};
app.manifest.tcpPorts = app.manifest.tcpPorts || {};
updateManifest.tcpPorts = updateManifest.tcpPorts || {};
$scope.appUpdate.manifest = angular.copy(manifest);
// Activate below two lines for testing the UI
// updateManifest.tcpPorts['TEST_HTTP'] = { defaultValue: 1337, description: 'HTTP server'};
// app.manifest.tcpPorts['TEST_FOOBAR'] = { defaultValue: 1338, description: 'FOOBAR server'};
// app.portBindings['TEST_SSH'] = 1339;
// ensure we always operate on objects here
app.portBindings = app.portBindings || {};
app.manifest.tcpPorts = app.manifest.tcpPorts || {};
manifest.tcpPorts = manifest.tcpPorts || {};
var portBindingsInfo = {}; // Portbinding map only for information
var portBindings = {}; // This is the actual model holding the env:port pair
var portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
var obsoletePortBindings = {}; // Info map for obsolete port bindings, this is for display use only and thus not in the model
var portsChanged = false;
var env;
// Activate below two lines for testing the UI
// manifest.tcpPorts['TEST_HTTP'] = { defaultValue: 1337, description: 'HTTP server'};
// app.manifest.tcpPorts['TEST_FOOBAR'] = { defaultValue: 1338, description: 'FOOBAR server'};
// app.portBindings['TEST_SSH'] = 1339;
// detect new portbindings and copy all from manifest.tcpPorts
for (env in updateManifest.tcpPorts) {
portBindingsInfo[env] = updateManifest.tcpPorts[env];
if (!app.manifest.tcpPorts[env]) {
portBindingsInfo[env].isNew = true;
portBindingsEnabled[env] = true;
var portBindingsInfo = {}; // Portbinding map only for information
var portBindings = {}; // This is the actual model holding the env:port pair
var portBindingsEnabled = {}; // This is the actual model holding the enabled/disabled flag
var obsoletePortBindings = {}; // Info map for obsolete port bindings, this is for display use only and thus not in the model
var portsChanged = false;
var env;
// use default integer port value in model
portBindings[env] = updateManifest.tcpPorts[env].defaultValue || 0;
// detect new portbindings and copy all from manifest.tcpPorts
for (env in manifest.tcpPorts) {
portBindingsInfo[env] = manifest.tcpPorts[env];
if (!app.manifest.tcpPorts[env]) {
portBindingsInfo[env].isNew = true;
portBindingsEnabled[env] = true;
// use default integer port value in model
portBindings[env] = manifest.tcpPorts[env].defaultValue || 0;
portsChanged = true;
} else {
// detect if the port binding was enabled
if (app.portBindings[env]) {
portBindings[env] = app.portBindings[env];
portBindingsEnabled[env] = true;
} else {
portBindings[env] = manifest.tcpPorts[env].defaultValue || 0;
portBindingsEnabled[env] = false;
}
}
}
// detect obsolete portbindings (mappings in app.portBindings, but not anymore in manifest.tcpPorts)
for (env in app.manifest.tcpPorts) {
// only list the port if it is not in the new manifest and was enabled previously
if (!manifest.tcpPorts[env] && app.portBindings[env]) {
obsoletePortBindings[env] = app.portBindings[env];
portsChanged = true;
}
}
// now inject the maps into the $scope, we only show those if ports have changed
$scope.appUpdate.portBindings = portBindings; // always inject the model, so it gets used in the actual update call
$scope.appUpdate.portBindingsEnabled = portBindingsEnabled; // always inject the model, so it gets used in the actual update call
if (portsChanged) {
$scope.appUpdate.portBindingsInfo = portBindingsInfo;
$scope.appUpdate.obsoletePortBindings = obsoletePortBindings;
portsChanged = true;
} else {
$scope.appUpdate.portBindingsInfo = {};
$scope.appUpdate.obsoletePortBindings = {};
// detect if the port binding was enabled
if (app.portBindings[env]) {
portBindings[env] = app.portBindings[env];
portBindingsEnabled[env] = true;
} else {
portBindings[env] = updateManifest.tcpPorts[env].defaultValue || 0;
portBindingsEnabled[env] = false;
}
}
}
$('#appUpdateModal').modal('show');
});
// detect obsolete portbindings (mappings in app.portBindings, but not anymore in updateManifest.tcpPorts)
for (env in app.manifest.tcpPorts) {
// only list the port if it is not in the new manifest and was enabled previously
if (!updateManifest.tcpPorts[env] && app.portBindings[env]) {
obsoletePortBindings[env] = app.portBindings[env];
portsChanged = true;
}
}
// now inject the maps into the $scope, we only show those if ports have changed
$scope.appUpdate.portBindings = portBindings; // always inject the model, so it gets used in the actual update call
$scope.appUpdate.portBindingsEnabled = portBindingsEnabled; // always inject the model, so it gets used in the actual update call
if (portsChanged) {
$scope.appUpdate.portBindingsInfo = portBindingsInfo;
$scope.appUpdate.obsoletePortBindings = obsoletePortBindings;
} else {
$scope.appUpdate.portBindingsInfo = {};
$scope.appUpdate.obsoletePortBindings = {};
}
$('#appUpdateModal').modal('show');
};
$scope.doUpdate = function (form) {
+115
View File
@@ -0,0 +1,115 @@
<!-- Modal enable email -->
<div class="modal fade" id="enableEmailModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Cloudron Email Server</h4>
</div>
<div class="modal-body" ng-show="dnsConfig.provider === 'noop' || dnsConfig.provider === 'manual'">
No DNS provider is setup. Displayed DNS records will have to be setup manually.<br/>
</div>
<div class="modal-body" ng-show="dnsConfig.provider === 'route53' || dnsConfig.provider === 'digitalocean'">
The Cloudron will setup Email related DNS records automatically.
If this domain is already configured to handle email with some other provider, it will <b>overwrite</b> those records.
<br/><br/>
Disabling Cloudron Email later will <b>not</b> put the old records back.
<br/><br/>
Status of DNS Records will show an error when DNS is propagating (~5 minutes).
<br/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" ng-click="email.enable()">I understand, enable</button>
</div>
</div>
</div>
</div>
<br/>
<div class="section-header">
<div class="text-left">
<h1>Email</h1>
</div>
</div>
<div class="section-header">
<div class="text-left">
<h3>IMAP and SMTP Server</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
Cloudron has a built-in email server that allows users to send and receive email for your domain.
The <a href="https://cloudron.io/references/usermanual.html#email" target="_blank">User manual</a> has information on how to setup email clients.
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12" ng-show="dnsConfig.provider !== 'caas'">
<button ng-class="mailConfig.enabled ? 'btn btn-danger' : 'btn btn-primary'" ng-click="email.toggle()" ng-enabled="mailConfig">{{ mailConfig.enabled ? "Disable Email" : "Enable Email" }}</button>
</div>
<div class="col-md-12" ng-show="dnsConfig.provider === 'caas'">
<span class="text-danger text-bold">This feature requires the Cloudron to be on <a href="https://cloudron.io/references/usermanual.html#entire-cloudron-on-a-custom-domain" target="_blank">custom domain</a>.</span>
</div>
</div>
</div>
<div class="section-header" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
<div class="text-left">
<h3>DNS Records</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;" ng-show="dnsConfig.provider && dnsConfig.provider !== 'caas'">
<div class="row">
<div class="col-md-12">
Set the following DNS records to guarantee email delivery:
<br/><br/>
<div ng-repeat="record in expectedDnsRecordsTypes">
<div class="row" ng-if="mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX')">
<div class="col-xs-12">
<p class="text-muted">
<i ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i> &nbsp;
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">{{ record.name }} record</a>
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
</p>
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
<div class="panel-body">
<p>Domain: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
<p>Record type: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
<p style="overflow: auto; white-space: nowrap;">Expected value: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].expected }}</tt></b></p>
<p style="overflow: auto; white-space: nowrap;">Current value: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].value ? expectedDnsRecords[record.value].value : '[not set]' }}</tt></b></p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<p class="text-muted">
<i ng-class="outboundPort25.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'"></i> &nbsp;
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_port">
Outbound SMTP (Port 25)
</a>
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!outboundPort25.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
</p>
<div id="collapse_dns_port" class="panel-collapse collapse">
<div class="panel-body">
<p><b> {{ outboundPort25.value }} </b> </p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Offset the footer -->
<br/><br/>
+117
View File
@@ -0,0 +1,117 @@
'use strict';
angular.module('Application').controller('EmailController', ['$scope', '$location', '$rootScope', 'Client', function ($scope, $location, $rootScope, Client) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.client = Client;
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.dnsConfig = {};
$scope.outboundPort25 = {};
$scope.expectedDnsRecords = {};
$scope.expectedDnsRecordsTypes = [
{ name: 'MX', value: 'mx' },
{ name: 'DKIM', value: 'dkim' },
{ name: 'SPF', value: 'spf' },
{ name: 'DMARC', value: 'dmarc' },
{ name: 'PTR', value: 'ptr' }
];
$scope.mailConfig = null;
$scope.showView = function (view) {
// wait for dialog to be fully closed to avoid modal behavior breakage when moving to a different view already
$('.modal').on('hidden.bs.modal', function () {
$('.modal').off('hidden.bs.modal');
$location.path(view);
});
$('.modal').modal('hide');
};
$scope.email = {
refreshBusy: false,
toggle: function () {
if ($scope.mailConfig.enabled) return $scope.email.disable();
// show warning first
$('#enableEmailModal').modal('show');
},
enable: function () {
$('#enableEmailModal').modal('hide');
Client.setMailConfig({ enabled: true }, function (error) {
if (error) return console.error(error);
$scope.mailConfig.enabled = true;
});
},
disable: function () {
Client.setMailConfig({ enabled: false }, function (error) {
if (error) return console.error(error);
$scope.mailConfig.enabled = false;
});
},
refresh: function () {
$scope.email.refreshBusy = true;
showExpectedDnsRecords(function (error) {
if (error) console.error(error);
$scope.email.refreshBusy = false;
});
}
};
function getMailConfig() {
Client.getMailConfig(function (error, mailConfig) {
if (error) return console.error(error);
$scope.mailConfig = mailConfig;
});
}
function getDnsConfig() {
Client.getDnsConfig(function (error, dnsConfig) {
if (error) return console.error(error);
$scope.dnsConfig = dnsConfig;
});
}
function showExpectedDnsRecords(callback) {
callback = callback || function (error) { if (error) console.error(error); };
Client.getEmailStatus(function (error, result) {
if (error) return callback(error);
$scope.expectedDnsRecords = result.dns;
$scope.outboundPort25 = result.outboundPort25;
// open the record details if they are not correct
for (var type in $scope.expectedDnsRecords) {
if (!$scope.expectedDnsRecords[type].status) {
$('#collapse_dns_' + type).collapse('show');
}
}
if (!$scope.outboundPort25.status) {
$('#collapse_dns_port').collapse('show');
}
callback(null);
});
}
Client.onReady(function () {
getMailConfig();
getDnsConfig();
$scope.email.refresh();
});
$('.modal-backdrop').remove();
}]);
+4 -2
View File
@@ -111,8 +111,10 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
Client.disks(function (error, disks) {
if (error) return console.log(error);
// We have to see if this is sufficient for all server configurations
var appDataDiskName = disks.appsDataDisk.slice(disks.appsDataDisk.lastIndexOf('/') + 1)
// /dev/sda1 -> sda1
// /dev/mapper/foo -> mapper_foo (see #348)
var appDataDiskName = disks.appsDataDisk.slice(disks.appsDataDisk.indexOf('/', 1) + 1)
appDataDiskName = appDataDiskName.replace(/\//g, '_');
Client.graphs([
'absolute(collectd.localhost.df-' + appDataDiskName + '.df_complex-free)',
+42 -56
View File
@@ -179,7 +179,7 @@
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.prefix }" ng-show="s3like(configureBackup.provider)">
<label class="control-label" for="inputConfigureBackupPrefix">Prefix</label>
<input type="text" class="form-control" ng-model="configureBackup.prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="configureBackup.busy" placeholder="Prefix for backup file names" ng-required="s3like(configureBackup.provider)">
<input type="text" class="form-control" ng-model="configureBackup.prefix" id="inputConfigureBackupPrefix" name="prefix" ng-disabled="configureBackup.busy" placeholder="Prefix for backup file names">
</div>
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.region }" ng-show="configureBackup.provider === 's3'">
@@ -197,12 +197,12 @@
<input type="text" class="form-control" ng-model="configureBackup.secretAccessKey" id="inputConfigureBackupSecretAccessKey" name="secretAccessKey" ng-disabled="configureBackup.busy" ng-required="s3like(configureBackup.provider)">
</div>
<div class="form-group">
<div class="form-group" ng-show="configureBackup.provider !== 'noop'">
<label class="control-label" for="storageRetention">Retention Time</label>
<select class="form-control" id="storageRetention" ng-model="configureBackup.retentionSecs" ng-options="a.value as a.name for a in retentionTimes"></select>
</div>
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.key }">
<div class="form-group" ng-class="{ 'has-error': configureBackup.error.key }" ng-show="configureBackup.provider !== 'noop'">
<label class="control-label" for="inputConfigureBackupKey">Encryption key (optional)</label>
<input type="text" class="form-control" ng-model="configureBackup.key" id="inputConfigureBackupKey" name="prefix" ng-disabled="configureBackup.busy" placeholder="Passphrase used to encrypt the backups">
</div>
@@ -221,6 +221,23 @@
</div>
</div>
<!-- Modal subscription required -->
<div class="modal" id="subscriptionRequiredModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
</div>
<div class="modal-body">
The Cloudron Email server is only available in the paid plans.<br/>
<br/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<br/>
<div class="section-header">
@@ -248,10 +265,6 @@
<td class="text-muted" style="vertical-align: top;">Name</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.cloudronName }} <a href="" ng-click="cloudronNameChange.show()"><i class="fa fa-pencil text-small"></i></a></td>
</tr>
<tr ng-show="appstoreConfig.profile">
<td class="text-muted" style="vertical-align: top;">App store account</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ appstoreConfig.profile.email }}</td>
</tr>
<tr ng-show="config.provider === 'caas'">
<td class="text-muted" style="vertical-align: top;">Model</td>
<td class="text-right" style="vertical-align: top; white-space: nowrap;">{{ config.size }} - {{ config.region }}</td>
@@ -278,7 +291,7 @@
<div class="card" style="margin-bottom: 15px;" ng-show="config.provider === 'caas'">
<div class="row">
<div class="col-xs-12 text-right">
<a href="{{ config.webServerOrigin }}/console.html#/userprofile" target="_blank">Change payment method</a>
<a href="{{ config.webServerOrigin }}/console.html#/userprofile?view=credit_card" target="_blank">Change payment method</a>
or
<a href="{{ config.webServerOrigin }}/console.html" target="_blank">Cancel this Cloudron</a>
</div>
@@ -303,67 +316,40 @@
</div>
</div>
<div class="section-header">
<div class="section-header" ng-show="backupConfig.provider !== 'caas'">
<div class="text-left">
<h3>Email</h3>
<h3>Cloudron.io Account</h3>
</div>
</div>
<div class="card" style="margin-bottom: 15px;">
<div class="row">
<div class="col-md-12">
Cloudron has a built-in email server that allows users to send and receive email for your domain.
The <a href="https://cloudron.io/references/usermanual.html#email" target="_blank">User manual</a> has information on how to setup email clients.
<div class="card" style="margin-bottom: 15px;" ng-show="backupConfig.provider !== 'caas'">
<div class="row" ng-show="currentSubscription.plan.id === 'free'">
<div class="col-xs-12">
A cloudron.io subscription will provide you with effortless automatic app and platform updates.
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12" ng-show="dnsConfig.provider !== 'caas'">
<button ng-class="mailConfig.enabled ? 'btn btn-danger' : 'btn btn-primary'" ng-click="email.toggle()" ng-enabled="mailConfig">{{ mailConfig.enabled ? "Disable Email" : "Enable Email" }}</button>
<div class="col-xs-6">
<span class="text-muted">Account Email</span>
</div>
<div class="col-md-12" ng-show="dnsConfig.provider === 'caas'">
<span class="text-danger text-bold">This feature requires the Cloudron to be on <a href="https://cloudron.io/references/usermanual.html#entire-cloudron-on-a-custom-domain" target="_blank">custom domain</a>.</span>
<div class="col-xs-6 text-right">
<a ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?email=' + appstoreConfig.profile.email }}" target="_blank">{{ appstoreConfig.profile.email }}</a>
</div>
</div>
<div class="row">
<div class="col-xs-6">
<span class="text-muted">Subscription</span>
</div>
<div class="col-xs-6 text-right">
<span>{{ currentSubscription.plan.name }}</span>
</div>
</div>
<br/>
<div class="row">
<div class="col-md-12" ng-show="(dnsConfig.provider !== 'caas')">
Set the following DNS records to guarantee email delivery.
<div ng-repeat="record in expectedDnsRecordsTypes">
<div class="row" ng-if="mailConfig.enabled || (record.name !== 'DMARC' && record.name !== 'MX')">
<div class="col-xs-12">
<h4 class="text-muted">
{{ record.name }} record <i ng-class="expectedDnsRecords[record.value].status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'" aria-hidden="true"></i>
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!expectedDnsRecords[record.value].status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
</h4>
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">Advanced</a>
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
<div class="panel-body">
<p>Domain: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].domain }}</tt></b></p>
<p>Record type: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].type }}</tt></b></p>
<p style="overflow: auto; white-space: nowrap;">Expected value: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].expected }}</tt></b></p>
<p style="overflow: auto; white-space: nowrap;">Current value: <b ng-click-select><tt>{{ expectedDnsRecords[record.value].value ? expectedDnsRecords[record.value].value : '[not set]' }}</tt></b></p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<h4 class="text-muted">
Outbound SMTP (Port 25) <i ng-class="outboundPort25.status ? 'fa fa-check-circle text-success' : 'fa fa-exclamation-triangle text-danger'" aria-hidden="true"></i>
<button class="btn btn-xs btn-default" ng-click="email.refresh()" ng-disabled="email.refreshBusy" ng-show="!outboundPort25.status"><i class="fa fa-refresh" ng-class="{ 'fa-pulse': email.refreshBusy }"></i></button>
</h4>
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse_dns_{{ record.value }}">Advanced</a>
<div id="collapse_dns_{{ record.value }}" class="panel-collapse collapse">
<div class="panel-body">
<p><b> {{ outboundPort25.value }} </b> </p>
</div>
</div>
</div>
</div>
<div class="col-xs-12">
<a class="btn btn-primary pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-show="currentSubscription.plan && currentSubscription.plan.id !== 'free'">Change Subscription</a>
<a class="btn btn-success pull-right" ng-href="{{ config.webServerOrigin + '/console.html#/userprofile?view=subscriptions&email=' + appstoreConfig.profile.email + '&cloudronId=' + appstoreConfig.cloudronId }}" target="_blank" ng-show="currentSubscription.plan.id === 'free'">Setup Subscription</a>
</div>
</div>
</div>
+13 -81
View File
@@ -1,26 +1,14 @@
'use strict';
angular.module('Application').controller('SettingsController', ['$scope', '$location', '$rootScope', 'Client', 'AppStore', function ($scope, $location, $rootScope, Client, AppStore) {
angular.module('Application').controller('SettingsController', ['$scope', '$location', '$rootScope', '$timeout', 'Client', 'AppStore', function ($scope, $location, $rootScope, $timeout, Client, AppStore) {
Client.onReady(function () { if (!Client.getUserInfo().admin) $location.path('/'); });
$scope.client = Client;
$scope.user = Client.getUserInfo();
$scope.config = Client.getConfig();
$scope.backupConfig = {};
$scope.dnsConfig = {};
$scope.outboundPort25 = {};
$scope.expectedDnsRecords = {};
$scope.expectedDnsRecordsTypes = [
{ name: 'MX', value: 'mx' },
{ name: 'DKIM', value: 'dkim' },
{ name: 'SPF', value: 'spf' },
{ name: 'DMARC', value: 'dmarc' },
{ name: 'PTR', value: 'ptr' }
];
$scope.appstoreConfig = {};
$scope.mailConfig = null;
$scope.lastBackup = null;
$scope.backups = [];
@@ -32,6 +20,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.availablePlans = [];
$scope.currentPlan = null;
$scope.currentSubscription = null;
// List is from http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region
$scope.s3Regions = [
{ name: 'Asia Pacific (Mumbai)', value: 'ap-south-1' },
@@ -53,7 +43,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
$scope.storageProvider = [
{ name: 'Amazon S3', value: 's3' },
{ name: 'Filesystem', value: 'filesystem' },
{ name: 'Minio', value: 'minio' }
{ name: 'Minio', value: 'minio' },
{ name: 'No-op (Only for testing)', value: 'noop' }
];
$scope.retentionTimes = [
@@ -302,45 +293,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
}
};
$scope.email = {
refreshBusy: false,
toggle: function () {
if ($scope.mailConfig.enabled) return $scope.email.disable();
// show warning first
$('#enableEmailModal').modal('show');
},
enable: function () {
$('#enableEmailModal').modal('hide');
Client.setMailConfig({ enabled: true }, function (error) {
if (error) return console.error(error);
$scope.mailConfig.enabled = true;
});
},
disable: function () {
Client.setMailConfig({ enabled: false }, function (error) {
if (error) return console.error(error);
$scope.mailConfig.enabled = false;
});
},
refresh: function () {
$scope.email.refreshBusy = true;
showExpectedDnsRecords(function (error) {
if (error) console.error(error);
$scope.email.refreshBusy = false;
});
}
};
$scope.s3like = function (provider) {
return provider === 's3' || provider === 'minio';
};
@@ -525,16 +477,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
}
function getMailConfig() {
Client.getMailConfig(function (error, mailConfig) {
if (error) return console.error(error);
$scope.mailConfig = mailConfig;
showExpectedDnsRecords();
});
}
function getBackupConfig() {
Client.getBackupConfig(function (error, backupConfig) {
if (error) return console.error(error);
@@ -551,14 +493,6 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
}
function getDnsConfig() {
Client.getDnsConfig(function (error, dnsConfig) {
if (error) return console.error(error);
$scope.dnsConfig = dnsConfig;
});
}
function getAutoupdatePattern() {
Client.getAutoupdatePattern(function (error, result) {
if (error) return console.error(error);
@@ -568,16 +502,14 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
});
}
function showExpectedDnsRecords(callback) {
callback = callback || function (error) { if (error) console.error(error); };
function getSubscription() {
AppStore.getSubscription($scope.appstoreConfig, function (error, result) {
if (error) return console.error(error);
Client.getEmailStatus(function (error, result) {
if (error) return callback(error);
$scope.currentSubscription = result;
$scope.expectedDnsRecords = result.dns;
$scope.outboundPort25 = result.outboundPort25;
callback(null);
// check again to give more immediate feedback once a subscription was setup
if (result.plan.id === 'free') $timeout(getSubscription, 10000);
});
}
@@ -676,9 +608,7 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
Client.onReady(function () {
fetchBackups();
getMailConfig();
getBackupConfig();
getDnsConfig();
getAutoupdatePattern();
if ($scope.config.provider === 'caas') {
@@ -697,6 +627,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
if (error) return console.error(error);
$scope.appstoreConfig.profile = result;
getSubscription();
});
}
});