Beispiel #1
0
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)
Beispiel #2
0
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)
Beispiel #3
0
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 != "")
Beispiel #4
0
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 != "")
Beispiel #5
0
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)
Beispiel #6
0
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)
Beispiel #7
0
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 != "")
Beispiel #8
0
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)
Beispiel #9
0
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)
Beispiel #10
0
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)
Beispiel #11
0
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)
Beispiel #12
0
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 != "")
Beispiel #13
0
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)
Beispiel #14
0
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 != "")
Beispiel #15
0
def dns_update():
    from dns_update import do_dns_update
    return do_dns_update(env)
Beispiel #16
0
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)
Beispiel #17
0
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)