From b46ae07b5a02dd40c07dad03d0435781a3b56d49 Mon Sep 17 00:00:00 2001 From: Piero Toffanin Date: Fri, 17 Nov 2017 13:42:34 -0500 Subject: [PATCH] Lets Encrypt support --- .env | 4 ++ .gitignore | 1 + Dockerfile | 25 ++++---- docker-compose.ssl-manual.yml | 7 +++ docker-compose.ssl.yml | 14 +++++ nginx/.gitignore | 1 + nginx/crontab | 4 ++ nginx/letsencrypt-autogen.sh | 43 +++++++++++++ nginx/nginx-ssl.conf.template | 88 ++++++++++++++++++++++++++ start.sh | 17 ++++- webodm.sh | 115 +++++++++++++++++++++++++++++----- 11 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 docker-compose.ssl-manual.yml create mode 100644 docker-compose.ssl.yml create mode 100644 nginx/crontab create mode 100644 nginx/letsencrypt-autogen.sh create mode 100644 nginx/nginx-ssl.conf.template diff --git a/.env b/.env index ea34ae2a..54a2639f 100644 --- a/.env +++ b/.env @@ -1,3 +1,7 @@ HOST=localhost PORT=8000 MEDIA_DIR=appmedia +SSL=NO +SSL_KEY= +SSL_CERT= +SSL_INSECURE_PORT_REDIRECT=80 diff --git a/.gitignore b/.gitignore index cdbf6af3..d7660349 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,4 @@ webpack-stats.json pip-selfcheck.json .idea/ package-lock.json +.cronenv diff --git a/Dockerfile b/Dockerfile index 0b73a535..2253c94e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,17 +8,7 @@ ENV PYTHONPATH $PYTHONPATH:/webodm RUN mkdir /webodm WORKDIR /webodm -# Install pip reqs -ADD requirements.txt /webodm/ -RUN pip install -r requirements.txt - -ADD . /webodm/ - -RUN git submodule update --init - -# Install Node.js RUN curl --silent --location https://deb.nodesource.com/setup_6.x | bash - -RUN apt-get install -y nodejs # Configure use of testing branch of Debian RUN printf "Package: *\nPin: release a=stable\nPin-Priority: 900\n" > /etc/apt/preferences.d/stable.pref @@ -26,8 +16,19 @@ RUN printf "Package: *\nPin: release a=testing\nPin-Priority: 750\n" > /etc/apt/ RUN printf "deb http://mirror.steadfast.net/debian/ stable main contrib non-free\ndeb-src http://mirror.steadfast.net/debian/ stable main contrib non-free" > /etc/apt/sources.list.d/stable.list RUN printf "deb http://mirror.steadfast.net/debian/ testing main contrib non-free\ndeb-src http://mirror.steadfast.net/debian/ testing main contrib non-free" > /etc/apt/sources.list.d/testing.list -# Install GDAL, nginx -RUN apt-get update && apt-get install -t testing -y binutils libproj-dev gdal-bin nginx gettext-base +# Install Node.js GDAL, nginx, letsencrypt +RUN apt-get update && apt-get install -t testing -y binutils libproj-dev gdal-bin nginx && apt-get install nodejs gettext-base cron certbot + +# Install pip reqs +ADD requirements.txt /webodm/ +RUN pip install -r requirements.txt + +ADD . /webodm/ + +# Setup cron +RUN ln -s /webodm/nginx/crontab /etc/cron.d/nginx-cron && chmod 0644 /webodm/nginx/crontab && service cron start + +RUN git submodule update --init WORKDIR /webodm/nodeodm/external/node-OpenDroneMap RUN npm install diff --git a/docker-compose.ssl-manual.yml b/docker-compose.ssl-manual.yml new file mode 100644 index 00000000..0321f2f5 --- /dev/null +++ b/docker-compose.ssl-manual.yml @@ -0,0 +1,7 @@ +# This configuration adds the volumes necessary for SSL manual setup +version: '2' +services: + webapp: + volumes: + - ${SSL_KEY}:/webodm/nginx/ssl/key.pem + - ${SSL_CERT}:/webodm/nginx/ssl/cert.pem diff --git a/docker-compose.ssl.yml b/docker-compose.ssl.yml new file mode 100644 index 00000000..dc9a3e41 --- /dev/null +++ b/docker-compose.ssl.yml @@ -0,0 +1,14 @@ +# This configuration adds support for SSL +version: '2' +volumes: + letsencrypt: + driver: local +services: + webapp: + ports: + - "${SSL_INSECURE_PORT_REDIRECT}:8080" + volumes: + - letsencrypt:/webodm/nginx/letsencrypt + environment: + - SSL + - SSL_KEY \ No newline at end of file diff --git a/nginx/.gitignore b/nginx/.gitignore index 077ba2b0..4aee8d40 100644 --- a/nginx/.gitignore +++ b/nginx/.gitignore @@ -1,2 +1,3 @@ ssl/ +letsencrypt/ *.conf diff --git a/nginx/crontab b/nginx/crontab new file mode 100644 index 00000000..41844f37 --- /dev/null +++ b/nginx/crontab @@ -0,0 +1,4 @@ +# Automatically renew the SSL certificate (if needed) +0 0 1 * * root source /webodm/.cronenv; bash -c "/webodm/nginx/letsencrypt-autogen.sh" + +# An empty line is required at the end of this file for a valid cron file. diff --git a/nginx/letsencrypt-autogen.sh b/nginx/letsencrypt-autogen.sh new file mode 100644 index 00000000..e6df519f --- /dev/null +++ b/nginx/letsencrypt-autogen.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -eo pipefail +__dirname=$(cd $(dirname "$0"); pwd -P) +cd ${__dirname} + +hash certbot 2>/dev/null || not_found=true +if [ $not_found ]; then + echo "Certbot not found. You need to install certbot to use this script." + exit 1 +fi + +if [ "$SSL" = "NO" ] || [ ! -z "$SSL_KEY" ]; then + echo "SSL not enabled, or manual SSL key specified, exiting." + exit 1 +fi + +DOMAIN="${HOST:=$1}" +if [ -z $DOMAIN ]; then + echo "Usage: $0 " + exit 1 +fi + +# Generate/update certificate +certbot certonly --work-dir ./letsencrypt --config-dir ./letsencrypt --logs-dir ./letsencrypt --standalone -d $DOMAIN --register-unsafely-without-email --agree-tos --keep + +# Create ssl dir if necessary +if [ ! -e ssl/ ]; then + mkdir ssl +fi + +# Update symlinks +if [ -e ssl/key.pem ]; then + rm ssl/key.pem +fi + +if [ -e ssl/cert.pem ]; then + rm ssl/cert.pem +fi + +if [ -e "letsencrypt/live/$DOMAIN" ]; then + ln -vs "letsencrypt/live/$DOMAIN/privkey.pem" ssl/key.pem + ln -vs "letsencrypt/live/$DOMAIN/chain.pem" ssl/cert.pem +fi \ No newline at end of file diff --git a/nginx/nginx-ssl.conf.template b/nginx/nginx-ssl.conf.template new file mode 100644 index 00000000..0517354e --- /dev/null +++ b/nginx/nginx-ssl.conf.template @@ -0,0 +1,88 @@ +worker_processes 1; + +# Change this if running outside docker! +user root root; +pid /tmp/nginx.pid; +error_log /tmp/nginx.error.log; + +events { + worker_connections 1024; # increase if you have lots of clients + accept_mutex off; # set to 'on' if nginx worker_processes > 1 + use epoll; +} + +http { + include /etc/nginx/mime.types; + + # fallback in case we can't determine a type + default_type application/octet-stream; + access_log /tmp/nginx.access.log combined; + sendfile on; + + upstream app_server { + # fail_timeout=0 means we always retry an upstream even if it failed + # to return a good HTTP response + + # for UNIX domain socket setups + server unix:/tmp/gunicorn.sock fail_timeout=0; + } + + # Redirect all non-encrypted to encrypted + server { + server_name $HOST; + listen 8080; + return 301 https://$HOST:$PORT$request_uri; + } + + server { + listen 8000 deferred; + client_max_body_size 0; + + server_name $HOST; + + ssl on; + ssl_certificate /webodm/nginx/ssl/cert.pem + ssl_certificate_key /webodm/nginx/ssl/key.pem + + keepalive_timeout 5; + + proxy_connect_timeout 360s; + proxy_read_timeout 360s; + + # path for static files + location /static { + root /webodm/build; + } + + # path for certain media files that don't need permissions enforced + location /media/CACHE { + root /webodm/app; + } + location /media/settings { + autoindex on; + root /webodm/app; + } + + location / { + # CORS settings + + # These settings are VERY permissive, consider tightening them + + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # enable this if and only if you use HTTPS + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Host $http_host; + + # we don't want nginx trying to do something clever with + # redirects, we set the Host: header above already. + proxy_redirect off; + proxy_pass http://app_server; + } + } +} diff --git a/start.sh b/start.sh index 6498bf77..58e8d4dc 100755 --- a/start.sh +++ b/start.sh @@ -61,6 +61,9 @@ fi export HOST="${HOST:=localhost}" export PORT="${PORT:=8000}" +# Dump environment to .cronenv +printenv > .cronenv + (sleep 5; echo echo -e "\033[92m" echo "Congratulations! └@(・◡・)@┐" @@ -86,7 +89,19 @@ else envsubst '\$HOST \$OTHER_VAR' < $templ > ${templ%.*} done - nginx -c $(pwd)/nginx/nginx.conf + # Check if we need to auto-generate SSL certs via letsencrypt + if [ "$SSL" = "YES" ] && [ -z "$SSL_KEY" ]; then + bash -c "nginx/letsencrypt-autogen.sh" + fi + + # Check if SSL key/certs are available + conf="nginx.conf" + if [ -e nginx/ssl ]; + echo "Using nginx SSL configuration" + conf="nginx-ssl.conf" + fi + + nginx -c $(pwd)/nginx/$conf gunicorn webodm.wsgi --bind unix:/tmp/gunicorn.sock --timeout 360 --preload fi diff --git a/webodm.sh b/webodm.sh index d8c69ac2..2e93a754 100755 --- a/webodm.sh +++ b/webodm.sh @@ -1,5 +1,7 @@ #!/bin/bash set -eo pipefail +__dirname=$(cd $(dirname "$0"); pwd -P) +cd ${__dirname} platform="Linux" # Assumed uname=$(uname) @@ -16,25 +18,86 @@ if [[ $platform = "Windows" ]]; then export COMPOSE_CONVERT_WINDOWS_PATHS=1 fi -# Set default env variables -export PORT="${WEBODM_PORT:=8000}" -export HOST="${WEBODM_HOST:=localhost}" -export MEDIA_DIR="${WEBODM_MEDIA_DIR:=appmedia}" +# Load default values +source .env +DEFAULT_PORT="$PORT" +DEFAULT_HOST="$HOST" +DEFAULT_MEDIA_DIR="$MEDIA_DIR" +DEFAULT_SSL="$SSL" +DEFAULT_SSL_INSECURE_PORT_REDIRECT="$SSL_INSECURE_PORT_REDIRECT" + +# Parse args for overrides +POSITIONAL=() +while [[ $# -gt 0 ]] +do +key="$1" + +case $key in + --port) + export PORT="$2" + shift # past argument + shift # past value + ;; + --hostname) + export HOST="$2" + shift # past argument + shift # past value + ;; + --media-dir) + export MEDIA_DIR=$(realpath "$2") + shift # past argument + shift # past value + ;; + --ssl) + SSL=YES + shift # past argument + ;; + --ssl-key) + export SSL_KEY=$(realpath "$2") + shift # past argument + shift # past value + ;; + --ssl-cert) + export SSL_CERT=$(realpath "$2") + shift # past argument + shift # past value + ;; + --ssl-insecure-port-redirect) + export SSL_INSECURE_PORT_REDIRECT="$2" + shift # past argument + shift # past value + ;; + *) # unknown option + POSITIONAL+=("$1") # save it in an array for later + shift # past argument + ;; +esac +done +set -- "${POSITIONAL[@]}" # restore positional parameter usage(){ - echo "Usage: $0 [options]" + echo "Usage: $0 " echo echo "This program helps to manage the setup/teardown of the docker containers for running WebODM. We recommend that you read the full documentation of docker at https://docs.docker.com if you want to customize your setup." echo echo "Command list:" - echo " start Start WebODM" - echo " stop Stop WebODM" - echo " down Stop and remove WebODM's docker containers" - echo " update Update WebODM to the latest release" - echo " rebuild Rebuild all docker containers and perform cleanups" - echo " checkenv Do an environment check and install missing components" - echo " test Run the unit test suite (developers only)" + echo " start [options] Start WebODM" + echo " stop Stop WebODM" + echo " down Stop and remove WebODM's docker containers" + echo " update Update WebODM to the latest release" + echo " rebuild Rebuild all docker containers and perform cleanups" + echo " checkenv Do an environment check and install missing components" + echo " test Run the unit test suite (developers only)" echo " resetadminpassword Reset the administrator's password to a new one. WebODM must be running when executing this command." + echo "" + echo "Options:" + echo " --port Set the port that WebODM should bind to (default: $DEFAULT_PORT)" + echo " --hostname Set the hostname that WebODM will be accessible from (default: $DEFAULT_HOST)" + echo " --media-dir Path where processing results will be stored to (default: $DEFAULT_MEDIA_DIR (docker named volume))" + echo " --ssl Enable SSL and automatically request and install a certificate from letsencrypt.org. (default: $DEFAULT_SSL)" + echo " --ssl-key Manually specify a path to the private key file (.pem) to use with nginx to enable SSL (default: None)" + echo " --ssl-cert Manually specify a path to the certificate file (.pem) to use with nginx to enable SSL (default: None)" + echo " --ssl-insecure-port-redirect Insecure port number to redirect from when SSL is enabled (default: $DEFAULT_SSL_INSECURE_PORT_REDIRECT)" exit } @@ -79,6 +142,26 @@ run(){ start(){ command="docker-compose -f docker-compose.yml -f docker-compose.nodeodm.yml" + if [ "$SSL" = "YES" ]; then + if [ ! -z "$SSL_KEY" ] && [ ! -e "$SSL_KEY" ]; then + echo -e "\033[91mSSL key file does not exist: $SSL_KEY\033[39m" + exit 1 + fi + if [ ! -z "$SSL_CERT" ] && [ ! -e "$SSL_CERT" ]; then + echo -e "\033[91mSSL certificate file does not exist: $SSL_CERT\033[39m" + exit 1 + fi + + command+=" -f docker-compose.ssl.yml" + + method="Lets Encrypt" + if [ ! -z "$SSL_KEY" ] && [ ! -z "$SSL_CERT" ]; then + method="Manual" + command+=" -f docker-compose.ssl-manual.yml" + fi + + echo "SSL will be enabled ($method)" + fi run "$command start || $command up" } @@ -128,11 +211,15 @@ if [[ $1 = "start" ]]; then echo "Starting WebODM..." echo "" echo "Using the following environment:" - echo "============" + echo "================================" echo "Host: $HOST" echo "Port: $PORT" echo "Media directory: $MEDIA_DIR" - echo "============" + echo "SSL: $SSL" + echo "SSL key: $SSL_KEY" + echo "SSL certificate: $SSL_CERT" + echo "SSL insecure port redirect: $SSL_INSECURE_PORT_REDIRECT" + echo "================================" echo "Make sure to issue a $0 down if you decide to change the environment." echo ""