diff --git a/conf/nginx.conf b/conf/nginx.conf index a197c628..61fa0097 100644 --- a/conf/nginx.conf +++ b/conf/nginx.conf @@ -1,11 +1,11 @@ # Redirect all HTTP to HTTPS. server { listen 80; - listen [::]:80 default_server ipv6only=on; + listen [::]:80; - server_name $PUBLIC_HOSTNAME; + server_name $HOSTNAME; root /tmp/invalid-path-nothing-here; - rewrite ^/(.*)$ https://$PUBLIC_HOSTNAME/$1 permanent; + rewrite ^/(.*)$ https://$HOSTNAME/$1 permanent; } # The secure HTTPS server. @@ -13,14 +13,14 @@ server { server { listen 443 ssl; - server_name $PUBLIC_HOSTNAME; + server_name $HOSTNAME; - ssl_certificate $STORAGE_ROOT/ssl/ssl_certificate.pem; - ssl_certificate_key $STORAGE_ROOT/ssl/ssl_private_key.pem; + ssl_certificate $SSL_CERTIFICATE; + ssl_certificate_key $SSL_KEY; include /etc/nginx/nginx-ssl.conf; # Expose this directory as static files. - root $STORAGE_ROOT/www/static; + root $ROOT; index index.html index.htm; # Roundcube Webmail configuration. diff --git a/management/daemon.py b/management/daemon.py index 5fa0110f..9d652dcd 100755 --- a/management/daemon.py +++ b/management/daemon.py @@ -66,6 +66,13 @@ def dns_get_ds_records(): except Exception as e: return (str(e), 500) +# WEB + +@app.route('/web/update', methods=['POST']) +def web_update(): + from web_update import do_web_update + return do_web_update(env) + # System @app.route('/system/updates') diff --git a/management/dns_update.py b/management/dns_update.py index fa6a0799..d6edc7ce 100755 --- a/management/dns_update.py +++ b/management/dns_update.py @@ -6,7 +6,7 @@ import os, os.path, urllib.parse, datetime, re, hashlib import rtyaml from mailconfig import get_mail_domains -from utils import shell, load_env_vars_from_file +from utils import shell, load_env_vars_from_file, safe_domain_name def get_dns_domains(env): # What domains should we serve DNS for? @@ -30,7 +30,7 @@ def get_dns_domains(env): # Make a nice and safe filename for each domain. zonefiles = [] for domain in domains: - zonefiles.append([domain, urllib.parse.quote(domain, safe='') + ".txt"]) + zonefiles.append([domain, safe_domain_name(domain) + ".txt"]) return zonefiles diff --git a/management/utils.py b/management/utils.py index 1697f0b4..c3c34533 100644 --- a/management/utils.py +++ b/management/utils.py @@ -11,6 +11,11 @@ def load_env_vars_from_file(fn): for line in open(fn): env.setdefault(*line.strip().split("=", 1)) return env +def safe_domain_name(name): + # Sanitize a domain name so it is safe to use as a file name on disk. + import urllib.parse + return urllib.parse.quote(name, safe='') + def exclusive_process(name): # Ensure that a process named `name` does not execute multiple # times concurrently. diff --git a/management/web_update.py b/management/web_update.py new file mode 100644 index 00000000..5f027c57 --- /dev/null +++ b/management/web_update.py @@ -0,0 +1,95 @@ +# Creates an nginx configuration file so we serve HTTP/HTTPS on all +# domains for which a mail account has been set up. +######################################################################## + +import os, os.path + +from mailconfig import get_mail_domains +from utils import shell, safe_domain_name + +def get_web_domains(env): + # What domains should we serve HTTP/HTTPS for? + domains = set() + + # Add all domain names in use by email users and mail aliases. + domains |= get_mail_domains(env) + + # Ensure the PUBLIC_HOSTNAME is in the list. + domains.add(env['PUBLIC_HOSTNAME']) + + # Sort the list. Put PUBLIC_HOSTNAME first so it becomes the + # default server (nginx's default_server). + domains = sorted(domains, key = lambda domain : (domain != env["PUBLIC_HOSTNAME"], list(reversed(domain.split(".")))) ) + + return domains + + +def do_web_update(env): + # Build an nginx configuration file. + nginx_conf = "" + template = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read() + for domain in get_web_domains(env): + nginx_conf += make_domain_config(domain, template, env) + + # Save the file. + with open("/etc/nginx/conf.d/local.conf", "w") as f: + f.write(nginx_conf) + + # Nick nginx. + shell('check_call', ["/usr/sbin/service", "nginx", "restart"]) + + return "OK" + +def make_domain_config(domain, template, env): + # How will we configure this domain. + + # Where will its root directory be for static files? Try STORAGE_ROOT/web/domain_name + # if it exists, but fall back to STORAGE_ROOT/web/default. + for test_domain in (domain, 'default'): + root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain)) + if os.path.exists(root): break + + # What SSL private key will we use? Allow the user to override this, but + # in many cases using the same private key for all domains would be fine. + # Don't allow the user to override the key for PUBLIC_HOSTNAME because + # that's what's in the main file. + ssl_key = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_private_key.pem') + alt_key = os.path.join(env["STORAGE_ROOT"], 'ssl/domains/%s_private_key.pem' % safe_domain_name(domain)) + if domain != env['PUBLIC_HOSTNAME'] and os.path.exists(alt_key): + ssl_key = alt_key + + # What SSL certificate will we use? This has to be differnet for each + # domain name. The certificate is already generated for PUBLIC_HOSTNAME. + # For other domains, generate a self-signed certificate if one doesn't + # already exist. See setup/mail.sh for documentation. + if domain == env['PUBLIC_HOSTNAME']: + ssl_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/ssl_certificate.pem') + else: + ssl_certificate = os.path.join(env["STORAGE_ROOT"], 'ssl/domains/%s_certifiate.pem' % safe_domain_name(domain)) + os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True) + if not os.path.exists(ssl_certificate): + # Generate a new self-signed certificate using the same private key that we already have. + + # Start with a CSR. + csr = os.path.join(env["STORAGE_ROOT"], 'ssl/domains/%s_cert_sign_req.csr' % safe_domain_name(domain)) + shell("check_call", [ + "openssl", "req", "-new", + "-key", ssl_key, + "-out", csr, + "-subj", "/C=%s/ST=/L=/O=/CN=%s" % (env["CSR_COUNTRY"], domain)]) + + # And then make the certificate. + shell("check_call", [ + "openssl", "x509", "-req", + "-days", "365", + "-in", csr, + "-signkey", ssl_key, + "-out", ssl_certificate]) + + # Replace substitution strings in the template & return. + nginx_conf = template + nginx_conf = nginx_conf.replace("$HOSTNAME", domain) + nginx_conf = nginx_conf.replace("$ROOT", root) + nginx_conf = nginx_conf.replace("$SSL_KEY", ssl_key) + nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) + return nginx_conf diff --git a/setup/start.sh b/setup/start.sh index 301adead..3c3b36ce 100755 --- a/setup/start.sh +++ b/setup/start.sh @@ -112,6 +112,7 @@ fi # Save the global options in /etc/mailinabox.conf so that standalone # tools know where to look for data. cat > /etc/mailinabox.conf << EOF; +STORAGE_USER=$STORAGE_USER STORAGE_ROOT=$STORAGE_ROOT PUBLIC_HOSTNAME=$PUBLIC_HOSTNAME PUBLIC_IP=$PUBLIC_IP @@ -129,9 +130,10 @@ EOF . setup/webmail.sh . setup/management.sh -# Write the DNS configuration files. +# Write the DNS and nginx configuration files. sleep 5 # wait for the daemon to start curl -s -d POSTDATA http://127.0.0.1:10222/dns/update +curl -s -d POSTDATA http://127.0.0.1:10222/web/update # If there aren't any mail users yet, create one. if [ -z "`tools/mail.py user`" ]; then diff --git a/setup/web.sh b/setup/web.sh index 34d684b3..8a2df8d5 100755 --- a/setup/web.sh +++ b/setup/web.sh @@ -1,27 +1,29 @@ +#!/bin/bash # HTTP: Turn on a web server serving static files ################################################# source setup/functions.sh # load our functions +source /etc/mailinabox.conf # load global vars apt_install nginx php5-cgi rm -f /etc/nginx/sites-enabled/default -STORAGE_ROOT_ESC=$(echo $STORAGE_ROOT|sed 's/[\\\/&]/\\&/g') -PUBLIC_HOSTNAME_ESC=$(echo $PUBLIC_HOSTNAME|sed 's/[\\\/&]/\\&/g') - -# copy in the nginx configuration file and substitute some -# variables -cat conf/nginx.conf \ - | sed "s/\$STORAGE_ROOT/$STORAGE_ROOT_ESC/g" \ - | sed "s/\$PUBLIC_HOSTNAME/$PUBLIC_HOSTNAME_ESC/g" \ - > /etc/nginx/conf.d/local.conf +# copy in a nginx configuration file for common and best-practices +# SSL settings from @konklone cp conf/nginx-ssl.conf /etc/nginx/nginx-ssl.conf +# Other nginx settings will be configured by the management service +# since it depends on what domains we're serving, which we don't know +# until mail accounts have been created. + # make a default homepage -mkdir -p $STORAGE_ROOT/www/static -cp conf/www_default.html $STORAGE_ROOT/www/static/index.html -chown -R $STORAGE_USER $STORAGE_ROOT/www/static/index.html +if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_ROOT/www/default; fi # migration +mkdir -p $STORAGE_ROOT/www/default +if [ ! -f STORAGE_ROOT/www/default/index.html ]; then + cp conf/www_default.html $STORAGE_ROOT/www/default/index.html + chown -R $STORAGE_USER $STORAGE_ROOT/www/default/index.html +fi # Create an init script to start the PHP FastCGI daemon and keep it # running after a reboot. Allows us to serve Roundcube for webmail.