def dns_update(): from dns_update import do_dns_update try: return do_dns_update(env, force=request.form.get("force", "") == "1") except Exception as e: return (str(e), 500)
def install_cert(domain, ssl_cert, ssl_chain, env): # Write the combined cert+chain to a temporary path and validate that it is OK. # The certificate always goes above the chain. import tempfile fd, fn = tempfile.mkstemp('.pem') os.write(fd, (ssl_cert + '\n' + ssl_chain).encode("ascii")) os.close(fd) # Do validation on the certificate before installing it. ssl_private_key = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem')) cert_status, cert_status_details = check_certificate(domain, fn, ssl_private_key) if cert_status != "OK": if cert_status == "SELF-SIGNED": cert_status = "This is a self-signed certificate. I can't install that." os.unlink(fn) if cert_status_details is not None: cert_status += " " + cert_status_details return cert_status # Where to put it? # Make a unique path for the certificate. from cryptography.hazmat.primitives import hashes from binascii import hexlify cert = load_pem(load_cert_chain(fn)[0]) all_domains, cn = get_certificate_domains(cert) path = "%s-%s-%s.pem" % ( safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix ) ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', path)) # Install the certificate. os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True) shutil.move(fn, ssl_certificate) ret = ["OK"] # When updating the cert for PRIMARY_HOSTNAME, symlink it from the system # certificate path, which is hard-coded for various purposes, and then # update DNS (because of the DANE TLSA record), postfix, and dovecot, # which all use the file. if domain == env['PRIMARY_HOSTNAME']: # Update symlink. system_ssl_certificate = os.path.join(os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) os.unlink(system_ssl_certificate) os.symlink(ssl_certificate, system_ssl_certificate) # Update DNS & restart postfix and dovecot so they pick up the new file. from dns_update import do_dns_update ret.append( do_dns_update(env) ) shell('check_call', ["/usr/sbin/service", "postfix", "restart"]) shell('check_call', ["/usr/sbin/service", "dovecot", "restart"]) ret.append("mail services restarted") # Update the web configuration so nginx picks up the new certificate file. from web_update import do_web_update ret.append( do_web_update(env) ) return "\n".join(ret)
def kick(env, mail_result=None): results = [] # Include the current operation's result in output. if mail_result is not None: results.append(mail_result + "\n") # Ensure every required alias exists. existing_users = get_mail_users(env) existing_aliases = get_mail_aliases(env) required_aliases = get_required_aliases(env) def ensure_admin_alias_exists(source): # If a user account exists with that address, we're good. if source in existing_users: return # Does this alias exists? for s, t in existing_aliases: if s == source: return # Doesn't exist. administrator = get_system_administrator(env) if source == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually 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) # Remove auto-generated postmaster/admin on domains we no # longer have any other email addresses for. for source, target in existing_aliases: user, domain = source.split("@") if ( user in ("postmaster", "admin") and source not in required_aliases and target == get_system_administrator(env) ): remove_mail_alias(source, env, do_kick=False) results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (source, target)) # Update DNS and nginx in case any domains are added/removed. from dns_update import do_dns_update results.append(do_dns_update(env)) from web_update import do_web_update results.append(do_web_update(env)) return "".join(s for s in results if s != "")
def kick(env, mail_result): # Update DNS and nginx in case any domains are added/removed. from dns_update import do_dns_update from web_update import do_web_update results = [ do_dns_update(env), mail_result + "\n", do_web_update(env), ] return "".join(s for s in results if s != "")
def remove_mail_user(email, env): conn, c = open_database(env, with_connection=True) c.execute("DELETE FROM users WHERE email=?", (email,)) if c.rowcount != 1: return ("That's not a user (%s)." % email, 400) conn.commit() # Update DNS in case any domains are removed. from dns_update import do_dns_update return do_dns_update(env)
def remove_mail_alias(source, env): conn, c = open_database(env, with_connection=True) c.execute("DELETE FROM aliases WHERE source=?", (source,)) if c.rowcount != 1: return ("That's not an alias (%s)." % source, 400) conn.commit() # Update DNS in case any domains are removed. from dns_update import do_dns_update return do_dns_update(env)
def kick(env, mail_result=None): results = [] # Include the current operation's result in output. if mail_result is not None: results.append(mail_result + "\n") # Ensure every required alias exists. existing_users = get_mail_users(env) existing_alias_records = get_mail_aliases(env) existing_aliases = set(a for a, *_ in existing_alias_records) # just first entry in tuple required_aliases = get_required_aliases(env) def ensure_admin_alias_exists(address): # If a user account exists with that address, we're good. if address in existing_users: return # If the alias already exists, we're good. if address in existing_aliases: return # Doesn't exist. administrator = get_system_administrator(env) if address == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually add_mail_alias(address, administrator, "", env, do_kick=False) if administrator not in existing_aliases: return # don't report the alias in output if the administrator alias isn't in yet -- this is a hack to supress confusing output on initial setup results.append("added alias %s (=> %s)\n" % (address, administrator)) for address in required_aliases: ensure_admin_alias_exists(address) # Remove auto-generated postmaster/admin on domains we no # longer have any other email addresses for. for address, forwards_to, *_ in existing_alias_records: user, domain = address.split("@") if user in ("postmaster", "admin", "abuse") \ and address not in required_aliases \ and forwards_to == get_system_administrator(env): remove_mail_alias(address, env, do_kick=False) results.append("removed alias %s (was to %s; domain no longer used for email)\n" % (address, forwards_to)) # Update DNS and nginx in case any domains are added/removed. from dns_update import do_dns_update results.append( do_dns_update(env) ) from web_update import do_web_update results.append( do_web_update(env) ) return "".join(s for s in results if s != "")
def dns_set_record(qname, rtype="A"): from dns_update import do_dns_update, set_custom_dns_record try: # Normalize. rtype = rtype.upper() # Read the record value from the request BODY, which must be # ASCII-only. Not used with GET. value = request.stream.read().decode("ascii", "ignore").strip() if request.method == "GET": # Get the existing records matching the qname and rtype. return dns_get_records(qname, rtype) elif request.method in ("POST", "PUT"): # There is a default value for A/AAAA records. if rtype in ("A", "AAAA") and value == "": value = request.environ.get( "HTTP_X_FORWARDED_FOR" ) # normally REMOTE_ADDR but we're behind nginx as a reverse proxy # Cannot add empty records. if value == "": return ("No value for the record provided.", 400) if request.method == "POST": # Add a new record (in addition to any existing records # for this qname-rtype pair). action = "add" elif request.method == "PUT": # In REST, PUT is supposed to be idempotent, so we'll # make this action set (replace all records for this # qname-rtype pair) rather than add (add a new record). action = "set" elif request.method == "DELETE": if value == "": # Delete all records for this qname-type pair. value = None else: # Delete just the qname-rtype-value record exactly. pass action = "remove" if set_custom_dns_record(qname, rtype, value, action, env): return do_dns_update(env) or "Something isn't right." return "OK" except ValueError as e: return (str(e), 400)
def add_mail_alias(source, destination, env): if not validate_email(source, False): return ("Invalid email address.", 400) conn, c = open_database(env, with_connection=True) try: c.execute("INSERT INTO aliases (source, destination) VALUES (?, ?)", (source, destination)) except sqlite3.IntegrityError: return ("Alias already exists (%s)." % source, 400) conn.commit() # Update DNS in case any new domains are added. from dns_update import do_dns_update return do_dns_update(env)
def dns_set_record(qname, rtype="A", value=None): from dns_update import do_dns_update, set_custom_dns_record try: # Get the value from the URL, then the POST parameters, or if it is not set then # use the remote IP address of the request --- makes dynamic DNS easy. To clear a # value, '' must be explicitly passed. if value is None: value = request.form.get("value") if value is None: value = request.environ.get( "HTTP_X_FORWARDED_FOR" ) # normally REMOTE_ADDR but we're behind nginx as a reverse proxy if value == '' or value == '__delete__': # request deletion value = None if set_custom_dns_record(qname, rtype, value, env): return do_dns_update(env) return "OK" except ValueError as e: return (str(e), 400)
def dns_update(): from dns_update import do_dns_update try: return do_dns_update(env, force=request.form.get('force', '') == '1') except Exception as e: return (str(e), 500)
def kick(env, mail_result=None): results = [] # Include the current operation's result in output. if mail_result is not None: results.append(mail_result + "\n") # Ensure every required alias exists. existing_users = get_mail_users(env, as_map=True) existing_aliases = get_mail_aliases(env, as_map=True) required_aliases = get_required_aliases(env) def ensure_admin_alias_exists(address): # If a user account exists with that address, we're good. if address in existing_users: return # If the alias already exists, we're good. if address in existing_aliases: return # Doesn't exist. administrator = get_system_administrator(env) if address == administrator: return # don't make an alias from the administrator to itself --- this alias must be created manually add_mail_alias(address, "Required alias", administrator, "", env, do_kick=False) if administrator not in existing_aliases: return # don't report the alias in output if the administrator alias isn't in yet -- this is a hack to supress confusing output on initial setup results.append("added alias %s (=> %s)\n" % (address, administrator)) for address in required_aliases: ensure_admin_alias_exists(address) # Remove auto-generated postmaster/admin on domains we no # longer have any other email addresses for. for address in existing_aliases: user, domain = address.split("@") forwards_to = ",".join(existing_aliases[address]["forward_tos"]) if user in ("postmaster", "admin", "abuse") \ and address not in required_aliases \ and forwards_to == get_system_administrator(env): remove_mail_alias(address, env, do_kick=False) results.append( "removed alias %s (was to %s; domain no longer used for email)\n" % (address, forwards_to)) # Update DNS and nginx in case any domains are added/removed. from dns_update import do_dns_update results.append(do_dns_update(env)) from web_update import do_web_update results.append(do_web_update(env)) return "".join(s for s in results if s != "")
def dns_set_record(qname, rtype="A"): from dns_update import do_dns_update, set_custom_dns_record try: # Normalize. rtype = rtype.upper() # Read the record value from the request BODY, which must be # ASCII-only. Not used with GET. rec = request.form value = "" ttl = None if isinstance(rec, dict): value = request.form.get("value", "") ttl = request.form.get("ttl", None) else: value = request.stream.read().decode("ascii", "ignore").strip() if ttl is not None: try: ttl = int(ttl) except Exception: ttl = None if request.method == "GET": # Get the existing records matching the qname and rtype. return dns_get_records(qname, rtype) elif request.method in ("POST", "PUT"): # There is a default value for A/AAAA records. if rtype in ("A", "AAAA") and value == "": value = request.environ.get( "HTTP_X_FORWARDED_FOR" ) # normally REMOTE_ADDR but we're behind nginx as a reverse proxy # Cannot add empty records. if value == '': return ("No value for the record provided.", 400) if request.method == "POST": # Add a new record (in addition to any existing records # for this qname-rtype pair). action = "add" elif request.method == "PUT": # In REST, PUT is supposed to be idempotent, so we'll # make this action set (replace all records for this # qname-rtype pair) rather than add (add a new record). action = "set" elif request.method == "DELETE": if value == '': # Delete all records for this qname-type pair. value = None else: # Delete just the qname-rtype-value record exactly. pass action = "remove" if set_custom_dns_record(qname, rtype, value, action, env, ttl=ttl): return do_dns_update(env) or "Something isn't right." return "OK" except ValueError as e: return (str(e), 400)
def kick(env, mail_result=None): results = [] # Inclde the current operation's result in output. if mail_result is not None: results.append(mail_result + "\n") # Create hostmaster@ for the primary domain if it does not already exist. # Default the target to administrator@ which the user is responsible for # setting and keeping up to date. existing_aliases = get_mail_aliases(env) administrator = "administrator@" + env['PRIMARY_HOSTNAME'] def ensure_admin_alias_exists(source): # Does this alias exists? for s, t in existing_aliases: if s == source: return # Doesn't exist. add_mail_alias(source, administrator, env, do_kick=False) results.append("added alias %s (=> %s)\n" % (source, administrator)) ensure_admin_alias_exists("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. 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] != administrator \ ) # Create postmaster@ and admin@ for all domains we serve mail on. # postmaster@ is assumed to exist by our Postfix configuration. admin@ # isn't anything, but it might save the user some trouble e.g. when # buying an SSL certificate. for domain in real_mail_domains: ensure_admin_alias_exists("postmaster@" + domain) ensure_admin_alias_exists("admin@" + domain) # Remove auto-generated hostmaster/postmaster/admin on domains we no # longer have any other email addresses for. for source, target in existing_aliases: user, domain = source.split("@") if user in ("postmaster", "admin") and domain not in real_mail_domains \ and target == administrator: remove_mail_alias(source, env, do_kick=False) results.append( "removed alias %s (was to %s; domain no longer used for email)\n" % (source, target)) # Update DNS and nginx in case any domains are added/removed. from dns_update import do_dns_update results.append(do_dns_update(env)) from web_update import do_web_update results.append(do_web_update(env)) return "".join(s for s in results if s != "")
def dns_update(): from dns_update import do_dns_update return do_dns_update(env)
def smtp_relay_set(): from editconf import edit_conf from os import chmod import re, socket, ssl config = utils.load_settings(env) newconf = request.form # Is DKIM configured? sel = newconf.get("dkim_selector") if sel is None or sel.strip() == "": config["SMTP_RELAY_DKIM_SELECTOR"] = None config["SMTP_RELAY_DKIM_RR"] = None elif re.fullmatch(r"[a-z\d\._]+", sel.strip()) is None: return ("The DKIM selector is invalid!", 400) elif sel.strip() == config.get("local_dkim_selector", "mail"): return ( f"The DKIM selector {sel.strip()} is already in use by the box!", 400) else: # DKIM selector looks good, try processing the RR rr = newconf.get("dkim_rr", "") if rr.strip() == "": return ("Cannot publish a selector with an empty key!", 400) components = {} for r in re.split(r"[;\s]+", rr): sp = re.split(r"\=", r) if len(sp) != 2: return ("DKIM public key RR is malformed!", 400) components[sp[0]] = sp[1] if not components.get("p"): return ("The DKIM public key doesn't exist!", 400) config["SMTP_RELAY_DKIM_SELECTOR"] = sel config["SMTP_RELAY_DKIM_RR"] = components relay_on = False implicit_tls = False if newconf.get("enabled") == "true": relay_on = True # Try negotiating TLS directly. We need to know this because we need to configure Postfix # to be aware of this detail. try: ctx = ssl.create_default_context() with socket.create_connection( (newconf.get("host"), int(newconf.get("port"))), 5) as sock: with ctx.wrap_socket(sock, server_hostname=newconf.get("host")): implicit_tls = True except ssl.SSLError as sle: # Couldn't connect via TLS, configure Postfix to send via STARTTLS print(sle.reason) except (socket.herror, socket.gaierror) as he: return ( f"Unable to resolve hostname (it probably is incorrect): {he.strerror}", 400) except socket.timeout: return ( "We couldn't connect to the server. Is it down or did you write the wrong port number?", 400) pw_file = "/etc/postfix/sasl_passwd" modify_password = True # Check that if the provided password is empty, that there was a password saved before if (newconf.get("key", "") == ""): if os.path.isfile(pw_file): modify_password = False else: return ( "Please provide a password/key (there is no existing password to retain).", 400) try: # Write on daemon settings config["SMTP_RELAY_ENABLED"] = relay_on config["SMTP_RELAY_HOST"] = newconf.get("host") config["SMTP_RELAY_PORT"] = int(newconf.get("port")) config["SMTP_RELAY_USER"] = newconf.get("user") config["SMTP_RELAY_AUTHORIZED_SERVERS"] = [ s.strip() for s in re.split(r"[, ]+", newconf.get("authorized_servers", []) or "") if s.strip() != "" ] utils.write_settings(config, env) # Write on Postfix configs edit_conf("/etc/postfix/main.cf", [ "relayhost=" + (f"[{config['SMTP_RELAY_HOST']}]:{config['SMTP_RELAY_PORT']}" if config["SMTP_RELAY_ENABLED"] else ""), f"smtp_tls_wrappermode={'yes' if implicit_tls else 'no'}" ], delimiter_re=r"\s*=\s*", delimiter="=", comment_char="#") # Edit the sasl password (still will edit the file, but keep the pw) with open(pw_file, "a+") as f: f.seek(0) pwm = re.match(r"\[.+\]\:[0-9]+\s.+\:(.*)", f.readline()) if (pwm is None or len(pwm.groups()) != 1) and not modify_password: # Well if this isn't a bruh moment return ( "Please provide a password/key (there is no existing password to retain).", 400) f.truncate(0) f.write( f"[{config['SMTP_RELAY_HOST']}]:{config['SMTP_RELAY_PORT']} {config['SMTP_RELAY_USER']}:{newconf.get('key') if modify_password else pwm[1]}\n" ) chmod(pw_file, 0o600) utils.shell("check_output", ["/usr/sbin/postmap", pw_file], capture_stderr=True) # Regenerate DNS (to apply whatever changes need to be made) from dns_update import do_dns_update do_dns_update(env) # Restart Postfix return utils.shell("check_output", ["/usr/sbin/postfix", "reload"], capture_stderr=True) except Exception as e: return (str(e), 400)
def install_cert(domain, ssl_cert, ssl_chain, env): if domain not in get_web_domains(env) + get_default_www_redirects(env): return "Invalid domain name." # Write the combined cert+chain to a temporary path and validate that it is OK. # The certificate always goes above the chain. import tempfile, os fd, fn = tempfile.mkstemp('.pem') os.write(fd, (ssl_cert + '\n' + ssl_chain).encode("ascii")) os.close(fd) # Do validation on the certificate before installing it. from status_checks import check_certificate ssl_private_key = os.path.join( os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_private_key.pem')) cert_status, cert_status_details = check_certificate( domain, fn, ssl_private_key) if cert_status != "OK": if cert_status == "SELF-SIGNED": cert_status = "This is a self-signed certificate. I can't install that." os.unlink(fn) if cert_status_details is not None: cert_status += " " + cert_status_details return cert_status # Where to put it? if domain == env['PRIMARY_HOSTNAME']: ssl_certificate = os.path.join( os.path.join(env["STORAGE_ROOT"], 'ssl', 'ssl_certificate.pem')) else: # Make a unique path for the certificate. from status_checks import load_cert_chain, load_pem, get_certificate_domains from cryptography.hazmat.primitives import hashes from binascii import hexlify cert = load_pem(load_cert_chain(fn)[0]) all_domains, cn = get_certificate_domains(cert) path = "%s-%s-%s" % ( cn, # common name cert.not_valid_after.date().isoformat().replace( "-", ""), # expiration date hexlify(cert.fingerprint( hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix ) ssl_certificate = os.path.join( os.path.join(env["STORAGE_ROOT"], 'ssl', path, 'ssl_certificate.pem')) # Install the certificate. os.makedirs(os.path.dirname(ssl_certificate), exist_ok=True) shutil.move(fn, ssl_certificate) ret = ["OK"] # When updating the cert for PRIMARY_HOSTNAME, also update DNS because it is # used in the DANE TLSA record and restart postfix and dovecot which use # that certificate. if domain == env['PRIMARY_HOSTNAME']: ret.append(do_dns_update(env)) shell('check_call', ["/usr/sbin/service", "postfix", "restart"]) shell('check_call', ["/usr/sbin/service", "dovecot", "restart"]) ret.append("mail services restarted") # Kick nginx so it sees the cert. ret.append(do_web_update(env)) return "\n".join(ret)