Merge branch 'master' of https://github.com/mail-in-a-box/mailinabox into typo
This commit is contained in:
commit
e4554a123e
|
@ -32,8 +32,8 @@ The components installed are:
|
|||
* Webmail ([Roundcube](http://roundcube.net/)), static website hosting ([nginx](http://nginx.org/))
|
||||
* Spam filtering ([spamassassin](https://spamassassin.apache.org/)), greylisting ([postgrey](http://postgrey.schweikert.ch/))
|
||||
* DNS ([nsd4](http://www.nlnetlabs.nl/projects/nsd/)) with [SPF](https://en.wikipedia.org/wiki/Sender_Policy_Framework), DKIM ([OpenDKIM](http://www.opendkim.org/)), [DMARC](https://en.wikipedia.org/wiki/DMARC), [DNSSEC](https://en.wikipedia.org/wiki/DNSSEC), [DANE TLSA](https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities), and [SSHFP](https://tools.ietf.org/html/rfc4255) records automatically set
|
||||
* Firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page))
|
||||
* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and system monitoring.
|
||||
* Firewall ([ufw](https://launchpad.net/ufw)), intrusion protection ([fail2ban](http://www.fail2ban.org/wiki/index.php/Main_Page)), system monitoring ([munin](http://munin-monitoring.org/))
|
||||
* A control panel and API for adding/removing mail users, aliases, custom DNS records, etc. and detailed system monitoring.
|
||||
|
||||
For more information on how Mail-in-a-Box handles your privacy, see the [security details page](security.md).
|
||||
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
# Expose this directory as static files.
|
||||
root $ROOT;
|
||||
index index.html index.htm;
|
||||
|
||||
location = /robots.txt {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /favicon.ico {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /mailinabox.mobileconfig {
|
||||
alias /var/lib/mailinabox/mobileconfig.xml;
|
||||
}
|
||||
location = /.well-known/autoconfig/mail/config-v1.1.xml {
|
||||
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
|
||||
}
|
||||
|
||||
# Roundcube Webmail configuration.
|
||||
rewrite ^/mail$ /mail/ redirect;
|
||||
rewrite ^/mail/$ /mail/index.php;
|
||||
location /mail/ {
|
||||
index index.php;
|
||||
alias /usr/local/lib/roundcubemail/;
|
||||
}
|
||||
location ~ /mail/config/.* {
|
||||
# A ~-style location is needed to give this precedence over the next block.
|
||||
return 403;
|
||||
}
|
||||
location ~ /mail/.*\.php {
|
||||
# note: ~ has precendence over a regular location block
|
||||
include fastcgi_params;
|
||||
fastcgi_split_path_info ^/mail(/.*)()$;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
|
||||
fastcgi_pass php-fpm;
|
||||
|
||||
# Outgoing mail also goes through this endpoint, so increase the maximum
|
||||
# file upload limit to match the corresponding Postfix limit.
|
||||
client_max_body_size 128M;
|
||||
}
|
||||
|
||||
# Z-Push (Microsoft Exchange ActiveSync)
|
||||
location /Microsoft-Server-ActiveSync {
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
|
||||
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
|
||||
fastcgi_read_timeout 630;
|
||||
fastcgi_pass php-fpm;
|
||||
|
||||
# Outgoing mail also goes through this endpoint, so increase the maximum
|
||||
# file upload limit to match the corresponding Postfix limit.
|
||||
client_max_body_size 128M;
|
||||
}
|
||||
location /autodiscover/autodiscover.xml {
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
|
||||
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
|
||||
fastcgi_pass php-fpm;
|
||||
}
|
||||
|
||||
|
||||
# ADDITIONAL DIRECTIVES HERE
|
||||
|
||||
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
|
||||
# This block is placed at the end. Nginx's precedence rules means this block
|
||||
# takes precedence over all non-regex matches and only regex matches that
|
||||
# come after it (i.e. none of those, since this is the last one.) That means
|
||||
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
|
||||
# handled locations for ownCloud (which serves user-uploaded files that might
|
||||
# have this pattern, see #414) or some of the other services.
|
||||
location ~ /\.(ht|svn|git|hg|bzr) {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
deny all;
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
# Proxy /admin to our Python based control panel daemon. It is
|
||||
# listening on IPv4 only so use an IP address and not 'localhost'.
|
||||
rewrite ^/admin$ /admin/;
|
||||
rewrite ^/admin/munin$ /admin/munin redirect;
|
||||
location /admin/ {
|
||||
proxy_pass http://127.0.0.1:10222/;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
|
@ -58,3 +59,4 @@
|
|||
rewrite ^/.well-known/carddav /cloud/remote.php/carddav/ redirect;
|
||||
rewrite ^/.well-known/caldav /cloud/remote.php/caldav/ redirect;
|
||||
|
||||
# ADDITIONAL DIRECTIVES HERE
|
||||
|
|
|
@ -33,84 +33,5 @@ server {
|
|||
ssl_certificate_key $SSL_KEY;
|
||||
include /etc/nginx/nginx-ssl.conf;
|
||||
|
||||
# Expose this directory as static files.
|
||||
root $ROOT;
|
||||
index index.html index.htm;
|
||||
|
||||
location = /robots.txt {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /favicon.ico {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location = /mailinabox.mobileconfig {
|
||||
alias /var/lib/mailinabox/mobileconfig.xml;
|
||||
}
|
||||
location = /.well-known/autoconfig/mail/config-v1.1.xml {
|
||||
alias /var/lib/mailinabox/mozilla-autoconfig.xml;
|
||||
}
|
||||
|
||||
# Roundcube Webmail configuration.
|
||||
rewrite ^/mail$ /mail/ redirect;
|
||||
rewrite ^/mail/$ /mail/index.php;
|
||||
location /mail/ {
|
||||
index index.php;
|
||||
alias /usr/local/lib/roundcubemail/;
|
||||
}
|
||||
location ~ /mail/config/.* {
|
||||
# A ~-style location is needed to give this precedence over the next block.
|
||||
return 403;
|
||||
}
|
||||
location ~ /mail/.*\.php {
|
||||
# note: ~ has precendence over a regular location block
|
||||
include fastcgi_params;
|
||||
fastcgi_split_path_info ^/mail(/.*)()$;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/roundcubemail/$fastcgi_script_name;
|
||||
fastcgi_pass php-fpm;
|
||||
|
||||
# Outgoing mail also goes through this endpoint, so increase the maximum
|
||||
# file upload limit to match the corresponding Postfix limit.
|
||||
client_max_body_size 128M;
|
||||
}
|
||||
|
||||
# Z-Push (Microsoft Exchange ActiveSync)
|
||||
location /Microsoft-Server-ActiveSync {
|
||||
include /etc/nginx/fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/index.php;
|
||||
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
|
||||
fastcgi_read_timeout 630;
|
||||
fastcgi_pass php-fpm;
|
||||
|
||||
# Outgoing mail also goes through this endpoint, so increase the maximum
|
||||
# file upload limit to match the corresponding Postfix limit.
|
||||
client_max_body_size 128M;
|
||||
}
|
||||
location /autodiscover/autodiscover.xml {
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/local/lib/z-push/autodiscover/autodiscover.php;
|
||||
fastcgi_param PHP_VALUE "include_path=.:/usr/share/php:/usr/share/pear:/usr/share/awl/inc";
|
||||
fastcgi_pass php-fpm;
|
||||
}
|
||||
|
||||
|
||||
# ADDITIONAL DIRECTIVES HERE
|
||||
|
||||
# Disable viewing dotfiles (.htaccess, .svn, .git, etc.)
|
||||
# This block is placed at the end. Nginx's precedence rules means this block
|
||||
# takes precedence over all non-regex matches and only regex matches that
|
||||
# come after it (i.e. none of those, since this is the last one.) That means
|
||||
# we're blocking dotfiles in the static hosted sites but not the FastCGI-
|
||||
# handled locations for ownCloud (which serves user-uploaded files that might
|
||||
# have this pattern, see #414) or some of the other services.
|
||||
location ~ /\.(ht|svn|git|hg|bzr) {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
deny all;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -88,8 +88,9 @@ class KeyAuthService:
|
|||
if email == "" or pw == "":
|
||||
raise ValueError("Enter an email address and password.")
|
||||
|
||||
# The password might be a user-specific API key.
|
||||
if hmac.compare_digest(self.create_user_key(email), pw):
|
||||
# The password might be a user-specific API key. create_user_key raises
|
||||
# a ValueError if the user does not exist.
|
||||
if hmac.compare_digest(self.create_user_key(email, env), pw):
|
||||
# OK.
|
||||
pass
|
||||
else:
|
||||
|
@ -111,18 +112,26 @@ class KeyAuthService:
|
|||
# Login failed.
|
||||
raise ValueError("Invalid password.")
|
||||
|
||||
# Get privileges for authorization. This call should never fail on a valid user,
|
||||
# but if the caller passed a user-specific API key then the user may no longer
|
||||
# exist --- in that case, get_mail_user_privileges will return a tuple of an
|
||||
# error message and an HTTP status code.
|
||||
# Get privileges for authorization. This call should never fail because by this
|
||||
# point we know the email address is a valid user. But on error the call will
|
||||
# return a tuple of an error message and an HTTP status code.
|
||||
privs = get_mail_user_privileges(email, env)
|
||||
if isinstance(privs, tuple): raise ValueError(privs[0])
|
||||
|
||||
# Return a list of privileges.
|
||||
return privs
|
||||
|
||||
def create_user_key(self, email):
|
||||
return hmac.new(self.key.encode('ascii'), b"AUTH:" + email.encode("utf8"), digestmod="sha1").hexdigest()
|
||||
def create_user_key(self, email, env):
|
||||
# Store an HMAC with the client. The hashed message of the HMAC will be the user's
|
||||
# email address & hashed password and the key will be the master API key. The user of
|
||||
# course has their own email address and password. We assume they do not have the master
|
||||
# API key (unless they are trusted anyway). The HMAC proves that they authenticated
|
||||
# with us in some other way to get the HMAC. Including the password means that when
|
||||
# a user's password is reset, the HMAC changes and they will correctly need to log
|
||||
# in to the control panel again. This method raises a ValueError if the user does
|
||||
# not exist, due to get_mail_password.
|
||||
msg = b"AUTH:" + email.encode("utf8") + b" " + get_mail_password(email, env).encode("utf8")
|
||||
return hmac.new(self.key.encode('ascii'), msg, digestmod="sha256").hexdigest()
|
||||
|
||||
def _generate_key(self):
|
||||
raw_key = os.urandom(32)
|
||||
|
|
|
@ -4,7 +4,7 @@ import os, os.path, re, json
|
|||
|
||||
from functools import wraps
|
||||
|
||||
from flask import Flask, request, render_template, abort, Response
|
||||
from flask import Flask, request, render_template, abort, Response, send_from_directory
|
||||
|
||||
import auth, utils
|
||||
from mailconfig import get_mail_users, get_mail_users_ex, get_admins, add_mail_user, set_mail_password, remove_mail_user
|
||||
|
@ -118,7 +118,7 @@ def me():
|
|||
|
||||
# Is authorized as admin? Return an API key for future use.
|
||||
if "admin" in privs:
|
||||
resp["api_key"] = auth_service.create_user_key(email)
|
||||
resp["api_key"] = auth_service.create_user_key(email, env)
|
||||
|
||||
# Return.
|
||||
return json_response(resp)
|
||||
|
@ -384,6 +384,17 @@ def backup_status():
|
|||
from backup import backup_status
|
||||
return json_response(backup_status(env))
|
||||
|
||||
# MUNIN
|
||||
|
||||
@app.route('/munin/')
|
||||
@app.route('/munin/<path:filename>')
|
||||
@authorized_personnel_only
|
||||
def munin(filename=""):
|
||||
# Checks administrative access (@authorized_personnel_only) and then just proxies
|
||||
# the request to static files.
|
||||
if filename == "": filename = "index.html"
|
||||
return send_from_directory("/var/cache/munin/www", filename)
|
||||
|
||||
# APP
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -57,13 +57,15 @@ def do_dns_update(env, force=False):
|
|||
|
||||
# Custom records to add to zones.
|
||||
additional_records = list(get_custom_dns_config(env))
|
||||
from web_update import get_default_www_redirects
|
||||
www_redirect_domains = get_default_www_redirects(env)
|
||||
|
||||
# Write zone files.
|
||||
os.makedirs('/etc/nsd/zones', exist_ok=True)
|
||||
updated_domains = []
|
||||
for i, (domain, zonefile) in enumerate(zonefiles):
|
||||
# Build the records to put in the zone.
|
||||
records = build_zone(domain, domains, additional_records, env)
|
||||
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
|
||||
|
||||
# See if the zone has changed, and if so update the serial number
|
||||
# and write the zone file.
|
||||
|
@ -126,7 +128,7 @@ def do_dns_update(env, force=False):
|
|||
|
||||
########################################################################
|
||||
|
||||
def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
||||
def build_zone(domain, all_domains, additional_records, www_redirect_domains, env, is_zone=True):
|
||||
records = []
|
||||
|
||||
# For top-level zones, define the authoritative name servers.
|
||||
|
@ -177,7 +179,7 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||
subdomains = [d for d in all_domains if d.endswith("." + domain)]
|
||||
for subdomain in subdomains:
|
||||
subdomain_qname = subdomain[0:-len("." + domain)]
|
||||
subzone = build_zone(subdomain, [], additional_records, env, is_zone=False)
|
||||
subzone = build_zone(subdomain, [], additional_records, www_redirect_domains, env, is_zone=False)
|
||||
for child_qname, child_rtype, child_value, child_explanation in subzone:
|
||||
if child_qname == None:
|
||||
child_qname = subdomain_qname
|
||||
|
@ -215,9 +217,12 @@ def build_zone(domain, all_domains, additional_records, env, is_zone=True):
|
|||
has_rec_base = records
|
||||
defaults = [
|
||||
(None, "A", env["PUBLIC_IP"], "Required. May have a different value. Sets the IP address that %s resolves to for web hosting and other services besides mail. The A record must be present but its value does not affect mail delivery." % domain),
|
||||
("www", "A", env["PUBLIC_IP"], "Optional. Sets the IP address that www.%s resolves to, e.g. for web hosting." % domain),
|
||||
(None, "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that %s resolves to, e.g. for web hosting. (It is not necessary for receiving mail on this domain.)" % domain),
|
||||
("www", "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that www.%s resolves to, e.g. for web hosting." % domain),
|
||||
]
|
||||
if "www." + domain in www_redirect_domains:
|
||||
defaults += [
|
||||
("www", "A", env["PUBLIC_IP"], "Optional. Sets the IP address that www.%s resolves to so that the box can provide a redirect to the parent domain." % domain),
|
||||
("www", "AAAA", env.get('PUBLIC_IPV6'), "Optional. Sets the IPv6 address that www.%s resolves to so that the box can provide a redirect to the parent domain." % domain),
|
||||
]
|
||||
for qname, rtype, value, explanation in defaults:
|
||||
if value is None or value.strip() == "": continue # skip IPV6 if not set
|
||||
|
@ -847,8 +852,10 @@ def build_recommended_dns(env):
|
|||
domains = get_dns_domains(env)
|
||||
zonefiles = get_dns_zones(env)
|
||||
additional_records = list(get_custom_dns_config(env))
|
||||
from web_update import get_default_www_redirects
|
||||
www_redirect_domains = get_default_www_redirects(env)
|
||||
for domain, zonefile in zonefiles:
|
||||
records = build_zone(domain, domains, additional_records, env)
|
||||
records = build_zone(domain, domains, additional_records, www_redirect_domains, env)
|
||||
|
||||
# remove records that we don't dislay
|
||||
records = [r for r in records if r[3] is not False]
|
||||
|
|
|
@ -211,7 +211,7 @@ def get_mail_aliases_ex(env):
|
|||
for source, destination in get_mail_aliases(env):
|
||||
# get alias info
|
||||
domain = get_domain(source)
|
||||
required = ((source in required_aliases) or (source == get_system_administrator(env)))
|
||||
required = (source in required_aliases)
|
||||
|
||||
# add to list
|
||||
if not domain in domains:
|
||||
|
@ -493,15 +493,17 @@ def get_required_aliases(env):
|
|||
# These are the aliases that must exist.
|
||||
aliases = set()
|
||||
|
||||
# The system administrator alias is required.
|
||||
aliases.add(get_system_administrator(env))
|
||||
|
||||
# The hostmaster alias is exposed in the DNS SOA for each zone.
|
||||
aliases.add("hostmaster@" + env['PRIMARY_HOSTNAME'])
|
||||
|
||||
# Get a list of domains we serve mail for, except ones for which the only
|
||||
# email on that domain is a postmaster/admin alias to the administrator
|
||||
# or a wildcard alias (since it will forward postmaster/admin).
|
||||
# email on that domain are the required aliases or a catch-all/domain-forwarder.
|
||||
real_mail_domains = get_mail_domains(env,
|
||||
filter_aliases = lambda alias :
|
||||
((not alias[0].startswith("postmaster@") and not alias[0].startswith("admin@")) or alias[1] != get_system_administrator(env))
|
||||
not alias[0].startswith("postmaster@") and not alias[0].startswith("admin@")
|
||||
and not alias[0].startswith("@")
|
||||
)
|
||||
|
||||
|
@ -538,12 +540,12 @@ def kick(env, mail_result=None):
|
|||
for s, t in existing_aliases:
|
||||
if s == source:
|
||||
return
|
||||
|
||||
# Doesn't exist.
|
||||
administrator = get_system_administrator(env)
|
||||
add_mail_alias(source, administrator, env, do_kick=False)
|
||||
results.append("added alias %s (=> %s)\n" % (source, administrator))
|
||||
|
||||
|
||||
for alias in required_aliases:
|
||||
ensure_admin_alias_exists(alias)
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import dns.reversename, dns.resolver
|
|||
import dateutil.parser, dateutil.tz
|
||||
|
||||
from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config, get_secondary_dns
|
||||
from web_update import get_web_domains, get_domain_ssl_files
|
||||
from web_update import get_web_domains, get_default_www_redirects, get_domain_ssl_files
|
||||
from mailconfig import get_mail_domains, get_mail_aliases
|
||||
|
||||
from utils import shell, sort_domains, load_env_vars_from_file
|
||||
|
@ -227,7 +227,7 @@ def run_domain_checks(rounded_time, env, output, pool):
|
|||
dns_domains = set(dns_zonefiles)
|
||||
|
||||
# Get the list of domains we serve HTTPS for.
|
||||
web_domains = set(get_web_domains(env))
|
||||
web_domains = set(get_web_domains(env) + get_default_www_redirects(env))
|
||||
|
||||
domains_to_check = mail_domains | dns_domains | web_domains
|
||||
|
||||
|
|
|
@ -98,9 +98,10 @@
|
|||
<li><a href="#ssl" onclick="return show_panel(this);">SSL Certificates</a></li>
|
||||
<li><a href="#system_backup" onclick="return show_panel(this);">Backup Status</a></li>
|
||||
<li class="divider"></li>
|
||||
<li class="dropdown-header">Advanced Options</li>
|
||||
<li class="dropdown-header">Advanced Pagess</li>
|
||||
<li><a href="#custom_dns" onclick="return show_panel(this);">Custom DNS</a></li>
|
||||
<li><a href="#external_dns" onclick="return show_panel(this);">External DNS</a></li>
|
||||
<li><a href="/admin/munin">Munin Monitoring</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
|
|
|
@ -164,9 +164,14 @@ function do_add_user() {
|
|||
|
||||
function users_set_password(elem) {
|
||||
var email = $(elem).parents('tr').attr('data-email');
|
||||
|
||||
var yourpw = "";
|
||||
if (api_credentials != null && email == api_credentials[0])
|
||||
yourpw = "<p class='text-danger'>If you change your own password, you will be logged out of this control panel and will need to log in again.</p>";
|
||||
|
||||
show_modal_confirm(
|
||||
"Archive User",
|
||||
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small></p>"),
|
||||
$("<p>Set a new password for <b>" + email + "</b>?</p> <p><label for='users_set_password_pw' style='display: block; font-weight: normal'>New Password:</label><input type='password' id='users_set_password_pw'></p><p><small>Passwords must be at least four characters and may not contain spaces.</small>" + yourpw + "</p>"),
|
||||
"Set Password",
|
||||
function() {
|
||||
api(
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
import os, os.path, shutil, re, tempfile, rtyaml
|
||||
|
||||
from mailconfig import get_mail_domains
|
||||
from dns_update import get_custom_dns_config, do_dns_update
|
||||
from dns_update import get_custom_dns_config, do_dns_update, get_dns_zones
|
||||
from utils import shell, safe_domain_name, sort_domains
|
||||
|
||||
def get_web_domains(env):
|
||||
|
@ -19,31 +19,71 @@ def get_web_domains(env):
|
|||
# Also serve web for all mail domains so that we might at least
|
||||
# provide auto-discover of email settings, and also a static website
|
||||
# if the user wants to make one. These will require an SSL cert.
|
||||
domains |= get_mail_domains(env)
|
||||
|
||||
# ...Unless the domain has an A/AAAA record that maps it to a different
|
||||
# IP address than this box. Remove those domains from our list.
|
||||
dns = get_custom_dns_config(env)
|
||||
for domain, rtype, value in dns:
|
||||
if domain not in domains: continue
|
||||
if rtype == "CNAME" or (rtype in ("A", "AAAA") and value != "local"):
|
||||
domains.remove(domain)
|
||||
domains |= (get_mail_domains(env) - get_domains_with_a_records(env))
|
||||
|
||||
# Sort the list. Put PRIMARY_HOSTNAME first so it becomes the
|
||||
# default server (nginx's default_server).
|
||||
# Sort the list so the nginx conf gets written in a stable order.
|
||||
domains = sort_domains(domains, env)
|
||||
|
||||
return domains
|
||||
|
||||
def get_domains_with_a_records(env):
|
||||
domains = set()
|
||||
dns = get_custom_dns_config(env)
|
||||
for domain, rtype, value in dns:
|
||||
if rtype == "CNAME" or (rtype in ("A", "AAAA") and value != "local"):
|
||||
domains.add(domain)
|
||||
return domains
|
||||
|
||||
def get_web_domains_with_root_overrides(env):
|
||||
# Load custom settings so we can tell what domains have a redirect or proxy set up on '/',
|
||||
# which means static hosting is not happening.
|
||||
root_overrides = { }
|
||||
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
||||
if os.path.exists(nginx_conf_custom_fn):
|
||||
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
|
||||
for domain, settings in custom_settings.items():
|
||||
for type, value in [('redirect', settings.get('redirects', {}).get('/')),
|
||||
('proxy', settings.get('proxies', {}).get('/'))]:
|
||||
if value:
|
||||
root_overrides[domain] = (type, value)
|
||||
return root_overrides
|
||||
|
||||
|
||||
def get_default_www_redirects(env):
|
||||
# Returns a list of www subdomains that we want to provide default redirects
|
||||
# for, i.e. any www's that aren't domains the user has actually configured
|
||||
# to serve for real. Which would be unusual.
|
||||
web_domains = set(get_web_domains(env))
|
||||
www_domains = set('www.' + zone for zone, zonefile in get_dns_zones(env))
|
||||
return sort_domains(www_domains - web_domains - get_domains_with_a_records(env), env)
|
||||
|
||||
def do_web_update(env):
|
||||
# Build an nginx configuration file.
|
||||
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
|
||||
|
||||
# Add configuration for each web domain.
|
||||
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
|
||||
# Load the templates.
|
||||
template0 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx.conf")).read()
|
||||
template1 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-alldomains.conf")).read()
|
||||
template2 = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-primaryonly.conf")).read()
|
||||
template3 = "\trewrite / https://$REDIRECT_DOMAIN permanent;\n"
|
||||
|
||||
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
|
||||
nginx_conf += make_domain_config(env['PRIMARY_HOSTNAME'], [template0, template1, template2], env)
|
||||
|
||||
# Add configuration all other web domains.
|
||||
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
|
||||
for domain in get_web_domains(env):
|
||||
nginx_conf += make_domain_config(domain, template1, template2, env)
|
||||
if domain == env['PRIMARY_HOSTNAME']: continue # handled above
|
||||
if domain not in has_root_proxy_or_redirect:
|
||||
nginx_conf += make_domain_config(domain, [template0, template1], env)
|
||||
else:
|
||||
nginx_conf += make_domain_config(domain, [template0], env)
|
||||
|
||||
# Add default www redirects.
|
||||
for domain in get_default_www_redirects(env):
|
||||
nginx_conf += make_domain_config(domain, [template0, template3], env)
|
||||
|
||||
# Did the file change? If not, don't bother writing & restarting nginx.
|
||||
nginx_conf_fn = "/etc/nginx/conf.d/local.conf"
|
||||
|
@ -64,11 +104,10 @@ def do_web_update(env):
|
|||
|
||||
return "web updated\n"
|
||||
|
||||
def make_domain_config(domain, template, template_for_primaryhost, env):
|
||||
# How will we configure this domain.
|
||||
def make_domain_config(domain, templates, env):
|
||||
# GET SOME VARIABLES
|
||||
|
||||
# Where will its root directory be for static files?
|
||||
|
||||
root = get_web_root(domain, env)
|
||||
|
||||
# What private key and SSL certificate will we use for this domain?
|
||||
|
@ -78,18 +117,9 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
|
|||
# available. Make a self-signed one now if one doesn't exist.
|
||||
ensure_ssl_certificate_exists(domain, ssl_key, ssl_certificate, env)
|
||||
|
||||
# Put pieces together.
|
||||
nginx_conf_parts = re.split("\s*# ADDITIONAL DIRECTIVES HERE\s*", template)
|
||||
nginx_conf = nginx_conf_parts[0] + "\n"
|
||||
if domain == env['PRIMARY_HOSTNAME']:
|
||||
nginx_conf += template_for_primaryhost + "\n"
|
||||
# ADDITIONAL DIRECTIVES.
|
||||
|
||||
# Replace substitution strings in the template & return.
|
||||
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
|
||||
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)
|
||||
nginx_conf_extra = ""
|
||||
|
||||
# Because the certificate may change, we should recognize this so we
|
||||
# can trigger an nginx update.
|
||||
|
@ -102,7 +132,7 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
|
|||
finally:
|
||||
f.close()
|
||||
return sha1.hexdigest()
|
||||
nginx_conf += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
|
||||
nginx_conf_extra += "# ssl files sha1: %s / %s\n" % (hashfile(ssl_key), hashfile(ssl_certificate))
|
||||
|
||||
# Add in any user customizations in YAML format.
|
||||
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
||||
|
@ -111,17 +141,29 @@ def make_domain_config(domain, template, template_for_primaryhost, env):
|
|||
if domain in yaml:
|
||||
yaml = yaml[domain]
|
||||
for path, url in yaml.get("proxies", {}).items():
|
||||
nginx_conf += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
|
||||
nginx_conf_extra += "\tlocation %s {\n\t\tproxy_pass %s;\n\t}\n" % (path, url)
|
||||
for path, url in yaml.get("redirects", {}).items():
|
||||
nginx_conf += "\trewrite %s %s permanent;\n" % (path, url)
|
||||
nginx_conf_extra += "\trewrite %s %s permanent;\n" % (path, url)
|
||||
|
||||
# Add in any user customizations in the includes/ folder.
|
||||
nginx_conf_custom_include = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(domain) + ".conf")
|
||||
if os.path.exists(nginx_conf_custom_include):
|
||||
nginx_conf += "\tinclude %s;\n" % (nginx_conf_custom_include)
|
||||
nginx_conf_extra += "\tinclude %s;\n" % (nginx_conf_custom_include)
|
||||
# PUT IT ALL TOGETHER
|
||||
|
||||
# Ending.
|
||||
nginx_conf += nginx_conf_parts[1]
|
||||
# Combine the pieces. Iteratively place each template into the "# ADDITIONAL DIRECTIVES HERE" placeholder
|
||||
# of the previous template.
|
||||
nginx_conf = "# ADDITIONAL DIRECTIVES HERE\n"
|
||||
for t in templates + [nginx_conf_extra]:
|
||||
nginx_conf = re.sub("[ \t]*# ADDITIONAL DIRECTIVES HERE *\n", t, nginx_conf)
|
||||
|
||||
# Replace substitution strings in the template & return.
|
||||
nginx_conf = nginx_conf.replace("$STORAGE_ROOT", env['STORAGE_ROOT'])
|
||||
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)
|
||||
nginx_conf = nginx_conf.replace("$REDIRECT_DOMAIN", re.sub(r"^www\.", "", domain)) # for default www redirects to parent domain
|
||||
|
||||
return nginx_conf
|
||||
|
||||
|
@ -255,14 +297,7 @@ def install_cert(domain, ssl_cert, ssl_chain, env):
|
|||
return "\n".join(ret)
|
||||
|
||||
def get_web_domains_info(env):
|
||||
# load custom settings so we can tell what domains have a redirect or proxy set up on '/',
|
||||
# which means static hosting is not happening
|
||||
custom_settings = { }
|
||||
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
||||
if os.path.exists(nginx_conf_custom_fn):
|
||||
custom_settings = rtyaml.load(open(nginx_conf_custom_fn))
|
||||
def has_root_proxy_or_redirect(domain):
|
||||
return custom_settings.get(domain, {}).get('redirects', {}).get('/') or custom_settings.get(domain, {}).get('proxies', {}).get('/')
|
||||
has_root_proxy_or_redirect = get_web_domains_with_root_overrides(env)
|
||||
|
||||
# for the SSL config panel, get cert status
|
||||
def check_cert(domain):
|
||||
|
@ -288,7 +323,15 @@ def get_web_domains_info(env):
|
|||
"root": get_web_root(domain, env),
|
||||
"custom_root": get_web_root(domain, env, test_exists=False),
|
||||
"ssl_certificate": check_cert(domain),
|
||||
"static_enabled": not has_root_proxy_or_redirect(domain),
|
||||
"static_enabled": domain not in has_root_proxy_or_redirect,
|
||||
}
|
||||
for domain in get_web_domains(env)
|
||||
] + \
|
||||
[
|
||||
{
|
||||
"domain": domain,
|
||||
"ssl_certificate": check_cert(domain),
|
||||
"static_enabled": False,
|
||||
}
|
||||
for domain in get_default_www_redirects(env)
|
||||
]
|
||||
|
|
|
@ -56,6 +56,8 @@ The cipher and protocol selection are chosen to support the following clients:
|
|||
|
||||
The passwords for mail users are stored on disk using the [SHA512-CRYPT](http://man7.org/linux/man-pages/man3/crypt.3.html) hashing scheme. ([source](management/mailconfig.py))
|
||||
|
||||
When using the web-based administrative control panel, after logging in an API key is placed in the browser's local storage (rather than, say, the user's actual password). The API key is an HMAC based on the user's email address and current password, and it is keyed by a secret known only to the control panel service. By resetting an administrator's password, any HMACs previously generated for that user will expire.
|
||||
|
||||
### Console access
|
||||
|
||||
Console access (e.g. via SSH) is configured by the system image used to create the box, typically from by a cloud virtual machine provider (e.g. Digital Ocean). Mail-in-a-Box does not set any console access settings, although it will warn the administrator in the System Status Checks if password-based login is turned on.
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
# Munin: resource monitoring tool
|
||||
#################################################
|
||||
|
||||
source setup/functions.sh # load our functions
|
||||
source /etc/mailinabox.conf # load global vars
|
||||
|
||||
# install Munin
|
||||
apt_install munin munin-node
|
||||
|
||||
# edit config
|
||||
cat > /etc/munin/munin.conf <<EOF;
|
||||
dbdir /var/lib/munin
|
||||
htmldir /var/cache/munin/www
|
||||
logdir /var/log/munin
|
||||
rundir /var/run/munin
|
||||
tmpldir /etc/munin/templates
|
||||
|
||||
includedir /etc/munin/munin-conf.d
|
||||
|
||||
# a simple host tree
|
||||
[$PRIMARY_HOSTNAME]
|
||||
address 127.0.0.1
|
||||
|
||||
# send alerts to the following address
|
||||
contacts admin
|
||||
contact.admin.command mail -s "Munin notification ${var:host}" administrator@$PRIMARY_HOSTNAME
|
||||
contact.admin.always_send warning critical
|
||||
EOF
|
||||
|
||||
# generate initial statistics so the directory isn't empty
|
||||
sudo -u munin munin-cron
|
|
@ -145,6 +145,7 @@ source setup/webmail.sh
|
|||
source setup/owncloud.sh
|
||||
source setup/zpush.sh
|
||||
source setup/management.sh
|
||||
source setup/munin.sh
|
||||
|
||||
# Ping the management daemon to write the DNS and nginx configuration files.
|
||||
while [ ! -f /var/lib/mailinabox/api.key ]; do
|
||||
|
|
Loading…
Reference in New Issue