# coding: utf-8

import click
import hashlib
import logging
import os
import random
import re
import signal
import string
import subprocess
import sys
from asn1crypto import pem, x509
from asn1crypto.csr import CertificationRequest
from asn1crypto.crl import CertificateList
from base64 import b64encode
from certbuilder import CertificateBuilder, pem_armor_certificate
from certidude import const
from csrbuilder import CSRBuilder, pem_armor_csr
from configparser import ConfigParser, NoOptionError
from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn, generate_serial
from datetime import datetime, timedelta
from glob import glob
from ipaddress import ip_network
from oscrypto import asymmetric

logger = logging.getLogger(__name__)

# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
# https://kjur.github.io/jsrsasign/
# keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_client_config.html
# strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA

NOW = datetime.utcnow()

def fqdn_required(func):
    def wrapped(**args):
        common_name = args.get("common_name")
        if "." in common_name:
            logger.info("Using fully qualified hostname %s" % common_name)
        else:
            raise ValueError("Fully qualified hostname not specified as common name, make sure hostname -f works")
        return func(**args)
    return wrapped

def setup_client(prefix="client_", dh=False):
    # Create section in /etc/certidude/client.conf
    def wrapper(func):
        def wrapped(**arguments):
            common_name = arguments.get("common_name")
            authority = arguments.get("authority")
            b = os.path.join("/etc/certidude/authority", authority)
            if dh:
                path = os.path.join("/etc/ssl/dhparam.pem")
                if not os.path.exists(path):
                    rpm("openssl")
                    apt("openssl")
                    cmd = "openssl", "dhparam", "-out", path, ("1024" if os.getenv("TRAVIS") else "2048")
                    subprocess.check_call(cmd)
                arguments["dhparam_path"] = path

            # Create corresponding section in Certidude client configuration file
            client_config = ConfigParser()
            if os.path.exists(const.CLIENT_CONFIG_PATH):
                client_config.readfp(open(const.CLIENT_CONFIG_PATH))
            if client_config.has_section(authority):
                click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
            else:
                client_config.add_section(authority)
                client_config.set(authority, "trigger", "interface up")
                client_config.set(authority, "common name", common_name)
                client_config.set(authority, "request path", os.path.join(b, prefix + "req.pem"))
                client_config.set(authority, "key path", os.path.join(b, prefix + "key.pem"))
                client_config.set(authority, "certificate path", os.path.join(b, prefix + "cert.pem"))
                client_config.set(authority, "authority path",  os.path.join(b, "ca_cert.pem"))
                client_config.set(authority, "revocations path",  os.path.join(b, "ca_crl.pem"))
                with open(const.CLIENT_CONFIG_PATH + ".part", 'w') as fh:
                    client_config.write(fh)
                os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
                click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))

            for j in ("key", "request", "certificate", "authority", "revocations"):
                arguments["%s_path" % j] = client_config.get(authority, "%s path" % j)

            return func(**arguments)
        return wrapped
    return wrapper


class ConfigTreeParser(ConfigParser):
    def __init__(self, path, *args, **kwargs):
        ConfigParser.__init__(self, *args, **kwargs)
        if os.path.exists(path):
            with open(path) as fh:
                click.echo("Parsing: %s" % fh.name)
                self.readfp(fh)
        if os.path.exists(path + ".d"):
            for filename in os.listdir(path + ".d"):
                if not filename.endswith(".conf"):
                    continue
                with open(os.path.join(path + ".d", filename)) as fh:
                    click.echo("Parsing: %s" % fh.name)
                    self.readfp(fh)


@click.command("enroll", help="Run processes for requesting certificates and configuring services")
@click.option("-k", "--kerberos", default=False, is_flag=True, help="Offer system keytab for auth")
@click.option("-r", "--renew", default=False, is_flag=True, help="Renew now")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
@click.option("-s", "--skip-self", default=False, is_flag=True, help="Skip self enroll")
@click.option("-nw", "--no-wait", default=False, is_flag=True, help="Return immideately if server doesn't autosign")
def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
    assert os.getuid() == 0 and os.getgid() == 0, "Can enroll only as root"

    if not skip_self and os.path.exists(const.SERVER_CONFIG_PATH):
        click.echo("Self-enrolling authority's web interface certificate")
        from certidude import authority
        authority.self_enroll()

    from jinja2 import Environment, PackageLoader
    context = globals()
    context.update(locals())
    env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)
    if not os.path.exists("/etc/systemd/system/certidude-enroll.timer"):
        click.echo("Creating systemd timer...")
        with open("/etc/systemd/system/certidude-enroll.timer", "w") as fh:
            fh.write(env.get_template("client/certidude.timer").render(context))
    if not os.path.exists("/etc/systemd/system/certidude-enroll.service"):
        click.echo("Creating systemd service...")
        with open("/etc/systemd/system/certidude-enroll.service", "w") as fh:
            fh.write(env.get_template("client/certidude.service").render(context))
    os.system("systemctl daemon-reload")
    os.system("systemctl enable certidude-enroll.timer")
    os.system("systemctl start certidude-enroll.timer")

    if not os.path.exists(const.CLIENT_CONFIG_PATH):
        click.echo("Client not configured, so not going to enroll")
        return

    import requests

    clients = ConfigTreeParser(const.CLIENT_CONFIG_PATH)
    service_config = ConfigTreeParser(const.SERVICES_CONFIG_PATH)

    # Process directories
    if not os.path.exists(const.RUN_DIR):
        click.echo("Creating: %s" % const.RUN_DIR)
        os.makedirs(const.RUN_DIR)

    for authority_name in clients.sections():
        # TODO: Create directories automatically

        try:
            trigger = clients.get(authority_name, "trigger")
        except NoOptionError:
            trigger = "interface up"

        if trigger == "domain joined":
            # Stop further processing if command line argument said so or trigger expects domain membership
            if not os.path.exists("/etc/krb5.keytab"):
                continue
            kerberos = True
        elif trigger == "interface up":
            pass
        else:
            raise


        #########################
        ### Fork if requested ###
        #########################

        pid_path = os.path.join(const.RUN_DIR, authority_name + ".pid")

        try:
            with open(pid_path) as fh:
                pid = int(fh.readline())
                os.kill(pid, signal.SIGTERM)
                click.echo("Terminated process %d" % pid)
            os.unlink(pid_path)
        except EnvironmentError:
            pass

        if fork:
            child_pid = os.fork()
        else:
            child_pid = None

        if child_pid:
            click.echo("Spawned certificate request process with PID %d" % (child_pid))
            continue

        with open(pid_path, "w") as fh:
            fh.write("%d\n" % os.getpid())

        try:
            authority_path = clients.get(authority_name, "authority path")
        except NoOptionError:
            authority_path = "/etc/certidude/authority/%s/ca_cert.pem" % authority_name
        finally:
            if os.path.exists(authority_path):
                click.echo("Found authority certificate in: %s" % authority_path)
                with open(authority_path, "rb") as fh:
                    header, _, certificate_der_bytes = pem.unarmor(fh.read())
                    authority_certificate = x509.Certificate.load(certificate_der_bytes)
            else:
                if not os.path.exists(os.path.dirname(authority_path)):
                    os.makedirs(os.path.dirname(authority_path))
                authority_url = "http://%s/api/certificate/" % authority_name
                click.echo("Attempting to fetch authority certificate from %s" % authority_url)
                try:
                    r = requests.get(authority_url,
                        headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
                    header, _, certificate_der_bytes = pem.unarmor(r.content)
                    authority_certificate = x509.Certificate.load(certificate_der_bytes)
                except: # TODO: catch correct exceptions
                    raise
                #    raise ValueError("Failed to parse PEM: %s" % r.text)
                authority_partial = authority_path + ".part"
                with open(authority_partial, "wb") as oh:
                    oh.write(r.content)
                click.echo("Writing authority certificate to: %s" % authority_path)
                selinux_fixup(authority_partial)
                os.rename(authority_partial, authority_path)

            authority_public_key = asymmetric.load_public_key(
                authority_certificate["tbs_certificate"]["subject_public_key_info"])



        # Attempt to install CA certificates system wide
        try:
            authority_system_wide = clients.getboolean(authority_name, "system wide")
        except NoOptionError:
            authority_system_wide = False
        finally:
            if authority_system_wide:
                # Firefox, Chromium, wget, curl on Fedora
                # Note that if ~/.pki/nssdb has been customized before, curl breaks
                if os.path.exists("/usr/bin/update-ca-trust"):
                    link_path = "/etc/pki/ca-trust/source/anchors/%s" % authority_name
                    if not os.path.lexists(link_path):
                        os.symlink(authority_path, link_path)
                    os.system("update-ca-trust")

                # curl on Fedora ?
                # pip

                # Firefox (?) on Debian, Ubuntu
                if os.path.exists("/usr/bin/update-ca-certificates") or os.path.exists("/usr/sbin/update-ca-certificates"):
                    link_path = "/usr/local/share/ca-certificates/%s" % authority_name
                    if not os.path.lexists(link_path):
                        os.symlink(authority_path, link_path)
                    os.system("update-ca-certificates")

                # TODO: test for curl, wget


        ###############
        ### Get CRL ###
        ###############

        try:
            revocations_path = clients.get(authority_name, "revocations path")
        except NoOptionError:
            revocations_path = None
        else:
            # Fetch certificate revocation list
            revoked_url = "http://%s/api/revoked/" % authority_name
            click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path))
            r = requests.get(revoked_url, headers={'accept': 'application/x-pem-file'})

            if r.status_code == 200:
                header, _, crl_der_bytes = pem.unarmor(r.content)
                revocations = CertificateList.load(crl_der_bytes)
                # TODO: check signature, parse reasons, remove keys if revoked
                revocations_partial = revocations_path + ".part"
                with open(revocations_partial, 'wb') as f:
                    f.write(r.content)
                os.rename(revocations_partial, revocations_path)
            elif r.status_code == 404:
                click.echo("CRL disabled, server said 404")
            else:
                click.echo("Failed to fetch CRL from %s, got %s" % (revoked_url, r.text))


        try:
            common_name = clients.get(authority_name, "common name")
        except NoOptionError:
            click.echo("No common name specified for %s, not requesting a certificate" % authority_name)
            continue

        # If deriving common name from *current* hostname is preferred
        if common_name == "$HOSTNAME":
            common_name = const.HOSTNAME
        elif common_name == "$FQDN":
            common_name = const.FQDN
        elif "$" in common_name:
            raise ValueError("Invalid variable '%s' supplied, only $HOSTNAME and $FQDN allowed" % common_name)
        if not re.match(const.RE_COMMON_NAME, common_name):
            raise ValueError("Supplied common name %s doesn't match the expression %s" % (common_name, const.RE_COMMON_NAME))


        ################################
        ### Generate keypair and CSR ###
        ################################

        try:
            key_path = clients.get(authority_name, "key path")
            request_path = clients.get(authority_name, "request path")
        except NoOptionError:
            key_path = "/etc/certidude/authority/%s/host_key.pem" % authority_name
            request_path = "/etc/certidude/authority/%s/host_csr.pem" % authority_name

        if os.path.exists(request_path):
            with open(request_path, "rb") as fh:
                header, _, der_bytes = pem.unarmor(fh.read())
                csr = CertificationRequest.load(der_bytes)
                if csr["certification_request_info"]["subject"].native["common_name"] != common_name:
                    click.echo("Stored request's common name differs from currently requested one, deleting old request")
                    os.remove(request_path)

        if not os.path.exists(request_path):
            key_partial = key_path + ".part"
            request_partial = request_path + ".part"

            if authority_public_key.algorithm == "ec":
                self_public_key, private_key = asymmetric.generate_pair("ec", curve=authority_public_key.curve)
            elif authority_public_key.algorithm == "rsa":
                self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=authority_public_key.bit_size)
            else:
                NotImplemented

            builder = CSRBuilder({"common_name": common_name}, self_public_key)
            request = builder.build(private_key)
            with open(key_partial, 'wb') as f:
                f.write(asymmetric.dump_private_key(private_key, None))
            with open(request_partial, 'wb') as f:
                f.write(pem_armor_csr(request))
            selinux_fixup(key_partial)
            selinux_fixup(request_partial)
            os.rename(key_partial, key_path)
            os.rename(request_partial, request_path)


        ##############################################
        ### Submit CSR and save signed certificate ###
        ##############################################

        try:
            certificate_path = clients.get(authority_name, "certificate path")
        except NoOptionError:
            certificate_path = "/etc/certidude/authority/%s/host_cert.pem" % authority_name

        try:
            renewal_overlap = clients.getint(authority_name, "renewal overlap")
        except NoOptionError: # Renewal not configured
            renewal_overlap = None

        try:
            with open(certificate_path, "rb") as ch, open(request_path, "rb") as rh, open(key_path, "rb") as kh:
                cert_buf = ch.read()
                cert = asymmetric.load_certificate(cert_buf)
                expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
                if renewal_overlap and NOW > expires - timedelta(days=renewal_overlap):
                    click.echo("Certificate will expire %s, will attempt to renew" % expires)
                    renew = True
        except EnvironmentError: # Certificate missing, can't renew
            pass

        try:
            autosign = clients.getboolean(authority_name, "autosign")
        except NoOptionError:
            autosign = True

        if not os.path.exists(certificate_path) or renew:
            # Set up URL-s
            request_params = set()
            request_params.add("autosign=%s" % ("yes" if autosign else "no"))
            if not no_wait:
                request_params.add("wait=forever")

            kwargs = {
                "data": open(request_path),
                "verify": authority_path,
                "headers": {
                    "Content-Type": "application/pkcs10",
                    "Accept": "application/x-x509-user-cert,application/x-pem-file"
                }
            }

            if renew: # Do mutually authenticated TLS handshake
                kwargs["cert"] = certificate_path, key_path
                click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
            else:
                # If machine is joined to domain attempt to present machine credentials for authentication
                if kerberos:
                    try:
                        from requests_kerberos import HTTPKerberosAuth, OPTIONAL
                    except ImportError:
                        click.echo("Kerberos bindings not available, please install requests-kerberos")
                    else:
                        os.environ["KRB5CCNAME"]="/tmp/ca.ticket"

                        # Mac OS X has keytab with lowercase hostname
                        cmd = "kinit -S HTTP/%s -k %s$" % (authority_name, const.HOSTNAME.lower())
                        click.echo("Executing: %s" % cmd)
                        if os.system(cmd):
                            # Fedora /w SSSD has keytab with uppercase hostname
                            cmd = "kinit -S HTTP/%s -k %s$" % (authority_name, const.HOSTNAME.upper())
                            if os.system(cmd):
                                # Failed, probably /etc/krb5.keytab contains spaghetti
                                raise ValueError("Failed to initialize Kerberos service ticket using machine keytab")
                        assert os.path.exists("/tmp/ca.ticket"), "Ticket not created!"
                        click.echo("Initialized Kerberos service ticket using machine keytab")
                        kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
                else:
                    click.echo("Not using machine keytab")

            request_url = "https://%s:8443/api/request/" % authority_name
            if request_params:
                request_url = request_url + "?" + "&".join(request_params)
            submission = requests.post(request_url, **kwargs)

            # Destroy service ticket
            if os.path.exists("/tmp/ca.ticket"):
                os.system("kdestroy")

            if submission.status_code == requests.codes.ok:
                pass
            if submission.status_code == requests.codes.accepted:
                click.echo("Server accepted the request, but refused to sign immideately (%s). Waiting was not requested, hence quitting for now" % submission.text)
                os.unlink(pid_path)
                continue
            if submission.status_code == requests.codes.conflict:
                raise errors.DuplicateCommonNameError("Different signing request with same CN is already present on server, server refuses to overwrite")
            elif submission.status_code == requests.codes.gone:
                # Should the client retry or disable request submission?
                raise ValueError("Server refused to sign the request") # TODO: Raise proper exception
            elif submission.status_code == requests.codes.bad_request:
                raise ValueError("Server said following, likely current certificate expired/revoked? %s" % submission.text)
            else:
                submission.raise_for_status()

            try:
                header, _, certificate_der_bytes = pem.unarmor(submission.content)
                cert = x509.Certificate.load(certificate_der_bytes)
            except: # TODO: catch correct exceptions
                raise ValueError("Failed to parse PEM: %s" % submission.text)

            assert cert.subject.native["common_name"] == common_name, \
                "Expected certificate with common name %s, but got %s instead" % \
                    (common_name, cert.subject.native["common_name"])

            os.umask(0o022)
            certificate_partial = certificate_path + ".part"
            with open(certificate_partial, "w") as fh:
                # Dump certificate
                fh.write(submission.text)

            click.echo("Writing certificate to: %s" % certificate_path)
            selinux_fixup(certificate_partial)
            os.rename(certificate_partial, certificate_path)

            # Nginx requires bundle
            try:
                bundle_path = clients.get(authority_name, "bundle path")
            except NoOptionError:
                pass
            else:
                bundle_partial = bundle_path + ".part"
                with open(bundle_partial, "w") as fh:
                    fh.write(submission.text)
                    with open(authority_path) as ch:
                        fh.write(ch.read())
                click.echo("Writing bundle to: %s" % bundle_path)
                os.rename(bundle_partial, bundle_path)
        else:
            click.echo("Certificate found at %s and no renewal requested" % certificate_path)


        ##################################
        ### Configure related services ###
        ##################################

        for endpoint in service_config.sections():
            if service_config.get(endpoint, "authority") != authority_name:
                continue

            click.echo("Configuring '%s'" % endpoint)
            csummer = hashlib.sha1()
            csummer.update(endpoint.encode("ascii"))
            csum = csummer.hexdigest()
            uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]

            # Intranet HTTPS handled by PKCS#12 bundle generation,
            # so it will not be implemented here

            # OpenVPN set up with initscripts
            if service_config.get(endpoint, "service") == "init/openvpn":
                if os.path.exists("/etc/openvpn/%s.disabled" % endpoint) and not os.path.exists("/etc/openvpn/%s.conf" % endpoint):
                    os.rename("/etc/openvpn/%s.disabled" % endpoint, "/etc/openvpn/%s.conf" % endpoint)
                if os.path.exists("/bin/systemctl"):
                    click.echo("Re-running systemd generators for OpenVPN...")
                    os.system("systemctl daemon-reload")
                if not os.path.exists("/etc/systemd/system/openvpn-reconnect.service"):
                    with open("/etc/systemd/system/openvpn-reconnect.service.part", "w") as fh:
                        fh.write(env.get_template("client/openvpn-reconnect.service").render(context))
                    os.rename("/etc/systemd/system/openvpn-reconnect.service.part",
                        "/etc/systemd/system/openvpn-reconnect.service")
                    click.echo("Created /etc/systemd/system/openvpn-reconnect.service")
                click.echo("Starting OpenVPN...")
                os.system("service openvpn start")
                continue

            # IPSec set up with initscripts
            if service_config.get(endpoint, "service") == "init/strongswan":
                from ipsecparse import loads
                config = loads(open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX).read())
                for section_type, section_name in config:
                    # Identify correct ipsec.conf section by leftcert
                    if section_type != "conn":
                        continue
                    if config[section_type,section_name]["leftcert"] != certificate_path:
                        continue

                    if config[section_type,section_name].get("left", "") == "%defaultroute":
                        config[section_type,section_name]["auto"] = "start" # This is client
                    elif config[section_type,section_name].get("leftsourceip", ""):
                        config[section_type,section_name]["auto"] = "add" # This is server
                    else:
                        config[section_type,section_name]["auto"] = "route" # This is site-to-site tunnel

                    with open("%s/ipsec.conf.part" % const.STRONGSWAN_PREFIX, "w") as fh:
                        fh.write(config.dumps())
                    os.rename(
                        "%s/ipsec.conf.part" % const.STRONGSWAN_PREFIX,
                        "%s/ipsec.conf" % const.STRONGSWAN_PREFIX)
                    break

                # Tune AppArmor profile, TODO: retain contents
                if os.path.exists("/etc/apparmor.d/local"):
                    with open("/etc/apparmor.d/local/usr.lib.ipsec.charon", "w") as fh:
                        fh.write(key_path + " r,\n")
                        fh.write(authority_path + " r,\n")
                        fh.write(certificate_path + " r,\n")

                # Attempt to reload config or start if it's not running
                if os.path.exists("/usr/sbin/strongswan"): # wtf fedora
                    if os.system("strongswan update"):
                        os.system("strongswan start")
                else:
                    if os.system("ipsec update"):
                        os.system("ipsec start")

                continue

            # OpenVPN set up with NetworkManager
            if service_config.get(endpoint, "service") == "network-manager/openvpn":
                # NetworkManager-strongswan-gnome
                nm_config_path = os.path.join("/etc/NetworkManager/system-connections", endpoint)
                if os.path.exists(nm_config_path):
                    click.echo("Not creating %s, remove to regenerate" % nm_config_path)
                    continue
                nm_config = ConfigParser()
                nm_config.add_section("connection")
                nm_config.set("connection", "certidude managed", "true")
                nm_config.set("connection", "id", endpoint)
                nm_config.set("connection", "uuid", uuid)
                nm_config.set("connection", "type", "vpn")
                nm_config.add_section("vpn")
                nm_config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn")
                nm_config.set("vpn", "connection-type", "tls")
                nm_config.set("vpn", "comp-lzo", "yes")
                nm_config.set("vpn", "cert-pass-flags", "0")
                nm_config.set("vpn", "tap-dev", "no")
                nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate
                nm_config.set("vpn", "remote", service_config.get(endpoint, "remote"))
                nm_config.set("vpn", "key", key_path)
                nm_config.set("vpn", "cert", certificate_path)
                nm_config.set("vpn", "ca", authority_path)
                nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-256-GCM-SHA384" % (
                    "ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA"))
                nm_config.set("vpn", "cipher", "AES-128-GCM")
                nm_config.set("vpn", "auth", "SHA384")
                nm_config.add_section("ipv4")
                nm_config.set("ipv4", "method", "auto")
                nm_config.set("ipv4", "never-default", "true")
                nm_config.add_section("ipv6")
                nm_config.set("ipv6", "method", "auto")

                try:
                    nm_config.set("vpn", "port", str(service_config.getint(endpoint, "port")))
                except NoOptionError:
                    nm_config.set("vpn", "port", "1194")

                try:
                    if service_config.get(endpoint, "proto") == "tcp":
                        nm_config.set("vpn", "proto-tcp", "yes")
                except NoOptionError:
                    pass

                # Prevent creation of files with liberal permissions
                os.umask(0o177)

                # Write NetworkManager configuration
                with open(nm_config_path, "w") as fh:
                    nm_config.write(fh)
                    click.echo("Created %s" % nm_config_path)
                if os.path.exists("/run/NetworkManager"):
                    os.system("nmcli con reload")
                continue


            # IPSec set up with NetworkManager
            if service_config.get(endpoint, "service") == "network-manager/strongswan":
                client_config = ConfigParser()
                nm_config = ConfigParser()
                nm_config.add_section("connection")
                nm_config.set("connection", "certidude managed", "true")
                nm_config.set("connection", "id", endpoint)
                nm_config.set("connection", "uuid", uuid)
                nm_config.set("connection", "type", "vpn")
                nm_config.add_section("vpn")
                nm_config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan")
                nm_config.set("vpn", "encap", "no")
                nm_config.set("vpn", "virtual", "yes")
                nm_config.set("vpn", "method", "key")
                nm_config.set("vpn", "ipcomp", "no")
                nm_config.set("vpn", "address", service_config.get(endpoint, "remote"))
                nm_config.set("vpn", "userkey", key_path)
                nm_config.set("vpn", "usercert", certificate_path)
                nm_config.set("vpn", "certificate", authority_path)
                dhgroup = "ecp384" if authority_public_key.algorithm == "ec" else "modp2048"
                nm_config.set("vpn", "ike", "aes256-sha384-prfsha384-" + dhgroup)
                nm_config.set("vpn", "esp", "aes128gcm16-aes128gmac-" + dhgroup)
                nm_config.set("vpn", "proposal", "yes")

                nm_config.add_section("ipv4")
                nm_config.set("ipv4", "method", "auto")

                # Add routes, may need some more tweaking
                if service_config.has_option(endpoint, "route"):
                    for index, subnet in enumerate(service_config.get(endpoint, "route").split(","), start=1):
                        nm_config.set("ipv4", "route%d" % index, subnet)

                # Prevent creation of files with liberal permissions
                os.umask(0o177)

                # Write NetworkManager configuration
                with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as fh:
                    nm_config.write(fh)
                    click.echo("Created %s" % fh.name)
                if os.path.exists("/run/NetworkManager"):
                    os.system("nmcli con reload")
                continue

            # TODO: Puppet, OpenLDAP, <insert awesomeness here>
            click.echo("Unknown service: %s" % service_config.get(endpoint, "service"))
        os.unlink(pid_path)


@click.command("server", help="Set up OpenVPN server")
@click.argument("authority")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces")
@click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default")
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
@click.option("--config", "-o",
    default="/etc/openvpn/site-to-client.conf",
    type=click.File(mode="w", atomic=True, lazy=True),
    help="OpenVPN configuration file")
@fqdn_required
@setup_client(prefix="server_", dh=True)
def certidude_setup_openvpn_server(authority, common_name, config, subnet, route, local, proto, port, **paths):
    # Install dependencies
    apt("openvpn")
    rpm("openvpn")

    # Create corresponding section in /etc/certidude/services.conf
    endpoint = "OpenVPN server %s of %s" % (common_name, authority)
    service_config = ConfigParser()
    if os.path.exists(const.SERVICES_CONFIG_PATH):
        service_config.readfp(open(const.SERVICES_CONFIG_PATH))
    if service_config.has_section(endpoint):
        click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
    else:
        service_config.add_section(endpoint)
        service_config.set(endpoint, "authority", authority)
        service_config.set(endpoint, "service", "init/openvpn")

        with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
            service_config.write(fh)
        os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
        click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))

    authority_hostname = authority.split(".")[0]
    config.write("server %s %s\n" % (subnet.network_address, subnet.netmask))
    config.write("dev tun-%s\n" % authority_hostname)
    config.write("proto %s\n" % proto)
    config.write("port %d\n" % port)
    config.write("local %s\n" % local)
    config.write("key %s\n" % paths.get("key_path"))
    config.write("cert %s\n" % paths.get("certificate_path"))
    config.write("ca %s\n" % paths.get("authority_path"))
    config.write("crl-verify %s\n" % paths.get("revocations_path"))
    config.write("dh %s\n" % paths.get("dhparam_path"))
    config.write("comp-lzo\n")
    config.write("user nobody\n")
    config.write("group nogroup\n")
    config.write("persist-tun\n")
    config.write("persist-key\n")
    config.write("#ifconfig-pool-persist /tmp/openvpn-leases.txt\n")


    click.echo("Generated %s" % config.name)
    click.echo("Inspect generated files and issue following to request certificate:")
    click.echo()
    click.echo("  certidude enroll")


@click.command("nginx", help="Set up nginx as HTTPS server")
@click.argument("authority")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
@click.option("--tls-config",
    default="/etc/nginx/conf.d/tls.conf",
    type=click.File(mode="w", atomic=True, lazy=True),
    help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default")
@click.option("--site-config", "-o",
    default="/etc/nginx/sites-available/%s.conf" % const.HOSTNAME,
    type=click.File(mode="w", atomic=True, lazy=True),
    help="Site configuration file of nginx, /etc/nginx/sites-available/%s.conf by default" % const.HOSTNAME)
@click.option("--verify-client", "-vc", default="optional", type=click.Choice(['optional', 'on', 'off']))
@fqdn_required
@setup_client(prefix="server_", dh=True)
def certidude_setup_nginx(authority, common_name, site_config, tls_config, verify_client, **paths):

    apt("nginx")
    rpm("nginx")
    from jinja2 import Environment, PackageLoader
    env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)

    context = globals() # Grab const.BLAH
    context.update(locals())
    context.update(paths)

    if os.path.exists(site_config.name):
        click.echo("Configuration file %s already exists, not overwriting" % site_config.name)
    else:
        site_config.write(env.get_template("nginx-https-site.conf").render(context))
        click.echo("Generated %s" % site_config.name)

    if os.path.exists(tls_config.name):
        click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
    else:
        tls_config.write(env.get_template("nginx-tls.conf").render(context))
        click.echo("Generated %s" % tls_config.name)


    click.echo()
    click.echo("Inspect configuration files, enable it and start nginx service:")
    click.echo()
    click.echo("  ln -s %s /etc/nginx/sites-enabled/%s" % (
        os.path.relpath(site_config.name, "/etc/nginx/sites-enabled"),
        os.path.basename(site_config.name)))
    click.echo("  service nginx restart")
    click.echo()


@click.command("client", help="Set up OpenVPN client")
@click.argument("authority")
@click.argument("remote")
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
@click.option("--config", "-o",
    default="/etc/openvpn/client-to-site.conf", # TODO: created initially disabled conf
    type=click.File(mode="w", atomic=True, lazy=True),
    help="OpenVPN configuration file")
@setup_client()
def certidude_setup_openvpn_client(authority, remote, common_name, config, proto, **paths):
    # Install dependencies
    apt("openvpn")
    rpm("openvpn")


    # Create corresponding section in /etc/certidude/services.conf
    endpoint = "OpenVPN to %s" % remote
    service_config = ConfigParser()
    if os.path.exists(const.SERVICES_CONFIG_PATH):
        service_config.readfp(open(const.SERVICES_CONFIG_PATH))
    if service_config.has_section(endpoint):
        click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
    else:
        service_config.add_section(endpoint)
        service_config.set(endpoint, "authority", authority)
        service_config.set(endpoint, "service", "init/openvpn")
        service_config.set(endpoint, "remote", remote)
        with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
            service_config.write(fh)
        os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
        click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))

    config.write("client\n")
    config.write("remote %s\n" % remote)
    config.write("remote-cert-tls server\n")
    config.write("proto %s\n" % proto)
    config.write("dev tun-%s\n" % remote.split(".")[0])
    config.write("nobind\n")
    config.write("key %s\n" % paths.get("key_path"))
    config.write("cert %s\n" % paths.get("certificate_path"))
    config.write("ca %s\n" % paths.get("authority_path"))
    config.write("crl-verify %s\n" % paths.get("revocations_path"))
    config.write("comp-lzo\n")
    config.write("user nobody\n")
    config.write("group nogroup\n")
    config.write("persist-tun\n")
    config.write("persist-key\n")
    config.write("up /etc/openvpn/update-resolv-conf\n")
    config.write("down /etc/openvpn/update-resolv-conf\n")

    click.echo("Generated %s" % config.name)
    click.echo("Inspect generated files and issue following to request certificate:")
    click.echo()
    click.echo("  certidude enroll")


@click.command("server", help="Set up strongSwan server")
@click.argument("authority")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
@click.option("--subnet", "-sn", default="192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
@fqdn_required
@setup_client(prefix="server_")
def certidude_setup_strongswan_server(authority, common_name, subnet, route, **paths):
    # Install dependencies
    apt("strongswan")
    rpm("strongswan")

    # Create corresponding section in /etc/certidude/services.conf
    endpoint = "IPsec gateway for %s" % authority
    service_config = ConfigParser()
    if os.path.exists(const.SERVICES_CONFIG_PATH):
        service_config.readfp(open(const.SERVICES_CONFIG_PATH))
    if service_config.has_section(endpoint):
        click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
    else:
        service_config.add_section(endpoint)
        service_config.set(endpoint, "authority", authority)
        service_config.set(endpoint, "service", "init/strongswan")
        with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
            service_config.write(fh)
        os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
        click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))

    # Create corresponding section to /etc/ipsec.conf
    from ipsecparse import loads
    ipsec_conf = loads(open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX).read())
    ipsec_conf["ca", authority] = dict(
        auto="add",
        cacert=paths.get("authority_path"))
    ipsec_conf["conn", authority] = dict(
        leftcert=paths.get("certificate_path"),
        leftsubnet=",".join(route),
        right="%any",
        rightsourceip=str(subnet),
        closeaction="restart",
        auto="ignore")
    with open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX, "w") as fh:
        fh.write(ipsec_conf.dumps())
    with open("%s/ipsec.secrets" % const.STRONGSWAN_PREFIX, "a") as fh:
        fh.write(": RSA %s\n" % paths.get("key_path"))

    click.echo()
    click.echo("If you're running Ubuntu make sure you're not affected by #1505222")
    click.echo("https://bugs.launchpad.net/ubuntu/+source/strongswan/+bug/1505222")


@click.command("client", help="Set up strongSwan client")
@click.argument("authority")
@click.argument("remote")
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
@setup_client()
def certidude_setup_strongswan_client(authority, remote, common_name, **paths):
    # Install dependencies
    apt("strongswan") or rpm("strongswan")

    # Create corresponding section in /etc/certidude/services.conf
    endpoint = "IPsec connection to %s" % remote
    service_config = ConfigParser()
    if os.path.exists(const.SERVICES_CONFIG_PATH):
        service_config.readfp(open(const.SERVICES_CONFIG_PATH))
    if service_config.has_section(endpoint):
        click.echo("Section '%s' already exists in %s, not reconfiguring" % (endpoint, const.SERVICES_CONFIG_PATH))
    else:
        service_config.add_section(endpoint)
        service_config.set(endpoint, "authority", authority)
        service_config.set(endpoint, "service", "init/strongswan")
        service_config.set(endpoint, "remote", remote)
        with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
            service_config.write(fh)
        os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
        click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))

    # Create corresponding section in /etc/ipsec.conf
    from ipsecparse import loads
    ipsec_conf = loads(open('%s/ipsec.conf' % const.STRONGSWAN_PREFIX).read())
    ipsec_conf["ca", authority] = dict(
        auto="add",
        cacert=paths.get("authority_path"))
    ipsec_conf["conn", remote] = dict(
        leftsourceip="%config",
        left="%defaultroute",
        leftcert=paths.get("certificate_path"),
        rightid="%any",
        right=remote,
        rightsubnet="0.0.0.0/0", # To allow anything suggested by gateway
        keyexchange="ikev2",
        keyingtries="300",
        dpdaction="restart",
        closeaction="restart",
        auto="ignore")
    with open("%s/ipsec.conf" % const.STRONGSWAN_PREFIX, "w") as fh:
        fh.write(ipsec_conf.dumps())
    with open("%s/ipsec.secrets" % const.STRONGSWAN_PREFIX, "a") as fh:
        fh.write(": RSA %s\n" % paths.get("key_path"))
    if os.path.exists("/etc/apparmor.d/local"):
        with open("/etc/apparmor.d/local/usr.lib.ipsec.charon", "w") as fh:
            fh.write(os.path.join(const.STORAGE_PATH, "**") + " r,\n")

    click.echo("Generated section %s in %s" % (authority, const.CLIENT_CONFIG_PATH))
    click.echo("Run 'certidude enroll' to request certificates and to enable services")


@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
@click.argument("authority") # Certidude server
@click.argument("remote") # StrongSwan gateway
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
@setup_client()
def certidude_setup_strongswan_networkmanager(authority, remote, common_name, **paths):
    # Install dependencies
    apt("network-manager strongswan-nm")
    rpm("NetworkManager NetworkManager-tui NetworkManager-strongswan-gnome")

    # Create corresponding section in /etc/certidude/services.conf
    endpoint = "IPSec to %s" % remote
    service_config = ConfigParser()
    if os.path.exists(const.SERVICES_CONFIG_PATH):
        service_config.readfp(open(const.SERVICES_CONFIG_PATH))
    if service_config.has_section(endpoint):
        click.echo("Section '%s' already exists in %s, remove to regenerate" % (endpoint, const.SERVICES_CONFIG_PATH))
    else:
        service_config.add_section(endpoint)
        service_config.set(endpoint, "authority", authority)
        service_config.set(endpoint, "remote", remote)
        service_config.set(endpoint, "service", "network-manager/strongswan")
        with open(const.SERVICES_CONFIG_PATH + ".part", 'w') as fh:
            service_config.write(fh)
        os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
        click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))


@click.command("networkmanager", help="Set up OpenVPN client via NetworkManager")
@click.argument("authority")
@click.argument("remote") # OpenVPN gateway
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
@setup_client()
def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **paths):
    apt("network-manager network-manager-openvpn-gnome")
    rpm("NetworkManager NetworkManager-tui NetworkManager-openvpn-gnome")

    # Create corresponding section in /etc/certidude/services.conf
    endpoint = "OpenVPN to %s" % remote

    service_config = ConfigParser()
    if os.path.exists(const.SERVICES_CONFIG_PATH):
        service_config.readfp(open(const.SERVICES_CONFIG_PATH))
    if service_config.has_section(endpoint):
        click.echo("Section '%s' already exists in %s, remove to regenerate" % (endpoint, const.SERVICES_CONFIG_PATH))
    else:
        service_config.add_section(endpoint)
        service_config.set(endpoint, "authority", authority)
        service_config.set(endpoint, "remote", remote)
        service_config.set(endpoint, "service", "network-manager/openvpn")
        service_config.write(open("/etc/certidude/services.conf", "w"))
        click.echo("Section %s added to /etc/certidude/client.conf" % endpoint)


@click.command("authority", help="Set up Certificate Authority in a directory")
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Kerberos keytab for using 'kerberos' authentication backend, /etc/certidude/server.keytab by default")
@click.option("--nginx-config", "-n",
    default="/etc/nginx/sites-available/certidude.conf",
    type=click.File(mode="w", atomic=True, lazy=True),
    help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
@click.option("--tls-config",
    default="/etc/nginx/conf.d/tls.conf",
    type=click.File(mode="w", atomic=True, lazy=True),
    help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default")
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN)
@click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN)
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
@click.option("--organization", "-o", default=None, help="Company or organization name")
@click.option("--organizational-unit", "-ou", default="Certificate Authority")
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
@click.option("--directory", default="/var/lib/certidude", help="Directory for authority files")
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
@click.option("--skip-assets", is_flag=True, help="Don't attempt to assemble JS/CSS/font assets")
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
@click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA")
@fqdn_required
def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate):
    assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment"
    assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"

    import pwd
    from jinja2 import Environment, PackageLoader
    env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True)

    if skip_packages:
        click.echo("Not attempting to install packages from APT as requested...")
    else:
        click.echo("Installing packages...")
        cmd = "DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \
            cython3 python3-dev \
            python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
            software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
            libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
            rsync attr wget unzip"
        click.echo("Running: %s" % cmd)
        if os.system(cmd):
            raise click.ClickException("Failed to install APT packages")
        if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"):
            raise click.ClickException("Failed to install Python packages")
        if os.system("pip3 install -q --pre --upgrade python-ldap"):
            raise click.ClickException("Failed to install python-ldap")

        if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
            click.echo("Enabling nginx PPA")
            if os.system("add-apt-repository -y ppa:nginx/stable"):
                raise click.ClickException("Failed to add nginx PPA")
            if os.system("apt-get update -q"):
                raise click.ClickException("Failed to update package lists")
            if os.system("apt-get install -y -q libnginx-mod-nchan"):
                raise click.ClickException("Failed to install nchan")
        else:
            click.echo("PPA for nginx already enabled")

        if not os.path.exists("/usr/sbin/nginx"):
            click.echo("Installing nginx from PPA")
            if os.system("apt-get install -y -q nginx"):
                raise click.ClickException("Failed to install nginx")
        else:
            click.echo("Web server nginx already installed")

        if not os.path.exists("/usr/bin/node"):
            os.symlink("/usr/bin/nodejs", "/usr/bin/node")

    # Generate secret for tokens
    token_url = "https://" + const.FQDN + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn"

    template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile")
    click.echo("Using templates from %s" % template_path)

    click.echo("Placing authority files in %s" % directory)

    certificate_url = "http://%s/api/certificate/" % common_name
    click.echo("Setting CA certificate URL to %s" % certificate_url)

    revoked_url = "http://%s/api/revoked/" % common_name
    click.echo("Setting revocation list URL to %s" % revoked_url)

    responder_url = "http://%s/api/ocsp/" % common_name
    click.echo("Setting OCSP responder URL to %s" % responder_url)


    # Expand variables
    assets_dir = os.path.join(directory, "assets")
    ca_key = os.path.join(directory, "ca_key.pem")
    ca_req = os.path.join(directory, "ca_req.pem")
    ca_cert = os.path.join(directory, "ca_cert.pem")
    self_key = os.path.join(directory, "self_key.pem")
    sqlite_path = os.path.join(directory, "meta", "db.sqlite")
    distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit)
    dhparam_path = "/etc/ssl/dhparam.pem"

    # Builder variables
    dhgroup = "ecp384" if elliptic_curve else "modp2048"

    try:
        pwd.getpwnam("certidude")
        click.echo("User 'certidude' already exists")
    except KeyError:
        cmd = "adduser", "--system", "--no-create-home", "--group", "certidude"
        if subprocess.call(cmd):
            raise click.ClickException("Failed to create system user 'certidude'")

    if os.path.exists(kerberos_keytab):
        click.echo("Service principal keytab found in '%s'" % kerberos_keytab)
    else:
        click.echo("To use 'kerberos' authentication backend join the domain and create service principal with:")
        click.echo()
        click.echo("  KRB5_KTNAME=FILE:%s net ads keytab add HTTP -P" % kerberos_keytab)
        click.echo("  chown %s %s" % (username, kerberos_keytab))
        click.echo()


    if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"):
        # Fetch Kerberos ticket for system account
        cp = ConfigParser()
        cp.read("/etc/samba/smb.conf")
        realm = cp.get("global", "realm")
        domain = realm.lower()
        name = cp.get("global", "netbios name")

        base = ",".join(["dc=" + j for j in domain.split(".")])
        if not os.path.exists("/etc/cron.hourly/certidude"):
            with open("/etc/cron.hourly/certidude", "w") as fh:
                fh.write(env.get_template("server/cronjob").render(vars()))
            os.chmod("/etc/cron.hourly/certidude", 0o755)
            click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly")
        os.system("/etc/cron.hourly/certidude")
    else:
        click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured")

    letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name
    letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name
    letsencrypt = os.path.exists(letsencrypt_fullchain)

    doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc")
    script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script")

    static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static")
    certidude_path = sys.argv[0]

    click.echo("Generating: %s" % nginx_config.name)
    nginx_config.write(env.get_template("server/nginx.conf").render(vars()))
    nginx_config.close()
    if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"):
        os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf")
        click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name)
    if os.path.exists("/etc/nginx/sites-enabled/default"):
        os.unlink("/etc/nginx/sites-enabled/default")
    if os.path.exists("/etc/systemd"):
        if os.path.exists("/etc/systemd/system/certidude.service"):
            click.echo("File /etc/systemd/system/certidude.service already exists, remove to regenerate")
        else:
            with open("/etc/systemd/system/certidude.service", "w") as fh:
                fh.write(env.get_template("server/systemd.service").render(vars()))
            click.echo("File /etc/systemd/system/certidude.service created")
    else:
        click.echo("Not systemd based OS, don't know how to set up initscripts")

    # Set umask to 0022
    os.umask(0o022)
    assert os.getuid() == 0 and os.getgid() == 0

    bootstrap_pid = os.fork()
    if not bootstrap_pid:

        # Create what's usually /var/lib/certidude
        if not os.path.exists(directory):
            os.makedirs(directory)
        assert os.stat(directory).st_mode == 0o40755

        # Create bundle directories
        bundle_js = os.path.join(assets_dir, "js", "bundle.js")
        bundle_css = os.path.join(assets_dir, "css", "bundle.css")
        for path in bundle_js, bundle_css:
            subdir = os.path.dirname(path)
            if not os.path.exists(subdir):
                click.echo("Creating directory %s" % subdir)
                os.makedirs(subdir)

        # Install JavaScript pacakges
        if skip_packages:
            click.echo("Not attempting to install packages from NPM as requested...")
        else:
            cmd = "npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg"
            click.echo("Installing JavaScript packages: %s" % cmd)

        if skip_assets:
            click.echo("Not attempting to assemble assets as requested...")
        else:
            # Copy fonts
            click.echo("Copying fonts...")
            if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir):
                raise click.ClickException("Failed to copy fonts")

            # Compile nunjucks templates
            cmd = 'nunjucks-precompile --include "\.html$" --include "\.ps1$" --include "\.sh$" --include "\.svg$" --include "\.yml$" --include "\.conf$" --include "\.mobileconfig$" %s > %s.part' % (static_path, bundle_js)
            click.echo("Compiling templates: %s" % cmd)
            if os.system(cmd):
                raise click.ClickException("Failed to compile nunjucks templates")

            # Assemble bundle.js
            click.echo("Assembling %s" % bundle_js)
            with open(bundle_js + ".part", "a") as fh:
                for pkg in "jquery/dist/jquery.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js", "node-forge/dist/forge.all.min.js", "qrcode-svg/dist/qrcode.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js":
                    for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
                        click.echo("- Merging: %s" % j)
                        with open(j) as ih:
                            fh.write(ih.read())

            # Assemble bundle.css
            click.echo("Assembling %s" % bundle_css)
            with open(bundle_css + ".part", "w") as fh:
                for pkg in "tether/dist/css/*.min.css", "bootstrap/dist/css/*.min.*css", "font-awesome/css/font-awesome.min.css":
                    for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
                        click.echo("- Merging: %s" % j)
                        with open(j) as ih:
                            fh.write(ih.read())

            os.rename(bundle_css + ".part", bundle_css)
            os.rename(bundle_js + ".part", bundle_js)

        assert os.getuid() == 0 and os.getgid() == 0
        _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude")
        os.setgid(gid)

        # Generate Certidude server config
        if not os.path.exists(const.CONFIG_DIR):
            click.echo("Creating %s" % const.CONFIG_DIR)
            os.makedirs(const.CONFIG_DIR)
        if not os.path.exists(const.SCRIPT_DIR):
            click.echo("Creating %s" % const.SCRIPT_DIR)
            os.makedirs(const.SCRIPT_DIR)

        os.umask(0o177) # 600

        if not os.path.exists(dhparam_path):
            cmd = "openssl", "dhparam", "-out", dhparam_path, ("1024" if os.getenv("TRAVIS") else str(const.KEY_SIZE))
            subprocess.check_call(cmd)

        if os.path.exists(tls_config.name):
            click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
        else:
            tls_config.write(env.get_template("nginx-tls.conf").render(locals()))
            click.echo("Generated %s" % tls_config.name)

        if os.path.exists(const.SERVER_CONFIG_PATH):
            click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
        else:
            push_token = "".join([random.choice(string.ascii_letters + string.digits) for j in range(0,32)])
            with open(const.SERVER_CONFIG_PATH, "w") as fh:
                fh.write(env.get_template("server/server.conf").render(vars()))
            click.echo("Generated %s" % const.SERVER_CONFIG_PATH)

        # Create image builder config
        if os.path.exists(const.BUILDER_CONFIG_PATH):
            click.echo("Image builder config %s already exists, remove to regenerate" % const.BUILDER_CONFIG_PATH)
        else:
            with open(const.BUILDER_CONFIG_PATH, "w") as fh:
                fh.write(env.get_template("server/builder.conf").render(vars()))
            click.echo("File %s created" % const.BUILDER_CONFIG_PATH)

        # Create image builder site script
        if os.path.exists(const.BUILDER_SITE_SCRIPT):
            click.echo("Image builder site customization script %s already exists, remove to regenerate" % const.BUILDER_SITE_SCRIPT)
        else:
            with open(const.BUILDER_SITE_SCRIPT, "w") as fh:
                fh.write(env.get_template("server/site.sh").render(vars()))
            click.echo("File %s created" % const.BUILDER_SITE_SCRIPT)

        # Create signature profile config
        if os.path.exists(const.PROFILE_CONFIG_PATH):
            click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
        else:
            with open(const.PROFILE_CONFIG_PATH, "w") as fh:
                fh.write(env.get_template("server/profile.conf").render(vars()))
            click.echo("File %s created" % const.PROFILE_CONFIG_PATH)

        # Create subdirectories with 770 permissions
        os.umask(0o007)
        for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta", "builder"):
            path = os.path.join(directory, subdir)
            if not os.path.exists(path):
                click.echo("Creating directory %s" % path)
                os.mkdir(path)
            else:
                click.echo("Directory already exists %s" % path)
            assert os.stat(path).st_mode == 0o40770, path

        # Create SQLite database file with correct permissions
        os.umask(0o117)
        if not os.path.exists(sqlite_path):
            with open(sqlite_path, "wb") as fh:
                pass

        # Generate and sign CA key
        if not os.path.exists(ca_key) or subordinate and not os.path.exists(ca_req):
            if elliptic_curve:
                click.echo("Generating %s EC key for CA ..." % const.CURVE_NAME)
                public_key, private_key = asymmetric.generate_pair("ec", curve=const.CURVE_NAME)
            else:
                click.echo("Generating %d-bit RSA key for CA ..." % const.KEY_SIZE)
                public_key, private_key = asymmetric.generate_pair("rsa", bit_size=const.KEY_SIZE)

            # Set permission bits to 600
            os.umask(0o177)
            with open(ca_key, 'wb') as f:
                f.write(asymmetric.dump_private_key(private_key, None))

            if subordinate:
                builder = CSRBuilder(distinguished_name, public_key)
                request = builder.build(private_key)
                with open(ca_req + ".part", 'wb') as f:
                    f.write(pem_armor_csr(request))
                os.rename(ca_req + ".part", ca_req)

        if not os.path.exists(ca_cert):
            if subordinate:
                click.echo("Request has been written to %s" % ca_req)
                click.echo()
                click.echo(open(ca_req).read())
                click.echo()
                click.echo("Get it signed and insert signed certificate into %s" % ca_cert)
                click.echo()
                click.echo("  cat > %s" % ca_cert)
                click.echo()
                click.echo("Paste contents and press Ctrl-D, adjust permissions:")
                click.echo()
                click.echo("  chown root:root %s" % ca_cert)
                click.echo("  chmod 0644 %s" % ca_cert)
                click.echo()
                click.echo("To finish setup procedure run 'certidude setup authority' again")
                sys.exit(1) # stop this fork here with error

            # https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
            builder = CertificateBuilder(distinguished_name, public_key)
            builder.self_signed = True
            builder.ca = True
            builder.serial_number = generate_serial()

            builder.begin_date = NOW - const.CLOCK_SKEW_TOLERANCE
            builder.end_date = NOW + timedelta(days=authority_lifetime)

            certificate = builder.build(private_key)

            # Set permission bits to 640
            os.umask(0o137)
            with open(ca_cert, 'wb') as f:
                f.write(pem_armor_certificate(certificate))
            click.echo("Authority certificate written to: %s" % ca_cert)

        sys.exit(0) # stop this fork here
    else:

        _, exitcode = os.waitpid(bootstrap_pid, 0)
        if exitcode:
            return 0
        from certidude import authority
        authority.self_enroll(skip_notify=True)
        assert os.path.exists(self_key)
        assert os.path.exists(os.path.join(directory, "signed", const.FQDN) + ".pem")
        assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
        assert os.stat(sqlite_path).st_mode == 0o100660
        assert os.stat(ca_cert).st_mode == 0o100640
        assert os.stat(ca_key).st_mode == 0o100600
        assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100600
        assert os.stat("/etc/certidude/server.conf").st_mode == 0o100600

        click.echo("Enabling and starting Certidude backend")
        os.system("systemctl enable certidude")
        os.system("systemctl restart certidude")
        click.echo("Enabling and starting nginx")
        os.system("systemctl enable nginx")
        os.system("systemctl start nginx")
        os.system("systemctl reload nginx")
        click.echo()

        click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH)
        click.echo()
        click.echo("Use following commands to inspect the newly created files:")
        click.echo()
        click.echo("  openssl x509 -text -noout -in %s | less" % ca_cert)
        click.echo("  openssl rsa -check -in %s" % ca_key)
        click.echo("  openssl verify -CAfile %s %s" % (ca_cert, ca_cert))
        click.echo()
        click.echo("To inspect logs and issued tokens:")
        click.echo()
        click.echo("  echo 'select * from log;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
        click.echo("  echo 'select * from token;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
        return 0


@click.command("users", help="List users")
def certidude_users():
    from certidude.user import User
    admins = set(User.objects.filter_admins())
    for user in User.objects.all():
        click.echo("%s;%s;%s;%s;%s" % (
            "admin" if user in admins else "user",
            user.name, user.given_name, user.surname, user.mail))


@click.command("list", help="List certificates")
@click.option("--verbose", "-v", default=False, is_flag=True, help="Verbose output")
@click.option("--show-key-type", "-k", default=False, is_flag=True, help="Show key type and length")
@click.option("--show-path", "-p", default=False, is_flag=True, help="Show filesystem paths")
@click.option("--show-extensions", "-e", default=False, is_flag=True, help="Show X.509 Certificate Extensions")
@click.option("--hide-requests", "-h", default=False, is_flag=True, help="Hide signing requests")
@click.option("--show-signed", "-s", default=False, is_flag=True, help="Show signed certificates")
@click.option("--show-revoked", "-r", default=False, is_flag=True, help="Show revoked certificates")
def certidude_list(verbose, show_key_type, show_extensions, show_path, show_signed, show_revoked, hide_requests):
    # Statuses:
    #   s - submitted
    #   v - valid
    #   e - expired
    #   y - not valid yet
    #   r - revoked
    from humanize import naturaltime
    from certidude import authority

    def dump_common(common_name, path, cert):
        click.echo("certidude revoke %s" % common_name)
        with open(path, "rb") as fh:
            buf = fh.read()
            click.echo("md5sum: %s" % hashlib.md5(buf).hexdigest())
            click.echo("sha1sum: %s" % hashlib.sha1(buf).hexdigest())
            click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest())
        click.echo()

    if not hide_requests:
        for common_name, path, buf, csr, submitted, server in authority.list_requests():
            created = 0
            if not verbose:
                click.echo("s " + path)
                continue
            click.echo()
            click.echo(click.style(common_name, fg="blue"))
            click.echo("=" * len(common_name))
            click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(created) + click.style(", %s" %created,  fg="white"))
            click.echo("openssl req -in %s -text -noout" % path)
            dump_common(common_name, path, csr)


    if show_signed:
        for common_name, path, buf, cert, signed, expires in authority.list_signed():
            if not verbose:
                if signed < NOW and NOW < expires:
                    click.echo("v " + path)
                elif expires < NOW:
                    click.echo("e " + path)
                else:
                    click.echo("y " + path)
                continue
            click.echo()
            click.echo(click.style(common_name, fg="blue") + " " + click.style("%040x" % cert.serial_number, fg="white"))
            click.echo("="*(len(common_name)+60))

            if signed < NOW and NOW < expires:
                click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(expires) + click.style(", %s" % expires,  fg="white"))
            elif NOW > expires:
                click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" % expires,  fg="white"))
            else:
                click.echo("Status: " + click.style("not valid yet", fg="red") + click.style(", %s" % expires,  fg="white"))
            click.echo()
            click.echo("openssl x509 -in %s -text -noout" % path)
            dump_common(common_name, path, cert)
            for ext in cert["tbs_certificate"]["extensions"]:
                click.echo(" - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)))

    if show_revoked:
        for common_name, path, buf, cert, signed, expires, revoked, reason in authority.list_revoked():
            if not verbose:
                click.echo("r " + path)
                continue
            click.echo()
            click.echo(click.style(common_name, fg="blue") + " " + click.style("%040x" % cert.serial_number, fg="white"))
            click.echo("="*(len(common_name)+60))

            click.echo("Status: " + click.style("revoked", fg="red") + " due to " + reason + " %s%s" % (naturaltime(NOW-revoked), click.style(", %s" % revoked, fg="white")))
            click.echo("openssl x509 -in %s -text -noout" % path)
            dump_common(common_name, path, cert)
            for ext in cert["tbs_certificate"]["extensions"]:
                click.echo(" - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)))


@click.command("sign", help="Sign certificate")
@click.argument("common_name")
@click.option("--profile", "-p", default="rw", help="Profile")
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
def certidude_sign(common_name, overwrite, profile):
    from certidude import authority, config
    drop_privileges()
    cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile])


@click.command("revoke", help="Revoke certificate")
@click.option("--reason", "-r", default="key_compromise", help="Revocation reason, one of: key_compromise affiliation_changed superseded cessation_of_operation privilege_withdrawn")
@click.argument("common_name")
def certidude_revoke(common_name, reason):
    from certidude import authority
    drop_privileges()
    authority.revoke(common_name, reason)


@click.command("expire", help="Move expired certificates")
def certidude_expire():
    from certidude import authority, config
    threshold = datetime.utcnow() - const.CLOCK_SKEW_TOLERANCE
    for common_name, path, buf, cert, signed, expires in authority.list_signed():
        if expires < threshold:
            expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
            click.echo("Moving %s to %s" % (path, expired_path))
            os.rename(path, expired_path)
            os.remove(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
    for common_name, path, buf, cert, signed, expires, revoked, reason in authority.list_revoked():
        if expires < threshold:
            expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
            click.echo("Moving %s to %s" % (path, expired_path))
            os.rename(path, expired_path)
    # TODO: Send e-mail


@click.command("serve", help="Run server")
@click.option("-p", "--port", default=8080, help="Listen port")
@click.option("-l", "--listen", default="127.0.1.1", help="Listen address")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
def certidude_serve(port, listen, fork):
    from certidude import authority, const, push

    if port == 80:
        click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!")

    click.echo("Using configuration from: %s" % const.SERVER_CONFIG_PATH)

    log_handlers = []

    from certidude import config

    click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS)
    click.echo("CRL subnets: %s" % config.CRL_SUBNETS)
    click.echo("SCEP subnets: %s" % config.SCEP_SUBNETS)

    click.echo("Loading signature profiles:")
    for profile in config.PROFILES.values():
         click.echo("- %s" % profile)
    click.echo()

    # Rebuild reverse mapping
    for cn, path, buf, cert, signed, expires in authority.list_signed():
        by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number)
        if not os.path.exists(by_serial):
            click.echo("Linking %s to ../%s.pem" % (by_serial, cn))
            os.symlink("../%s.pem" % cn, by_serial)

    # Process directories
    if not os.path.exists(const.RUN_DIR):
        click.echo("Creating: %s" % const.RUN_DIR)
        os.makedirs(const.RUN_DIR)
        os.chmod(const.RUN_DIR, 0o755)

    click.echo("Users subnets: %s" %
        ", ".join([str(j) for j in config.USER_SUBNETS]))
    click.echo("Administrative subnets: %s" %
        ", ".join([str(j) for j in config.ADMIN_SUBNETS]))
    click.echo("Auto-sign enabled for following subnets: %s" %
        ", ".join([str(j) for j in config.AUTOSIGN_SUBNETS]))
    click.echo("Request submissions allowed from following subnets: %s" %
        ", ".join([str(j) for j in config.REQUEST_SUBNETS]))

    click.echo("Serving API at %s:%d" % (listen, port))
    from wsgiref.simple_server import make_server, WSGIServer
    from certidude.api import certidude_app


    click.echo("Listening on %s:%d" % (listen, port))

    app = certidude_app(log_handlers)
    httpd = make_server(listen, port, app, WSGIServer)


    """
    Drop privileges
    """


    # Initialize LDAP service ticket
    if os.path.exists("/etc/cron.hourly/certidude"):
        os.system("/etc/cron.hourly/certidude")

    from certidude.push import EventSourceLogHandler
    log_handlers.append(EventSourceLogHandler())

    for j in logging.Logger.manager.loggerDict.values():
        if isinstance(j, logging.Logger): # PlaceHolder is what?
            if j.name.startswith("certidude."):
                j.setLevel(logging.DEBUG)
                for handler in log_handlers:
                    j.addHandler(handler)

    if not fork or not os.fork():
        pid = os.getpid()
        with open(const.SERVER_PID_PATH, "w") as pidfile:
            pidfile.write("%d\n" % pid)

        def cleanup_handler(*args):
            push.publish("server-stopped")
            logger.debug("Shutting down Certidude")
            sys.exit(0) # TODO: use another code, needs test refactor

        import signal
        signal.signal(signal.SIGTERM, cleanup_handler) # Handle SIGTERM from systemd

        push.publish("server-started")
        logger.debug("Started Certidude at %s", const.FQDN)

        drop_privileges()
        try:
            httpd.serve_forever()
        except KeyboardInterrupt:
            cleanup_handler() # FIXME


@click.command("yubikey", help="Set up Yubikey as client authentication token")
@click.argument("authority")
@click.option("-p", "--pin", default="123456", help="Slot pincode, 123456 by default")
@click.option("-s", "--slot", default="9a", help="Yubikey slot to use, 9a by default")
@click.option("-u", "--username", default=os.getenv("USER"), help="Username to use, %s by default" % os.getenv("USER"))
def certidude_setup_yubikey(authority, slot, username, pin):
    import requests
    cmd = "ykinfo", "-q", "-s"
    click.echo("Executing: %s" % " ".join(cmd))
    serial = subprocess.check_output(cmd).strip()

    dn = "/CN=%s@yk-%s-%s" % (username, slot, serial)

    cmd = "yubico-piv-tool", "-a", "generate", "-s", slot, "-o", "/tmp/pk.pem"
    click.echo("Executing: %s" % " ".join(cmd))
    subprocess.call(cmd)

    cmd = "yubico-piv-tool", \
        "-i", "/tmp/pk.pem", "-o", "/tmp/req.pem", \
        "-P", pin, \
        "-S", dn, \
        "-a", "verify", "-a", "request", \
        "-s", slot
    click.echo("Executing: %s" % " ".join(cmd))

    scheme = "http"
    request_url = "%s://%s/api/request/?wait=true" % (scheme, authority)

    subprocess.check_output(cmd)
    click.echo("Submitting to %s, waiting for response..." % request_url)
    headers={
        "Content-Type": "application/pkcs10",
        "Accept": "application/x-x509-user-cert,application/x-pem-file"
    }

    submission = requests.post(request_url, data=open("/tmp/req.pem"), headers=headers)
    with open("/tmp/cert.pem", "w") as fh:
        fh.write(submission.text)

    cmd = "yubico-piv-tool", "-a", "import-certificate", "-s", slot, "-i", "/tmp/cert.pem"
    click.echo("Executing: %s" % " ".join(cmd))
    subprocess.call(cmd)


@click.command("test", help="Test mailer")
@click.argument("recipient")
def certidude_test(recipient):
    from certidude import mailer
    mailer.send(
        "test.md",
        to=recipient
    )

@click.command("list", help="List tokens")
def certidude_token_list():
    from certidude import config
    from certidude.tokens import TokenManager
    token_manager = TokenManager(config.TOKEN_DATABASE)
    cols = "uuid", "expires", "subject", "state"
    now = datetime.utcnow()
    for token in token_manager.list(expired=True, used=True, token=True):
        token["state"] = "used" if token.get("used") else ("valid" if token.get("expires") > now  else "expired")
        print(";".join([str(token.get(col)) for col in cols]))

@click.command("purge", help="Purge tokens")
@click.option("-a", "--all", default=False, is_flag=True, help="Purge all not only expired tokens")
def certidude_token_purge(all):
    from certidude import config
    from certidude.tokens import TokenManager
    token_manager = TokenManager(config.TOKEN_DATABASE)
    print(token_manager.purge(all))

@click.command("issue", help="Issue token")
@click.option("-m", "--subject-mail", default=None, help="Subject e-mail override")
@click.argument("subject")
def certidude_token_issue(subject, subject_mail):
    from certidude import config
    from certidude.tokens import TokenManager
    from certidude.user import User
    token_manager = TokenManager(config.TOKEN_DATABASE)
    token_manager.issue(None, User.objects.get(subject), subject_mail)


@click.group("strongswan", help="strongSwan helpers")
def certidude_setup_strongswan(): pass

@click.group("openvpn", help="OpenVPN helpers")
def certidude_setup_openvpn(): pass

@click.group("setup", help="Getting started section")
def certidude_setup(): pass

@click.group("token", help="Token management")
def certidude_token(): pass

@click.group()
def entry_point(): pass

certidude_setup_strongswan.add_command(certidude_setup_strongswan_server)
certidude_setup_strongswan.add_command(certidude_setup_strongswan_client)
certidude_setup_strongswan.add_command(certidude_setup_strongswan_networkmanager)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_server)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_client)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_networkmanager)
certidude_setup.add_command(certidude_setup_authority)
certidude_setup.add_command(certidude_setup_openvpn)
certidude_setup.add_command(certidude_setup_strongswan)
certidude_setup.add_command(certidude_setup_nginx)
certidude_setup.add_command(certidude_setup_yubikey)
certidude_token.add_command(certidude_token_list)
certidude_token.add_command(certidude_token_purge)
certidude_token.add_command(certidude_token_issue)
entry_point.add_command(certidude_token)
entry_point.add_command(certidude_setup)
entry_point.add_command(certidude_serve)
entry_point.add_command(certidude_enroll)
entry_point.add_command(certidude_sign)
entry_point.add_command(certidude_revoke)
entry_point.add_command(certidude_list)
entry_point.add_command(certidude_expire)
entry_point.add_command(certidude_users)
entry_point.add_command(certidude_test)

if __name__ == "__main__":
    entry_point()
