mirror of
https://github.com/mail-in-a-box/mailinabox.git
synced 2026-03-12 17:07:23 +01:00
Merge branch 'main' of https://github.com/mail-in-a-box/mailinabox
The roundcube password plugin is not disabled. # Conflicts: # management/utils.py # setup/start.sh # setup/system.sh # setup/webmail.sh # tools/editconf.py
This commit is contained in:
@@ -223,9 +223,18 @@ def get_duplicity_additional_args(env):
|
||||
config = get_backup_config(env)
|
||||
|
||||
if get_target_type(config) == 'rsync':
|
||||
# Extract a port number for the ssh transport. Duplicity accepts the
|
||||
# optional port number syntax in the target, but it doesn't appear to act
|
||||
# on it, so we set the ssh port explicitly via the duplicity options.
|
||||
from urllib.parse import urlsplit
|
||||
try:
|
||||
port = urlsplit(config["target"]).port
|
||||
except ValueError:
|
||||
port = 22
|
||||
|
||||
return [
|
||||
"--ssh-options= -i /root/.ssh/id_rsa_miab",
|
||||
"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p 22 -i /root/.ssh/id_rsa_miab\"",
|
||||
f"--ssh-options= -i /root/.ssh/id_rsa_miab -p {port}",
|
||||
f"--rsync-options= -e \"/usr/bin/ssh -oStrictHostKeyChecking=no -oBatchMode=yes -p {port} -i /root/.ssh/id_rsa_miab\"",
|
||||
]
|
||||
elif get_target_type(config) == 's3':
|
||||
# See note about hostname in get_duplicity_target_url.
|
||||
@@ -438,6 +447,14 @@ def list_target_files(config):
|
||||
rsync_fn_size_re = re.compile(r'.* ([^ ]*) [^ ]* [^ ]* (.*)')
|
||||
rsync_target = '{host}:{path}'
|
||||
|
||||
# Strip off any trailing port specifier because it's not valid in rsync's
|
||||
# DEST syntax. Explicitly set the port number for the ssh transport.
|
||||
user_host, *_ = target.netloc.rsplit(':', 1)
|
||||
try:
|
||||
port = target.port
|
||||
except ValueError:
|
||||
port = 22
|
||||
|
||||
target_path = target.path
|
||||
if not target_path.endswith('/'):
|
||||
target_path = target_path + '/'
|
||||
@@ -446,11 +463,11 @@ def list_target_files(config):
|
||||
|
||||
rsync_command = [ 'rsync',
|
||||
'-e',
|
||||
'/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes',
|
||||
f'/usr/bin/ssh -i /root/.ssh/id_rsa_miab -oStrictHostKeyChecking=no -oBatchMode=yes -p {port}',
|
||||
'--list-only',
|
||||
'-r',
|
||||
rsync_target.format(
|
||||
host=target.netloc,
|
||||
host=user_host,
|
||||
path=target_path)
|
||||
]
|
||||
|
||||
@@ -561,7 +578,8 @@ def get_backup_config(env, for_save=False, for_ui=False):
|
||||
|
||||
# Merge in anything written to custom.yaml.
|
||||
try:
|
||||
custom_config = rtyaml.load(open(os.path.join(backup_root, 'custom.yaml')))
|
||||
with open(os.path.join(backup_root, 'custom.yaml'), 'r') as f:
|
||||
custom_config = rtyaml.load(f)
|
||||
if not isinstance(custom_config, dict): raise ValueError() # caught below
|
||||
config.update(custom_config)
|
||||
except:
|
||||
@@ -586,7 +604,8 @@ def get_backup_config(env, for_save=False, for_ui=False):
|
||||
config["target"] = "file://" + config["file_target_directory"]
|
||||
ssh_pub_key = os.path.join('/root', '.ssh', 'id_rsa_miab.pub')
|
||||
if os.path.exists(ssh_pub_key):
|
||||
config["ssh_pub_key"] = open(ssh_pub_key, 'r').read()
|
||||
with open(ssh_pub_key, 'r') as f:
|
||||
config["ssh_pub_key"] = f.read()
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ def read_password():
|
||||
return first
|
||||
|
||||
def setup_key_auth(mgmt_uri):
|
||||
key = open('/var/lib/mailinabox/api.key').read().strip()
|
||||
with open('/var/lib/mailinabox/api.key', 'r') as f:
|
||||
key = f.read().strip()
|
||||
|
||||
auth_handler = urllib.request.HTTPBasicAuthHandler()
|
||||
auth_handler.add_password(
|
||||
|
||||
@@ -831,7 +831,8 @@ def write_opendkim_tables(domains, env):
|
||||
|
||||
def get_custom_dns_config(env, only_real_records=False):
|
||||
try:
|
||||
custom_dns = rtyaml.load(open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml')))
|
||||
with open(os.path.join(env['STORAGE_ROOT'], 'dns/custom.yaml'), 'r') as f:
|
||||
custom_dns = rtyaml.load(f)
|
||||
if not isinstance(custom_dns, dict): raise ValueError() # caught below
|
||||
except:
|
||||
return [ ]
|
||||
@@ -1008,6 +1009,7 @@ def set_custom_dns_record(qname, rtype, value, action, env):
|
||||
def get_secondary_dns(custom_dns, mode=None):
|
||||
resolver = dns.resolver.get_default_resolver()
|
||||
resolver.timeout = 10
|
||||
resolver.lifetime = 10
|
||||
|
||||
values = []
|
||||
for qname, rtype, value in custom_dns:
|
||||
@@ -1025,10 +1027,17 @@ def get_secondary_dns(custom_dns, mode=None):
|
||||
# doesn't.
|
||||
if not hostname.startswith("xfr:"):
|
||||
if mode == "xfr":
|
||||
response = dns.resolver.resolve(hostname+'.', "A", raise_on_no_answer=False)
|
||||
values.extend(map(str, response))
|
||||
response = dns.resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False)
|
||||
values.extend(map(str, response))
|
||||
try:
|
||||
response = resolver.resolve(hostname+'.', "A", raise_on_no_answer=False)
|
||||
values.extend(map(str, response))
|
||||
except dns.exception.DNSException:
|
||||
pass
|
||||
|
||||
try:
|
||||
response = resolver.resolve(hostname+'.', "AAAA", raise_on_no_answer=False)
|
||||
values.extend(map(str, response))
|
||||
except dns.exception.DNSException:
|
||||
pass
|
||||
continue
|
||||
values.append(hostname)
|
||||
|
||||
@@ -1046,15 +1055,17 @@ def set_secondary_dns(hostnames, env):
|
||||
# Validate that all hostnames are valid and that all zone-xfer IP addresses are valid.
|
||||
resolver = dns.resolver.get_default_resolver()
|
||||
resolver.timeout = 5
|
||||
resolver.lifetime = 5
|
||||
|
||||
for item in hostnames:
|
||||
if not item.startswith("xfr:"):
|
||||
# Resolve hostname.
|
||||
try:
|
||||
response = resolver.resolve(item, "A")
|
||||
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
|
||||
try:
|
||||
response = resolver.resolve(item, "AAAA")
|
||||
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
|
||||
except (dns.resolver.NoNameservers, dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.Timeout):
|
||||
raise ValueError("Could not resolve the IP address of %s." % item)
|
||||
else:
|
||||
# Validate IP address.
|
||||
@@ -1087,7 +1098,7 @@ def get_custom_dns_records(custom_dns, qname, rtype):
|
||||
def build_recommended_dns(env):
|
||||
ret = []
|
||||
for (domain, zonefile, records) in build_zones(env):
|
||||
# remove records that we don't dislay
|
||||
# remove records that we don't display
|
||||
records = [r for r in records if r[3] is not False]
|
||||
|
||||
# put Required at the top, then Recommended, then everythiing else
|
||||
|
||||
@@ -82,7 +82,8 @@ def scan_files(collector):
|
||||
continue
|
||||
elif fn[-3:] == '.gz':
|
||||
tmp_file = tempfile.NamedTemporaryFile()
|
||||
shutil.copyfileobj(gzip.open(fn), tmp_file)
|
||||
with gzip.open(fn, 'rb') as f:
|
||||
shutil.copyfileobj(f, tmp_file)
|
||||
|
||||
if VERBOSE:
|
||||
print("Processing file", fn, "...")
|
||||
|
||||
@@ -548,7 +548,8 @@ def check_certificate(domain, ssl_certificate, ssl_private_key, warn_if_expiring
|
||||
# Second, check that the certificate matches the private key.
|
||||
if ssl_private_key is not None:
|
||||
try:
|
||||
priv_key = load_pem(open(ssl_private_key, 'rb').read())
|
||||
with open(ssl_private_key, 'rb') as f:
|
||||
priv_key = load_pem(f.read())
|
||||
except ValueError as e:
|
||||
return ("The private key file %s is not a private key file: %s" % (ssl_private_key, str(e)), None)
|
||||
|
||||
|
||||
@@ -107,6 +107,12 @@ def run_services_checks(env, output, pool):
|
||||
fatal = fatal or fatal2
|
||||
output2.playback(output)
|
||||
|
||||
# Check fail2ban.
|
||||
code, ret = shell('check_output', ["fail2ban-client", "status"], capture_stderr=True, trap=True)
|
||||
if code != 0:
|
||||
output.print_error("fail2ban is not running.")
|
||||
all_running = False
|
||||
|
||||
if all_running:
|
||||
output.print_ok("All system services are running.")
|
||||
|
||||
@@ -219,7 +225,8 @@ def check_ssh_password(env, output):
|
||||
# the configuration file.
|
||||
if not os.path.exists("/etc/ssh/sshd_config"):
|
||||
return
|
||||
sshd = open("/etc/ssh/sshd_config").read()
|
||||
with open("/etc/ssh/sshd_config", "r") as f:
|
||||
sshd = f.read()
|
||||
if re.search("\nPasswordAuthentication\s+yes", sshd) \
|
||||
or not re.search("\nPasswordAuthentication\s+no", sshd):
|
||||
output.print_error("""The SSH server on this machine permits password-based login. A more secure
|
||||
@@ -320,6 +327,8 @@ def run_network_checks(env, output):
|
||||
output.print_ok("IP address is not blacklisted by zen.spamhaus.org.")
|
||||
elif zen == "[timeout]":
|
||||
output.print_warning("Connection to zen.spamhaus.org timed out. We could not determine whether your server's IP address is blacklisted. Please try again later.")
|
||||
elif zen == "[Not Set]":
|
||||
output.print_warning("Could not connect to zen.spamhaus.org. We could not determine whether your server's IP address is blacklisted. Please try again later.")
|
||||
else:
|
||||
output.print_error("""The IP address of this machine %s is listed in the Spamhaus Block List (code %s),
|
||||
which may prevent recipients from receiving your email. See http://www.spamhaus.org/query/ip/%s."""
|
||||
@@ -553,7 +562,7 @@ def check_dns_zone(domain, env, output, dns_zonefiles):
|
||||
for ns in custom_secondary_ns:
|
||||
# We must first resolve the nameserver to an IP address so we can query it.
|
||||
ns_ips = query_dns(ns, "A")
|
||||
if not ns_ips:
|
||||
if not ns_ips or ns_ips in {'[Not Set]', '[timeout]'}:
|
||||
output.print_error("Secondary nameserver %s is not valid (it doesn't resolve to an IP address)." % ns)
|
||||
continue
|
||||
# Choose the first IP if nameserver returns multiple
|
||||
@@ -604,7 +613,8 @@ def check_dnssec(domain, env, output, dns_zonefiles, is_checking_primary=False):
|
||||
# record that we suggest using is for the KSK (and that's how the DS records were generated).
|
||||
# We'll also give the nice name for the key algorithm.
|
||||
dnssec_keys = load_env_vars_from_file(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/%s.conf' % alg_name_map[ds_alg]))
|
||||
dnsssec_pubkey = open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key')).read().split("\t")[3].split(" ")[3]
|
||||
with open(os.path.join(env['STORAGE_ROOT'], 'dns/dnssec/' + dnssec_keys['KSK'] + '.key'), 'r') as f:
|
||||
dnsssec_pubkey = f.read().split("\t")[3].split(" ")[3]
|
||||
|
||||
expected_ds_records[ (ds_keytag, ds_alg, ds_digalg, ds_digest) ] = {
|
||||
"record": rr_ds,
|
||||
@@ -756,6 +766,8 @@ def check_mail_domain(domain, env, output):
|
||||
output.print_ok("Domain is not blacklisted by dbl.spamhaus.org.")
|
||||
elif dbl == "[timeout]":
|
||||
output.print_warning("Connection to dbl.spamhaus.org timed out. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain))
|
||||
elif dbl == "[Not Set]":
|
||||
output.print_warning("Could not connect to dbl.spamhaus.org. We could not determine whether the domain {} is blacklisted. Please try again later.".format(domain))
|
||||
else:
|
||||
output.print_error("""This domain is listed in the Spamhaus Domain Block List (code %s),
|
||||
which may prevent recipients from receiving your mail.
|
||||
@@ -800,12 +812,17 @@ def query_dns(qname, rtype, nxdomain='[Not Set]', at=None, as_list=False):
|
||||
# running bind server), or if the 'at' argument is specified, use that host
|
||||
# as the nameserver.
|
||||
resolver = dns.resolver.get_default_resolver()
|
||||
if at:
|
||||
|
||||
# Make sure at is not a string that cannot be used as a nameserver
|
||||
if at and at not in {'[Not set]', '[timeout]'}:
|
||||
resolver = dns.resolver.Resolver()
|
||||
resolver.nameservers = [at]
|
||||
|
||||
# Set a timeout so that a non-responsive server doesn't hold us back.
|
||||
resolver.timeout = 5
|
||||
# The number of seconds to spend trying to get an answer to the question. If the
|
||||
# lifetime expires a dns.exception.Timeout exception will be raised.
|
||||
resolver.lifetime = 5
|
||||
|
||||
# Do the query.
|
||||
try:
|
||||
@@ -959,7 +976,8 @@ def run_and_output_changes(env, pool):
|
||||
# Load previously saved status checks.
|
||||
cache_fn = "/var/cache/mailinabox/status_checks.json"
|
||||
if os.path.exists(cache_fn):
|
||||
prev = json.load(open(cache_fn))
|
||||
with open(cache_fn, 'r') as f:
|
||||
prev = json.load(f)
|
||||
|
||||
# execute hooks
|
||||
hook_data = {
|
||||
|
||||
@@ -72,11 +72,6 @@
|
||||
html {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}
|
||||
|
||||
/* Set explicit background color (necessary for Firefox) */
|
||||
html {
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
/* Override Boostrap theme here to give more contrast. The black turns to white by the filter. */
|
||||
.form-control {
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
<label for="backup-target-rsync-host" class="col-sm-2 control-label">Hostname</label>
|
||||
<div class="col-sm-8">
|
||||
<input type="text" placeholder="hostname.local" class="form-control" rows="1" id="backup-target-rsync-host">
|
||||
<div class="small" style="margin-top: 2px">
|
||||
The hostname at your rsync provider, e.g. <tt>da2327.rsync.net</tt>. Optionally includes a colon
|
||||
and the provider's non-standard ssh port number, e.g. <tt>u215843.your-storagebox.de:23</tt>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group backup-target-rsync">
|
||||
@@ -259,12 +263,11 @@ function show_custom_backup() {
|
||||
} else if (r.target == "off") {
|
||||
$("#backup-target-type").val("off");
|
||||
} else if (r.target.substring(0, 8) == "rsync://") {
|
||||
$("#backup-target-type").val("rsync");
|
||||
var path = r.target.substring(8).split('//');
|
||||
var host_parts = path.shift().split('@');
|
||||
$("#backup-target-rsync-user").val(host_parts[0]);
|
||||
$("#backup-target-rsync-host").val(host_parts[1]);
|
||||
$("#backup-target-rsync-path").val('/'+path[0]);
|
||||
const spec = url_split(r.target);
|
||||
$("#backup-target-type").val(spec.scheme);
|
||||
$("#backup-target-rsync-user").val(spec.user);
|
||||
$("#backup-target-rsync-host").val(spec.host);
|
||||
$("#backup-target-rsync-path").val(spec.path);
|
||||
} else if (r.target.substring(0, 5) == "s3://") {
|
||||
$("#backup-target-type").val("s3");
|
||||
var hostpath = r.target.substring(5).split('/');
|
||||
@@ -344,4 +347,31 @@ function init_inputs(target_type) {
|
||||
set_host($('#backup-target-s3-host-select').val());
|
||||
}
|
||||
}
|
||||
|
||||
// Return a two-element array of the substring preceding and the substring following
|
||||
// the first occurence of separator in string. Return [undefined, string] if the
|
||||
// separator does not appear in string.
|
||||
const split1_rest = (string, separator) => {
|
||||
const index = string.indexOf(separator);
|
||||
return (index >= 0) ? [string.substring(0, index), string.substring(index + separator.length)] : [undefined, string];
|
||||
};
|
||||
|
||||
// Note: The manifest JS URL class does not work in some security-conscious
|
||||
// settings, e.g. Brave browser, so we roll our own that handles only what we need.
|
||||
//
|
||||
// Use greedy separator parsing to get parts of a MIAB backup target url.
|
||||
// Note: path will not include a leading forward slash '/'
|
||||
const url_split = url => {
|
||||
const [ scheme, scheme_rest ] = split1_rest(url, '://');
|
||||
const [ user, user_rest ] = split1_rest(scheme_rest, '@');
|
||||
const [ host, path ] = split1_rest(user_rest, '/');
|
||||
|
||||
return {
|
||||
scheme,
|
||||
user,
|
||||
host,
|
||||
path,
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -36,8 +36,9 @@ def load_environment():
|
||||
def load_env_vars_from_file(fn, strip_quotes=False, merge_env=None):
|
||||
# Load settings from a KEY=VALUE file.
|
||||
env = Environment()
|
||||
for line in open(fn):
|
||||
env.setdefault(*line.strip().split("=", 1))
|
||||
with open(fn, 'r') as f:
|
||||
for line in f:
|
||||
env.setdefault(*line.strip().split("=", 1))
|
||||
if strip_quotes:
|
||||
for k in env: env[k]=('' if env[k] is None else env[k].strip('"'))
|
||||
if merge_env is not None:
|
||||
@@ -61,7 +62,8 @@ def load_settings(env):
|
||||
import rtyaml
|
||||
fn = os.path.join(env['STORAGE_ROOT'], 'settings.yaml')
|
||||
try:
|
||||
config = rtyaml.load(open(fn, "r"))
|
||||
with open(fn, "r") as f:
|
||||
config = rtyaml.load(f)
|
||||
if not isinstance(config, dict): raise ValueError() # caught below
|
||||
return config
|
||||
except:
|
||||
@@ -146,13 +148,16 @@ def shell(method, cmd_args, env={}, capture_stderr=False, return_bytes=False, tr
|
||||
if method == "check_output" and input is not None:
|
||||
kwargs['input'] = input
|
||||
|
||||
if not trap:
|
||||
try:
|
||||
ret = getattr(subprocess, method)(cmd_args, **kwargs)
|
||||
else:
|
||||
try:
|
||||
ret = getattr(subprocess, method)(cmd_args, **kwargs)
|
||||
code = 0
|
||||
except subprocess.CalledProcessError as e:
|
||||
code = 0
|
||||
except subprocess.CalledProcessError as e:
|
||||
if not trap:
|
||||
# Reformat exception.
|
||||
msg = "Command failed with exit code {}: {}".format(e.returncode, subprocess.list2cmdline(cmd_args))
|
||||
if e.output: msg += "\n\nOutput:\n" + e.output
|
||||
raise Exception(msg)
|
||||
else:
|
||||
ret = e.output
|
||||
code = e.returncode
|
||||
if not return_bytes and isinstance(ret, bytes): ret = ret.decode("utf8")
|
||||
|
||||
@@ -74,7 +74,8 @@ def get_web_domains_with_root_overrides(env):
|
||||
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))
|
||||
with open(nginx_conf_custom_fn, 'r') as f:
|
||||
custom_settings = rtyaml.load(f)
|
||||
for domain, settings in custom_settings.items():
|
||||
for type, value in [('redirect', settings.get('redirects', {}).get('/')),
|
||||
('proxy', settings.get('proxies', {}).get('/'))]:
|
||||
@@ -86,13 +87,18 @@ def do_web_update(env):
|
||||
# Pre-load what SSL certificates we will use for each domain.
|
||||
ssl_certificates = get_ssl_certificates(env)
|
||||
|
||||
# Helper for reading config files and templates
|
||||
def read_conf(conf_fn):
|
||||
with open(os.path.join(os.path.dirname(__file__), "../conf", conf_fn), "r") as f:
|
||||
return f.read()
|
||||
|
||||
# Build an nginx configuration file.
|
||||
nginx_conf = open(os.path.join(os.path.dirname(__file__), "../conf/nginx-top.conf")).read()
|
||||
nginx_conf = read_conf("nginx-top.conf")
|
||||
|
||||
# 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()
|
||||
template0 = read_conf("nginx.conf")
|
||||
template1 = read_conf("nginx-alldomains.conf")
|
||||
template2 = read_conf("nginx-primaryonly.conf")
|
||||
template3 = "\trewrite ^(.*) https://$REDIRECT_DOMAIN$1 permanent;\n"
|
||||
|
||||
# Add the PRIMARY_HOST configuration first so it becomes nginx's default server.
|
||||
@@ -160,11 +166,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
|
||||
def hashfile(filepath):
|
||||
import hashlib
|
||||
sha1 = hashlib.sha1()
|
||||
f = open(filepath, 'rb')
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
sha1.update(f.read())
|
||||
finally:
|
||||
f.close()
|
||||
return sha1.hexdigest()
|
||||
nginx_conf_extra += "\t# ssl files sha1: %s / %s\n" % (hashfile(tls_cert["private-key"]), hashfile(tls_cert["certificate"]))
|
||||
|
||||
@@ -172,7 +175,8 @@ def make_domain_config(domain, templates, ssl_certificates, env):
|
||||
hsts = "yes"
|
||||
nginx_conf_custom_fn = os.path.join(env["STORAGE_ROOT"], "www/custom.yaml")
|
||||
if os.path.exists(nginx_conf_custom_fn):
|
||||
yaml = rtyaml.load(open(nginx_conf_custom_fn))
|
||||
with open(nginx_conf_custom_fn, 'r') as f:
|
||||
yaml = rtyaml.load(f)
|
||||
if domain in yaml:
|
||||
yaml = yaml[domain]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user