Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a50409bdca | |||
| 60a722e6cc | |||
| 4d6cafa589 | |||
| 63e557430b | |||
| 04acb4423d | |||
| ea813acf4c | |||
| b1198dfdbf | |||
| 4342de3747 | |||
| ef8bc7e7e9 | |||
| e18e401f6b | |||
| ab998c47e8 | |||
| 9fb830b2e1 | |||
| 415c3f90a1 | |||
| 60c8ff7fb1 | |||
| 037816313c | |||
| 3d285d1ac6 | |||
| 135338786f | |||
| 661f1fce31 | |||
| 03664ef784 | |||
| d2111ef2b6 |
@@ -805,21 +805,3 @@
|
||||
* (mail) Set maximum email size to 25MB
|
||||
* Remove SimpleAuth addon
|
||||
|
||||
[0.107.0]
|
||||
* Support CSP for webinterface and OAuth views
|
||||
* (mail) Fix issue where Cloudron is only used to send emails
|
||||
|
||||
[0.108.0]
|
||||
* Redirect to /setupdns.html when restoring
|
||||
* Fix setting custom avatar
|
||||
* Do not allocate more than 4GB swap
|
||||
* Generate real passwords for sendmail/recvmail addons
|
||||
* Rate limit all authentication routes to prevent password brute force
|
||||
* Generate 128 byte password for MySQL multi-db addon
|
||||
|
||||
[0.109.0]
|
||||
* Add Referrer-policy
|
||||
* Add tooltip for admin email field explaining it is local & private
|
||||
* Verify AMI instance id during DNS setup instead of admin account setup
|
||||
* Split platform and app data folders and get rid of btrfs volumes
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
|
||||
apt-get -y install \
|
||||
acl \
|
||||
awscli \
|
||||
btrfs-tools \
|
||||
build-essential \
|
||||
cron \
|
||||
curl \
|
||||
@@ -123,6 +124,3 @@ if ! apt-get install -y collectd collectd-utils; then
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
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
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
# Best practices
|
||||
|
||||
## Overview
|
||||
|
||||
This document explains the spirit of what makes a Cloudron app.
|
||||
|
||||
## No Setup
|
||||
|
||||
Cloudron apps do not show a setup screen after installation and should choose reasonable
|
||||
defaults.
|
||||
|
||||
Databases, email configuration should be automatically picked up using [addons](/references/addons.html).
|
||||
|
||||
Admin role for the application can be detected dynamically using one of the [authentication](/references/authentication.html)
|
||||
strategies.
|
||||
|
||||
## Image
|
||||
|
||||
The Dockerfile contains a specification for building an application image.
|
||||
|
||||
* Install any required software packages in the Dockerfile.
|
||||
|
||||
* Create static configuration files in the Dockerfile.
|
||||
|
||||
* Create symlinks to dynamic configuration files under `/run` in the Dockerfile.
|
||||
|
||||
* Docker supports restarting processes natively. Should your application crash, it will
|
||||
be restarted automatically. If your application is a single process, you do not require
|
||||
any process manager.
|
||||
|
||||
* The main process must handle `SIGTERM` and forward it as required to child processes. `bash`
|
||||
does not automatically forward signals to child processes. For this reason, when using a startup
|
||||
shell script, remember to use `exec <app>` as the last line. Doing so will replace bash with your
|
||||
program and allows your program to handle signals as required.
|
||||
|
||||
* Use `supervisor`, `pm2` or any of the other process managers if you application has more
|
||||
then one component. This excludes web servers like apache, nginx which can already manage their
|
||||
children by themselves. Be sure to pick a process manager that forwards signals to child processes.
|
||||
|
||||
* Disable auto updates for apps. Updates must be triggered through the Cloudron Store. This allows the admin
|
||||
to manage updates and downtime in a central location (the Cloudron Webadmin).
|
||||
|
||||
## File system
|
||||
|
||||
The Cloudron runs the application image as read-only. The app can only write to the following directories:
|
||||
|
||||
* `/tmp` - use this for temporary files.
|
||||
|
||||
* `/run` - use this for runtime configration and any dynamic data.
|
||||
|
||||
* `/app/data` - When the `localstorage` addon is enabled, any data under this directory is automatically backed up.
|
||||
|
||||
## Logging
|
||||
|
||||
Cloudron applications stream their logs to stdout and stderr. In contrast to logging
|
||||
to files, this approach has many advantages:
|
||||
|
||||
* App does not need to rotate logs and the Cloudron takes care of managing logs
|
||||
* App does not need special mechanism to release log file handles (on a log rotate)
|
||||
* Integrates better with tooling like `cloudron cli`
|
||||
|
||||
This document gives you some recipes for configuring popular libraries to log to stdout. See
|
||||
[base image](/references/baseimage.html#configuring) on how to configure various libraries to log to stdout/stderr.
|
||||
|
||||
|
||||
## Memory
|
||||
|
||||
By default, applications get 256MB RAM (including swap). This can be changed using the `memoryLimit` field in the manifest.
|
||||
|
||||
Design your application runtime for concurrent use by 10s of users. The Cloudron is not designed for concurrent access by
|
||||
100s or 1000s of users.
|
||||
|
||||
## Startup
|
||||
|
||||
* Apps must not present a post-installation screen on first run. It should be already pre-configured for
|
||||
a specific purpose.
|
||||
|
||||
* Do not run as `root`. Apps can use the `cloudron` user which is part of the [base image](/references/baseimage.html)
|
||||
for this purpose or create their own.
|
||||
|
||||
* When using the `localstorage` addon, the application must change the ownership of files in `/app/data` as desired using `chown`. This
|
||||
is necessary because file permissions may not be correctly preserved across backup, restore, application and base image
|
||||
updates.
|
||||
|
||||
* Addon information (mail, database) is exposed as environment variables. An application must use these values directly
|
||||
and not cache them across restarts. If the variables are stored in a configuration file, then the configuration file
|
||||
must be regenerated on every application start. This is usually done using a configuration template that is patched
|
||||
on every startup.
|
||||
|
||||
## Authentication
|
||||
|
||||
Apps should integrate with one of the [authentication strategies](/references/authentication.html).
|
||||
This saves the user from having to manage separate set of users for different apps.
|
||||
@@ -53,10 +53,6 @@ Cloudron has a built-in firewall and ports are opened and closed dynamically, as
|
||||
apps are installed, re-configured or removed. For this reason, be sure to open all TCP and
|
||||
UDP traffic to the server and leave the traffic management to the Cloudron.
|
||||
|
||||
### Kimsufi
|
||||
|
||||
Be sure to check the "use the distribution kernel" checkbox in the personalized installation mode.
|
||||
|
||||
### Linode
|
||||
|
||||
Since Linode does not manage SSH keys, be sure to add the public key to
|
||||
@@ -272,8 +268,8 @@ 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).
|
||||
In most cases, you can apply for removal of your IP by filling out a form at the DNSBL manager site.
|
||||
* Check if your IP is listed in any DNSBL list [here](http://multirbl.valli.org/). In most cases,
|
||||
you can apply for removal of your IP by filling out a form at the DNSBL manager site.
|
||||
|
||||
* When using wildcard or manual DNS backends, you have to setup the DMARC, MX records manually.
|
||||
|
||||
@@ -378,72 +374,6 @@ To restore a Cloudron from a specific backup:
|
||||
|
||||
* Make the box backup private, once the upgrade is complete.
|
||||
|
||||
# Security
|
||||
|
||||
Security is a core feature of the Cloudron and we continue to push out updates to tighten the Cloudron's security policy. Our goal is that Cloudron users should be able to rely on Cloudron being secure out of the box without having to do manual configuration.
|
||||
|
||||
This section lists various security measures in place to protect the Cloudron.
|
||||
|
||||
## HTTP Security
|
||||
|
||||
* Cloudron admin has a CSP policy that prevents XSS attacks.
|
||||
* Cloudron set various security related HTTP headers like `X-XSS-Protection`, `X-Download-Options`,
|
||||
`X-Content-Type-Options`, `X-Permitted-Cross-Domain-Policies`, `X-Frame-Options` across all apps.
|
||||
|
||||
## SSL
|
||||
|
||||
* Cloudron enforces HTTPS across all apps. HTTP requests are automatically redirected to
|
||||
HTTPS.
|
||||
* The Cloudron automatically installs and renews certificates for your apps as needed. Should
|
||||
installation of certificate fail for reasons beyond it's control, Cloudron admins will get a notification about it.
|
||||
* Cloudron sets the `Strict-Transport-Security` header to protect apps against downgrade attacks
|
||||
and cookie hijacking
|
||||
* Cloudron has A+ rating for SSL from [SSL Labs](https://cloudron.io/blog/2017-02-22-release-0.102.0.html).
|
||||
|
||||
## App isolation
|
||||
|
||||
* Apps are isolated completely from one another. One app cannot tamper with another apps' database or
|
||||
local files. We achieve this using Linux Containers.
|
||||
* Apps run with a read-only rootfs preventing attacks where the application code can be tampered with.
|
||||
* Apps can only connect to addons like databases, LDAP, email relay using authentication.
|
||||
|
||||
## Email
|
||||
|
||||
* Cloudron checks against the [Zen Spamhaus DNSBL](https://www.spamhaus.org/zen/) before accepting mail.
|
||||
* Email can only be accessed with IMAP over TLS (IMAPS).
|
||||
* Email can only be relayed (including same-domain emails) by authenticated users using SMTP/STARTTLS.
|
||||
* Cloudron ensures that `MAIL FROM` is the same as the authenticated user. Users cannot spoof each other.
|
||||
* All outbound mails from Cloudron are `DKIM` signed.
|
||||
* Cloudron automatically sets up SPF, DMARC policies in the DNS for best email delivery.
|
||||
* All incoming mail is scanned via `Spamassasin`.
|
||||
|
||||
## Firewall
|
||||
|
||||
* Cloudron blocks all incoming ports except 22 (ssh), 80 (http), 443 (https)
|
||||
* When email is enabled, Cloudron allows 25 (SMTP), 587 (MSA), 993 (IMAPS) and 4190 (WebSieve)
|
||||
|
||||
## OS Updates
|
||||
|
||||
* Ubuntu [automatic security updates](https://help.ubuntu.com/community/AutomaticSecurityUpdates) are enabled
|
||||
|
||||
## Rate limits
|
||||
|
||||
The goal of rate limits is to prevent password brute force attacks.
|
||||
|
||||
* Cloudron password verification routes - 10 requests per second per IP.
|
||||
* HTTP and HTTPS requests - 5000 requests per second per IP.
|
||||
* SSH access - 5 connections per 10 seconds per IP.
|
||||
* Email access (Port 25, 587, 993, 4190) - 50 connections per second per IP/App.
|
||||
* Database addons access - 5000 connections per second per app (addons use 128 byte passwords).
|
||||
* Email relay access - 500 connections per second per app.
|
||||
* Email receive access - 50 connections per second per app.
|
||||
* Auth addon access - 500 connections per second per app.
|
||||
|
||||
## Password restrictions
|
||||
|
||||
* Cloudron requires user passwords to have 1 uppercase, 1 number and 1 symbol.
|
||||
* Minimum length for user passwords is 8
|
||||
|
||||
# Debug
|
||||
|
||||
You can SSH into your Cloudron and collect logs:
|
||||
|
||||
@@ -330,28 +330,16 @@ the apps on your Cloudron and also tracks configuration changes.
|
||||
|
||||
<img src="/docs/img/activity.png" class="shadow">
|
||||
|
||||
# API Access
|
||||
|
||||
All the operations listed in this manual like installing app, configuring users and groups, are
|
||||
completely programmable with a [REST API](/references/api.html).
|
||||
|
||||
# Domains and SSL Certificates
|
||||
|
||||
All apps on the Cloudron can only be reached by `https`. The Cloudron automatically installs and
|
||||
renews certificates for your apps as needed. Should installation of certificate fail for reasons
|
||||
beyond it's control, Cloudron admins will get a notification about it.
|
||||
|
||||
# OAuth Provider
|
||||
# API Access
|
||||
|
||||
Cloudron is an OAuth 2.0 provider. To integrate Cloudron login into an external application, create
|
||||
an OAuth application under `API Access`.
|
||||
|
||||
You can use the following OAuth URLs to add Cloudron in the external app:
|
||||
```
|
||||
authorizationURL: https://my.<domain>/api/v1/oauth/dialog/authorize
|
||||
|
||||
tokenURL: https://my.<domain>/api/v1/oauth/token
|
||||
```
|
||||
All the operations listed in this manual like installing app, configuring users and groups, are
|
||||
completely programmable with a [REST API](/references/api.html).
|
||||
|
||||
# Moving to a larger Cloudron
|
||||
|
||||
|
||||
@@ -42,12 +42,12 @@ Creating an application for Cloudron can be summarized as follows:
|
||||
1. Create a web application using any language/framework. This web application must run a HTTP server
|
||||
and can optionally provide other services using custom protocols (like git, ssh, TCP etc).
|
||||
|
||||
2. Create a [Dockerfile](http://docs.docker.com/engine/reference/builder/) that specifies how to create
|
||||
2. Create a [Dockerfile](http://docs.docker.com/engine/reference/builder/) that specifies how to create
|
||||
an application ```image```. An ```image``` is essentially a bundle of the application source code
|
||||
and it's dependencies.
|
||||
|
||||
3. Create a [CloudronManifest.json](/references/manifest.html) file that provides essential information
|
||||
about the app. This includes information required for the Cloudron Store like title, version, icon and
|
||||
about the app. This includes information required for the Cloudron Store like title, version, icon and
|
||||
runtime requirements like `addons`.
|
||||
|
||||
## Simple Web application
|
||||
@@ -79,7 +79,7 @@ FROM cloudron/base:0.10.0
|
||||
|
||||
ADD server.js /app/code/server.js
|
||||
|
||||
CMD [ "/usr/local/node-6.9.5/bin/node", "/app/code/server.js" ]
|
||||
CMD [ "/usr/local/node-0.12.7/bin/node", "/app/code/server.js" ]
|
||||
```
|
||||
|
||||
The `FROM` command specifies that we want to start off with Cloudron's [base image](/references/baseimage.html).
|
||||
@@ -90,7 +90,7 @@ While this example only copies a single file, the ADD command can be used to cop
|
||||
See the [Dockerfile](https://docs.docker.com/reference/builder/#add) documentation for more details.
|
||||
|
||||
The `CMD` command specifies how to run the server. There are multiple versions of node available under `/usr/local`. We
|
||||
choose node v6.9.5 for our app.
|
||||
choose node v0.12.7 for our app.
|
||||
|
||||
## CloudronManifest.json
|
||||
|
||||
@@ -176,7 +176,7 @@ Step 0 : FROM cloudron/base:0.10.0
|
||||
Step 1 : ADD server.js /app/code
|
||||
---> b09b97ecdfbc
|
||||
Removing intermediate container 03c1e1f77acb
|
||||
Step 2 : CMD /usr/local/node-6.9.5/bin/node /app/code/main.js
|
||||
Step 2 : CMD /usr/local/node-0.12.7/bin/node /app/code/main.js
|
||||
---> Running in 370f59d87ab2
|
||||
---> 53b51eabcb89
|
||||
Removing intermediate container 370f59d87ab2
|
||||
@@ -335,15 +335,13 @@ File `tutorial/Dockerfile`
|
||||
```dockerfile
|
||||
FROM cloudron/base:0.10.0
|
||||
|
||||
ENV PATH /usr/local/node-6.9.5/bin:$PATH
|
||||
|
||||
ADD server.js /app/code/server.js
|
||||
ADD package.json /app/code/package.json
|
||||
|
||||
WORKDIR /app/code
|
||||
RUN npm install --production
|
||||
|
||||
CMD [ "node", "/app/code/server.js" ]
|
||||
CMD [ "/usr/local/node-0.12.7/bin/node", "/app/code/server.js" ]
|
||||
```
|
||||
|
||||
Notice the new `RUN` command which installs the node module dependencies in package.json using `npm install`.
|
||||
|
||||
+21
-231
@@ -5,7 +5,7 @@ This tutorial outlines how to package an existing web application for the Cloudr
|
||||
If you are aware of Docker and Heroku, you should feel at home packaging for the
|
||||
Cloudron. Roughly, the steps involved are:
|
||||
|
||||
* Create a Dockerfile for your application. If your application already has a Dockerfile, it
|
||||
* Create a Dockerfile for your application. If your application already has a Dockerfile, it
|
||||
is a good starting point for packaging for the Cloudron. By virtue of Docker, the Cloudron
|
||||
is able to run apps written in any language/framework.
|
||||
|
||||
@@ -348,64 +348,12 @@ show any setup screen after installation and should simply choose reasonable def
|
||||
Databases, email configuration should be automatically picked up from the environment variables using
|
||||
addons.
|
||||
|
||||
## Docker
|
||||
## Dockerfile
|
||||
|
||||
Cloudron uses Docker in the backend, so the package build script is a regular `Dockerfile`.
|
||||
|
||||
The app is run as a read-only docker container. Only `/run` (dynamic data), `/app/data` (backup data) and `/tmp` (temporary files) are writable at runtime. Because of this:
|
||||
|
||||
* Install any required packages in the Dockerfile.
|
||||
* Create static configuration files in the Dockerfile.
|
||||
* Create symlinks to dynamic configuration files under `/run` in the Dockerfile.
|
||||
|
||||
### Source directory
|
||||
|
||||
By convention, Cloudron apps install the source code in `/app/code`. Do not forget to create the directory for the code of the app:
|
||||
```sh
|
||||
RUN mkdir -p /app/code
|
||||
WORKDIR /app/code
|
||||
```
|
||||
|
||||
### Download archives
|
||||
|
||||
When packaging an app you often want to download and extract archives (e.g. from github).
|
||||
This can be done in one line by combining `wget` and `tar` like this:
|
||||
|
||||
```docker
|
||||
ENV VERSION 1.6.2
|
||||
RUN wget "https://github.com/FreshRSS/FreshRSS/archive/${VERSION}.tar.gz" -O - \
|
||||
| tar -xz -C /app/code --strip-components=1
|
||||
```
|
||||
|
||||
The `--strip-components=1` causes the topmost directory in the archive to be skipped.
|
||||
|
||||
Always pin the download to a specific tag or commit instead of using `HEAD` or `master`
|
||||
so that the builds are reasonably reproducible.
|
||||
|
||||
### Applying patches
|
||||
|
||||
To get the app working in Cloudron, sometimes it is necessary to patch the original sources. Patch is a safe way to modify sources, as it fails when the expected original sources changed too much.
|
||||
|
||||
First create a backup copy of the full sources (to be able to calculate the differences):
|
||||
|
||||
```sh
|
||||
cp -a extensions extensions-orig
|
||||
```
|
||||
|
||||
Then modify the sources in the original path and when finished, create a patch like this:
|
||||
|
||||
```sh
|
||||
diff -Naru extensions-orig/ extensions/ > change-ttrss-file-path.patch
|
||||
```
|
||||
|
||||
Add and apply this patch to the sources in the Dockerfile:
|
||||
|
||||
```docker
|
||||
ADD change-ttrss-file-path.patch /app/code/change-ttrss-file-path.patch
|
||||
RUN patch -p1 -d /app/code/extensions < /app/code/change-ttrss-file-path.patch
|
||||
```
|
||||
|
||||
The `-p1` causes patch to ignore the topmost directory in the patch.
|
||||
The app is run as a read-only docker container. Because of this:
|
||||
* Install any required packages in the Dockerfile.
|
||||
* Create static configuration files in the Dockerfile.
|
||||
* Create symlinks to dynamic configuration files under /run in the Dockerfile.
|
||||
|
||||
## Process manager
|
||||
|
||||
@@ -414,7 +362,7 @@ automatically. If your application is a single process, you do not require any p
|
||||
|
||||
Use supervisor, pm2 or any of the other process managers if you application has more then one component.
|
||||
This **excludes** web servers like apache, nginx which can already manage their children by themselves.
|
||||
Be sure to pick a process manager that [forwards signals](#sigterm-handling) to child processes.
|
||||
Be sure to pick a process manager that forwards signals to child processes.
|
||||
|
||||
## Automatic updates
|
||||
|
||||
@@ -448,184 +396,26 @@ An app can determine it's memory limit by reading `/sys/fs/cgroup/memory/memory.
|
||||
Apps should integrate with one of the [authentication strategies](/references/authentication.html).
|
||||
This saves the user from having to manage separate set of credentials for each app.
|
||||
|
||||
## Start script
|
||||
## Startup Script
|
||||
|
||||
Many apps do not launch the server directly, as we did in our basic example. Instead, they execute
|
||||
a `start.sh` script (named so by convention) which is used as the app entry point.
|
||||
a `start.sh` script (named so by convention) which launches the server. Before starting the server,
|
||||
the `start.sh` script does the following:
|
||||
|
||||
At the end of the Dockerfile you should add your start script (`start.sh`) and set it as the default command.
|
||||
Ensure that the `start.sh` is executable in the app package repo. This can be done with `chmod +x start.sh`.
|
||||
```docker
|
||||
ADD start.sh /app/code/start.sh
|
||||
CMD [ "/app/code/start.sh" ]
|
||||
```
|
||||
* When using the `localstorage` addon, it changes the ownership of files in `/app/data` as desired using `chown`. This
|
||||
is necessary because file permissions may not be correctly preserved across backup, restore, application and base image
|
||||
updates.
|
||||
|
||||
### One-time init
|
||||
* Addon information (mail, database) exposed as environment are subject to change across restarts and an application
|
||||
must use these values directly (i.e not cache them across restarts). For this reason, it usually regenerates
|
||||
any config files with the current database settings on each invocation.
|
||||
|
||||
One common pattern is to initialize the data directory with some commands once depending on the existence of a special `.initialized` file.
|
||||
* Finally, it starts the server as a non-root user.
|
||||
|
||||
```sh
|
||||
if ! [ -f /app/data/.initialized ]; then
|
||||
echo "Fresh installation, setting up data directory..."
|
||||
# Setup commands here
|
||||
touch /app/data/.initialized
|
||||
echo "Done."
|
||||
fi
|
||||
```
|
||||
|
||||
To copy over some files from the code directory you can use the following command:
|
||||
|
||||
```sh
|
||||
rsync -a /app/code/config/ /app/data/config/
|
||||
```
|
||||
|
||||
### chown data files
|
||||
|
||||
Since the app containers use other user ids than the host, it is sometimes necessary to change the permissions on the data directory:
|
||||
|
||||
```sh
|
||||
chown -R cloudron.cloudron /app/data
|
||||
```
|
||||
|
||||
For Apache+PHP apps you might need to change permissions to `www-data.www-data` instead.
|
||||
|
||||
### Persisting random values
|
||||
|
||||
Some apps need a random value that is initialized once and does not change afterwards (e.g. a salt for security purposes). This can be accomplished by creating a random value and storing it in a file in the data directory like this:
|
||||
|
||||
```sh
|
||||
if ! [ -e /app/data/.salt ]; then
|
||||
dd if=/dev/urandom bs=1 count=1024 2>/dev/null | sha1sum | awk '{ print $1 }' > /app/data/.salt
|
||||
fi
|
||||
SALT=$(cat /app/data/.salt)
|
||||
```
|
||||
|
||||
### Generate config
|
||||
|
||||
Addon information (mail, database) exposed as environment are subject to change across restarts and an application must use these values directly (i.e not cache them across restarts). For this reason, it usually regenerates any config files with the current database settings on each invocation.
|
||||
|
||||
First create a config file template like this:
|
||||
```sh
|
||||
... snipped ...
|
||||
'mysql' => array(
|
||||
'driver' => 'mysql',
|
||||
'host' => '##MYSQL_HOST',
|
||||
'port' => '##MYSQL_PORT',
|
||||
'database' => '##MYSQL_DATABASE',
|
||||
'username' => '##MYSQL_USERNAME',
|
||||
'password' => '##MYSQL_PASSWORD',
|
||||
'charset' => 'utf8',
|
||||
'collation' => 'utf8_general_ci',
|
||||
'prefix' => '',
|
||||
),
|
||||
... snipped ...
|
||||
```
|
||||
|
||||
Add the template file to the Dockerfile and create a symlink to the dynamic configuration file as follows:
|
||||
|
||||
```docker
|
||||
ADD database.php.template /app/code/database.php.template
|
||||
RUN ln -s /run/paperwork/database.php /app/code/database.php
|
||||
```
|
||||
|
||||
Then in `start.sh`, generate the real config file under `/run` from the template like this:
|
||||
|
||||
```sh
|
||||
sed -e "s/##MYSQL_HOST/${MYSQL_HOST}/" \
|
||||
-e "s/##MYSQL_PORT/${MYSQL_PORT}/" \
|
||||
-e "s/##MYSQL_DATABASE/${MYSQL_DATABASE}/" \
|
||||
-e "s/##MYSQL_USERNAME/${MYSQL_USERNAME}/" \
|
||||
-e "s/##MYSQL_PASSWORD/${MYSQL_PASSWORD}/" \
|
||||
-e "s/##REDIS_HOST/${REDIS_HOST}/" \
|
||||
-e "s/##REDIS_PORT/${REDIS_PORT}/" \
|
||||
/app/code/database.php.template > /run/paperwork/database.php
|
||||
```
|
||||
|
||||
### Non-root user
|
||||
|
||||
The cloudron runs the `start.sh` as root user. This is required for various commands like `chown` to
|
||||
work as expected. However, to keep the app and cloudron secure, always run the app with the least
|
||||
required permissions.
|
||||
|
||||
The `gosu` tool lets you run a binary with a specific user/group as follows:
|
||||
|
||||
```sh
|
||||
/usr/local/bin/gosu cloudron:cloudron node /app/code/.build/bundle/main.js
|
||||
```
|
||||
|
||||
### SIGTERM handling
|
||||
|
||||
bash, by default, does not automatically forward signals to child processes. This would mean that a SIGTERM sent to the parent processes does not reach the children. For this reason, be sure to `exec` as the
|
||||
last line of the start.sh script. Programs like gosu, nginx, apache do proper SIGTERM handling.
|
||||
|
||||
For example, start apache using `exec` as below:
|
||||
|
||||
```sh
|
||||
echo "Starting apache"
|
||||
APACHE_CONFDIR="" source /etc/apache2/envvars
|
||||
rm -f "${APACHE_PID_FILE}"
|
||||
exec /usr/sbin/apache2 -DFOREGROUND
|
||||
```
|
||||
|
||||
## Popular stacks
|
||||
|
||||
### Apache
|
||||
|
||||
Apache requires some configuration changes to work properly with Cloudron. The following commands configure Apache in the following way:
|
||||
|
||||
* Disable all default sites
|
||||
* Print errors into the app's log and disable other logs
|
||||
* Limit server processes to `5` (good default value)
|
||||
* Change the port number to Cloudrons default `8000`
|
||||
|
||||
```docker
|
||||
RUN rm /etc/apache2/sites-enabled/* \
|
||||
&& sed -e 's,^ErrorLog.*,ErrorLog "/dev/stderr",' -i /etc/apache2/apache2.conf \
|
||||
&& sed -e "s,MaxSpareServers[^:].*,MaxSpareServers 5," -i /etc/apache2/mods-available/mpm_prefork.conf \
|
||||
&& a2disconf other-vhosts-access-log \
|
||||
&& echo "Listen 8000" > /etc/apache2/ports.conf
|
||||
```
|
||||
|
||||
Afterwards, add your site config to Apache:
|
||||
|
||||
```docker
|
||||
ADD apache2.conf /etc/apache2/sites-available/app.conf
|
||||
RUN a2ensite app
|
||||
```
|
||||
|
||||
In `start.sh` Apache can be started using these commands:
|
||||
|
||||
```sh
|
||||
echo "Starting apache..."
|
||||
APACHE_CONFDIR="" source /etc/apache2/envvars
|
||||
rm -f "${APACHE_PID_FILE}"
|
||||
exec /usr/sbin/apache2 -DFOREGROUND
|
||||
```
|
||||
|
||||
### PHP
|
||||
|
||||
PHP wants to store session data at `/var/lib/php/sessions` which is read-only in Cloudron. To fix this problem you can move this data to `/run/php/sessions` with these commands:
|
||||
|
||||
```docker
|
||||
RUN rm -rf /var/lib/php/sessions && ln -s /run/php/sessions /var/lib/php/sessions
|
||||
```
|
||||
|
||||
Don't forget to create this directory and it's ownership in the `start.sh`:
|
||||
|
||||
```sh
|
||||
mkdir -p /run/php/sessions
|
||||
chown www-data:www-data /run/php/sessions
|
||||
```
|
||||
|
||||
### Java
|
||||
|
||||
Java scales its memory usage dynamically according to the available system memory. Due to how Docker works, Java sees the hosts total memory instead of the memory limit of the app. To restrict Java to the apps memory limit it is necessary to add a special parameter to Java calls.
|
||||
|
||||
```sh
|
||||
LIMIT=$(($(cat /sys/fs/cgroup/memory/memory.memsw.limit_in_bytes)/2**20))
|
||||
export JAVA_OPTS="-XX:MaxRAM=${LIMIT}M"
|
||||
java ${JAVA_OPTS} -jar ...
|
||||
```
|
||||
The app's main process must handle SIGTERM and forward it as required to child processes. bash does not
|
||||
automatically forward signals to child processes. For this reason, when using a startup shell script,
|
||||
remember to use exec <app> as the last line. Doing so will replace bash with your program and allows
|
||||
your program to handle signals as required.
|
||||
|
||||
# Beta Testing
|
||||
|
||||
|
||||
+14
-17
@@ -2,18 +2,17 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
var argv = require('yargs').argv,
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
concat = require('gulp-concat'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
del = require('del'),
|
||||
ejs = require('gulp-ejs'),
|
||||
var ejs = require('gulp-ejs'),
|
||||
gulp = require('gulp'),
|
||||
sass = require('gulp-sass'),
|
||||
serve = require('gulp-serve'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
del = require('del'),
|
||||
concat = require('gulp-concat'),
|
||||
uglify = require('gulp-uglify'),
|
||||
url = require('url');
|
||||
serve = require('gulp-serve'),
|
||||
sass = require('gulp-sass'),
|
||||
sourcemaps = require('gulp-sourcemaps'),
|
||||
cssnano = require('gulp-cssnano'),
|
||||
autoprefixer = require('gulp-autoprefixer'),
|
||||
argv = require('yargs').argv;
|
||||
|
||||
gulp.task('3rdparty', function () {
|
||||
gulp.src([
|
||||
@@ -55,16 +54,14 @@ gulp.task('js', ['js-index', 'js-setup', 'js-setupdns', 'js-update'], function (
|
||||
var oauth = {
|
||||
clientId: argv.clientId || 'cid-webadmin',
|
||||
clientSecret: argv.clientSecret || 'unused',
|
||||
apiOrigin: argv.apiOrigin || '',
|
||||
apiOriginHostname: argv.apiOrigin ? url.parse(argv.apiOrigin).hostname : ''
|
||||
apiOrigin: argv.apiOrigin || ''
|
||||
};
|
||||
|
||||
console.log();
|
||||
console.log('Using OAuth credentials:');
|
||||
console.log(' ClientId: %s', oauth.clientId);
|
||||
console.log(' ClientSecret: %s', oauth.clientSecret);
|
||||
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
|
||||
console.log(' Cloudron Host: %s', oauth.apiOriginHostname);
|
||||
console.log(' ClientId: %s', oauth.clientId);
|
||||
console.log(' ClientSecret: %s', oauth.clientSecret);
|
||||
console.log(' Cloudron API: %s', oauth.apiOrigin || 'default');
|
||||
console.log();
|
||||
|
||||
|
||||
@@ -143,7 +140,7 @@ gulp.task('js-update', function () {
|
||||
// --------------
|
||||
|
||||
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
|
||||
return gulp.src('webadmin/src/*.html').pipe(ejs({ apiOriginHostname: oauth.apiOriginHostname }, { ext: '.html' })).pipe(gulp.dest('webadmin/dist'));
|
||||
return gulp.src('webadmin/src/*.html').pipe(gulp.dest('webadmin/dist'));
|
||||
});
|
||||
|
||||
gulp.task('html-update', function () {
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE appAddonConfigs ADD COLUMN name VARCHAR(128)', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE appAddonConfigs DROP COLUMN name', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -97,7 +97,6 @@ CREATE TABLE IF NOT EXISTS settings(
|
||||
CREATE TABLE IF NOT EXISTS appAddonConfigs(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
addonId VARCHAR(32) NOT NULL,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
value VARCHAR(512) NOT NULL,
|
||||
FOREIGN KEY(appId) REFERENCES apps(id));
|
||||
|
||||
|
||||
Generated
+6
-2
@@ -886,8 +886,7 @@
|
||||
"defaults": {
|
||||
"version": "1.0.3",
|
||||
"from": "defaults@>=1.0.3 <2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
|
||||
"dev": true
|
||||
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz"
|
||||
},
|
||||
"defined": {
|
||||
"version": "1.0.0",
|
||||
@@ -1172,6 +1171,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"express-rate-limit": {
|
||||
"version": "2.6.0",
|
||||
"from": "express-rate-limit@>=2.6.0 <3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-2.6.0.tgz"
|
||||
},
|
||||
"express-session": {
|
||||
"version": "1.15.1",
|
||||
"from": "express-session@>=1.11.3 <2.0.0",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"ejs": "^2.2.4",
|
||||
"ejs-cli": "^1.2.0",
|
||||
"express": "^4.12.4",
|
||||
"express-rate-limit": "^2.6.0",
|
||||
"express-session": "^1.11.3",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"hat": "0.0.3",
|
||||
|
||||
@@ -15,15 +15,16 @@ fi
|
||||
# change this to a hash when we make a upgrade release
|
||||
readonly LOG_FILE="/var/log/cloudron-setup.log"
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
|
||||
readonly MINIMUM_DISK_SIZE_GB="19" # this is the size of "/" and required to fit in docker images 19 is a safe bet for different reporting on 20GB min
|
||||
readonly MINIMUM_MEMORY="974" # this is mostly reported for 1GB main memory (DO 992, EC2 990, Linode 989, Serverdiscounter.com 974)
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
# copied from cloudron-resize-fs.sh
|
||||
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
|
||||
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024))
|
||||
readonly disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mountpoint -x $d)" ] && echo $d && break; done)"
|
||||
readonly disk_size_bytes=$(LC_ALL=C fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }')
|
||||
readonly disk_size_gb=$((${disk_size_bytes}/1024/1024/1024))
|
||||
|
||||
# verify the system has minimum requirements met
|
||||
if [[ "${physical_memory}" -lt "${MINIMUM_MEMORY}" ]]; then
|
||||
@@ -32,7 +33,7 @@ if [[ "${physical_memory}" -lt "${MINIMUM_MEMORY}" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "${disk_size_gb}" -lt "${MINIMUM_DISK_SIZE_GB}" ]]; then
|
||||
echo "Error: Cloudron requires atleast 20GB disk space (Disk space on / is ${disk_size_gb}GB)"
|
||||
echo "Error: Cloudron requires atleast 20GB disk space (Disk space on ${disk_device} is ${disk_size_gb}GB)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -93,7 +94,7 @@ done
|
||||
# validate arguments in the absence of data
|
||||
if [[ -z "${dataJson}" ]]; then
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required (azure, digitalocean, ec2, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
echo "--provider is required (azure, digitalocean, ec2, lightsail, linode, ovh, scaleway, vultr or generic)"
|
||||
exit 1
|
||||
elif [[ \
|
||||
"${provider}" != "ami" && \
|
||||
|
||||
+6
-6
@@ -6,12 +6,12 @@ readonly SETUP_WEBSITE_DIR="/home/yellowtent/setup/website"
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_dir="$(realpath ${script_dir}/..)"
|
||||
readonly PLATFORM_DATA_DIR="/home/yellowtent/platformdata"
|
||||
readonly DATA_DIR="/home/yellowtent/data"
|
||||
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
|
||||
|
||||
echo "Setting up nginx update page"
|
||||
|
||||
if [[ ! -f "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf" ]]; then
|
||||
if [[ ! -f "${DATA_DIR}/nginx/applications/admin.conf" ]]; then
|
||||
echo "No admin.conf found. This Cloudron has no domain yet. Skip splash setup"
|
||||
exit
|
||||
fi
|
||||
@@ -29,16 +29,16 @@ cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
|
||||
# create nginx config
|
||||
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
|
||||
existing_infra="none"
|
||||
[[ -f "${PLATFORM_DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${PLATFORM_DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
|
||||
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
|
||||
if [[ "${arg_retire_reason}" != "" || "${existing_infra}" != "${current_infra}" ]]; then
|
||||
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire_reason} existing: ${existing_infra} current: ${current_infra}"
|
||||
rm -f ${PLATFORM_DATA_DIR}/nginx/applications/*
|
||||
rm -f ${DATA_DIR}/nginx/applications/*
|
||||
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
else
|
||||
echo "Show progress bar only on admin domain for normal update"
|
||||
${box_src_dir}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${PLATFORM_DATA_DIR}/nginx/applications/admin.conf"
|
||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\", \"xFrameOptions\": \"SAMEORIGIN\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||
fi
|
||||
|
||||
if [[ "${arg_retire_reason}" == "migrate" ]]; then
|
||||
|
||||
+80
-54
@@ -5,11 +5,10 @@ set -eu -o pipefail
|
||||
echo "==> Cloudron Start"
|
||||
|
||||
readonly USER="yellowtent"
|
||||
readonly DATA_FILE="/root/user_data.img"
|
||||
readonly HOME_DIR="/home/${USER}"
|
||||
readonly BOX_SRC_DIR="${HOME_DIR}/box"
|
||||
readonly OLD_DATA_DIR="${HOME_DIR}/data";
|
||||
readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
|
||||
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
|
||||
readonly DATA_DIR="${HOME_DIR}/data" # app and platform data
|
||||
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
|
||||
readonly CONFIG_DIR="${HOME_DIR}/configs"
|
||||
readonly SETUP_PROGRESS_JSON="${HOME_DIR}/setup/website/progress.json"
|
||||
@@ -34,6 +33,36 @@ timedatectl set-ntp 1
|
||||
timedatectl set-timezone UTC
|
||||
hostnamectl set-hostname "${arg_fqdn}"
|
||||
|
||||
echo "==> Setting up firewall"
|
||||
iptables -t filter -N CLOUDRON || true
|
||||
iptables -t filter -F CLOUDRON # empty any existing rules
|
||||
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# caas has ssh on port 202
|
||||
if [[ "${arg_provider}" == "caas" ]]; then
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 25,80,202,443,587,993,4190 -j ACCEPT
|
||||
else
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 25,80,22,443,587,993,4190 -j ACCEPT
|
||||
fi
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -s 172.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
|
||||
iptables -t filter -A CLOUDRON -i lo -j ACCEPT # required for localhost connections (mysql)
|
||||
|
||||
# log dropped incoming. keep this at the end of all the rules
|
||||
iptables -t filter -A CLOUDRON -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
|
||||
iptables -t filter -A CLOUDRON -j DROP
|
||||
|
||||
if ! iptables -t filter -C INPUT -j CLOUDRON 2>/dev/null; then
|
||||
iptables -t filter -I INPUT -j CLOUDRON
|
||||
fi
|
||||
|
||||
# so it gets restored across reboot
|
||||
mkdir -p /etc/iptables && iptables-save > /etc/iptables/rules.v4
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
systemctl enable apparmor
|
||||
@@ -67,51 +96,46 @@ if [[ "${arg_provider}" == "caas" ]]; then
|
||||
systemctl reload sshd
|
||||
fi
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
mkdir -p "${APPS_DATA_DIR}"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}"
|
||||
echo "==> Setup btrfs data"
|
||||
if [[ ! -d "${DATA_DIR}" ]]; then
|
||||
echo "==> Mounting loopback btrfs"
|
||||
truncate -s "8192m" "${DATA_FILE}" # 8gb start (this will get resized dynamically by cloudron-resize-fs.service)
|
||||
mkfs.btrfs -L UserDataHome "${DATA_FILE}"
|
||||
mkdir -p "${DATA_DIR}"
|
||||
mount -t btrfs -o loop,nosuid "${DATA_FILE}" ${DATA_DIR}
|
||||
fi
|
||||
|
||||
# keep these in sync with paths.js
|
||||
echo "==> Ensuring directories"
|
||||
if [[ ! -d "${PLATFORM_DATA_DIR}/mail" ]]; then
|
||||
if [[ -d "${OLD_DATA_DIR}/mail" ]]; then
|
||||
echo "==> Migrate old mail data"
|
||||
# Migrate mail data to new format
|
||||
docker stop mail || true # otherwise the move below might fail if mail container writes in the middle
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mail"
|
||||
# we can't move the whole folder as it is a btrfs subvolume mount
|
||||
mv -f "${OLD_DATA_DIR}/mail/"* "${PLATFORM_DATA_DIR}/mail/" # this used to be mail container's run directory
|
||||
else
|
||||
echo "==> Create new mail data dir"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mail"
|
||||
fi
|
||||
if ! btrfs subvolume show "${DATA_DIR}/mail" &> /dev/null; then
|
||||
# Migrate mail data to new format
|
||||
docker stop mail || true # otherwise the move below might fail if mail container writes in the middle
|
||||
rm -rf "${DATA_DIR}/mail" # this used to be mail container's run directory
|
||||
btrfs subvolume create "${DATA_DIR}/mail"
|
||||
[[ -d "${DATA_DIR}/box/mail" ]] && mv "${DATA_DIR}/box/mail/"* "${DATA_DIR}/mail"
|
||||
rm -rf "${DATA_DIR}/box/mail"
|
||||
fi
|
||||
mkdir -p "${DATA_DIR}/graphite"
|
||||
mkdir -p "${DATA_DIR}/mail/dkim"
|
||||
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/graphite"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mail/dkim"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/snapshots"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
mkdir -p "${DATA_DIR}/mysql"
|
||||
mkdir -p "${DATA_DIR}/postgresql"
|
||||
mkdir -p "${DATA_DIR}/mongodb"
|
||||
mkdir -p "${DATA_DIR}/snapshots"
|
||||
mkdir -p "${DATA_DIR}/addons/mail"
|
||||
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${DATA_DIR}/acme"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
if btrfs subvolume show "${DATA_DIR}/box" &> /dev/null; then
|
||||
# Migrate box data out of data volume
|
||||
mv "${DATA_DIR}/box/"* "${BOX_DATA_DIR}"
|
||||
btrfs subvolume delete "${DATA_DIR}/box"
|
||||
fi
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/certs"
|
||||
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
|
||||
|
||||
echo "==> Check for old btrfs volumes"
|
||||
if mountpoint -q "${OLD_DATA_DIR}"; then
|
||||
echo "==> Cleanup btrfs volumes"
|
||||
# First stop all container to be able to unmount
|
||||
docker ps -q | xargs docker stop
|
||||
umount "${OLD_DATA_DIR}"
|
||||
rm -rf "/root/user_data.img"
|
||||
else
|
||||
echo "==> No btrfs volumes found";
|
||||
fi
|
||||
|
||||
echo "==> Configuring journald"
|
||||
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
|
||||
@@ -144,10 +168,7 @@ cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
systemctl daemon-reload
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron.target
|
||||
systemctl enable cloudron-firewall
|
||||
|
||||
# update firewall rules
|
||||
systemctl restart cloudron-firewall
|
||||
systemctl enable iptables-restore
|
||||
|
||||
# For logrotate
|
||||
systemctl enable --now cron
|
||||
@@ -161,18 +182,18 @@ cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
|
||||
echo "==> Configuring collectd"
|
||||
rm -rf /etc/collectd
|
||||
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
|
||||
cp "${script_dir}/start/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
|
||||
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
|
||||
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
|
||||
systemctl restart collectd
|
||||
|
||||
echo "==> Configuring nginx"
|
||||
# link nginx config to system config
|
||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||
ln -s "${PLATFORM_DATA_DIR}/nginx" /etc/nginx
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/nginx/applications"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/nginx/cert"
|
||||
cp "${script_dir}/start/nginx/nginx.conf" "${PLATFORM_DATA_DIR}/nginx/nginx.conf"
|
||||
cp "${script_dir}/start/nginx/mime.types" "${PLATFORM_DATA_DIR}/nginx/mime.types"
|
||||
ln -s "${DATA_DIR}/nginx" /etc/nginx
|
||||
mkdir -p "${DATA_DIR}/nginx/applications"
|
||||
mkdir -p "${DATA_DIR}/nginx/cert"
|
||||
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
|
||||
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
|
||||
if ! grep -q "^Restart=" /etc/systemd/system/multi-user.target.wants/nginx.service; then
|
||||
# default nginx service file does not restart on crash
|
||||
echo -e "\n[Service]\nRestart=always\n" >> /etc/systemd/system/multi-user.target.wants/nginx.service
|
||||
@@ -183,6 +204,11 @@ systemctl start nginx
|
||||
# bookkeep the version as part of data
|
||||
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${BOX_DATA_DIR}/version"
|
||||
|
||||
# remove old snapshots. if we do want to keep this around, we will have to fix the chown -R below
|
||||
# which currently fails because these are readonly fs
|
||||
echo "==> Cleaning up snapshots"
|
||||
find "${DATA_DIR}/snapshots" -mindepth 1 -maxdepth 1 | xargs --no-run-if-empty btrfs subvolume delete
|
||||
|
||||
# restart mysql to make sure it has latest config
|
||||
if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf >/dev/null; then
|
||||
# wait for all running mysql jobs
|
||||
@@ -208,7 +234,7 @@ if [[ -n "${arg_restore_url}" ]]; then
|
||||
|
||||
while true; do
|
||||
if $curl -L "${arg_restore_url}" | openssl aes-256-cbc -d -pass "pass:${arg_restore_key}" \
|
||||
| tar -zxf - --overwrite --transform="s,^box/\?,boxdata/," --transform="s,^mail/\?,platformdata/mail/," --show-transformed-names -C "${HOME_DIR}"; then break; fi
|
||||
| tar -zxf - --overwrite --transform="s,^box/\?,boxdata/," --transform="s,^mail/\?,data/mail/," --show-transformed-names -C "${HOME_DIR}"; then break; fi
|
||||
echo "Failed to download data, trying again"
|
||||
done
|
||||
|
||||
@@ -263,11 +289,11 @@ CONF_END
|
||||
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
|
||||
chown "${USER}:${USER}" -R "${BOX_DATA_DIR}"
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
chown "${USER}:${USER}" -R "${DATA_DIR}/mail/dkim" # this is owned by box currently since it generates the keys
|
||||
chown "${USER}:${USER}" "${DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${DATA_DIR}"
|
||||
|
||||
echo "==> Adding automated configs"
|
||||
if [[ ! -z "${arg_backup_config}" ]]; then
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
echo "==> Setting up firewall"
|
||||
iptables -t filter -N CLOUDRON || true
|
||||
iptables -t filter -F CLOUDRON # empty any existing rules
|
||||
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# caas has ssh on port 202
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
|
||||
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -s 172.18.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
|
||||
iptables -t filter -A CLOUDRON -i lo -j ACCEPT # required for localhost connections (mysql)
|
||||
|
||||
# log dropped incoming. keep this at the end of all the rules
|
||||
iptables -t filter -A CLOUDRON -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
|
||||
iptables -t filter -A CLOUDRON -j DROP
|
||||
|
||||
if ! iptables -t filter -C INPUT -j CLOUDRON 2>/dev/null; then
|
||||
iptables -t filter -I INPUT -j CLOUDRON
|
||||
fi
|
||||
|
||||
# Setup rate limit chain (the recent info is at /proc/net/xt_recent)
|
||||
iptables -t filter -N CLOUDRON_RATELIMIT || true
|
||||
iptables -t filter -F CLOUDRON_RATELIMIT # empty any existing rules
|
||||
|
||||
# log dropped incoming. keep this at the end of all the rules
|
||||
iptables -t filter -N CLOUDRON_RATELIMIT_LOG || true
|
||||
iptables -t filter -F CLOUDRON_RATELIMIT_LOG # empty any existing rules
|
||||
iptables -t filter -A CLOUDRON_RATELIMIT_LOG -m limit --limit 2/min -j LOG --log-prefix "IPTables RateLimit: " --log-level 7
|
||||
iptables -t filter -A CLOUDRON_RATELIMIT_LOG -j DROP
|
||||
|
||||
# http https
|
||||
for port in 80 443; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# ssh smtp ssh msa imap sieve
|
||||
for port in 22 202; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --set --name "public-${port}"
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --update --name "public-${port}" --seconds 10 --hitcount 5 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# TODO: move docker platform rules to platform.js so it can be specialized to rate limit only when destination is the mail container
|
||||
|
||||
# docker translates (dnat) 25, 587, 993, 4190 in the PREROUTING step
|
||||
for port in 2525 4190 9993; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# msa, ldap, imap, sieve
|
||||
for port in 2525 3002 4190 9993; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 500 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# cloudron docker network: mysql postgresql redis mongodb
|
||||
for port in 3306 5432 6379 27017; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# For ssh, http, https
|
||||
if ! iptables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null; then
|
||||
iptables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
|
||||
fi
|
||||
|
||||
# For smtp, imap etc routed via docker/nat
|
||||
# Workaroud issue where Docker insists on adding itself first in FORWARD table
|
||||
iptables -D FORWARD -j CLOUDRON_RATELIMIT || true
|
||||
iptables -I FORWARD 1 -j CLOUDRON_RATELIMIT
|
||||
@@ -2,42 +2,49 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly USER_HOME="/home/yellowtent"
|
||||
readonly APPS_SWAP_FILE="/apps.swap"
|
||||
readonly USER_DATA_FILE="/root/user_data.img"
|
||||
readonly USER_DATA_DIR="/home/yellowtent/data"
|
||||
|
||||
# detect device of rootfs (http://forums.fedoraforum.org/showthread.php?t=270316)
|
||||
disk_device="$(for d in $(find /dev -type b); do [ "$(mountpoint -d /)" = "$(mountpoint -x $d)" ] && echo $d && break; done)"
|
||||
|
||||
existing_swap=$(cat /proc/meminfo | grep SwapTotal | awk '{ printf "%.0f", $2/1024 }')
|
||||
|
||||
# all sizes are in mb
|
||||
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly swap_size=$((${physical_memory} > 4096 ? 4096 : ${physical_memory})) # min(RAM, 4GB) if you change this, fix enoughResourcesAvailable() in client.js
|
||||
readonly swap_size=$((${physical_memory} - ${existing_swap})) # if you change this, fix enoughResourcesAvailable() in client.js
|
||||
readonly app_count=$((${physical_memory} / 200)) # estimated app count
|
||||
readonly disk_size_bytes=$(LC_ALL=C df --output=size / | tail -n1)
|
||||
readonly disk_size=$((${disk_size_bytes}/1024))
|
||||
readonly disk_size_bytes=$(LC_ALL=C fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ printf $5 }') # can't rely on fdisk human readable units, using bytes instead
|
||||
readonly disk_size=$((${disk_size_bytes}/1024/1024))
|
||||
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code, data and tmp
|
||||
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
|
||||
|
||||
echo "Disk device: ${disk_device}"
|
||||
echo "Physical memory: ${physical_memory}"
|
||||
echo "Estimated app count: ${app_count}"
|
||||
echo "Disk size: ${disk_size}M"
|
||||
|
||||
# Allocate swap for general app usage
|
||||
readonly current_swap=$(swapon --show="name,size" --noheadings --bytes | awk 'BEGIN{s=0}{s+=$2}END{printf "%.0f", s/1024/1024}')
|
||||
readonly needed_swap_size=$((swap_size - current_swap))
|
||||
if [[ ${needed_swap_size} -gt 0 ]]; then
|
||||
echo "Need more swap of ${needed_swap_size}M"
|
||||
# compute size of apps.swap ignoring what is already set
|
||||
without_apps_swap=$(swapon --show="name,size" --noheadings --bytes | awk 'BEGIN{s=0}{if ($1!="/apps.swap") s+=$2}END{printf "%.0f", s/1024/1024}')
|
||||
apps_swap_size=$((swap_size - without_apps_swap))
|
||||
echo "Creating Apps swap file of size ${apps_swap_size}M"
|
||||
if [[ -f "${APPS_SWAP_FILE}" ]]; then
|
||||
echo "Swapping off before resizing swap"
|
||||
swapoff "${APPS_SWAP_FILE}" || true
|
||||
fi
|
||||
fallocate -l "${apps_swap_size}m" "${APPS_SWAP_FILE}"
|
||||
if [[ ! -f "${APPS_SWAP_FILE}" && ${swap_size} -gt 0 ]]; then
|
||||
echo "Creating Apps swap file of size ${swap_size}M"
|
||||
fallocate -l "${swap_size}m" "${APPS_SWAP_FILE}"
|
||||
chmod 600 "${APPS_SWAP_FILE}"
|
||||
mkswap "${APPS_SWAP_FILE}"
|
||||
swapon "${APPS_SWAP_FILE}"
|
||||
if ! grep -q "${APPS_SWAP_FILE}" /etc/fstab; then
|
||||
echo "Adding swap to fstab"
|
||||
echo "${APPS_SWAP_FILE} none swap sw 0 0" >> /etc/fstab
|
||||
fi
|
||||
echo "${APPS_SWAP_FILE} none swap sw 0 0" >> /etc/fstab
|
||||
else
|
||||
echo "Swap requirements already met"
|
||||
echo "Apps Swap file already exists"
|
||||
fi
|
||||
|
||||
# see start.sh for the initial default size of 8gb. On small disks the calculation might be lower than 8gb resulting in a failure to resize here.
|
||||
echo "Resizing data volume"
|
||||
home_data_size=$((disk_size - system_size - swap_size - ext4_reserved))
|
||||
echo "Resizing up btrfs user data to size ${home_data_size}M"
|
||||
umount "${USER_DATA_DIR}" || true
|
||||
# Do not preallocate (non-sparse). Doing so overallocates for data too much in advance and causes problems when using many apps with smaller data
|
||||
# fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
|
||||
truncate -s "${home_data_size}m" "${USER_DATA_FILE}" # this will shrink it if the file had existed. this is useful when running this script on a live system
|
||||
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
|
||||
btrfs filesystem resize max "${USER_DATA_DIR}"
|
||||
|
||||
@@ -194,6 +194,7 @@ LoadPlugin write_graphite
|
||||
|
||||
<Plugin df>
|
||||
FSType "ext4"
|
||||
FSType "btrfs"
|
||||
|
||||
ReportByDevice true
|
||||
IgnoreSelected false
|
||||
@@ -259,3 +260,4 @@ LoadPlugin write_graphite
|
||||
<Include "/etc/collectd/collectd.conf.d">
|
||||
Filter "*.conf"
|
||||
</Include>
|
||||
|
||||
|
||||
@@ -32,21 +32,14 @@ server {
|
||||
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/X-Frame-Options
|
||||
add_header X-Frame-Options "<%= xFrameOptions %>";
|
||||
proxy_hide_header X-Frame-Options;
|
||||
|
||||
# https://github.com/twitter/secureheaders
|
||||
# https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Compatibility_Matrix
|
||||
# https://wiki.mozilla.org/Security/Guidelines/Web_Security
|
||||
add_header X-XSS-Protection "1; mode=block";
|
||||
proxy_hide_header X-XSS-Protection;
|
||||
add_header X-Download-Options "noopen";
|
||||
proxy_hide_header X-Download-Options;
|
||||
add_header X-Content-Type-Options "nosniff";
|
||||
proxy_hide_header X-Content-Type-Options;
|
||||
add_header X-Permitted-Cross-Domain-Policies "none";
|
||||
proxy_hide_header X-Permitted-Cross-Domain-Policies;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade";
|
||||
proxy_hide_header Referrer-Policy;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_intercept_errors on;
|
||||
@@ -85,19 +78,13 @@ server {
|
||||
client_max_body_size 1m;
|
||||
}
|
||||
|
||||
location ~ ^/api/v1/(developer|session)/login$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
client_max_body_size 1m;
|
||||
limit_req zone=admin_login burst=5;
|
||||
}
|
||||
|
||||
# the read timeout is between successive reads and not the whole connection
|
||||
location ~ ^/api/v1/apps/.*/exec$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_read_timeout 30m;
|
||||
}
|
||||
|
||||
# graphite paths (uncomment block below and visit /graphite/index.html)
|
||||
# graphite paths
|
||||
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||
# proxy_pass http://127.0.0.1:8000;
|
||||
# client_max_body_size 1m;
|
||||
@@ -107,6 +94,7 @@ server {
|
||||
root <%= sourceDir %>/webadmin/dist;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
<% } else if ( endpoint === 'app' ) { %>
|
||||
proxy_pass http://127.0.0.1:<%= port %>;
|
||||
<% } else if ( endpoint === 'splash' ) { %>
|
||||
@@ -146,3 +134,4 @@ server {
|
||||
<% } %>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,6 @@ http {
|
||||
# keep-alive connections timeout in 65s. this is because many browsers timeout in 60 seconds
|
||||
keepalive_timeout 65s;
|
||||
|
||||
# zones for rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
|
||||
|
||||
# HTTP server
|
||||
server {
|
||||
listen 80;
|
||||
@@ -51,7 +48,7 @@ http {
|
||||
# acme challenges
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
alias /home/yellowtent/data/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
@@ -62,3 +59,4 @@ http {
|
||||
|
||||
include applications/*.conf;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Cloudron Firewall
|
||||
After=docker.service
|
||||
PartOf=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart="/home/yellowtent/box/setup/start/cloudron-firewall.sh"
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=IPTables Restore
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+46
-58
@@ -194,11 +194,7 @@ function getEnvironment(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
appdb.getAddonConfigByAppId(app.id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
return callback(null, result.map(function (e) { return e.name + '=' + e.value; }));
|
||||
});
|
||||
appdb.getAddonConfigByAppId(app.id, callback);
|
||||
}
|
||||
|
||||
function getBindsSync(app, addons) {
|
||||
@@ -211,7 +207,7 @@ function getBindsSync(app, addons) {
|
||||
|
||||
for (var addon in addons) {
|
||||
switch (addon) {
|
||||
case 'localstorage': binds.push(path.join(paths.APPS_DATA_DIR, app.id, 'data') + ':/app/data:rw'); break;
|
||||
case 'localstorage': binds.push(path.join(paths.DATA_DIR, app.id, 'data') + ':/app/data:rw'); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
@@ -258,9 +254,9 @@ function setupOauth(app, options, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var env = [
|
||||
{ name: 'OAUTH_CLIENT_ID', value: result.id },
|
||||
{ name: 'OAUTH_CLIENT_SECRET', value: result.clientSecret },
|
||||
{ name: 'OAUTH_ORIGIN', value: config.adminOrigin() }
|
||||
'OAUTH_CLIENT_ID=' + result.id,
|
||||
'OAUTH_CLIENT_SECRET=' + result.clientSecret,
|
||||
'OAUTH_ORIGIN=' + config.adminOrigin()
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting oauth addon config to %j', env);
|
||||
@@ -291,13 +287,13 @@ function setupEmail(app, options, callback) {
|
||||
|
||||
// note that "external" access info can be derived from MAIL_DOMAIN (since it's part of user documentation)
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_SIEVE_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SIEVE_PORT', value: '4190' },
|
||||
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
|
||||
'MAIL_SMTP_SERVER=mail',
|
||||
'MAIL_SMTP_PORT=2525',
|
||||
'MAIL_IMAP_SERVER=mail',
|
||||
'MAIL_IMAP_PORT=9993',
|
||||
'MAIL_SIEVE_SERVER=mail',
|
||||
'MAIL_SIEVE_PORT=4190',
|
||||
'MAIL_DOMAIN=' + config.fqdn()
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up Email');
|
||||
@@ -323,13 +319,13 @@ function setupLdap(app, options, callback) {
|
||||
if (!app.sso) return callback(null);
|
||||
|
||||
var env = [
|
||||
{ name: 'LDAP_SERVER', value: '172.18.0.1' },
|
||||
{ name: 'LDAP_PORT', value: '' + config.get('ldapPort') },
|
||||
{ name: 'LDAP_URL', value: 'ldap://172.18.0.1:' + config.get('ldapPort') },
|
||||
{ name: 'LDAP_USERS_BASE_DN', value: 'ou=users,dc=cloudron' },
|
||||
{ name: 'LDAP_GROUPS_BASE_DN', value: 'ou=groups,dc=cloudron' },
|
||||
{ name: 'LDAP_BIND_DN', value: 'cn='+ app.id + ',ou=apps,dc=cloudron' },
|
||||
{ name: 'LDAP_BIND_PASSWORD', value: hat(4 * 128) } // this is ignored
|
||||
'LDAP_SERVER=172.18.0.1',
|
||||
'LDAP_PORT=' + config.get('ldapPort'),
|
||||
'LDAP_URL=ldap://172.18.0.1:' + config.get('ldapPort'),
|
||||
'LDAP_USERS_BASE_DN=ou=users,dc=cloudron',
|
||||
'LDAP_GROUPS_BASE_DN=ou=groups,dc=cloudron',
|
||||
'LDAP_BIND_DN=cn='+ app.id + ',ou=apps,dc=cloudron',
|
||||
'LDAP_BIND_PASSWORD=' + hat(4 * 128) // this is ignored
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting up LDAP');
|
||||
@@ -358,15 +354,14 @@ function setupSendMail(app, options, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_SMTP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_SMTP_PORT', value: '2525' },
|
||||
{ name: 'MAIL_SMTP_USERNAME', value: mailbox.name },
|
||||
{ name: 'MAIL_SMTP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_FROM', value: mailbox.name + '@' + config.fqdn() },
|
||||
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
|
||||
"MAIL_SMTP_SERVER=mail",
|
||||
"MAIL_SMTP_PORT=2525",
|
||||
"MAIL_SMTP_USERNAME=" + mailbox.name,
|
||||
"MAIL_SMTP_PASSWORD=" + app.id,
|
||||
"MAIL_FROM=" + mailbox.name + '@' + config.fqdn(),
|
||||
"MAIL_DOMAIN=" + config.fqdn()
|
||||
];
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'sendmail', env, callback);
|
||||
@@ -394,15 +389,14 @@ function setupRecvMail(app, options, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var mailbox = results.filter(function (r) { return !r.aliasTarget; })[0];
|
||||
var password = generatePassword(128, false /* memorable */, /[\w\d_]/);
|
||||
|
||||
var env = [
|
||||
{ name: 'MAIL_IMAP_SERVER', value: 'mail' },
|
||||
{ name: 'MAIL_IMAP_PORT', value: '9993' },
|
||||
{ name: 'MAIL_IMAP_USERNAME', value: mailbox.name },
|
||||
{ name: 'MAIL_IMAP_PASSWORD', value: password },
|
||||
{ name: 'MAIL_TO', value: mailbox.name + '@' + config.fqdn() },
|
||||
{ name: 'MAIL_DOMAIN', value: config.fqdn() }
|
||||
"MAIL_IMAP_SERVER=mail",
|
||||
"MAIL_IMAP_PORT=9993",
|
||||
"MAIL_IMAP_USERNAME=" + mailbox.name,
|
||||
"MAIL_IMAP_PASSWORD=" + app.id,
|
||||
"MAIL_TO=" + mailbox.name + '@' + config.fqdn(),
|
||||
"MAIL_DOMAIN=" + config.fqdn()
|
||||
];
|
||||
|
||||
debugApp(app, 'Setting sendmail addon config to %j', env);
|
||||
@@ -432,9 +426,7 @@ function setupMySql(app, options, callback) {
|
||||
docker.execContainer('mysql', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
|
||||
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting mysql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mysql', env, callback);
|
||||
});
|
||||
@@ -461,7 +453,7 @@ function backupMySql(app, options, callback) {
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'backup-prefix' : 'backup', app.id ];
|
||||
@@ -477,7 +469,7 @@ function restoreMySql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'restoreMySql');
|
||||
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mysqldump'));
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mysqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/mysql/service.sh', options.multipleDatabases ? 'restore-prefix' : 'restore', app.id ];
|
||||
@@ -497,9 +489,7 @@ function setupPostgreSql(app, options, callback) {
|
||||
docker.execContainer('postgresql', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
|
||||
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting postgresql addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'postgresql', env, callback);
|
||||
});
|
||||
@@ -526,7 +516,7 @@ function backupPostgreSql(app, options, callback) {
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'backup', app.id ];
|
||||
@@ -542,7 +532,7 @@ function restorePostgreSql(app, options, callback) {
|
||||
|
||||
debugApp(app, 'restorePostgreSql');
|
||||
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'postgresqldump'));
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'postgresqldump'));
|
||||
input.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/postgresql/service.sh', 'restore', app.id ];
|
||||
@@ -563,9 +553,7 @@ function setupMongoDb(app, options, callback) {
|
||||
docker.execContainer('mongodb', cmd, { bufferStdout: true }, function (error, stdout) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var result = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
var env = result.map(function (r) { var idx = r.indexOf('='); return { name: r.substr(0, idx), value: r.substr(idx + 1) }; });
|
||||
|
||||
var env = stdout.toString('utf8').split('\n').slice(0, -1); // remove trailing newline
|
||||
debugApp(app, 'Setting mongodb addon config to %j', env);
|
||||
appdb.setAddonConfig(app.id, 'mongodb', env, callback);
|
||||
});
|
||||
@@ -592,7 +580,7 @@ function backupMongoDb(app, options, callback) {
|
||||
|
||||
callback = once(callback); // ChildProcess exit may or may not be called after error
|
||||
|
||||
var output = fs.createWriteStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
|
||||
var output = fs.createWriteStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
|
||||
output.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'backup', app.id ];
|
||||
@@ -608,7 +596,7 @@ function restoreMongoDb(app, options, callback) {
|
||||
|
||||
debugApp(app, 'restoreMongoDb');
|
||||
|
||||
var input = fs.createReadStream(path.join(paths.APPS_DATA_DIR, app.id, 'mongodbdump'));
|
||||
var input = fs.createReadStream(path.join(paths.DATA_DIR, app.id, 'mongodbdump'));
|
||||
input.on('error', callback);
|
||||
|
||||
var cmd = [ '/addons/mongodb/service.sh', 'restore', app.id ];
|
||||
@@ -622,9 +610,9 @@ function setupRedis(app, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var redisPassword = generatePassword(128, false /* memorable */, /[\w\d_]/); // ensure no / in password for being sed friendly (and be uri friendly)
|
||||
var redisPassword = generatePassword(64, false /* memorable */, /[\w\d_]/); // ensure no / in password for being sed friendly (and be uri friendly)
|
||||
var redisVarsFile = path.join(paths.ADDON_CONFIG_DIR, 'redis-' + app.id + '_vars.sh');
|
||||
var redisDataDir = path.join(paths.APPS_DATA_DIR, app.id + '/redis');
|
||||
var redisDataDir = path.join(paths.DATA_DIR, app.id + '/redis');
|
||||
|
||||
if (!safe.fs.writeFileSync(redisVarsFile, 'REDIS_PASSWORD=' + redisPassword)) {
|
||||
return callback(new Error('Error writing redis config'));
|
||||
@@ -643,10 +631,10 @@ function setupRedis(app, options, callback) {
|
||||
--read-only -v /tmp -v /run ${tag}`;
|
||||
|
||||
var env = [
|
||||
{ name: 'REDIS_URL', value: 'redis://redisuser:' + redisPassword + '@redis-' + app.id },
|
||||
{ name: 'REDIS_PASSWORD', value: redisPassword },
|
||||
{ name: 'REDIS_HOST', value: redisName },
|
||||
{ name: 'REDIS_PORT', value: '6379' }
|
||||
'REDIS_URL=redis://redisuser:' + redisPassword + '@redis-' + app.id,
|
||||
'REDIS_PASSWORD=' + redisPassword,
|
||||
'REDIS_HOST=' + redisName,
|
||||
'REDIS_PORT=6379'
|
||||
];
|
||||
|
||||
async.series([
|
||||
|
||||
+13
-21
@@ -14,7 +14,6 @@ exports = module.exports = {
|
||||
setAddonConfig: setAddonConfig,
|
||||
getAddonConfig: getAddonConfig,
|
||||
getAddonConfigByAppId: getAddonConfigByAppId,
|
||||
getAddonConfigByName: getAddonConfigByName,
|
||||
unsetAddonConfig: unsetAddonConfig,
|
||||
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
|
||||
|
||||
@@ -414,11 +413,11 @@ function setAddonConfig(appId, addonId, env, callback) {
|
||||
|
||||
if (env.length === 0) return callback(null);
|
||||
|
||||
var query = 'INSERT INTO appAddonConfigs(appId, addonId, name, value) VALUES ';
|
||||
var query = 'INSERT INTO appAddonConfigs(appId, addonId, value) VALUES ';
|
||||
var args = [ ], queryArgs = [ ];
|
||||
for (var i = 0; i < env.length; i++) {
|
||||
args.push(appId, addonId, env[i].name, env[i].value);
|
||||
queryArgs.push('(?, ?, ?, ?)');
|
||||
args.push(appId, addonId, env[i]);
|
||||
queryArgs.push('(?, ?, ?)');
|
||||
}
|
||||
|
||||
database.query(query + queryArgs.join(','), args, function (error) {
|
||||
@@ -457,10 +456,13 @@ function getAddonConfig(appId, addonId, callback) {
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) {
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
var config = [ ];
|
||||
results.forEach(function (v) { config.push(v.value); });
|
||||
|
||||
callback(null, config);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -468,23 +470,13 @@ function getAddonConfigByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) {
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
var config = [ ];
|
||||
results.forEach(function (v) { config.push(v.value); });
|
||||
|
||||
callback(null, config);
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfigByName(appId, addonId, name, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name = ?', [ appId, addonId, name ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
|
||||
callback(null, results[0].value);
|
||||
});
|
||||
}
|
||||
|
||||
+2
-2
@@ -529,7 +529,7 @@ function install(data, auditSource, callback) {
|
||||
// if sso was unspecified, enable it by default if possible
|
||||
if (sso === null) sso = !!manifest.addons['ldap'] || !!manifest.addons['oauth'];
|
||||
|
||||
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
|
||||
if (altDomain !== null && !validator.isFQDN(altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
|
||||
|
||||
var appId = uuid.v4();
|
||||
|
||||
@@ -606,7 +606,7 @@ function configure(appId, data, auditSource, callback) {
|
||||
|
||||
if ('altDomain' in data) {
|
||||
values.altDomain = data.altDomain;
|
||||
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid external domain'));
|
||||
if (values.altDomain !== null && !validator.isFQDN(values.altDomain)) return callback(new AppsError(AppsError.BAD_FIELD, 'Invalid alt domain'));
|
||||
}
|
||||
|
||||
if ('portBindings' in data) {
|
||||
|
||||
+1
-1
@@ -318,7 +318,7 @@ function backupApp(app, manifest, prefix, callback) {
|
||||
appConfig.manifest = manifest;
|
||||
backupFunction = createNewAppBackup.bind(null, app, manifest, prefix);
|
||||
|
||||
if (!safe.fs.writeFileSync(path.join(paths.APPS_DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||
return callback(safe.error);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -868,7 +868,8 @@ function checkDiskSpace(callback) {
|
||||
}
|
||||
|
||||
var oos = entries.some(function (entry) {
|
||||
return (entry.mount === '/' && entry.available <= (1.25 * 1024 * 1024)); // 1.5G
|
||||
return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) ||
|
||||
(entry.mount === '/' && entry.available <= (1.25 * 1024 * 1024)); // 1.5G
|
||||
});
|
||||
|
||||
debug('Disk space checked. ok: %s', !oos);
|
||||
|
||||
@@ -6,18 +6,19 @@
|
||||
|
||||
exports = module.exports = {
|
||||
// a version bump means that all app containers are recreated
|
||||
'version': '48.0.0',
|
||||
'version': 46,
|
||||
|
||||
'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.15.0' },
|
||||
'mysql': { repo: 'cloudron/mysql', tag: 'cloudron/mysql:0.14.0' },
|
||||
'postgresql': { repo: 'cloudron/postgresql', tag: 'cloudron/postgresql:0.16.0' },
|
||||
'mongodb': { repo: 'cloudron/mongodb', tag: 'cloudron/mongodb:0.12.0' },
|
||||
'redis': { repo: 'cloudron/redis', tag: 'cloudron/redis:0.11.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.31.0' },
|
||||
'mail': { repo: 'cloudron/mail', tag: 'cloudron/mail:0.30.3' },
|
||||
'graphite': { repo: 'cloudron/graphite', tag: 'cloudron/graphite:0.11.0' }
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+12
-24
@@ -6,7 +6,6 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
@@ -319,28 +318,18 @@ function authenticateMailbox(req, res, next) {
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
|
||||
if (mailbox.ownerType === mailboxdb.TYPE_APP) {
|
||||
var addonId = req.dn.rdns[1].attrs.ou.value.toLowerCase(); // 'sendmail' or 'recvmail'
|
||||
var name;
|
||||
if (addonId === 'sendmail') name = 'MAIL_SMTP_PASSWORD';
|
||||
else if (addonId === 'recvmail') name = 'MAIL_IMAP_PASSWORD';
|
||||
else return next(new ldap.OperationsError('Invalid DN'));
|
||||
|
||||
appdb.getAddonConfigByName(mailbox.ownerId, addonId, name, function (error, value) {
|
||||
if (error) return next(new ldap.OperationsError(error.message));
|
||||
if (req.credentials !== value) return next(new ldap.InvalidCredentialsError(req.dn.toString()));
|
||||
|
||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId, addonId: addonId });
|
||||
return res.end();
|
||||
});
|
||||
} else if (mailbox.ownerType === mailboxdb.TYPE_USER) {
|
||||
authenticateUser(req, res, function (error) {
|
||||
if (error) return next(error);
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: name }, { userId: req.user.username });
|
||||
res.end();
|
||||
});
|
||||
} else {
|
||||
return next(new ldap.OperationsError('Unknown ownerType for mailbox'));
|
||||
if (req.credentials !== mailbox.ownerId) return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
eventlog.add(eventlog.ACTION_APP_LOGIN, { authType: 'ldap', mailboxId: name }, { appId: mailbox.ownerId });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
assert.strictEqual(mailbox.ownerType, mailboxdb.TYPE_USER);
|
||||
|
||||
authenticateUser(req, res, function (error) {
|
||||
if (error) return next(error);
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, { authType: 'ldap', mailboxId: name }, { userId: req.user.username });
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -367,8 +356,7 @@ function start(callback) {
|
||||
gServer.search('ou=mailaliases,dc=cloudron', mailAliasSearch);
|
||||
gServer.search('ou=mailinglists,dc=cloudron', mailingListSearch);
|
||||
|
||||
gServer.bind('ou=recvmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=sendmail,dc=cloudron', authenticateMailbox);
|
||||
gServer.bind('ou=mailboxes,dc=cloudron', authenticateMailbox);
|
||||
|
||||
// this is the bind for addons (after bind, they might search and authenticate)
|
||||
gServer.bind('ou=addons,dc=cloudron', function(req, res, next) {
|
||||
|
||||
@@ -6,6 +6,7 @@ exports = module.exports = {
|
||||
|
||||
listAliases: listAliases,
|
||||
listMailboxes: listMailboxes,
|
||||
// listGroups: listGroups, // this is beyond my SQL skillz
|
||||
|
||||
getMailbox: getMailbox,
|
||||
getGroup: getGroup,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<% include header %>
|
||||
|
||||
<!-- tester -->
|
||||
|
||||
<script>
|
||||
|
||||
'use strict';
|
||||
|
||||
// very basic angular app
|
||||
var app = angular.module('Application', []);
|
||||
app.controller('Controller', ['$scope', function ($scope) {
|
||||
$scope.success = <%= success %>;
|
||||
$scope.error = '<%= error %>';
|
||||
}]);
|
||||
|
||||
</script>
|
||||
|
||||
<div class="container" ng-app="Application" ng-controller="Controller" ng-cloak>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<br/>
|
||||
<h4 ng-hide="success">Hello there, welcome to <%= cloudronName %>.</h4>
|
||||
<h2 ng-hide="success">Sign up with your email address.</h2>
|
||||
<h3 ng-show="success">You have received an email invitation to this Cloudron to finish the signup.</h3>
|
||||
<br/><br/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3" ng-show="!success">
|
||||
<form action="/api/v1/session/account/create" method="post" name="createForm" autocomplete="off" role="form" novalidate>
|
||||
<input type="password" style="display: none;">
|
||||
<input type="hidden" name="_csrf" value="<%= csrf %>"/>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (createForm.email.$dirty && createForm.email.$invalid) || (!createForm.email.$dirty && error) }">
|
||||
<label class="control-label" for="inputEmail">Email</label>
|
||||
<input type="email" class="form-control" id="inputEmail" ng-model="email" name="email" autofocus required>
|
||||
<div class="control-label" ng-show="(createForm.email.$dirty && createForm.email.$invalid) || (!createForm.email.$dirty && error)">
|
||||
<small ng-show="createForm.email.$dirty && createForm.email.$invalid">Must be a valid email address</small>
|
||||
<small ng-show="!createForm.email.$dirty && error">{{ error }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="createForm.$invalid"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% include footer %>
|
||||
@@ -32,19 +32,19 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
<center><p class="has-error"><%= error %></p></center>
|
||||
|
||||
<% if (user && user.username) { %>
|
||||
<div class="form-group"">
|
||||
<div class="form-group">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" readonly required>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.username.$dirty && setupForm.username.$invalid) }">
|
||||
<label class="control-label">Username</label>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
|
||||
<div class="control-label" ng-show="setupForm.username.$dirty && setupForm.username.$invalid">
|
||||
<small ng-show="setupForm.username.$error.minlength">The username is too short</small>
|
||||
<small ng-show="setupForm.username.$error.maxlength">The username is too long</small>
|
||||
<small ng-show="setupForm.username.$dirty && setupForm.username.$invalid">Not a valid username</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="username" name="username" required autofocus>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
@@ -55,18 +55,18 @@ app.controller('Controller', ['$scope', function ($scope) {
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.password.$dirty && setupForm.password.$invalid) }">
|
||||
<label class="control-label">New Password</label>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
<div class="control-label" ng-show="setupForm.password.$dirty && setupForm.password.$invalid">
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': (setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)) }">
|
||||
<label class="control-label">Repeat Password</label>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
<div class="control-label" ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="setupForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="setupForm.$invalid || password !== passwordRepeat"/>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self'; img-src 'self'" />
|
||||
|
||||
<title> <%= title %> </title>
|
||||
|
||||
|
||||
@@ -26,17 +26,17 @@ app.controller('Controller', [function () {}]);
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.password.$dirty && resetForm.password.$invalid }">
|
||||
<label class="control-label" for="inputPassword">New Password</label>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
<div class="control-label" ng-show="resetForm.password.$dirty && resetForm.password.$invalid">
|
||||
<small ng-show="resetForm.password.$dirty && resetForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPassword" ng-model="password" name="password" ng-pattern="/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,30}$/" autofocus required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': resetForm.passwordRepeat.$dirty && (password !== passwordRepeat) }">
|
||||
<label class="control-label" for="inputPasswordRepeat">Repeat Password</label>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
<div class="control-label" ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">
|
||||
<small ng-show="resetForm.passwordRepeat.$dirty && (password !== passwordRepeat)">Passwords don't match</small>
|
||||
</div>
|
||||
<input type="password" class="form-control" id="inputPasswordRepeat" ng-model="passwordRepeat" name="passwordRepeat" required>
|
||||
</div>
|
||||
<input class="btn btn-primary btn-outline pull-right" type="submit" value="Create" ng-disabled="resetForm.$invalid || password !== passwordRepeat"/>
|
||||
</form>
|
||||
|
||||
+9
-11
@@ -6,20 +6,18 @@ var config = require('./config.js'),
|
||||
// keep these values in sync with start.sh
|
||||
exports = module.exports = {
|
||||
CLOUDRON_DEFAULT_AVATAR_FILE: path.join(__dirname + '/../assets/avatar.png'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'platformdata/INFRA_VERSION'),
|
||||
INFRA_VERSION_FILE: path.join(config.baseDir(), 'data/INFRA_VERSION'),
|
||||
|
||||
OLD_DATA_DIR: path.join(config.baseDir(), 'data'),
|
||||
PLATFORM_DATA_DIR: path.join(config.baseDir(), 'platformdata'),
|
||||
APPS_DATA_DIR: path.join(config.baseDir(), 'appsdata'),
|
||||
DATA_DIR: path.join(config.baseDir(), 'data'),
|
||||
BOX_DATA_DIR: path.join(config.baseDir(), 'boxdata'),
|
||||
|
||||
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'platformdata/acme'),
|
||||
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/addons'),
|
||||
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/collectd/collectd.conf.d'),
|
||||
MAIL_DATA_DIR: path.join(config.baseDir(), 'platformdata/mail'),
|
||||
NGINX_CONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx'),
|
||||
NGINX_APPCONFIG_DIR: path.join(config.baseDir(), 'platformdata/nginx/applications'),
|
||||
NGINX_CERT_DIR: path.join(config.baseDir(), 'platformdata/nginx/cert'),
|
||||
ACME_CHALLENGES_DIR: path.join(config.baseDir(), 'data/acme'),
|
||||
ADDON_CONFIG_DIR: path.join(config.baseDir(), 'data/addons'),
|
||||
COLLECTD_APPCONFIG_DIR: path.join(config.baseDir(), 'data/collectd/collectd.conf.d'),
|
||||
MAIL_DATA_DIR: path.join(config.baseDir(), 'data/mail'),
|
||||
NGINX_CONFIG_DIR: path.join(config.baseDir(), 'data/nginx'),
|
||||
NGINX_APPCONFIG_DIR: path.join(config.baseDir(), 'data/nginx/applications'),
|
||||
NGINX_CERT_DIR: path.join(config.baseDir(), 'data/nginx/cert'),
|
||||
|
||||
// this is not part of appdata because an icon may be set before install
|
||||
ACME_ACCOUNT_KEY_FILE: path.join(config.baseDir(), 'boxdata/acme/acme.key'),
|
||||
|
||||
+13
-22
@@ -25,7 +25,6 @@ var apps = require('./apps.js'),
|
||||
os = require('os'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
subdomains = require('./subdomains.js'),
|
||||
@@ -126,8 +125,7 @@ function removeOldImages(callback) {
|
||||
function stopContainers(existingInfra, callback) {
|
||||
// TODO: be nice and stop addons cleanly (example, shutdown commands)
|
||||
|
||||
// always stop addons to restart them on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
if (existingInfra.version !== infra.version) { // infra upgrade
|
||||
debug('stopping all containers for infra upgrade');
|
||||
shell.execSync('stopContainers', 'docker ps -qa | xargs --no-run-if-empty docker rm -f');
|
||||
} else {
|
||||
@@ -148,7 +146,7 @@ function stopContainers(existingInfra, callback) {
|
||||
|
||||
function startGraphite(callback) {
|
||||
const tag = infra.images.graphite.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const dataDir = paths.DATA_DIR;
|
||||
|
||||
const cmd = `docker run --restart=always -d --name="graphite" \
|
||||
--net cloudron \
|
||||
@@ -168,11 +166,11 @@ function startGraphite(callback) {
|
||||
|
||||
function startMysql(callback) {
|
||||
const tag = infra.images.mysql.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const dataDir = paths.DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mysql_vars.sh',
|
||||
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mysql_vars.sh',
|
||||
'MYSQL_ROOT_PASSWORD=' + rootPassword +'\nMYSQL_ROOT_HOST=172.18.0.1', 'utf8')) {
|
||||
return callback(new Error('Could not create mysql var file:' + safe.error.message));
|
||||
}
|
||||
@@ -193,11 +191,11 @@ function startMysql(callback) {
|
||||
|
||||
function startPostgresql(callback) {
|
||||
const tag = infra.images.postgresql.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const dataDir = paths.DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 256;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/postgresql_vars.sh', 'POSTGRESQL_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
|
||||
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/postgresql_vars.sh', 'POSTGRESQL_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
|
||||
return callback(new Error('Could not create postgresql var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
@@ -217,11 +215,11 @@ function startPostgresql(callback) {
|
||||
|
||||
function startMongodb(callback) {
|
||||
const tag = infra.images.mongodb.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const dataDir = paths.DATA_DIR;
|
||||
const rootPassword = hat(8 * 128);
|
||||
const memoryLimit = (1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 200;
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mongodb_vars.sh', 'MONGODB_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
|
||||
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mongodb_vars.sh', 'MONGODB_ROOT_PASSWORD=' + rootPassword, 'utf8')) {
|
||||
return callback(new Error('Could not create mongodb var file:' + safe.error.message));
|
||||
}
|
||||
|
||||
@@ -250,7 +248,7 @@ function createMailConfig(callback) {
|
||||
var alertsTo = config.provider() === 'caas' ? [ 'support@cloudron.io' ] : [ ];
|
||||
alertsTo.concat(error ? [] : owner.email).join(',');
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/mail.ini',
|
||||
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mail/mail.ini',
|
||||
`mail_domain=${fqdn}\nmail_server_name=${mailFqdn}\nalerts_from=${alertsFrom}\nalerts_to=${alertsTo}`, 'utf8')) {
|
||||
return callback(new Error('Could not create mail var file:' + safe.error.message));
|
||||
}
|
||||
@@ -266,15 +264,15 @@ function startMail(callback) {
|
||||
// mail container uses /app/data for backed up data and /run for restart-able data
|
||||
|
||||
const tag = infra.images.mail.tag;
|
||||
const dataDir = paths.PLATFORM_DATA_DIR;
|
||||
const dataDir = paths.DATA_DIR;
|
||||
const memoryLimit = Math.max((1 + Math.round(os.totalmem()/(1024*1024*1024)/4)) * 128, 256);
|
||||
|
||||
// admin and mail share the same certificate
|
||||
certificates.getAdminCertificate(function (error, cert, key) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_cert.pem', cert)) return callback(new Error('Could not create cert file:' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(paths.ADDON_CONFIG_DIR + '/mail/tls_key.pem', key)) return callback(new Error('Could not create key file:' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mail/tls_cert.pem', cert)) return callback(new Error('Could not create cert file:' + safe.error.message));
|
||||
if (!safe.fs.writeFileSync(paths.DATA_DIR + '/addons/mail/tls_key.pem', key)) return callback(new Error('Could not create key file:' + safe.error.message));
|
||||
|
||||
settings.getMailConfig(function (error, mailConfig) {
|
||||
if (error) return callback(error);
|
||||
@@ -291,7 +289,6 @@ function startMail(callback) {
|
||||
--net-alias mail \
|
||||
-m ${memoryLimit}m \
|
||||
--memory-swap ${memoryLimit * 2}m \
|
||||
--env ENABLE_MDA=${mailConfig.enabled} \
|
||||
-v "${dataDir}/mail:/app/data" \
|
||||
-v "${dataDir}/addons/mail:/etc/mail" \
|
||||
${ports} \
|
||||
@@ -319,7 +316,6 @@ function startMail(callback) {
|
||||
function startAddons(existingInfra, callback) {
|
||||
var startFuncs = [ ];
|
||||
|
||||
// always start addons on any infra change, regardless of minor or major update
|
||||
if (existingInfra.version !== infra.version) {
|
||||
debug('startAddons: no existing infra or infra upgrade. starting all addons');
|
||||
startFuncs.push(startGraphite, startMysql, startPostgresql, startMongodb, startMail);
|
||||
@@ -339,15 +335,10 @@ function startAddons(existingInfra, callback) {
|
||||
}
|
||||
|
||||
function startApps(existingInfra, callback) {
|
||||
// Infra version change strategy:
|
||||
// * no existing version - restore apps
|
||||
// * major versions - restore apps
|
||||
// * minor versions - reconfigure apps
|
||||
|
||||
if (existingInfra.version === infra.version) {
|
||||
debug('startApp: apps are already uptodate');
|
||||
callback();
|
||||
} else if (existingInfra.version === 'none' || !semver.valid(existingInfra.version) || semver.major(existingInfra.version) !== semver.major(infra.version)) {
|
||||
} else if (existingInfra.version === 'none') {
|
||||
debug('startApps: restoring installed apps');
|
||||
apps.restoreInstalledApps(callback);
|
||||
} else {
|
||||
|
||||
+3
-12
@@ -4,7 +4,6 @@ exports = module.exports = {
|
||||
activate: activate,
|
||||
dnsSetup: dnsSetup,
|
||||
setupTokenAuth: setupTokenAuth,
|
||||
providerTokenAuth: providerTokenAuth,
|
||||
getStatus: getStatus,
|
||||
reboot: reboot,
|
||||
migrate: migrate,
|
||||
@@ -103,22 +102,14 @@ function setupTokenAuth(req, res, next) {
|
||||
|
||||
next();
|
||||
});
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
function providerTokenAuth(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (config.provider() === 'ami') {
|
||||
if (typeof req.body.providerToken !== 'string' || !req.body.providerToken) return next(new HttpError(400, 'providerToken must be a non empty string'));
|
||||
} else if (config.provider() === 'ami') {
|
||||
if (typeof req.query.setupToken !== 'string' || !req.query.setupToken) return next(new HttpError(400, 'setupToken must be a non empty string'));
|
||||
|
||||
superagent.get('http://169.254.169.254/latest/meta-data/instance-id').timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return next(new HttpError(500, error));
|
||||
if (result.statusCode !== 200) return next(new HttpError(500, 'Unable to get meta data'));
|
||||
|
||||
if (result.text !== req.body.providerToken) return next(new HttpError(403, 'Invalid providerToken'));
|
||||
if (result.text !== req.query.setupToken) return next(new HttpError(403, 'Invalid token'));
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ var appdb = require('../appdb'),
|
||||
DatabaseError = require('../databaseerror'),
|
||||
debug = require('debug')('box:routes/oauth2'),
|
||||
eventlog = require('../eventlog.js'),
|
||||
generatePassword = require('../password.js').generate,
|
||||
hat = require('hat'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
middleware = require('../middleware/index.js'),
|
||||
@@ -357,6 +358,87 @@ function accountSetup(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
// -> POST /api/v1/session/account/setup
|
||||
function accountSetup(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.resetToken !== 'string') return next(new HttpError(400, 'Missing resetToken'));
|
||||
if (typeof req.body.password !== 'string') return next(new HttpError(400, 'Missing password'));
|
||||
if (typeof req.body.username !== 'string') return next(new HttpError(400, 'Missing username'));
|
||||
if (typeof req.body.displayName !== 'string') return next(new HttpError(400, 'Missing displayName'));
|
||||
|
||||
debug('accountSetup: with token %s.', req.body.resetToken);
|
||||
|
||||
user.getByResetToken(req.body.resetToken, function (error, userObject) {
|
||||
if (error) return sendError(req, res, 'Invalid Reset Token');
|
||||
|
||||
var data = _.pick(req.body, 'username', 'displayName');
|
||||
user.update(userObject.id, data, auditSource(req), function (error) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountSetupSite(res, req, userObject, 'Username already exists');
|
||||
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
|
||||
if (error && error.reason === UserError.NOT_FOUND) return renderAccountSetupSite(res, req, userObject, 'No such user');
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
userObject.username = req.body.username;
|
||||
userObject.displayName = req.body.displayName;
|
||||
|
||||
// setPassword clears the resetToken
|
||||
user.setPassword(userObject.id, req.body.password, function (error, result) {
|
||||
if (error && error.reason === UserError.BAD_FIELD) return renderAccountSetupSite(res, req, userObject, error.message);
|
||||
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
res.redirect(util.format('%s?accessToken=%s&expiresAt=%s', config.adminOrigin(), result.token, result.expiresAt));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderAccountCreateSite(res, req, error, success) {
|
||||
renderTemplate(res, 'account_create', {
|
||||
error: error,
|
||||
success: !!success,
|
||||
csrf: req.csrfToken(),
|
||||
title: 'Account Create'
|
||||
});
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/account/create.html
|
||||
function accountCreateSite(req, res, next) {
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (!enabled) return sendError(req, res, 'User creation is not allowed on this Cloudron');
|
||||
|
||||
renderAccountCreateSite(res, req, '', '');
|
||||
});
|
||||
}
|
||||
|
||||
// -> POST /api/v1/session/account/create
|
||||
function accountCreate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.email !== 'string') return next(new HttpError(400, 'Missing email'));
|
||||
|
||||
debug('accountCreate: with email %s.', req.body.email);
|
||||
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
if (!enabled) return sendError(req, res, 'User signup is not allowed on this Cloudron');
|
||||
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
var auditSource = { ip: ip, username: req.body.email, userId: null };
|
||||
|
||||
user.create(null, generatePassword(), req.body.email, '', auditSource, { sendInvite: true }, function (error, result) {
|
||||
if (error && error.reason === UserError.ALREADY_EXISTS) return renderAccountCreateSite(res, req, 'User with this email address already exists');
|
||||
if (error) return sendError(req, res, 'Internal Error');
|
||||
|
||||
debug('accountCreate: success for email %s now with id %s', req.body.remail, result.id);
|
||||
|
||||
renderAccountCreateSite(res, req, '', true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -> GET /api/v1/session/password/reset.html
|
||||
function passwordResetSite(req, res, next) {
|
||||
if (!req.query.reset_token) return next(new HttpError(400, 'Missing reset_token'));
|
||||
@@ -555,6 +637,8 @@ exports = module.exports = {
|
||||
passwordReset: passwordReset,
|
||||
accountSetupSite: accountSetupSite,
|
||||
accountSetup: accountSetup,
|
||||
accountCreateSite: accountCreateSite,
|
||||
accountCreate: accountCreate,
|
||||
authorization: authorization,
|
||||
token: token,
|
||||
validateRequestedScopes: validateRequestedScopes,
|
||||
|
||||
@@ -27,6 +27,9 @@ exports = module.exports = {
|
||||
getAppstoreConfig: getAppstoreConfig,
|
||||
setAppstoreConfig: setAppstoreConfig,
|
||||
|
||||
getOpenRegistration: getOpenRegistration,
|
||||
setOpenRegistration: setOpenRegistration,
|
||||
|
||||
setFallbackCertificate: setFallbackCertificate,
|
||||
setAdminCertificate: setAdminCertificate
|
||||
};
|
||||
@@ -234,6 +237,27 @@ function setAppstoreConfig(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
function getOpenRegistration(req, res, next) {
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200, { enabled: enabled }));
|
||||
});
|
||||
}
|
||||
|
||||
function setOpenRegistration(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
if (typeof req.body.enabled !== 'boolean') return next(new HttpError(400, 'enabled is required'));
|
||||
|
||||
settings.setOpenRegistration(req.body.enabled, function (error) {
|
||||
if (error && error.reason === SettingsError.BAD_FIELD) return next(new HttpError(400, error.message));
|
||||
if (error) return next(new HttpError(500, error));
|
||||
|
||||
next(new HttpSuccess(200));
|
||||
});
|
||||
}
|
||||
|
||||
// default fallback cert
|
||||
function setFallbackCertificate(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
@@ -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 = '20.0.0';
|
||||
var TEST_IMAGE = TEST_IMAGE_REPO + ':' + TEST_IMAGE_TAG;
|
||||
// var TEST_IMAGE_ID = child_process.execSync('docker inspect --format={{.Id}} ' + TEST_IMAGE).toString('utf8').trim();
|
||||
|
||||
@@ -747,7 +747,7 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('installation - volume created', function (done) {
|
||||
expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP_ID));
|
||||
expect(fs.existsSync(paths.DATA_DIR + '/' + APP_ID));
|
||||
done();
|
||||
});
|
||||
|
||||
@@ -758,10 +758,7 @@ describe('App installation', function () {
|
||||
superagent.get('http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath)
|
||||
.end(function (err, res) {
|
||||
if (err || res.statusCode !== 200) {
|
||||
if (--tryCount === 0) {
|
||||
console.log('Unable to curl http://localhost:' + appEntry.httpPort + appResult.manifest.healthCheckPath);
|
||||
return done(new Error('Timedout'));
|
||||
}
|
||||
if (--tryCount === 0) return done(new Error('Timedout'));
|
||||
return setTimeout(healthCheck, 2000);
|
||||
}
|
||||
|
||||
@@ -787,9 +784,9 @@ describe('App installation', function () {
|
||||
|
||||
// support newer docker versions
|
||||
if (data.Volumes) {
|
||||
expect(data.Volumes['/app/data']).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data');
|
||||
expect(data.Volumes['/app/data']).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
|
||||
} else {
|
||||
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.APPS_DATA_DIR + '/' + APP_ID + '/data');
|
||||
expect(data.Mounts.filter(function (mount) { return mount.Destination === '/app/data'; })[0].Source).to.eql(paths.DATA_DIR + '/' + APP_ID + '/data');
|
||||
}
|
||||
|
||||
done();
|
||||
@@ -1143,7 +1140,7 @@ describe('App installation', function () {
|
||||
});
|
||||
|
||||
it('uninstalled - volume destroyed', function (done) {
|
||||
expect(!fs.existsSync(paths.APPS_DATA_DIR + '/' + APP_ID));
|
||||
expect(!fs.existsSync(paths.DATA_DIR + '/' + APP_ID));
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
+137
-100
@@ -533,8 +533,6 @@ describe('Settings API', function () {
|
||||
var dnsAnswerQueue = [];
|
||||
var dkimDomain, spfDomain, mxDomain, dmarcDomain;
|
||||
|
||||
this.timeout(10000);
|
||||
|
||||
before(function (done) {
|
||||
var dns = require('native-dns');
|
||||
|
||||
@@ -566,7 +564,7 @@ describe('Settings API', function () {
|
||||
});
|
||||
|
||||
it('does not fail when dns errors', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_status')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
@@ -585,42 +583,42 @@ describe('Settings API', function () {
|
||||
it('succeeds with dns errors', function (done) {
|
||||
clearDnsAnswerQueue();
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_status')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
|
||||
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(null);
|
||||
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.dkim).to.be.an('object');
|
||||
expect(res.body.dkim.domain).to.eql(dkimDomain);
|
||||
expect(res.body.dkim.type).to.eql('TXT');
|
||||
expect(res.body.dkim.value).to.eql(null);
|
||||
expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.spf).to.be.an('object');
|
||||
expect(res.body.spf.domain).to.eql(spfDomain);
|
||||
expect(res.body.spf.type).to.eql('TXT');
|
||||
expect(res.body.spf.value).to.eql(null);
|
||||
expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.dmarc).to.be.an('object');
|
||||
expect(res.body.dmarc.type).to.eql('TXT');
|
||||
expect(res.body.dmarc.value).to.eql(null);
|
||||
expect(res.body.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.mx).to.be.an('object');
|
||||
expect(res.body.mx.type).to.eql('MX');
|
||||
expect(res.body.mx.value).to.eql(null);
|
||||
expect(res.body.mx.expected).to.eql('10 ' + config.mailFqdn());
|
||||
expect(res.body.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).to.be.an('object');
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.ptr.expected).to.eql(config.mailFqdn());
|
||||
expect(res.body.ptr.status).to.eql(false);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -634,34 +632,34 @@ describe('Settings API', function () {
|
||||
dnsAnswerQueue[mxDomain].MX = null;
|
||||
dnsAnswerQueue[dmarcDomain].TXT = null;
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_status')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
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.status).to.eql(false);
|
||||
expect(res.body.dns.spf.value).to.eql(null);
|
||||
expect(res.body.spf).to.be.an('object');
|
||||
expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
|
||||
expect(res.body.spf.status).to.eql(false);
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.dns.dkim.value).to.eql(null);
|
||||
expect(res.body.dkim).to.be.an('object');
|
||||
expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
|
||||
expect(res.body.dkim.status).to.eql(false);
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.dns.dmarc.value).to.eql(null);
|
||||
expect(res.body.dmarc).to.be.an('object');
|
||||
expect(res.body.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
|
||||
expect(res.body.dmarc.status).to.eql(false);
|
||||
expect(res.body.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.value).to.eql(null);
|
||||
expect(res.body.mx).to.be.an('object');
|
||||
expect(res.body.mx.status).to.eql(false);
|
||||
expect(res.body.mx.expected).to.eql('10 ' + config.mailFqdn());
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.ptr).to.be.an('object');
|
||||
expect(res.body.ptr.expected).to.eql(config.mailFqdn());
|
||||
expect(res.body.ptr.status).to.eql(false);
|
||||
// expect(res.body.ptr.value).to.eql(null); this will be anything random
|
||||
|
||||
done();
|
||||
@@ -676,38 +674,36 @@ describe('Settings API', function () {
|
||||
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')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
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.status).to.eql(false);
|
||||
expect(res.body.dns.spf.value).to.eql('v=spf1 a:random.com ~all');
|
||||
expect(res.body.spf).to.be.an('object');
|
||||
expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' a:random.com ~all');
|
||||
expect(res.body.spf.status).to.eql(false);
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.dns.dkim.value).to.eql('v=DKIM2; t=s; p=' + cloudron.readDkimPublicKeySync());
|
||||
expect(res.body.dkim).to.be.an('object');
|
||||
expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
|
||||
expect(res.body.dkim.status).to.eql(false);
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.dns.dmarc.value).to.eql('v=DMARC2; p=reject; pct=100');
|
||||
expect(res.body.dmarc).to.be.an('object');
|
||||
expect(res.body.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
|
||||
expect(res.body.dmarc.status).to.eql(false);
|
||||
expect(res.body.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.mx).to.be.an('object');
|
||||
expect(res.body.mx.status).to.eql(false);
|
||||
expect(res.body.mx.expected).to.eql('10 ' + config.mailFqdn());
|
||||
expect(res.body.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.status).to.eql(false);
|
||||
expect(res.body.ptr).to.be.an('object');
|
||||
expect(res.body.ptr.expected).to.eql(config.mailFqdn());
|
||||
expect(res.body.ptr.status).to.eql(false);
|
||||
// expect(res.body.ptr.value).to.eql(null); this will be anything random
|
||||
|
||||
expect(res.body.outboundPort25).to.be.an('object');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -717,17 +713,17 @@ describe('Settings API', function () {
|
||||
|
||||
dnsAnswerQueue[spfDomain].TXT = [['v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all']];
|
||||
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_status')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
|
||||
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.status).to.eql(true);
|
||||
expect(res.body.spf).to.be.an('object');
|
||||
expect(res.body.spf.domain).to.eql(spfDomain);
|
||||
expect(res.body.spf.type).to.eql('TXT');
|
||||
expect(res.body.spf.value).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
|
||||
expect(res.body.spf.expected).to.eql('v=spf1 a:example.com a:' + config.mailFqdn() + ' ~all');
|
||||
expect(res.body.spf.status).to.eql(true);
|
||||
|
||||
done();
|
||||
});
|
||||
@@ -741,37 +737,78 @@ describe('Settings API', function () {
|
||||
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')
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/email_dns_records')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
|
||||
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.status).to.eql(true);
|
||||
expect(res.body.dkim).to.be.an('object');
|
||||
expect(res.body.dkim.domain).to.eql(dkimDomain);
|
||||
expect(res.body.dkim.type).to.eql('TXT');
|
||||
expect(res.body.dkim.value).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
|
||||
expect(res.body.dkim.expected).to.eql('v=DKIM1; t=s; p=' + cloudron.readDkimPublicKeySync());
|
||||
expect(res.body.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.status).to.eql(true);
|
||||
expect(res.body.spf).to.be.an('object');
|
||||
expect(res.body.spf.domain).to.eql(spfDomain);
|
||||
expect(res.body.spf.type).to.eql('TXT');
|
||||
expect(res.body.spf.value).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
|
||||
expect(res.body.spf.expected).to.eql('v=spf1 a:' + config.adminFqdn() + ' ~all');
|
||||
expect(res.body.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.status).to.eql(true);
|
||||
expect(res.body.dns.dmarc.value).to.eql('v=DMARC1; p=reject; pct=100');
|
||||
expect(res.body.dmarc).to.be.an('object');
|
||||
expect(res.body.dmarc.expected).to.eql('v=DMARC1; p=reject; pct=100');
|
||||
expect(res.body.dmarc.status).to.eql(true);
|
||||
expect(res.body.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.mx).to.be.an('object');
|
||||
expect(res.body.mx.status).to.eql(true);
|
||||
expect(res.body.mx.expected).to.eql('10 ' + config.mailFqdn());
|
||||
expect(res.body.mx.value).to.eql('10 ' + config.mailFqdn());
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('open_registration', function () {
|
||||
it('get open_registration succeeds without being set', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/open_registration')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.enabled).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('cannot set without data', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/open_registration')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(400);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('set succeeds', function (done) {
|
||||
superagent.post(SERVER_URL + '/api/v1/settings/open_registration')
|
||||
.query({ access_token: token })
|
||||
.send({ enabled: true })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('get succeeds', function (done) {
|
||||
superagent.get(SERVER_URL + '/api/v1/settings/open_registration')
|
||||
.query({ access_token: token })
|
||||
.end(function (err, res) {
|
||||
expect(res.statusCode).to.equal(200);
|
||||
expect(res.body.enabled).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ function sync(callback) {
|
||||
debug('sync: checking apps %j', allAppIds);
|
||||
async.eachSeries(allApps, function (app, iteratorDone) {
|
||||
var appState = gState[app.id] || null;
|
||||
var schedulerConfig = app.manifest.addons ? app.manifest.addons.scheduler : null;
|
||||
var schedulerConfig = app.manifest.addons.scheduler || null;
|
||||
|
||||
if (!appState && !schedulerConfig) return iteratorDone(); // nothing changed
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ if [[ $# == 1 && "$1" == "--check" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
readonly APPS_DATA_DIR="${HOME}/appsdata"
|
||||
readonly DATA_DIR="${HOME}/data"
|
||||
|
||||
# verify argument count
|
||||
if [[ "$1" == "s3" && $# -lt 9 ]]; then
|
||||
@@ -50,7 +50,11 @@ elif [[ "$1" == "filesystem" ]]; then
|
||||
fi
|
||||
|
||||
# perform backup
|
||||
readonly app_data_dir="${APPS_DATA_DIR}/${app_id}"
|
||||
readonly now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
readonly app_data_dir="${DATA_DIR}/${app_id}"
|
||||
readonly app_data_snapshot="${DATA_DIR}/snapshots/${app_id}-${now}"
|
||||
|
||||
btrfs subvolume snapshot -r "${app_data_dir}" "${app_data_snapshot}"
|
||||
|
||||
# will be checked at the end
|
||||
try=0
|
||||
@@ -69,18 +73,24 @@ if [[ "$1" == "s3" ]]; then
|
||||
|
||||
# use aws instead of curl because curl will always read entire stream memory to set Content-Length
|
||||
# aws will do multipart upload
|
||||
if cat "${app_data_dir}/config.json" \
|
||||
if cat "${app_data_snapshot}/config.json" \
|
||||
| aws ${optional_args} s3 cp - "${s3_config_url}" 2>"${error_log}"; then
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
done
|
||||
|
||||
if [[ ${try} -eq 5 ]]; then
|
||||
echo "Backup failed uploading config.json"
|
||||
btrfs subvolume delete "${app_data_snapshot}"
|
||||
exit 3
|
||||
fi
|
||||
|
||||
for try in `seq 1 5`; do
|
||||
echo "Uploading backup to ${s3_data_url} (try ${try})"
|
||||
error_log=$(mktemp)
|
||||
|
||||
if tar -czf - -C "${app_data_dir}" . \
|
||||
if tar -czf - -C "${app_data_snapshot}" . \
|
||||
| openssl aes-256-cbc -e -pass "pass:${password}" \
|
||||
| aws ${optional_args} s3 cp - "${s3_data_url}" 2>"${error_log}"; then
|
||||
break
|
||||
@@ -91,12 +101,14 @@ elif [[ "$1" == "filesystem" ]]; then
|
||||
mkdir -p $(dirname "${backup_folder}/${backup_config_fileName}")
|
||||
|
||||
echo "Storing backup config to ${backup_folder}/${backup_config_fileName}"
|
||||
cat "${app_data_dir}/config.json" > "${backup_folder}/${backup_config_fileName}"
|
||||
cat "${app_data_snapshot}/config.json" > "${backup_folder}/${backup_config_fileName}"
|
||||
|
||||
echo "Storing backup data to ${backup_folder}/${backup_data_fileName}"
|
||||
tar -czf - -C "${app_data_dir}" . | openssl aes-256-cbc -e -pass "pass:${password}" > "${backup_folder}/${backup_data_fileName}"
|
||||
tar -czf - -C "${app_data_snapshot}" . | openssl aes-256-cbc -e -pass "pass:${password}" > "${backup_folder}/${backup_data_fileName}"
|
||||
fi
|
||||
|
||||
btrfs subvolume delete "${app_data_snapshot}"
|
||||
|
||||
if [[ ${try} -eq 5 ]]; then
|
||||
echo "Backup failed uploading backup tarball"
|
||||
exit 3
|
||||
|
||||
@@ -45,10 +45,16 @@ fi
|
||||
|
||||
# perform backup
|
||||
BOX_DATA_DIR="${HOME}/boxdata"
|
||||
MAIL_DATA_DIR="${HOME}/data/mail"
|
||||
mail_snapshot_dir="${HOME}/data/snapshots/mail"
|
||||
|
||||
echo "Creating MySQL dump"
|
||||
mysqldump -u root -ppassword --single-transaction --routines --triggers box > "${BOX_DATA_DIR}/box.mysqldump"
|
||||
|
||||
echo "Snapshotting mail"
|
||||
btrfs subvolume delete "${mail_snapshot_dir}" &> /dev/null || true
|
||||
btrfs subvolume snapshot -r "${MAIL_DATA_DIR}" "${mail_snapshot_dir}"
|
||||
|
||||
# will be checked at the end
|
||||
try=0
|
||||
|
||||
@@ -65,7 +71,7 @@ if [[ "$1" == "s3" ]]; then
|
||||
|
||||
# use aws instead of curl because curl will always read entire stream memory to set Content-Length
|
||||
# aws will do multipart upload
|
||||
if tar -czf - -C "${HOME}" --transform="s,^boxdata/\?,box/," --transform="s,^platformdata/mail/\?,mail/," --show-transformed-names boxdata platformdata/mail \
|
||||
if tar -czf - -C "${HOME}" --transform="s,^boxdata/\?,box/," --transform="s,^data/mail/\?,mail/," --show-transformed-names boxdata data/mail \
|
||||
| openssl aes-256-cbc -e -pass "pass:${password}" \
|
||||
| aws ${optional_args} s3 cp - "${s3_url}" 2>"${error_log}"; then
|
||||
break
|
||||
@@ -77,10 +83,13 @@ elif [[ "$1" == "filesystem" ]]; then
|
||||
|
||||
mkdir -p $(dirname "${backup_folder}/${backup_fileName}")
|
||||
|
||||
tar -czf - -C "${HOME}" --transform="s,^boxdata/\?,box/," --transform="s,^platformdata/mail/\?,mail/," --show-transformed-names boxdata platformdata/mail \
|
||||
tar -czf - -C "${HOME}" --transform="s,^boxdata/\?,box/," --transform="s,^data/mail/\?,mail/," --show-transformed-names boxdata data/mail \
|
||||
| openssl aes-256-cbc -e -pass "pass:${password}" > "${backup_folder}/${backup_fileName}"
|
||||
fi
|
||||
|
||||
echo "Deleting backup snapshot"
|
||||
btrfs subvolume delete "${mail_snapshot_dir}"
|
||||
|
||||
if [[ ${try} -eq 5 ]]; then
|
||||
echo "Backup failed"
|
||||
exit 3
|
||||
|
||||
@@ -18,14 +18,20 @@ if [[ "$1" == "--check" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
readonly app_data_dir="${HOME}/appsdata/$1"
|
||||
readonly app_data_dir="${HOME}/data/$1"
|
||||
|
||||
# Only create subvolume if it does not exist
|
||||
if [[ ! -d "${app_data_dir}" ]]; then
|
||||
btrfs subvolume create "${app_data_dir}"
|
||||
fi
|
||||
|
||||
mkdir -p "${app_data_dir}/data"
|
||||
# only the top level ownership is changed because containers own the subdirectores
|
||||
# and will chown them as necessary
|
||||
chown yellowtent:yellowtent "${app_data_dir}"
|
||||
else
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/$1"
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/data/$1"
|
||||
mkdir -p "${app_data_dir}/data"
|
||||
chown ${SUDO_USER}:${SUDO_USER} "${app_data_dir}"
|
||||
fi
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ if [ $# -lt 3 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly APPS_DATA_DIR="${HOME}/appsdata"
|
||||
readonly DATA_DIR="${HOME}/data"
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
app_id="$1"
|
||||
@@ -33,8 +33,8 @@ for try in `seq 1 5`; do
|
||||
|
||||
if $curl -L "${restore_url}" \
|
||||
| openssl aes-256-cbc -d -pass "pass:${restore_key}" \
|
||||
| tar -zxf - -C "${APPS_DATA_DIR}/${app_id}" 2>"${error_log}"; then
|
||||
chown -R yellowtent:yellowtent "${APPS_DATA_DIR}/${app_id}"
|
||||
| tar -zxf - -C "${DATA_DIR}/${app_id}" 2>"${error_log}"; then
|
||||
chown -R yellowtent:yellowtent "${DATA_DIR}/${app_id}"
|
||||
break
|
||||
fi
|
||||
cat "${error_log}" && rm "${error_log}"
|
||||
@@ -46,3 +46,4 @@ if [[ ${try} -eq 5 ]]; then
|
||||
else
|
||||
echo "restore successful"
|
||||
fi
|
||||
|
||||
|
||||
@@ -18,9 +18,13 @@ if [[ "$1" == "--check" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "${BOX_ENV}" == "cloudron" ]]; then
|
||||
readonly app_data_dir="${HOME}/appsdata/$1"
|
||||
rm -rf "${app_data_dir}"
|
||||
readonly app_data_dir="${HOME}/data/$1"
|
||||
if [[ -d "${app_data_dir}" ]]; then
|
||||
find "${app_data_dir}" -mindepth 1 -delete
|
||||
rm -rf "${app_data_dir}" || btrfs subvolume delete "${app_data_dir}"
|
||||
fi
|
||||
else
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/appsdata/$1"
|
||||
readonly app_data_dir="${HOME}/.cloudron_test/data/$1"
|
||||
rm -rf "${app_data_dir}"
|
||||
fi
|
||||
|
||||
|
||||
+13
-2
@@ -18,6 +18,7 @@ var assert = require('assert'),
|
||||
middleware = require('./middleware'),
|
||||
passport = require('passport'),
|
||||
path = require('path'),
|
||||
RateLimit = require('express-rate-limit'),
|
||||
routes = require('./routes/index.js');
|
||||
|
||||
var gHttpServer = null;
|
||||
@@ -43,12 +44,19 @@ function initializeExpressSync() {
|
||||
// for rate limiting
|
||||
app.enable('trust proxy');
|
||||
|
||||
var limiter = new RateLimit({
|
||||
windowMs: 60*1000, // 1 minute
|
||||
max: 200, // limit each IP to 200 requests per windowMs
|
||||
delayMs: 0 // disable delaying - full speed until the max limit is reached
|
||||
});
|
||||
|
||||
if (process.env.BOX_ENV !== 'test') app.use(middleware.morgan('Box :method :url :status :response-time ms - :res[content-length]', { immediate: false }));
|
||||
|
||||
var router = new express.Router();
|
||||
router.del = router.delete; // amend router.del for readability further on
|
||||
|
||||
app
|
||||
.use(limiter)
|
||||
.use(middleware.timeout(REQUEST_TIMEOUT))
|
||||
.use(json)
|
||||
.use(urlencoded)
|
||||
@@ -79,7 +87,7 @@ function initializeExpressSync() {
|
||||
|
||||
// public routes
|
||||
router.post('/api/v1/cloudron/activate', routes.cloudron.setupTokenAuth, routes.cloudron.activate);
|
||||
router.post('/api/v1/cloudron/dns_setup', routes.cloudron.providerTokenAuth, routes.cloudron.dnsSetup); // only available until no-domain
|
||||
router.post('/api/v1/cloudron/dns_setup', routes.cloudron.dnsSetup); // only available until no-domain
|
||||
router.get ('/api/v1/cloudron/progress', routes.cloudron.getProgress);
|
||||
router.get ('/api/v1/cloudron/status', routes.cloudron.getStatus);
|
||||
router.get ('/api/v1/cloudron/avatar', routes.settings.getCloudronAvatar); // this is a public alias for /api/v1/settings/cloudron_avatar
|
||||
@@ -140,6 +148,8 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/session/password/reset', csrf, routes.oauth2.passwordReset);
|
||||
router.get ('/api/v1/session/account/setup.html', csrf, routes.oauth2.accountSetupSite);
|
||||
router.post('/api/v1/session/account/setup', csrf, routes.oauth2.accountSetup);
|
||||
router.get ('/api/v1/session/account/create.html', csrf, routes.oauth2.accountCreateSite);
|
||||
router.post('/api/v1/session/account/create', csrf, routes.oauth2.accountCreate);
|
||||
|
||||
// oauth2 routes
|
||||
router.get ('/api/v1/oauth/dialog/authorize', routes.oauth2.authorization);
|
||||
@@ -186,7 +196,6 @@ function initializeExpressSync() {
|
||||
router.get ('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.getBackupConfig);
|
||||
router.post('/api/v1/settings/backup_config', settingsScope, routes.user.requireAdmin, routes.settings.setBackupConfig);
|
||||
router.post('/api/v1/settings/certificate', settingsScope, routes.user.requireAdmin, routes.settings.setFallbackCertificate);
|
||||
|
||||
router.post('/api/v1/settings/admin_certificate', settingsScope, routes.user.requireAdmin, routes.settings.setAdminCertificate);
|
||||
router.get ('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.getTimeZone);
|
||||
router.post('/api/v1/settings/time_zone', settingsScope, routes.user.requireAdmin, routes.settings.setTimeZone);
|
||||
@@ -194,6 +203,8 @@ function initializeExpressSync() {
|
||||
router.post('/api/v1/settings/appstore_config', settingsScope, routes.user.requireAdmin, routes.settings.setAppstoreConfig);
|
||||
router.get ('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.getMailConfig);
|
||||
router.post('/api/v1/settings/mail_config', settingsScope, routes.user.requireAdmin, routes.settings.setMailConfig);
|
||||
router.get ('/api/v1/settings/open_registration', settingsScope, routes.user.requireAdmin, routes.settings.getOpenRegistration);
|
||||
router.post('/api/v1/settings/open_registration', settingsScope, routes.user.requireAdmin, routes.settings.setOpenRegistration);
|
||||
|
||||
// eventlog route
|
||||
router.get('/api/v1/eventlog', settingsScope, routes.user.requireAdmin, routes.eventlog.get);
|
||||
|
||||
@@ -44,6 +44,9 @@ exports = module.exports = {
|
||||
getMailConfig: getMailConfig,
|
||||
setMailConfig: setMailConfig,
|
||||
|
||||
getOpenRegistration: getOpenRegistration,
|
||||
setOpenRegistration: setOpenRegistration,
|
||||
|
||||
getDefaultSync: getDefaultSync,
|
||||
getAll: getAll,
|
||||
|
||||
@@ -58,6 +61,7 @@ exports = module.exports = {
|
||||
UPDATE_CONFIG_KEY: 'update_config',
|
||||
APPSTORE_CONFIG_KEY: 'appstore_config',
|
||||
MAIL_CONFIG_KEY: 'mail_config',
|
||||
OPEN_REGISTRATION_KEY: 'open_registration',
|
||||
|
||||
events: null
|
||||
};
|
||||
@@ -102,6 +106,7 @@ var gDefaults = (function () {
|
||||
result[exports.UPDATE_CONFIG_KEY] = { prerelease: false };
|
||||
result[exports.APPSTORE_CONFIG_KEY] = {};
|
||||
result[exports.MAIL_CONFIG_KEY] = { enabled: false };
|
||||
result[exports.OPEN_REGISTRATION_KEY] = false;
|
||||
|
||||
return result;
|
||||
})();
|
||||
@@ -645,6 +650,31 @@ function setMailConfig(mailConfig, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getOpenRegistration(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.get(exports.OPEN_REGISTRATION_KEY, function (error, value) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, gDefaults[exports.OPEN_REGISTRATION_KEY]);
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
// settingsdb holds string values only
|
||||
callback(null, !!value);
|
||||
});
|
||||
}
|
||||
|
||||
function setOpenRegistration(enabled, callback) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settingsdb.set(exports.OPEN_REGISTRATION_KEY, enabled ? 'enabled' : '', function (error) {
|
||||
if (error) return callback(new SettingsError(SettingsError.INTERNAL_ERROR, error));
|
||||
|
||||
exports.events.emit(exports.OPEN_REGISTRATION_KEY, enabled);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getAppstoreConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
+6
-8
@@ -13,7 +13,6 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:ssh'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
@@ -51,16 +50,17 @@ SshError.INTERNAL_ERROR = 'Internal Error';
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
safe.fs.unlinkSync(AUTHORIZED_KEYS_FILEPATH);
|
||||
callback();
|
||||
fs.unlink(AUTHORIZED_KEYS_FILEPATH, function (error) {
|
||||
if (error && error.code !== 'ENOENT') return callback(error);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
function saveKeys(keys) {
|
||||
assert(Array.isArray(keys));
|
||||
|
||||
if (!safe.fs.writeFileSync(AUTHORIZED_KEYS_TMP_FILEPATH, keys.map(function (k) { return k.key; }).join('\n'))) {
|
||||
debug('Error writing to temporary file', safe.error);
|
||||
console.error(safe.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ function saveKeys(keys) {
|
||||
// 600 = rw-------
|
||||
fs.chmodSync(AUTHORIZED_KEYS_TMP_FILEPATH, '600');
|
||||
} catch (e) {
|
||||
debug('Failed to adjust permissions of %s %j', AUTHORIZED_KEYS_TMP_FILEPATH, e);
|
||||
console.error('Failed to adjust permissions of %s', AUTHORIZED_KEYS_TMP_FILEPATH, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -89,8 +89,6 @@ function getKeys() {
|
||||
.map(function (k) { return { identifier: k.split(' ')[2], key: k }; })
|
||||
.filter(function (k) { return k.identifier && k.key; });
|
||||
|
||||
safe.fs.unlinkSync(AUTHORIZED_KEYS_TMP_FILEPATH);
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ describe('apptask', function () {
|
||||
|
||||
it('create volume', function (done) {
|
||||
apptask._createVolume(APP, function (error) {
|
||||
expect(fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id + '/data')).to.be(true);
|
||||
expect(fs.existsSync(paths.DATA_DIR + '/' + APP.id + '/data')).to.be(true);
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -138,7 +138,7 @@ describe('apptask', function () {
|
||||
|
||||
it('delete volume', function (done) {
|
||||
apptask._deleteVolume(APP, function (error) {
|
||||
expect(!fs.existsSync(paths.APPS_DATA_DIR + '/' + APP.id + '/data')).to.be(true);
|
||||
expect(!fs.existsSync(paths.DATA_DIR + '/' + APP.id + '/data')).to.be(true);
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -241,3 +241,5 @@ describe('apptask', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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:18.0.0"
|
||||
|
||||
# reset sudo timestamp to avoid wrong success
|
||||
sudo -k || sudo --reset-timestamp
|
||||
|
||||
@@ -789,14 +789,14 @@ describe('database', function () {
|
||||
});
|
||||
|
||||
it('setAddonConfig succeeds', function (done) {
|
||||
appdb.setAddonConfig(APP_1.id, 'addonid1', [ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' } ], function (error) {
|
||||
appdb.setAddonConfig(APP_1.id, 'addonid1', [ 'ENV1=env', 'ENV2=env' ], function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('setAddonConfig succeeds', function (done) {
|
||||
appdb.setAddonConfig(APP_1.id, 'addonid2', [ { name: 'ENV3', value: 'env' } ], function (error) {
|
||||
appdb.setAddonConfig(APP_1.id, 'addonid2', [ 'ENV3=env' ], function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
@@ -805,7 +805,7 @@ describe('database', function () {
|
||||
it('getAddonConfig succeeds', function (done) {
|
||||
appdb.getAddonConfig(APP_1.id, 'addonid1', function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.eql([ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' } ]);
|
||||
expect(results).to.eql([ 'ENV1=env', 'ENV2=env' ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -813,22 +813,7 @@ describe('database', function () {
|
||||
it('getAddonConfigByAppId succeeds', function (done) {
|
||||
appdb.getAddonConfigByAppId(APP_1.id, function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.eql([ { name: 'ENV1', value: 'env' }, { name: 'ENV2', value: 'env2' }, { name: 'ENV3', value: 'env' } ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAddonConfigByName succeeds', function (done) {
|
||||
appdb.getAddonConfigByName(APP_1.id, 'addonid1', 'ENV2', function (error, value) {
|
||||
expect(error).to.be(null);
|
||||
expect(value).to.be('env2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('getAddonConfigByName of unknown value succeeds', function (done) {
|
||||
appdb.getAddonConfigByName(APP_1.id, 'addonid1', 'NOPE', function (error, value) {
|
||||
expect(error.reason).to.be(DatabaseError.NOT_FOUND);
|
||||
expect(results).to.eql([ 'ENV1=env', 'ENV2=env', 'ENV3=env' ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -843,7 +828,7 @@ describe('database', function () {
|
||||
it('unsetAddonConfig did remove configs', function (done) {
|
||||
appdb.getAddonConfigByAppId(APP_1.id, function (error, results) {
|
||||
expect(error).to.be(null);
|
||||
expect(results).to.eql([ { name: 'ENV3', value: 'env' }]);
|
||||
expect(results).to.eql([ 'ENV3=env' ]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
+4
-100
@@ -16,7 +16,6 @@ var appdb = require('../appdb.js'),
|
||||
groups = require('../groups.js'),
|
||||
http = require('http'),
|
||||
ldapServer = require('../ldap.js'),
|
||||
mailboxdb = require('../mailboxdb.js'),
|
||||
settings = require('../settings.js'),
|
||||
settingsdb = require('../settingsdb.js'),
|
||||
ldap = require('ldapjs'),
|
||||
@@ -87,10 +86,6 @@ function setup(done) {
|
||||
ldapServer.start.bind(null),
|
||||
appdb.add.bind(null, APP_0.id, APP_0.appStoreId, APP_0.manifest, APP_0.location, APP_0.portBindings, APP_0),
|
||||
appdb.update.bind(null, APP_0.id, { containerId: APP_0.containerId }),
|
||||
appdb.setAddonConfig.bind(null, APP_0.id, 'sendmail', [{ name: 'MAIL_SMTP_PASSWORD', value : 'sendmailpassword' }]),
|
||||
appdb.setAddonConfig.bind(null, APP_0.id, 'recvmail', [{ name: 'MAIL_IMAP_PASSWORD', value : 'recvmailpassword' }]),
|
||||
mailboxdb.add.bind(null, APP_0.location + '.app', APP_0.id, mailboxdb.TYPE_APP),
|
||||
|
||||
function (callback) {
|
||||
user.createOwner(USER_0.username, USER_0.password, USER_0.email, USER_0.displayName, AUDIT_SOURCE, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
@@ -739,11 +734,11 @@ describe('Ldap', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('user sendmail bind', function () {
|
||||
describe('bind mailbox', function () {
|
||||
it('does not allow with invalid password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=sendmail,dc=cloudron', USER_0.password + 'nope', function (error) {
|
||||
client.bind('cn=' + USER_0.username + ',ou=mailboxes,dc=cloudron', USER_0.password + 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.InvalidCredentialsError);
|
||||
done();
|
||||
});
|
||||
@@ -752,7 +747,7 @@ describe('Ldap', function () {
|
||||
it('allows with valid password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=sendmail,dc=cloudron', USER_0.password, function (error) {
|
||||
client.bind('cn=' + USER_0.username + ',ou=mailboxes,dc=cloudron', USER_0.password, function (error) {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
@@ -764,7 +759,7 @@ describe('Ldap', function () {
|
||||
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + '@' + config.fqdn() + ',ou=sendmail,dc=cloudron', USER_0.password, function (error) {
|
||||
client.bind('cn=' + USER_0.username + '@' + config.fqdn() + ',ou=mailboxes,dc=cloudron', USER_0.password, function (error) {
|
||||
expect(error).not.to.be.ok();
|
||||
|
||||
settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: false }), done);
|
||||
@@ -772,95 +767,4 @@ describe('Ldap', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('app sendmail bind', function () {
|
||||
it('does not allow with invalid app', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=hacker.app,ou=sendmail,dc=cloudron', 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not allow with invalid password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + APP_0.location + '.app,ou=sendmail,dc=cloudron', 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.InvalidCredentialsError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows with valid password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + APP_0.location + '.app,ou=sendmail,dc=cloudron', 'sendmailpassword', function (error) {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('user recvmail bind', function () {
|
||||
it('does not allow with invalid password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=recvmail,dc=cloudron', USER_0.password + 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.InvalidCredentialsError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows with valid password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=recvmail,dc=cloudron', USER_0.password, function (error) {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
|
||||
it('allows with valid email', function (done) {
|
||||
// user settingsdb instead of settings, to not trigger further events
|
||||
settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: true }), function (error) {
|
||||
expect(error).not.to.be.ok();
|
||||
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + '@' + config.fqdn() + ',ou=recvmail,dc=cloudron', USER_0.password, function (error) {
|
||||
expect(error).not.to.be.ok();
|
||||
|
||||
settingsdb.set(settings.MAIL_CONFIG_KEY, JSON.stringify({ enabled: false }), done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('app recvmail bind', function () {
|
||||
it('does not allow with invalid app', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=hacker.app,ou=recvmail,dc=cloudron', 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not allow with invalid password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + APP_0.location + '.app,ou=recvmail,dc=cloudron', 'nope', function (error) {
|
||||
expect(error).to.be.a(ldap.InvalidCredentialsError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('allows with valid password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + APP_0.location + '.app,ou=recvmail,dc=cloudron', 'recvmailpassword', function (error) {
|
||||
done(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -215,6 +215,31 @@ describe('Server', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('rate limit', function () {
|
||||
before(function (done) {
|
||||
server.start(done);
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
server.stop(done);
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('gets throttled after 200 requests', function (done) {
|
||||
async.times(200, function (n, next) {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (error, result) {
|
||||
expect(result.statusCode).to.equal(200);
|
||||
next();
|
||||
});
|
||||
}, function () {
|
||||
superagent.get(SERVER_URL + '/api/v1/cloudron/status', function (error, result) {
|
||||
expect(result.statusCode).to.equal(429);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cors', function () {
|
||||
before(function (done) {
|
||||
server.start(function (error) {
|
||||
|
||||
@@ -188,5 +188,28 @@ describe('Settings', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get open registration default value', function (done) {
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
expect(enabled).to.equal(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can set open registration', function (done) {
|
||||
settings.setOpenRegistration(true, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can get open registration', function (done) {
|
||||
settings.getOpenRegistration(function (error, enabled) {
|
||||
expect(error).to.be(null);
|
||||
expect(enabled).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -10,10 +10,10 @@ readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")"/../.. && pwd)"
|
||||
rm -rf $HOME/.cloudron_test 2>/dev/null || true # some of those docker container data requires sudo to be removed
|
||||
mkdir -p $HOME/.cloudron_test
|
||||
cd $HOME/.cloudron_test
|
||||
mkdir -p appdata boxdata/appicons platformdata/mail platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons configs boxdata/certs platformdata/mail/dkim/localhost platformdata/mail/dkim/foobar.com
|
||||
mkdir -p data/appdata boxdata/appicons data/mail data/addons/mail data/nginx/cert data/nginx/applications data/collectd/collectd.conf.d data/addons configs boxdata/certs data/mail/dkim/localhost data/mail/dkim/foobar.com
|
||||
|
||||
# put cert
|
||||
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes
|
||||
openssl req -x509 -newkey rsa:2048 -keyout data/nginx/cert/host.key -out data/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes
|
||||
|
||||
# create docker network (while the infra code does this, most tests skip infra setup)
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
|
||||
+6
-6
@@ -7,13 +7,13 @@ angular.module('ngTld', [])
|
||||
.directive('checkTld', checkTld);
|
||||
|
||||
function ngTld() {
|
||||
function tldExists(path) {
|
||||
function tldExists($path) {
|
||||
// https://github.com/oncletom/tld.js/issues/58
|
||||
return (path.slice(-1) !== '.') && path === tld.getDomain(path);
|
||||
return ($path.$viewValue.slice(-1) !== '.') && $path.$viewValue === tld.getDomain($path.$viewValue);
|
||||
}
|
||||
|
||||
function isSubdomain(path) {
|
||||
return (path.slice(-1) !== '.') && !!tld.getDomain(path) && path !== tld.getDomain(path);
|
||||
function isSubdomain($path) {
|
||||
return ($path.$viewValue.slice(-1) !== '.') && !!tld.getDomain($path.$viewValue) && $path.$viewValue !== tld.getDomain($path.$viewValue);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -28,11 +28,11 @@ 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);
|
||||
};
|
||||
|
||||
ngModel.$validators.invalidSubdomain = function(modelValue, viewValue) {
|
||||
return !ngTld.isSubdomain(ngModel.$viewValue);
|
||||
return !ngTld.isSubdomain(ngModel);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron App Error </title>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron Error </title>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src * data:;" />
|
||||
|
||||
<!-- this gets changed once we get the config (because angular has not loaded yet, we see template string for a flash) -->
|
||||
<title> Cloudron </title>
|
||||
|
||||
@@ -439,6 +439,20 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.setOpenRegistration = function (enabled, callback) {
|
||||
post('/api/v1/settings/open_registration', { enabled: enabled }).success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getOpenRegistration = function (callback) {
|
||||
get('/api/v1/settings/open_registration').success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
callback(null, data.enabled);
|
||||
}).error(defaultErrorHandler(callback));
|
||||
};
|
||||
|
||||
Client.prototype.getEmailStatus = function (callback) {
|
||||
get('/api/v1/settings/email_status').success(function(data, status) {
|
||||
if (status !== 200) return callback(new ClientError(status, data));
|
||||
@@ -1004,6 +1018,8 @@ angular.module('Application').service('Client', ['$http', 'md5', 'Notification',
|
||||
var totalMemory = roundedMemory * 1.2; // cloudron-system-setup.sh creates equal amount of swap. 1.2 factor is arbitrary
|
||||
var available = (totalMemory || 0) - used;
|
||||
|
||||
console.log(needed, used, roundedMemory, totalMemory, available);
|
||||
|
||||
return (available - needed) >= 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -92,13 +92,6 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
Client.getStatus(function (error, status) {
|
||||
if (error) return $scope.error(error);
|
||||
|
||||
// WARNING if anything about the routing is changed here test these use-cases:
|
||||
//
|
||||
// 1. Caas
|
||||
// 2. selfhosted with --domain argument
|
||||
// 3. selfhosted restore
|
||||
// 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';
|
||||
@@ -107,7 +100,7 @@ angular.module('Application').controller('MainController', ['$scope', '$route',
|
||||
|
||||
// support local development with localhost check
|
||||
if (window.location.hostname !== status.adminFqdn && window.location.hostname !== 'localhost') {
|
||||
window.location.href = '/setupdns.html';
|
||||
window.location.href = 'https://' + status.adminFqdn + '/noapp.html';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
// create main application module
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ui.bootstrap']);
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification']);
|
||||
|
||||
app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope, $http, Client) {
|
||||
// Stupid angular location provider either wants html5 location mode or not, do the query parsing on my own
|
||||
@@ -20,11 +20,12 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
|
||||
$scope.provider = '';
|
||||
$scope.apiServerOrigin = '';
|
||||
$scope.setupToken = '';
|
||||
$scope.instanceId = '';
|
||||
|
||||
$scope.activateCloudron = function () {
|
||||
$scope.busy = true;
|
||||
|
||||
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken, function (error) {
|
||||
Client.createAdmin($scope.account.username, $scope.account.password, $scope.account.email, $scope.account.displayName, $scope.setupToken || $scope.instanceId, function (error) {
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.busy = false;
|
||||
$scope.error = $scope.provider === 'ami' ? 'Wrong instance id' : 'Wrong setup token';
|
||||
@@ -81,13 +82,9 @@ app.controller('SetupController', ['$scope', '$http', 'Client', function ($scope
|
||||
$scope.account.displayName = search.displayName || $scope.account.displayName;
|
||||
$scope.account.requireEmail = !search.email;
|
||||
$scope.provider = status.provider;
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.apiServerOrigin = status.apiServerOrigin;
|
||||
|
||||
$scope.initialized = true;
|
||||
|
||||
// Ensure we have a good autofocus
|
||||
setTimeout(function () {
|
||||
$(document).find("[autofocus]:first").focus();
|
||||
}, 250);
|
||||
});
|
||||
}]);
|
||||
|
||||
@@ -4,14 +4,11 @@
|
||||
var app = angular.module('Application', ['angular-md5', 'ui-notification', 'ngTld']);
|
||||
|
||||
app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', function ($scope, $http, Client, ngTld) {
|
||||
var search = decodeURIComponent(window.location.search).slice(1).split('&').map(function (item) { return item.split('='); }).reduce(function (o, k) { o[k[0]] = k[1]; return o; }, {});
|
||||
|
||||
$scope.initialized = false;
|
||||
$scope.busy = false;
|
||||
$scope.error = null;
|
||||
$scope.provider = '';
|
||||
$scope.showDNSSetup = false;
|
||||
$scope.instanceId = '';
|
||||
|
||||
// keep in sync with certs.js
|
||||
$scope.dnsProvider = [
|
||||
@@ -33,16 +30,13 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', func
|
||||
|
||||
$scope.setDnsCredentials = function () {
|
||||
$scope.dnsCredentials.busy = true;
|
||||
$scope.dnsCredentials.error = null;
|
||||
$scope.error = null;
|
||||
|
||||
var data = {
|
||||
domain: $scope.dnsCredentials.domain,
|
||||
provider: $scope.dnsCredentials.provider,
|
||||
accessKeyId: $scope.dnsCredentials.accessKeyId,
|
||||
secretAccessKey: $scope.dnsCredentials.secretAccessKey,
|
||||
token: $scope.dnsCredentials.digitalOceanToken,
|
||||
providerToken: $scope.instanceId
|
||||
token: $scope.dnsCredentials.digitalOceanToken
|
||||
};
|
||||
|
||||
// special case the wildcard provider
|
||||
@@ -52,11 +46,7 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', func
|
||||
}
|
||||
|
||||
Client.setupDnsConfig(data, function (error) {
|
||||
if (error && error.statusCode === 403) {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
$scope.error = 'Wrong instance id provided.';
|
||||
return;
|
||||
} else if (error) {
|
||||
if (error) {
|
||||
$scope.dnsCredentials.busy = false;
|
||||
$scope.dnsCredentials.error = error.message;
|
||||
return;
|
||||
@@ -94,7 +84,6 @@ app.controller('SetupDNSController', ['$scope', '$http', 'Client', 'ngTld', func
|
||||
$scope.dnsCredentials.provider = 'wildcard';
|
||||
}
|
||||
|
||||
$scope.instanceId = search.instanceId;
|
||||
$scope.provider = status.provider;
|
||||
$scope.initialized = true;
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<html>
|
||||
<head>
|
||||
<title> Cloudron OAuth Callback </title>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>" />
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron </title>
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron Admin Setup </title>
|
||||
|
||||
@@ -26,9 +25,6 @@
|
||||
<script src="3rdparty/js/angular-ui-notification.min.js"></script>
|
||||
<script src="3rdparty/js/autofill-event.js"></script>
|
||||
|
||||
<!-- Angular directives for bootstrap https://angular-ui.github.io/bootstrap/ -->
|
||||
<script src="3rdparty/js/ui-bootstrap-tpls-1.3.3.min.js"></script>
|
||||
|
||||
<!-- Setup Application -->
|
||||
<script src="js/setup.js"></script>
|
||||
|
||||
@@ -70,7 +66,7 @@
|
||||
<input type="text" class="form-control" ng-model="account.displayName" id="inputDisplayName" name="displayName" placeholder="Display Name" required autocomplete="off" autofocus>
|
||||
</div>
|
||||
<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.">
|
||||
<input type="email" class="form-control" ng-model="account.email" id="inputEmail" name="email" placeholder="Email" required autocomplete="off">
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.username.$dirty && setupForm.username.$invalid }">
|
||||
<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">
|
||||
@@ -81,6 +77,11 @@
|
||||
<small ng-show="setupForm.password.$dirty && setupForm.password.$invalid">Password must be 8-30 character with at least one uppercase, one numeric and one special character</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': setupForm.instanceId.$dirty && (setupForm.instanceId.$invalid || error) }" ng-show="provider === 'ami'">
|
||||
<p>Provide the EC2 instance id to verify you are the owner</p>
|
||||
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="AWS EC2 instance id" ng-maxlength="20" ng-minlength="10" ng-required="provider === 'ami'" autocomplete="off">
|
||||
<p ng-show="error" class="has-error">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' <%= apiOriginHostname %>;" />
|
||||
|
||||
<title> Cloudron Setup </title>
|
||||
|
||||
@@ -102,17 +101,6 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="provider === 'ami'">
|
||||
<div class="col-md-10 col-md-offset-1 text-center">
|
||||
<br/>
|
||||
<h3>Owner verification</h3>
|
||||
<p>Provide the EC2 instance id to verify you have access to this server.</p>
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.instanceId.$dirty && (dnsCredentialsForm.instanceId.$invalid || error) }">
|
||||
<input type="text" class="form-control" ng-model="instanceId" id="inputInstanceId" name="instanceId" placeholder="AWS EC2 instance id" ng-maxlength="20" ng-minlength="10" ng-required="provider === 'ami'" autocomplete="off">
|
||||
</div>
|
||||
<p> <span ng-show="error" class="text-danger">{{ error }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center">
|
||||
<button type="submit" class="btn btn-primary" ng-disabled="dnsCredentialsForm.$invalid"/><i class="fa fa-circle-o-notch fa-spin" ng-show="dnsCredentials.busy"></i> Next</button>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"/>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'unsafe-inline' 'unsafe-eval' 'self' *.cloudron.io <%= apiOriginHostname %>; img-src 'self' *.cloudron.io <%= apiOriginHostname %>" />
|
||||
|
||||
<title> Cloudron </title>
|
||||
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (!appUpdateForm.password.$dirty && appUpdate.error.password) || (appUpdateForm.password.$dirty && appUpdateForm.password.$invalid) }">
|
||||
<label class="control-label" for="inputUpdatePassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" required autofocus>
|
||||
<input type="password" class="form-control" ng-model="appUpdate.password" id="inputUpdatePassword" name="password" ng-maxlength="30" ng-minlength="8" required autofocus>
|
||||
</div>
|
||||
<input class="ng-hide" type="submit" ng-disabled="appUpdateForm.$invalid || busy"/>
|
||||
</form>
|
||||
@@ -412,7 +412,7 @@
|
||||
<a href="" ng-click="showRestore(app)" title="Restore App"><i class="fa fa-undo scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app.installationState === 'installed' || app.installationState === 'pending_configure') && !(app | installError)">
|
||||
<div ng-show="app.installationState === 'installed' || app.installationState === 'pending_configure'">
|
||||
<a href="" ng-click="showConfigure(app)" title="Configure App"><i class="fa fa-pencil scale"></i></a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': dnsCredentialsForm.customDomainId.$invalid }" uib-tooltip="{{ config.provider === 'caas' ? '' : 'Changing the domain is not yet supported' }}">
|
||||
<label class="control-label" for="customDomainId">Domain name</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.customDomain" id="customDomainId" name="customDomainId" ng-disabled="dnsCredentials.busy || config.provider !== 'caas'" check-tld placeholder="example.com" required autofocus>
|
||||
<p> <span ng-show="dnsCredentialsForm.customDomainId.$error.invalidSubdomain && dnsCredentials.customDomain !== config.fqdn" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
|
||||
<p> <span ng-show="dnsCredentialsForm.customDomainId.$error.invalidSubdomain" class="text-danger">Subdomains are <a href="https://cloudron.io/references/selfhosting.html#domain-setup" target="_blank" title="Domain documentation">not supported</a></span></p>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
@@ -28,15 +28,19 @@
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'route53'">
|
||||
<label class="control-label" for="dnsCredentialsSecretAccessKey">Secret access key</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.secretAccessKey" id="dnsCredentialsSecretAccessKey" name="secretAccessKey" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'route53'">
|
||||
<br/>
|
||||
<p>This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.</p>
|
||||
</div>
|
||||
|
||||
<!-- DigitalOcean -->
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
<label class="control-label" for="dnsCredentialsDigitalOceanToken">DigitalOcean token</label>
|
||||
<input type="text" class="form-control" ng-model="dnsCredentials.digitalOceanToken" id="dnsCredentialsDigitalOceanToken" name="digitalOceanToken" ng-disabled="dnsCredentials.busy" ng-required="dnsCredentials.provider === 'digitalocean'">
|
||||
<br/>
|
||||
<p>This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{ 'has-error': false }">
|
||||
<div class="form-group" ng-class="{ 'has-error': false }" ng-if="config.fqdn !== dnsCredentials.customDomain && !dnsCredentialsForm.customDomainId.$invalid">
|
||||
<label class="control-label" for="dnsCredentialsPassword">Provide your password to confirm this action</label>
|
||||
<input type="password" class="form-control" ng-model="dnsCredentials.password" id="dnsCredentialsPassword" name="password" ng-disabled="dnsCredentials.busy" required>
|
||||
</div>
|
||||
@@ -45,20 +49,14 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'route53'">
|
||||
This domain must be hosted on <a href="https://aws.amazon.com/route53/?nc2=h_m1" target="_blank">AWS Route53</a>.
|
||||
</p>
|
||||
|
||||
<p ng-show="dnsCredentials.provider === 'digitalocean'">
|
||||
This domain must be hosted on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-host-name-with-digitalocean#step-two%E2%80%94change-your-domain-server" target="_blank">DigitalOcean</a>.
|
||||
</p>
|
||||
|
||||
<!-- Wildcard -->
|
||||
<p ng-show="dnsCredentials.provider === 'wildcard'">
|
||||
Setup <i>A</i> records for <b>*.{{ dnsCredentials.customDomain || 'example.com' }}</b> and <b>{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP.
|
||||
Setup <i>A</i> records for <b>*.{{ dnsCredentials.domain || 'example.com' }}</b> and <b>{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP.
|
||||
</p>
|
||||
|
||||
<!-- Manual -->
|
||||
<p ng-show="dnsCredentials.provider === 'manual'">
|
||||
Setup an <i>A</i> record for <b>my.{{ dnsCredentials.customDomain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
Setup an <i>A</i> record for <b>my.{{ dnsCredentials.domain || 'example.com' }}</b> to this server's IP. All DNS records have to be setup manually <i>before</i> each app installation.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer ">
|
||||
|
||||
@@ -184,8 +184,7 @@ angular.module('Application').controller('CertsController', ['$scope', '$locatio
|
||||
$scope.showChangeDnsCredentials = function () {
|
||||
dnsCredentialsReset();
|
||||
|
||||
// clear the input box for non-custom domain
|
||||
$scope.dnsCredentials.customDomain = $scope.config.isCustomDomain ? $scope.config.fqdn : '';
|
||||
$scope.dnsCredentials.customDomain = $scope.config.fqdn;
|
||||
$scope.dnsCredentials.accessKeyId = $scope.dnsConfig.accessKeyId;
|
||||
$scope.dnsCredentials.secretAccessKey = $scope.dnsConfig.secretAccessKey;
|
||||
$scope.dnsCredentials.digitalOceanToken = $scope.dnsConfig.provider === 'digitalocean' ? $scope.dnsConfig.token : '';
|
||||
|
||||
@@ -296,15 +296,6 @@
|
||||
</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>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-show="(dnsConfig.provider !== 'caas')">
|
||||
Set the following DNS records to guarantee email delivery.
|
||||
@@ -345,6 +336,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12" ng-show="dnsConfig.provider !== 'caas'">
|
||||
<button ng-class="mailConfig.enabled ? 'btn btn-danger pull-right' : 'btn btn-primary pull-right'" ng-click="email.toggle()" ng-enabled="mailConfig">{{ mailConfig.enabled ? "Disable Email" : "Enable Email" }}</button>
|
||||
</div>
|
||||
<div class="col-md-12" ng-show="dnsConfig.provider === 'caas'">
|
||||
<span class="text-danger text-bold">This feature requires the Cloudron to be on <a href="https://cloudron.io/references/usermanual.html#entire-cloudron-on-a-custom-domain" target="_blank">custom domain</a>.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
@@ -450,5 +450,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>User Registration</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom: 15px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
By default the Cloudron only allows admins to invite other users.
|
||||
You may enable user registration, allowing users to signup without such an invite.
|
||||
</p>
|
||||
<p ng-show="openRegistrationEnabled">
|
||||
The user signup link is: <a ng-href="{{ signupLink }}" target="_blank">{{ signupLink }}</a>
|
||||
</p>
|
||||
<br/>
|
||||
<button class="btn btn-primary pull-right" ng-click="toggleOpenRegistration()">{{ openRegistrationEnabled ? 'Disable user registration' : 'Enable user registration' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offset the footer -->
|
||||
<br/><br/>
|
||||
|
||||
@@ -6,6 +6,8 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
$scope.client = Client;
|
||||
$scope.user = Client.getUserInfo();
|
||||
$scope.config = Client.getConfig();
|
||||
$scope.openRegistrationEnabled = false;
|
||||
$scope.signupLink = '';
|
||||
$scope.backupConfig = {};
|
||||
$scope.dnsConfig = {};
|
||||
$scope.outboundPort25 = {};
|
||||
@@ -507,6 +509,16 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
});
|
||||
}
|
||||
|
||||
function getOpenRegistration() {
|
||||
Client.getOpenRegistration(function (error, enabled) {
|
||||
if (error) return console.error(error);
|
||||
|
||||
$scope.openRegistrationEnabled = enabled;
|
||||
|
||||
$scope.signupLink = window.location.origin + '/api/v1/session/account/create.html';
|
||||
});
|
||||
}
|
||||
|
||||
function showExpectedDnsRecords(callback) {
|
||||
callback = callback || function (error) { if (error) console.error(error); };
|
||||
|
||||
@@ -613,12 +625,20 @@ angular.module('Application').controller('SettingsController', ['$scope', '$loca
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggleOpenRegistration = function () {
|
||||
Client.setOpenRegistration(!$scope.openRegistrationEnabled, function (error) {
|
||||
if (error) return console.error(error);
|
||||
$scope.openRegistrationEnabled = !$scope.openRegistrationEnabled;
|
||||
});
|
||||
};
|
||||
|
||||
Client.onReady(function () {
|
||||
fetchBackups();
|
||||
getMailConfig();
|
||||
getBackupConfig();
|
||||
getDnsConfig();
|
||||
getAutoupdatePattern();
|
||||
getOpenRegistration();
|
||||
|
||||
if ($scope.config.provider === 'caas') {
|
||||
getPlans();
|
||||
|
||||
@@ -4,25 +4,18 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add OAuth Client</h4>
|
||||
<h4 class="modal-title">Add API Client</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="clientAddForm" role="form" novalidate ng-submit="clientAdd.submit()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.name.$dirty && clientAddForm.name.$invalid) || (!clientAddForm.name.$dirty && clientAdd.error.name) }">
|
||||
<label class="control-label">Application name</label>
|
||||
<label class="control-label">Name</label>
|
||||
<div class="control-label" ng-show="(!clientAddForm.name.$dirty && clientAdd.error.name) || (clientAddForm.name.$dirty && clientAddForm.name.$invalid)">
|
||||
<small ng-show="clientAddForm.name.$error.required">A name is required</small>
|
||||
<small ng-show="!clientAddForm.name.$dirty && clientAdd.error.name">{{ clientAdd.error.name }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="clientAdd.name" name="name" id="clientAddName" required autofocus>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.redirectURI.$dirty && clientAddForm.redirectURI.$invalid) || (!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI) }">
|
||||
<label class="control-label">Authorization callback URL</label>
|
||||
<div class="control-label" ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">
|
||||
<small ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">{{ clientAdd.error.redirectURI }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="clientAdd.redirectURI" name="redirectURI" id="clientAddRedirectURI" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.scope.$dirty && clientAddForm.scope.$invalid) || (!clientAddForm.scope.$dirty && clientAdd.error.scope) }">
|
||||
<label class="control-label">Scope</label>
|
||||
<div class="control-label" ng-show="(!clientAddForm.scope.$dirty && clientAdd.error.scope) || (clientAddForm.scope.$dirty && clientAddForm.scope.$invalid)">
|
||||
@@ -31,12 +24,19 @@
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="clientAdd.scope" name="scope" id="clientAddScope" placeholder="Specify any number of scope separated by a comma ','" required>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{ 'has-error': (clientAddForm.redirectURI.$dirty && clientAddForm.redirectURI.$invalid) || (!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI) }">
|
||||
<label class="control-label">Redirect URI</label>
|
||||
<div class="control-label" ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">
|
||||
<small ng-show="!clientAddForm.redirectURI.$dirty && clientAdd.error.redirectURI">{{ clientAdd.error.redirectURI }}</small>
|
||||
</div>
|
||||
<input type="text" class="form-control" ng-model="clientAdd.redirectURI" name="redirectURI" id="clientAddRedirectURI" placeholder="Only required if OAuth logins are used">
|
||||
</div>
|
||||
<input class="hide" type="submit" ng-disabled="clientAddForm.$invalid || clientAdd.busy"/>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientAdd.busy"></i> Add OAuth Client</button>
|
||||
<button type="button" class="btn btn-success" ng-click="clientAdd.submit()" ng-disabled="clientAddForm.$invalid || clientAdd.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientAdd.busy"></i> Add API Client</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,7 +47,7 @@
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Remove OAuth Client</h4>
|
||||
<h4 class="modal-title">Remove API Client</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientRemove.busy"></i> Remove OAuth Client</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="clientRemove.submit()" ng-disabled="clientRemove.busy"><i class="fa fa-circle-o-notch fa-spin" ng-show="clientRemove.busy"></i> Remove API Client</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,11 +116,10 @@
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p>These tokens can be used to access the <a href="https://cloudron.io/references/api.html" target="_blank">Cloudron API</a>. They have the <b>admin</b> <a href="https://cloudron.io/references/api.html#scopes" target="_blank">scope</a> and do not expire.</p>
|
||||
<br/>
|
||||
<h4 class="text-muted">Active Tokens</h4>
|
||||
<hr/>
|
||||
<p ng-repeat="token in apiClient.activeTokens">
|
||||
<span ng-click-select>{{ token.accessToken }}</span> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
|
||||
<b ng-click-select>{{ token.accessToken }}</b> <button class="btn btn-xs btn-danger pull-right" ng-click="removeToken(apiClient, token)" title="Revoke Token"><i class="fa fa-trash-o"></i></button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,7 +130,7 @@
|
||||
|
||||
<div class="section-header">
|
||||
<div class="text-left">
|
||||
<h3>OAuth Apps<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New OAuth Client</button></h3>
|
||||
<h3>Apps<button class="btn btn-xs btn-primary btn-outline pull-right" ng-click="clientAdd.show()"><i class="fa fa-plus"></i> New API Client</button></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +155,7 @@
|
||||
<a href="" data-toggle="collapse" data-parent="#accordion" data-target="#collapse{{client.id}}">Advanced</a>
|
||||
<div id="collapse{{client.id}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove OAuth Client" ng-show="client.type === 'external'">Remove OAuth Client</button></h4>
|
||||
<h4 class="text-muted">Credentials <button class="btn btn-xs btn-danger pull-right" ng-click="clientRemove.show(client)" title="Remove API Client" ng-show="client.type === 'external'">Remove API Client</button></h4>
|
||||
<hr/>
|
||||
<p>Scope: <b ng-click-select>{{ client.scope }}</b></p>
|
||||
<p>RedirectURI: <b ng-click-select>{{ client.redirectURI }}</b></p>
|
||||
|
||||
@@ -19,7 +19,7 @@ angular.module('Application').controller('TokensController', ['$scope', 'Client'
|
||||
|
||||
$scope.clientAdd.error = {};
|
||||
$scope.clientAdd.name = '';
|
||||
$scope.clientAdd.scope = 'profile';
|
||||
$scope.clientAdd.scope = '*';
|
||||
$scope.clientAdd.redirectURI = '';
|
||||
|
||||
$scope.clientAddForm.$setUntouched();
|
||||
|
||||
@@ -221,8 +221,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>An email has been sent to <b>{{ inviteSent.email }}</b>.</p>
|
||||
<p>You can also share this invite link directly:</p>
|
||||
<p style="overflow: auto; white-space: nowrap;" eng-click-select>{{ inviteSent.setupLink }}</p>
|
||||
<p>You can also share this invite link directly.</p>
|
||||
<p ng-click-select>{{ inviteSent.setupLink }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">OK</button>
|
||||
|
||||
Reference in New Issue
Block a user