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

import subprocess
import os
import sys
import re
import logging
import shutil
import tempfile
import urllib2
import socket
import errno
from contextlib import closing

TORTIX_WAF_FILENAME = "tortix_waf.conf"
PLESK_MANAGED_DIRECTIVES = [
    "SecRuleEngine",
    "SecRequestBodyAccess",
    "SecRequestBodyLimit",
    "SecResponseBodyLimit",
    "SecAuditEngine",
    "SecAuditLogRelevantStatus",
    "SecAuditLog",
    "SecAuditLogType",
]

BIN_AUM = "/var/asl/bin/aum"
ASL_CONFIG = "/etc/asl/config"
ASL_LICENSE = "/etc/asl/license.key"
ASL_LICENSE_SAVED = ASL_LICENSE + ".saved_by_plesk"

AUM_EXIT_NO_ERROR = 0
AUM_EXIT_NO_ARGS = 1
AUM_EXIT_HANDLED = 2
AUM_EXIT_FATAL = 3


ATOMIC_MOD_SECURITY_CONF = "/etc/apache2/conf-enabled/00_mod_security.conf"

AUTOGENERATED_CONFIGS = "#ATTENTION!\n#\n#DO NOT MODIFY THIS FILE BECAUSE IT WAS GENERATED AUTOMATICALLY,\n#SO ALL YOUR CHANGES WILL BE LOST THE NEXT TIME THE FILE IS GENERATED.\n"

AUM_PLESK_INSTALLER_NAME = "aum"
AUM_PLESK_INSTALLER_URL = "https://updates.atomicorp.com/installers/" + AUM_PLESK_INSTALLER_NAME
AUM_PLESK_INSTALLER_SIGNATURE_URL = AUM_PLESK_INSTALLER_URL + ".asc"

GPGHOMEDIR = "/var/lib/plesk/modsec/.gnupg"
ATOMICORP_GPG_PUBLIC_KEY_PATH = "/var/lib/plesk/modsec/atomicorp.gpg.key"

def get_apache_version():
    apache_ctl_path = ["/usr/sbin/apache2ctl", "/usr/sbin/apachectl"]
    for apache_ctl in apache_ctl_path:
        if os.path.exists(apache_ctl):
            popen = subprocess.Popen([apache_ctl, "-v"], stdout=subprocess.PIPE)
            (out, err) = popen.communicate()
            result = re.search('Server version:\s+\w+/(\d+\.\d+\.\d+)', out)
            if result is None:
                raise Exception("Unable to parse apache version: %s" % out)
            return [int(x) for x in result.group(1).split('.')]
    raise Exception("Unable to find apachectl unility")

def apache_version_ge_2_4():
    version = get_apache_version()
    return version[0] > 2 or (version[0] == 2 and version[1] >= 4)

def create_include_conf(conf_path):
    inc = "Include"
    if apache_version_ge_2_4():
        inc = "IncludeOptional"
    with open(conf_path, 'w') as conf:
        conf.write(inc + " @ruleset_base_dir@/modsec/" + TORTIX_WAF_FILENAME + "\n")
        conf.write(inc + " @ruleset_base_dir@/modsec/00*exclude.conf\n")
        conf.write(inc + " @ruleset_base_dir@/modsec/*asl*.conf\n")
        conf.write(inc + " @ruleset_base_dir@/modsec/99*exclude.conf\n")

def comment_plesk_managed_directives(tortix_waf):
    args = ["sed", "-i"]
    for i in PLESK_MANAGED_DIRECTIVES:
        args.append("-e")
        args.append("s|^%s\\b|# \\0|g" % i)
    args.append(tortix_waf)
    subprocess.check_call(args)

def download(url, path, retries=3):
    try_number = 1
    while True:
        try:
            with open(path, "w") as fout:
                with closing(urllib2.urlopen(url, timeout=15)) as fin:
                    for line in fin:
                        fout.write(line)
            break
        except urllib2.URLError, e:
            if try_number < retries and                     (isinstance(e.reason, socket.timeout) or                     (isinstance(e.reason, socket.error) and                     (e.reason.errno == errno.ETIMEDOUT))):
                logging.warning("%s. Retry", e)
                try_number += 1
            else:
                raise

def verify_gpg_signature(path, signature_url, public_key):
    signature_path = path + ".asc"
    download(signature_url, signature_path)
    subprocess.check_call(["gpg", "--homedir", GPGHOMEDIR, "--import", public_key])
    subprocess.check_call(["gpg", "--homedir", GPGHOMEDIR, "--verify", signature_path])

def run_aum_plesk_installer():
    installer_dir = None
    try:
        installer_dir = tempfile.mkdtemp()
        installer_path = os.path.join(installer_dir, AUM_PLESK_INSTALLER_NAME)
        download(AUM_PLESK_INSTALLER_URL, installer_path)
        verify_gpg_signature(installer_path, AUM_PLESK_INSTALLER_SIGNATURE_URL, ATOMICORP_GPG_PUBLIC_KEY_PATH)
        subprocess.check_call("/bin/bash < %s" % installer_path, shell=True)
    finally:
        if installer_dir:
            shutil.rmtree(installer_dir, ignore_errors=True)

def prepare_ruleset_layout(base_dir):
    modsec_d = os.path.join(base_dir, "modsec")
    if not os.path.isdir(modsec_d):
        os.makedirs(modsec_d)
    open(os.path.join(base_dir, "plesk_init.conf"), "w").close() # file for aum not to create 00_modsecurity.conf
    create_include_conf(os.path.join(base_dir, "plesk_init.conf.tpl"))

def fix_asl_config(configs_dir, username=None, password=None):
    cmd = ["sed", "-i",
        "-e", "s#^MODSEC_RULES_PATH\s*=.*#MODSEC_RULES_PATH=\"%s\"#g" % configs_dir,
        "-e", "s#^RESTART_APACHE\s*=.*#RESTART_APACHE=\"no\"#g",
        "-e", "s#^AUTOMATIC_UPDATES\s*=.*#AUTOMATIC_UPDATES=\"no\"#g"]
    if username and password:
        # atomic subscription ruleset
        cmd.append("-e")
        cmd.append("s#^USERNAME\s*=.*#USERNAME=\"%s\"#g" % username)
        cmd.append("-e")
        cmd.append("s#^PASSWORD\s*=.*#PASSWORD=\"%s\"#g" % password)
        cmd.append("-e")
        cmd.append("s#^FEED_SOURCE\s*=.*#FEED_SOURCE=\"plesk\"#g")
        cmd.append("-e")
        cmd.append("s#^UPDATE_PATH\s*=.*#UPDATE_PATH=\"/channels/rules/subscription\"#g")
# prevent aum from configuring ASL:
        cmd.append("-e")
        cmd.append("s#^CONFIGURED\s*=.*#CONFIGURED=\"yes\"#g")
    cmd.append(ASL_CONFIG)
    subprocess.check_call(cmd)

def deactivate_00_mod_security_conf():
    if os.path.isfile(ATOMIC_MOD_SECURITY_CONF):
        with open(ATOMIC_MOD_SECURITY_CONF, "w") as f:
            f.write(AUTOGENERATED_CONFIGS)
    # W/A for case if our mod_security package was replaced by atomic one:
    plesk_conf_path = "/etc/apache2/conf-enabled/security2.conf"
    plesk_conf_path_rpmsave = plesk_conf_path + ".rpmsave"
    if os.path.isfile(plesk_conf_path_rpmsave):
        os.rename(plesk_conf_path_rpmsave, plesk_conf_path)

def run_aum():
    p = subprocess.Popen([BIN_AUM, "-uf"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    (out, err) = p.communicate()
    if p.returncode == AUM_EXIT_NO_ERROR:
        return
    # handle errors
    if p.returncode == AUM_EXIT_HANDLED:
        sys.stderr.write("Non-fatal error during aum execution.\nstdout: %s\nstderr: %s\nContinue execution." % (out, err))
    else:
        raise Exception("aum failed with exitcode %d.\nstdout: %s\nstderr: %s" % (p.returncode, out, err))

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