# Copyright 1999-2017. Parallels IP Holdings GmbH. All Rights Reserved.

import logging as log
import os
import plesk_subprocess as subprocess
import pwd
import grp
import re
import shutil

from distutils.version import StrictVersion
from common import *

class SslModuleException(Exception):
    pass

class ApacheModule:
    _config = "/etc/apache2/mods-available/ssl.conf"
    _prepend = "<IfModule mod_ssl.c>"
    _append = "</IfModule>"
    _ctl_tool_path = ""

    def __init__(self):
        self._ctl_tool_path = self._get_ctl_tool_path()

    def _get_ctl_tool_path(self):
        apache_ctl_path = ["/usr/sbin/apache2ctl", "/usr/sbin/apachectl"]
        for apache_ctl in apache_ctl_path:
            if os.path.exists(apache_ctl):
                return apache_ctl
        return None

    def _get_apache_version(self):
        popen = subprocess.Popen([self._ctl_tool_path, "-v"], stdout=subprocess.PIPE)
        (out, err) = popen.communicate()
        result = re.search('Server version:\s+\w+/(\d+\.\d+\.\d+)', out)
        if result is None:
            raise SslModuleException("Unable to parse apache version: %s" % out)
        return [int(x) for x in result.group(1).split('.')]

    def is_installed(self):
        if self._ctl_tool_path:
            return True
        return False

    def ciphers(self, ciphers):
        config_set("SSLCipherSuite", "SSLCipherSuite %s" % ciphers, self._config, self._prepend, self._append)
        config_set("SSLHonorCipherOrder", "SSLHonorCipherOrder on", self._config, self._prepend, self._append)

    def protocols(self, protocols):
        if False:
            # Ubuntu 12 : https://bugs.launchpad.net/ubuntu/+source/apache2/+bug/1400473
            protocols_string = "all " + ' '.join(["-%s" % x for x in "SSLv3 TLSv1 TLSv1.1 TLSv1.2".split() if x not in protocols.split()])
        else:
            protocols_string = ' '.join(["+%s" % x for x in protocols.split()])

        config_set("SSLProtocol", "SSLProtocol %s" % protocols_string, self._config, self._prepend, self._append)

    def tls_compression(self, enable = False):
        apache_ver = StrictVersion(".".join([str(x) for x in self._get_apache_version()]))
        if apache_ver < StrictVersion("2.2.24"):
            log.debug("* Skip disabling TLS compression on apache %s < 2.2.24", apache_ver)
            return

        config_set("SSLCompression", "SSLCompression off", self._config, self._prepend, self._append)

    def dh(self, dh_size, regenerate = False):
        pass

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        log.debug("- At the moment, SSL certificate management is not supported for the apache service.")

    def services_list(self):
        return ["apache2"]

class AutoinstallerModule:
    _config = ""

    def __init__(self):
        self._config = os.path.expanduser('~root/.autoinstallerrc')

    def is_installed(self):
        return True

    def ciphers(self, ciphers):
        config_set('SSL_CIPHERS', 'SSL_CIPHERS="%s"' % ciphers, self._config)

    def protocols(self, protocols):
        config_set('SSL_PROTOCOLS', 'SSL_PROTOCOLS="%s"' % protocols, self._config)

    def tls_compression(self, enable = False):
        pass

    def dh(self, dh_size, regenerate = False):
        pass

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        log.debug("- At the moment, SSL certificate management is not supported for autoinstaller.")

    def services_list(self):
        return list()

class CourierImapModule:
    """ configure courier-imap, by writing proper data in its config. Note, it doesn't support protocols list, and
        we write only minimal protocol """
    _confdir = "/etc/courier-imap"
    _configs = ["imapd-ssl", "pop3d-ssl"]

    def is_installed(self):
        if os.path.isfile(os.path.join(self._confdir, "imapd")):
            return True
        return False

    def ciphers(self, ciphers):
        for config in self._configs:
            config_set("TLS_CIPHER_LIST", "TLS_CIPHER_LIST=%s" % ciphers, os.path.join(self._confdir, config))

    def protocols(self, protocols):
        protos = {
            "SSLv3": "SSL3+",
            "TLSv1": "TLSv1",
            "TLSv1.1": "TLSv1.1+",
            "TLSv1.2": "TLSv1.2+"
        }
        proto = protos.get(protocols.split()[0])

        for config in self._configs:
            config_set("TLS_PROTOCOL", "TLS_PROTOCOL=%s" % proto, os.path.join(self._confdir, config))
            config_set("TLS_STARTTLS_PROTOCOL", "TLS_STARTTLS_PROTOCOL=%s" % proto, os.path.join(self._confdir, config))

    def tls_compression(self, enable = False):
        pass

    def dh(self, dh_size, regenerate = False):
        shutil.copyfile(get_dhparams_path(dh_size), "/usr/share/dhparams.pem")

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        try:
            # Deinstall installed certificate
            if not certfile:
                config_set("TLS_CERTFILE", "TLS_CERTFILE=/usr/share/imapd.pem", os.path.join(self._confdir, "imapd-ssl"))
                config_set("TLS_TRUSTCERTS", "TLS_TRUSTCERTS=/usr/share/imapd.pem", os.path.join(self._confdir, "imapd-ssl"))

                config_set("TLS_CERTFILE", "TLS_CERTFILE=/usr/share/pop3d.pem", os.path.join(self._confdir, "pop3d-ssl"))
                config_set("TLS_TRUSTCERTS", "TLS_TRUSTCERTS=/usr/share/pop3d.pem", os.path.join(self._confdir, "pop3d-ssl"))
                return

            validate_cert(certfile, private_key, chained_certs)

            dst_certfile="/etc/courier-imap/courier.pem"
            file_concat(dst_certfile, certfile, private_key, chained_certs)

            for config in self._configs:
                config_set("TLS_CERTFILE", "TLS_CERTFILE=%s" % dst_certfile, os.path.join(self._confdir, config))
                config_set("TLS_TRUSTCERTS", "TLS_TRUSTCERTS=%s" % dst_certfile, os.path.join(self._confdir, config))

            config_set("IMAPDSTARTTLS", "IMAPDSTARTTLS=YES", os.path.join(self._confdir, "imapd-ssl"))

        except Exception, ex:
            raise SslModuleException("Unable to configure SSL certificate '%s'", ex)

    def services_list(self):
        return ["courier-imapd", "courier-imaps", "courier-pop3d", "courier-pop3s"]

class DovecotModule:
    _config = ""
    _include_dir = "/etc/dovecot/conf.d"

    def __init__(self):
        self._config = os.path.join(self._include_dir, "11-plesk-security-ssl.conf")

    def is_installed(self):
        if os.path.isfile(os.path.join("/etc/dovecot", "dovecot.conf")):
            return True
        return False

    def ciphers(self, ciphers):
        config_set("ssl_cipher_list", "ssl_cipher_list = %s" % ciphers, self._config)

    def protocols(self, protocols):
        config_set("ssl_protocols", "ssl_protocols = %s" % protocols, self._config)

    def tls_compression(self, enable = False):
        pass

    def dh(self, dh_size, regenerate = False):
        config_set("ssl_dh_parameters_length", "ssl_dh_parameters_length = %d" % dh_size, self._config)

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        certdir = "/etc/dovecot/private"

        try:
            # Here is self-signed Plesk certificate
            default_certfile= os.path.join(certdir, "ssl-cert-and-key.pem")

            # Here we will place a custom certificate
            custom_certfile= os.path.join(certdir, "dovecot.pem")

            config_set("ssl", "ssl= yes", self._config)
            dst_certfile = default_certfile

            # Use custom certificate if it exists or default if doesn't
            if certfile:
                dst_certfile = custom_certfile
                validate_cert(certfile, private_key, chained_certs)
                file_concat(dst_certfile, certfile, private_key, chained_certs)

            # Configure dovecot service with new certificate file
            config_set("ssl_cert", "ssl_cert= <%s" % dst_certfile, self._config)
            config_set("ssl_key", "ssl_key= <%s" % dst_certfile, self._config)

        except Exception, ex:
            raise SslModuleException("Unable to configure SSL certificate '%s'", ex)

    def services_list(self):
        return ["dovecot"]

class NginxModule:
    _confdir = "/etc/nginx/conf.d"
    _config = ""

    def __init__(self):
        self._config = os.path.join(self._confdir, "ssl.conf")

    def is_installed(self):
        if os.path.isfile("/etc/nginx/nginx.conf"):
            return True
        return False

    def ciphers(self, ciphers):
        config_set("ssl_ciphers", "ssl_ciphers %s;" % ciphers, self._config)
        config_set("ssl_prefer_server_ciphers", "ssl_prefer_server_ciphers on;", self._config)

    def protocols(self, protocols):
        config_set("ssl_protocols", "ssl_protocols %s;" % protocols, self._config)

    def tls_compression(self, enable = False):
        pass

    def dh(self, dh_size, regenerate = False):
        config_set("ssl_dhparam", "ssl_dhparam %s;" % get_dhparams_path(dh_size), self._config)

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        log.debug("- At the moment, SSL certificate management is not supported for the nginx service.")

    def services_list(self):
        return ["nginx"]

class PostfixModule:
    _postconf = "/usr/sbin/postconf"

    def is_installed(self):
        if os.path.isfile(self._postconf):
            return True
        return False

    def ciphers(self, ciphers):
        subprocess.check_call([self._postconf, "-e", "smtpd_tls_ciphers=medium"])
        subprocess.check_call([self._postconf, "-e", "smtpd_tls_mandatory_ciphers=medium"])
        subprocess.check_call([self._postconf, "-e", "tls_medium_cipherlist=%s" % ciphers])

    def protocols(self, protocols):
        # See https://bettercrypto.org/static/applied-crypto-hardening.pdf
        subprocess.check_call([self._postconf, "-e", "smtpd_tls_mandatory_protocols=%s" % protocols])
        subprocess.check_call([self._postconf, "-e", "smtpd_tls_protocols=%s" % protocols])

    def _postfix_version(self):
        popen = subprocess.Popen([self._postconf, "-d", "mail_version"], stdout=subprocess.PIPE)
        (out, err) = popen.communicate()

        result = re.search(r'mail_version\s=\s(\d+\.\d+\.\d+)', out)
        if result is None:
            raise SslModuleException("Unable to parse postfix version: %s" % out)
        return result.group(1)

    def tls_compression(self, enable = False):
        postfix_ver = StrictVersion(self._postfix_version())
        if postfix_ver < StrictVersion("2.11"):
            log.debug("* Skip disabling TLS compression on postfix %s < 2.11", postfix_ver)
            return

        subprocess.check_call([self._postconf, "-e", "tls_ssl_options=no_compression"])

    def dh(self, dh_size, regenerate = False):
        subprocess.check_call([self._postconf, "-e", "smtpd_tls_dh1024_param_file=%s" % get_dhparams_path(dh_size)])

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        try:
            # Expose STARTTLS in SMTP session(default value)
            subprocess.check_call([self._postconf, "-e", "smtpd_tls_security_level=may"])

            # We expect a private key is always in certificate file.
            subprocess.check_call([self._postconf, "-e", "smtpd_tls_key_file=$smtpd_tls_cert_file"])

            confdir="/etc/postfix"
            default_certfile= os.path.join(confdir, "postfix_default.pem")
            custom_certfile= os.path.join(confdir, "postfix.pem")
            dst_certfile= default_certfile

            # Install custom certificate if exists or use default if doesn't
            if certfile:
                dst_certfile= custom_certfile
                validate_cert(certfile, private_key, chained_certs)
                file_concat(dst_certfile, certfile, private_key, chained_certs)

            # Configure postfix to work with our certificate
            subprocess.check_call([self._postconf, "-e", "smtpd_tls_cert_file=%s" % dst_certfile])

        except Exception, ex:
            raise SslModuleException("Unable to configure SSL certificate '%s'", ex)

    def services_list(self):
        return ["postfix"]

class ProftpdModule:
    _config = "/etc/proftpd.d/ssl.conf"
    _prepend = "<Global>\n<IfModule mod_tls.c>"
    _append = "</IfModule>\n</Global>\n"

    def is_installed(self):
        if os.path.isfile("/etc/proftpd.conf"):
            return True
        return False

    def ciphers(self, ciphers):
        config_set("TLSCipherSuite", "TLSCipherSuite %s" % ciphers, self._config, self._prepend, self._append)

    def protocols(self, protocols):
        config_set("TLSProtocol", "TLSProtocol %s" % protocols, self._config, self._prepend, self._append)

    def tls_compression(self, enable = False):
        pass

    def dh(self, dh_size, regenerate = False):
        config_set("TLSDHParamFile", "TLSDHParamFile %s" % get_dhparams_path(dh_size), self._config, self._prepend, self._append)

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        log.debug("- At the moment, SSL certificate management is not supported for the proftpd service.")

    def services_list(self):
        return list()

class QmailModule:
    # Do not rely on configs while detecting if service is installed: they may be preserved during mailservers switch.
    # Check for executable files instead.
    _qmail_start = "/var/qmail/bin/qmail-start"

    def is_installed(self):
        if os.path.isfile(self._qmail_start):
            return True
        return False

    def ciphers(self, ciphers):
        log.debug("- At the moment, ciphers management is not supported for the qmail service.")

    def protocols(self, protocols):
        log.debug("- At the moment, protocol management is not supported for the qmail service.")

    def tls_compression(self, enable = False):
        log.debug("- At the moment, TLS compression management is not supported for the qmail service.")

    def dh(self, dh_size, regenerate = False):
        log.debug("- At the moment, DH param size management is not supported for the qmail service.")

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        try:
            default_certfile="/var/qmail/control/default_servercert.pem"
            dst_certfile="/var/qmail/control/servercert.pem"

            uid = pwd.getpwnam("qmaild").pw_uid
            gid = grp.getgrnam('qmail').gr_gid

            # Backup default certificate
            if not os.path.isfile(default_certfile):
                    shutil.copyfile(dst_certfile, default_certfile)
                    os.shown(default_certfile, uid, gid)
                    os.chmod(default_certfile, 0640)

            # Install a custom certificate if exists or default if doesn't
            if certfile:
                validate_cert(certfile, private_key, chained_certs)
                file_concat(dst_certfile, certfile, private_key, chained_certs)
            else:
                if os.path.isfile(dst_certfile):
                    os.unlink(dst_certfile)
                shutil.copyfile(default_certfile, dst_certfile)

            os.chown(dst_certfile, uid, gid)
            os.chmod(dst_certfile, 0640)

        except Exception, ex:
            raise SslModuleException("Unable to configure SSL certificate '%s'", ex)

    def services_list(self):
        return ["qmail"]

class MsmtpModule:
    _config = "/etc/msmtprc"

    def is_installed(self):
        if os.path.isfile(self._config):
            return True
        return False

    def ciphers(self, ciphers):
        log.debug("- At the moment, ciphers management is not supported for the msmtp service.")

    def protocols(self, protocols):
        log.debug("- At the moment, protocols management is not supported for the msmtp service.")

    def tls_compression(self, enable = False):
        log.debug("- At the moment, TLS compression management is not supported for the msmtp service.")

    def dh(self, dh_size, regenerate = False):
        log.debug("- At the moment, DH param size management is not supported for the msmtp service.")

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        # We do not use certificates for SMTP clients yet. Need to ask PMs.
        log.debug("- At the moment, SSL certificate management is not supported for the msmtp service.")
        return
        try:
            dst_certfile="/etc/msmtp.pem"

            # Deinstall installed certificate
            if not certfile:
                if os.path.isfile(dst_certfile):
                    os.unlink(dst_certfile)
                return

            validate_cert(certfile, private_key, chained_certs)
            file_concat(dst_certfile, certfile, private_key, chained_certs)

            config_set("tls_cert_file", "tls_cert_file %s" % dst_certfile, self._config)
            config_set("tls_key_file", "tls_key_file %s" % dst_certfile, self._config)
            config_set("tls_trust_file", "tls_trust_file %s" % dst_certfile, self._config)

        except Exception, ex:
            raise SslModuleException("Unable to configure SSL certificate '%s'", ex)

    def services_list(self):
        return list()

class CPServerModule:
    _config = "/etc/sw-cp-server/conf.d/ssl.conf"

    def is_installed(self):
        return True

    def ciphers(self, ciphers):
        config_set("ssl_ciphers", "ssl_ciphers %s;" % ciphers, self._config)
        config_set("ssl_prefer_server_ciphers", "ssl_prefer_server_ciphers on;", self._config)

    def protocols(self, protocols):
        config_set("ssl_protocols", "ssl_protocols %s;" % protocols, self._config)

    def tls_compression(self, enable = False):
        pass

    def dh(self, dh_size, regenerate = False):
        config_set("ssl_dhparam", "ssl_dhparam %s;" % get_dhparams_path(dh_size), self._config)

    def certificate(self, certfile = None, private_key = None, chained_certs = None):
        log.debug("- At the moment, SSL certificate management is not supported for the sw-cp-server service.")

    def services_list(self):
        return ["sw-cp-server"]

SERVICES = {
    "apache": ApacheModule,
    "autoinstaller": AutoinstallerModule,
    "courier": CourierImapModule,

    # Add compatibility with mailmng --features to simplify BL behaviour
    "courier-imap": CourierImapModule,

    "dovecot": DovecotModule,
    "msmtp": MsmtpModule,
    "nginx": NginxModule,
    "postfix": PostfixModule,
    "proftpd": ProftpdModule,
    "qmail": QmailModule,
    "sw-cp-server": CPServerModule
}

# vim: ts=4 sts=4 sw=4 et :
