Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50c344033c | |||
| 0c269925c2 | |||
| dcd84a9636 | |||
| fa7273025b | |||
| eb3ae2c34f | |||
| eba79cd859 | |||
| d7d8cf97ed | |||
| 089f7301b8 | |||
| fb4f13eb13 | |||
| 89878ff9ad | |||
| ba62f577fa | |||
| 4c5bd2d318 | |||
| 3c318a72f7 | |||
| 23532eafea | |||
| 5b7a080d98 | |||
| 0a44b8c23b | |||
| c0c07c2839 | |||
| 96d2b32a9f | |||
| 795c2ad91c | |||
| fc9a9c3f87 | |||
| d141d6ba21 | |||
| 479da5393a | |||
| 307334ef81 | |||
| c1ec7a06bf | |||
| 1126a0fc1e | |||
| b5f678613b | |||
| b7e3447a46 | |||
| 32fa3b8a51 | |||
| fe0e4000a6 | |||
| 9ceeb70fc2 | |||
| aa8b4f1fba | |||
| 95ba51dfb2 | |||
| c74fb07ff7 | |||
| 03f1326073 | |||
| daa4c66e7f | |||
| 571abc56fe | |||
| 4aaeccecbd | |||
| 4287d69397 | |||
| de328e34d8 | |||
| 8d45ce6971 | |||
| fa3f173e8a | |||
| 414e9bdf05 | |||
| c342e52e7d | |||
| 78aa9c66f7 | |||
| 986ec02ac6 | |||
| 4e0bb9187a | |||
| 9c8a8571b4 | |||
| 7f30b8de9d | |||
| d1bfa4875a | |||
| 0250e1ea59 | |||
| 924fb337e8 | |||
| 0c9dce0c9f | |||
| 9e9470c6af | |||
| 471539d64b | |||
| 95127a868d | |||
| f34d429052 | |||
| 82e53bce36 | |||
| b04a417cfc | |||
| 77641f4b51 | |||
| 765d20c8be | |||
| d2420de594 | |||
| 8e9da38451 | |||
| ddb69eb25c | |||
| 11697f11cf | |||
| 35a2a656d3 | |||
| 6fc69c05ca | |||
| 65cff35be6 | |||
| 7467907c09 | |||
| d6c32a2632 | |||
| 7dc277a80c | |||
| 4881d090f0 | |||
| 48330423c6 | |||
| 88e844b545 | |||
| f45da2efc4 | |||
| f422614e7b | |||
| d164f881ca | |||
| 4994a5da49 | |||
| 393317d114 | |||
| 8de940ae36 | |||
| 374130d12a | |||
| 05fcdb0a67 | |||
| 23827974d8 | |||
| ae2c0f3503 | |||
| cbb93ef7ad | |||
| 4d3c6f7caa | |||
| 4f3c846e2b | |||
| 6ef2f974ae | |||
| 180cafad0c | |||
| f707f59765 | |||
| 969ef3fb11 | |||
| 7af3f85d7c | |||
| ffc0a75545 | |||
| d5b5bdb104 | |||
| 8ae65661dd | |||
| 423c4446de | |||
| 53cffd5133 | |||
| 15ff1fb093 | |||
| 195d388990 | |||
| d008e871da | |||
| 3e6295de92 | |||
| 788004245a | |||
| be5221d5b8 | |||
| dacc66bb35 | |||
| 5f26c3a2c1 | |||
| 228af62c39 | |||
| b531922175 | |||
| dad58efc94 | |||
| 7a3d3a3c74 | |||
| e5c42f2b90 | |||
| 6cbf64b88e | |||
| 9635f9aa24 | |||
| 893f9d87bc | |||
| bfda0d4891 | |||
| 65a62f9fbf | |||
| 6d74f7e26f | |||
| 14ca0c1623 | |||
| 3f6e8273a7 | |||
| 287b96925a | |||
| 608cc1e036 | |||
| 5fa27c4954 | |||
| 8deadece05 | |||
| 797dc26f47 | |||
| ddf7823b19 | |||
| 923e1d0524 | |||
| 339bc71435 | |||
| 863612356d | |||
| 56cdaefecc | |||
| 9e611b6ae3 | |||
| 7e26b4091b | |||
| d7702b96e5 | |||
| 41edd3778d | |||
| 0ac69cc6c9 | |||
| fbb01b1ce7 | |||
| a723203b28 | |||
| 851e70be6e | |||
| f0ba126156 | |||
| 9dd51575ab |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Generated
+5
-49
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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}"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
@@ -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
@@ -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" \
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 "$@"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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());
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
@@ -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">×</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>
|
||||
|
||||
@@ -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();
|
||||
}]);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
<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>
|
||||
<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/>
|
||||
@@ -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();
|
||||
}]);
|
||||
@@ -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)',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user