#!/usr/bin/env python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# .------------------------------------------------------------------------.
# |                ____ _               _        __  __ _  __              |
# |               / ___| |__   ___  ___| | __   |  \/  | |/ /              |
# |              | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /               |
# |              | |___| | | |  __/ (__|   <    | |  | | . \               |
# |               \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\              |
# |                                        |_____|                         |
# |             _____       _                       _                      |
# |            | ____|_ __ | |_ ___ _ __ _ __  _ __(_)___  ___             |
# |            |  _| | '_ \| __/ _ \ '__| '_ \| '__| / __|/ _ \            |
# |            | |___| | | | ||  __/ |  | |_) | |  | \__ \  __/            |
# |            |_____|_| |_|\__\___|_|  | .__/|_|  |_|___/\___|            |
# |                                     |_|                                |
# |                     _____    _ _ _   _                                 |
# |                    | ____|__| (_) |_(_) ___  _ __                      |
# |                    |  _| / _` | | __| |/ _ \| '_ \                     |
# |                    | |__| (_| | | |_| | (_) | | | |                    |
# |                    |_____\__,_|_|\__|_|\___/|_| |_|                    |
# |                                                                        |
# | mathias-kettner.com                                 mathias-kettner.de |
# '------------------------------------------------------------------------'
#  This file is part of the Check_MK Enterprise Edition (CEE).
#  Copyright by Mathias Kettner and Mathias Kettner GmbH.  All rights reserved.
#
#  Distributed under the Check_MK Enterprise License.
#
#  You should have  received  a copy of the Check_MK Enterprise License
#  along with Check_MK. If not, email to mk@mathias-kettner.de
#  or write to the postal address provided at www.mathias-kettner.de


# FIXME We do not know the python version on client side. One Problem is
# The definitive document is PEP-3110: Catching Exceptions
# Summary:
# - In Python 3.x, using as is required to assign an exception to a variable.
# - In Python 2.6+, use the as syntax, since it is far less ambiguous and forward compatible with Python 3.x.
# - In Python 2.5 and earlier, use the comma version, since as isn't supported.


import getopt
import getpass
import os
import re
import sys
import tempfile
import shutil
import time
import subprocess
from distutils.spawn import find_executable
import errno

__version__ = "1.4.0p34"


# The default is to use the pyOpenSSL bindings, but when it is not available,
# the regular openssl commands are used. Hope they are available.
try:
    import OpenSSL
    if OpenSSL.__version__ != '0.12':
        from OpenSSL import crypto
    else:
        # Workaround for broken version of pyOpenSSL
        crypto = None
except:
    crypto = None


try:
    import syslog
except ImportError:
    # no syslog on windows
    syslog = None

if os.name == "posix":
    import fcntl
    import pwd
elif os.name == "nt":
    import msvcrt
    import win32api
    import win32security
    import ntsecuritycon


""" not using windows event log, at least for now
try:
    import win32evtlog
except ImportError:
    # no win32evtlog on anything but win32, and even there it requries pywin32
    win32evtlog = None
"""
win32evtlog = None

# Please note that this will be replaced dynamically when being deployed by the
# bakery below /usr/bin. This is done while baking the packages.
default_agent_config_path = "/etc/check_mk"

#   .--Registration--------------------------------------------------------.
#   |        ____            _     _             _   _                     |
#   |       |  _ \ ___  __ _(_)___| |_ _ __ __ _| |_(_) ___  _ __          |
#   |       | |_) / _ \/ _` | / __| __| '__/ _` | __| |/ _ \| '_ \         |
#   |       |  _ <  __/ (_| | \__ \ |_| | | (_| | |_| | (_) | | | |        |
#   |       |_| \_\___|\__, |_|___/\__|_|  \__,_|\__|_|\___/|_| |_|        |
#   |                  |___/                                               |
#   +----------------------------------------------------------------------+
#   |  Before we can do updates the agent needs to exchange a secret with  |
#   |  the deployment server. This is called registration.                 |
#   '----------------------------------------------------------------------'

def main_register(config):
    short_options = generic_short_options + "s:i:p:H:U:P:S:"
    long_options = generic_long_options + [ "server=", "site=", "protocol=", "hostname=", "user=", "password=", "secret=" ]

    opts = getopt.getopt(sys.argv[2:], short_options, long_options)[0]
    parse_generic_options(opts)

    for o, a in opts:
        if o in [ '-s', '--server' ]:
            config["server"] = a
        elif o in [ '-i', '--site' ]:
            config["site"] = a
        elif o in [ '-p', '--protocol' ]:
            config["protocol"] = a
        elif o in [ '-H', '--hostname' ]:
            config["host_name"] = a.decode("utf-8")
        elif o in [ '-U', '--user']:
            config["user"] = a
        elif o in [ '-P', '--password']:
            config["password"] = a
        elif o in [ '-S', '--secret' ]:
            config["secret"] = a

    do_register(config)


def do_register(config):
    show_command_line_hint = False
    if need_interaction_for_registration(config):

        global opt_verbose
        if not opt_verbose:
            opt_verbose = 1
        verbose(
          "+-------------------------------------------------------------------+\n"
          "|                                                                   |\n"
          "|  Check_MK Agent Updater - Registration                            |\n"
          "|                                                                   |\n"
          "|  Activation of automatic agent updates. Your first step is to     |\n"
          "|  register this host at your deployment server for agent updates.  |\n"
          "|  For this step you need an administration account on WATO for     |\n"
          "|  that server.                                                     |\n"
          "|                                                                   |\n"
          "+-------------------------------------------------------------------+\n")

        if read_state_file():
            verbose("Using previous settings from %s.\n" % state_file_path())

        if interactively_complete_config(config):
            show_command_line_hint = True

    host_secret = register_agent(config)

    update = { "host_secret": host_secret }
    for key in [ "server", "site", "host_name", "protocol", "user" ]:
        update[key] = config[key]
    update_deployment_state(update)
    verbose("You can now update your agent by running 'cmk-update-agent -v'\n")
    verbose("Saved your registration settings to %s.\n" % state_file_path())

    if show_command_line_hint:
        show_command_line(config)


def need_interaction_for_registration(config):
    return "server"    not in config or \
           "protocol"  not in config or \
           "site"      not in config or \
           "host_name" not in config or \
           "user"      not in config or \
           ("password" not in config and "secret" not in config)


def interactively_complete_config(config):
    some_missing = False
    if "server" not in config:
        config["server"] = read_line("Deployment server to connect to:")
        some_missing = True

    while "protocol" not in config or config["protocol"] not in [ "http", "https" ]:
        config["protocol"] = read_line("Protocol to use for connection [http/https]:")
        if config["protocol"] == "https" and not config.get("certificates"):
            warn("No certificates installed yet. Using insecure connection.")
        some_missing = True

    if "site" not in config:
        config["site"] = read_line("Check_MK Site on deployment server:")
        some_missing = True

    if "host_name" not in config:
        config["host_name"] = read_line("Our host name in the monitoring:")
        some_missing = True

    if "user" not in config:
        config["user"] = read_line("WATO User with admin permissions:")

    if "password" not in config and "secret" not in config:
        config["password"] = read_line("Password:", echo=False)

    return some_missing


def is_registered(config):
    if not os.path.exists(state_file_path()):
        return False
    state = read_state_file(catch_exc=False)
    if "host_name" not in state or "host_secret" not in state:
        return False
    return True


def show_command_line(config):
    def quoted(string):
        if os.name == 'nt':
            return '"' + string + '"'
        else:
            return "'" + string + "'"

    command_line = sys.argv[0] + " register -s %s -i %s -H %s -p %s -U %s " % (
         config["server"], config["site"], config["host_name"], config["protocol"],
         quoted(config["user"]))

    if "secret" in config:
        command_line += "-S %s" % quoted("*"*len(config["secret"]))
    else:
        command_line += "-P %s" % quoted("*"*len(config["password"]))

    if opt_verbose:
        command_line += " -v"
    sys.stdout.write("\nHint: you can do this in scripts with the command:\n\n%s\n\n" %
                                                                            command_line)



#.
#   .--Update--------------------------------------------------------------.
#   |                   _   _           _       _                          |
#   |                  | | | |_ __   __| | __ _| |_ ___                    |
#   |                  | | | | '_ \ / _` |/ _` | __/ _ \                   |
#   |                  | |_| | |_) | (_| | (_| | ||  __/                   |
#   |                   \___/| .__/ \__,_|\__,_|\__\___|                   |
#   |                        |_|                                           |
#   +----------------------------------------------------------------------+
#   |  Main entry for agent update                                         |
#   '----------------------------------------------------------------------'

opt_run_as_plugin = False
opt_reinstall = False
opt_skip_signatures = False

def main_update(config):
    global opt_run_as_plugin
    global opt_reinstall
    global opt_skip_signatures

    short_options = generic_short_options + "urGf"
    long_options = generic_long_options + [ "run-as-plugin", "reinstall", "skip-signature", "force" ]

    opts = getopt.getopt(sys.argv[1:], short_options, long_options)[0]
    parse_generic_options(opts)

    for o, _unused_a in opts:
        if o in [ '-r', '--reinstall' ]:
            opt_reinstall = True
        elif o in [ '-G', '--skip-signature' ]:
            opt_skip_signatures = True
        elif o in [ '-f', '--force' ]:
            opt_reinstall = True
            opt_skip_signatures = True
        elif o in [ '-u', '--run-as-plugin']:
            opt_run_as_plugin = True

    if is_run_as_plugin():
        do_update_as_plugin(config)
    else:
        do_update_as_command(config)


def is_run_as_plugin():
    return not not os.getenv("MK_CONFDIR", "") or opt_run_as_plugin


def do_update_as_command(config):
    space_filler = " "*(32-len(__version__)) # ugly, but we don't have str.format in Python < 2.6
    verbose(
      "\n" # Prevent drawing the first line directly after the prompt
      "+-------------------------------------------------------------------+\n"
      "|                                                                   |\n"
      "|  Check_MK Agent Updater v%s - Update%s|\n"
      "|                                                                   |\n"
      "+-------------------------------------------------------------------+\n"
      % (__version__, space_filler)
    )

    do_update_agent(config)


def do_update_as_plugin(config):
    calling_user = ""
    try:
        if os.name == "nt":
            calling_user = win32api.GetUserName()
        if os.name == "posix":
            calling_user = pwd.getpwuid(os.getuid())[0]
    except Exception:
        pass

    init_logger()
    verbose("Check_MK Agent Updater v%s \n" % __version__) # goes to syslog
    if calling_user:
        optional_log("called by user %s\n" % calling_user)

    try:
        if not is_registered(config):
            raise Exception("The agent updater is not registered at the deployment server")
        send_feedback = do_update_agent(config)
        update_deployment_state({ "last_error": None })

    except Exception, e:
        if opt_debug:
            raise
        send_feedback = True
        update_deployment_state({ "last_error": str(e) })

    deployment_state = read_state_file()
    sys.stdout.write("<<<check_mk>>>\n")
    sys.stdout.write("AgentUpdate: last_check %s last_update %s aghash %s error %s\n" % (
        deployment_state.get("last_check"),
        deployment_state.get("last_update"),
        deployment_state.get("aghash"),
        deployment_state.get("last_error"),
    ))

    if send_feedback:
        try:
            # Once again get exchange information with the deployment server -
            # just in order to give immediate feedback about the new situation.
            config.update(deployment_state)
            fetch_agent_info(config, mode="status")
        except:
            if opt_debug:
                raise
            pass


def do_update_agent(config):
    if not is_registered(config):
        raise Exception("Not yet registered at deployment server. Please run 'cmk-update-agent register' first")

    # Get target configuration
    target_state = get_target_state(config)
    update_deployment_state({ "last_check" : time.time() })
    if not target_state["agent_available"]:
        verbose("No agent available for us.\n")
        return

    if opt_verbose:
        verbose("Target state (from deployment server):\n")
        for key, value in sorted(target_state.items()):
            if key == "signatures":
                value = len(value)
            verbose("  %-20s %s\n" % (key.title().replace("_", " ") + ":", value))

    target_hash = target_state["target_hash"]

    # Should we update?
    if "installed_aghash" in config and target_hash == config["installed_aghash"]:
        verbose("Agent %s already installed.\n" % target_hash)
        if opt_reinstall:
            verbose("Forcing reinstallation.\n")
        else:
            return

    config["aghash"] = target_hash

    # If there is no signature or we do not accept any signature keys
    # then there is no point in going on.
    if not opt_skip_signatures:
        check_signatures(None, config, target_state)

    # Download agent and update
    agent = download_agent(config)
    verbose("Downloaded agent has size %d bytes.\n" % len(agent))

    if opt_skip_signatures:
        verbose("Skipping signature check (as you requested).\n")
    else:
        check_signatures(agent, config, target_state)

    install_agent(config, agent)
    update_deployment_state({
        "installed_aghash" : target_hash,
        "last_update" : time.time()
    })

    return True


def check_signatures(agent, config, target_state):
    if "signatures" not in target_state:
        raise Exception("The deployment server provides an agent but that is not signed.")

    if not config.get("signature_keys"):
        raise Exception("No signature keys are configured.")

    for nr, (certificate, signature) in enumerate(target_state["signatures"]):
        if certificate not in config["signature_keys"]:
            verbose("Ignoring signature #%d for certificate: certificate is unknown.\n" % (nr+1))
            continue

        if agent != None:
            if check_signature(certificate, signature, agent):
                verbose("Signature check OK.\n")
                return
            else:
                verbose("Signature #%d is invalid.\n" % (nr+1))
        else:
            return # for just checking if there are any signature available

    raise Exception("No valid signature found.")


def check_signature(certificate, signature, content):
    if crypto and hasattr(crypto, "verify"):
        return check_signature_with_bindings(certificate, signature, content)
    else:
        return check_signature_with_commands(certificate, signature, content)


def check_signature_with_bindings(certificate, signature, content):
    cert = crypto.load_certificate(crypto.FILETYPE_PEM, certificate)
    try:
        crypto.verify(cert, signature, content, "sha1")
        return True
    except crypto.Error:
        return False


# Format a public key as a PEM, use "openssl x509 -pubkey -noout -in cert.pem  > pubkey.pem"
def pem_publickey(certificate):
    try:
        p = subprocess.Popen(["openssl", "x509", "-pubkey", "-noout"],
                             stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                             stdin=subprocess.PIPE)
    except OSError, e:
        if e.errno == 2:
            sys.stderr.write("ERROR: Failed to find the Python OpenSSL (python-openssl) module "
                             "and \"openssl\" command.\nYou need at least one of them installed to"
                             " be able to use this script.\n")
            sys.exit(1)
        else:
            raise
    stdout, stderr = p.communicate(certificate)
    exit_code = p.wait()

    if exit_code != 0:
        raise Exception("Failed to get public key for certificate (Exit-Code: %d):\n%s" %
                                                                        (exit_code, stderr))

    return stdout


def check_signature_with_commands(certificate, signature, content):
    pkey_fd, pkey_path = tempfile.mkstemp(prefix="cmk-update-agent-verify-pkey-")
    os.write(pkey_fd, pem_publickey(certificate))
    os.close(pkey_fd)

    sig_fd, sig_path = tempfile.mkstemp(prefix="cmk-update-agent-verify-sig-")
    os.write(sig_fd, signature)
    os.close(sig_fd)

    p = subprocess.Popen(["openssl", "dgst", "-sha1",
                          "-verify", pkey_path, "-signature", sig_path],
                            stdout=open(os.devnull, 'w'),
                            stdin=subprocess.PIPE)
    p.stdin.write(content)
    p.stdin.close()
    exit_code = p.wait()

    # TODO: This should better be cleaned up using "finally" to be sure that this is
    # removed or during startup of the next agent updater execution to catch really
    # all cases. But please be aware that there may be Python version incompatibilities
    # when using finally.
    for path in [ sig_path, pkey_path ]:
        try:
            os.unlink(path)
        except OSError, e:
            if e.errno == 2:
                pass
            else:
                raise

    return exit_code == 0


def install_agent(config, agent):
    fd, filename = tempfile.mkstemp(prefix="check-mk-agent-")
    try:
        os.write(fd, agent)
        os.close(fd)
        if config["opsys"] == "linux_rpm":
            install_agent_linux_rpm(filename)
        elif config["opsys"] == "linux_deb":
            install_agent_linux_deb(filename)
        #elif config["opsys"] == "linux_tgz":
        #    install_agent_linux_tgz(filename)
        elif config["opsys"] == "windows_msi":
            install_agent_windows_msi(filename)
        else:
            raise Exception("opsys %s not implemented" % config["opsys"])
    except Exception, e:
        verbose("Keeping %s for error diagnosis. Error: %s\n" % (filename, e))
        raise

    verbose(tty_bold + "Successfully installed agent %s.\n" % config["aghash"] + tty_normal)
    os.remove(filename)


def install_agent_linux_deb(filename):
    install_agent_linux(filename, "dpkg -i '%s'")


def install_agent_linux_rpm(filename):
    install_agent_linux(filename, "rpm -vU --oldpackage --replacepkgs '%s'")


def install_agent_linux(filename, extract_command):
    if opt_verbose:
        redirection = "2>&1"
    else:
        redirection = "2>&1 >/dev/null"

    pipe = os.popen((extract_command + " %s") % (filename, redirection))
    output = pipe.read()
    status = pipe.close()
    if status:
        raise Exception("Error during installation of package:\n%s" % output)


def install_agent_windows_msi(filename):
    # Base command
    command = ["msiexec", "/i", filename, "/qn", "/quiet", "/norestart"]

    # Optional debugging, if available
    logdir = local_logdir()
    if logdir:
        command.extend(["/L*V", os.path.join(logdir, "msi_installer.log")])

    verify_file_permissions() # verify permissions immediately before...

    subprocess.call(command)

    verify_file_permissions() # ... and after msiexec

    # TODO: make this workaround obsolete
    # Currently, the msi installer have problems unpacking the plugins.cap
    ensure_correct_capfile_installation()


def register_agent(config):
    verbose("Going to register agent at deployment server\n")

    username = config["user"]
    if "secret" in config:
        secret = config["secret"]
        password = None
    else:
        secret = None
        password = config["password"]

    rel_uri = make_uri("register_agent.py", [("host", config["host_name"])])
    host_secret = get_url(config["protocol"], config.get("certificates",[]), config["server"], config["site"],
                          rel_uri, username, password, secret).strip()
    if '<div id="login_error">' in host_secret:
        raise Exception("Cannot authenticate, invalid user/passwort/secret.")

    if len(host_secret) != 64:
        raise Exception("Invalid host secret '%s' (length is not 64)" % host_secret)
    verbose(tty_bold + "Successfully registered agent for deployment.\n" + tty_normal)
    vverbose("Secret is %s.\n" % host_secret)
    return host_secret


def get_target_state(config):
    verbose("Getting target agent configuration from deployment server\n")
    response = fetch_agent_info(config, mode="status")
    vverbose("Response from deployment server:\n%s\n" % response)
    return parse_agent_target_state(response)


def parse_agent_target_state(response):
    try:
        status = {}
        for line in response.splitlines():
            varname, value = line.rstrip().split(":", 1)
            value = value.lstrip()
            if varname == "AgentAvailable":
                status["agent_available"] = (value == "True")
            elif varname == "Signature":
                parts = value.split()
                certificate = parse_hex(parts[0])
                signature = parse_hex(parts[1])
                status.setdefault("signatures", []).append((certificate, signature))
            elif varname == "TargetHash":
                status["target_hash"] = value
        return status

    except Exception, e:
        if opt_debug:
            raise
        verbose("Garbled response from deployment server:\n%s\n" % response)
        raise Exception("Garbled response from deployment server: %s" % e)


def parse_hex(s):
    r = ""
    while s:
        r += chr(int(s[:2], 16))
        s = s[2:]
    return r


def download_agent(config):
    return fetch_agent_info(config, mode="agent")


def fetch_agent_info(config, mode):
    get_vars = [
       ( "mode",             mode ),
       ( "host",             config["host_name"] ),
       ( "host_secret",      config["host_secret"] ),
       ( "installed_aghash", config.get("installed_aghash", ""), ),
       ( "aghash",           config.get("aghash", ""), ),
       ( "os",               config["opsys"], ),
       ( "last_error",       (config.get("last_error") or "")[:512] ), # avoid recursive error message getting too long
    ]
    if opt_debug:
        get_vars.append(("debug", "1"))

    rel_uri = make_uri("deploy_agent.py", get_vars)

    return get_url(config["protocol"], config.get("certificates", []), config["server"], config["site"], rel_uri)


#.
#   .--HTTP/HTTPS----------------------------------------------------------.
#   |      _   _ _____ _____ ____   ___   _ _____ _____ ____  ____         |
#   |     | | | |_   _|_   _|  _ \ / / | | |_   _|_   _|  _ \/ ___|        |
#   |     | |_| | | |   | | | |_) / /| |_| | | |   | | | |_) \___ \        |
#   |     |  _  | | |   | | |  __/ / |  _  | | |   | | |  __/ ___) |       |
#   |     |_| |_| |_|   |_| |_| /_/  |_| |_| |_|   |_| |_|   |____/        |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   |  Fetching of URLs                                                    |
#   '----------------------------------------------------------------------'

def get_url(*args):
    disable_proxies()
    try:
        response = get_url_requests(*args)
    except ImportError:
        if not find_executable("curl"):
            raise Exception("You either need to install \"curl\" or the python requests module.")

        response = get_url_curl(*args)

    if response.startswith("ERROR:"):
        raise Exception(response[6:].strip())
    elif response.startswith("<meta") and "Site Not Started" in response:
        raise Exception("Check_MK Site on deployment server not started")
    elif response.startswith("<!DOCTYPE HTML PUBLIC") and "login.py" in response:
        raise Exception("Authentication not successful")
    elif response.startswith("<!DOCTYPE HTML PUBLIC") and "401 Unauthorized" in response:
        raise Exception("You are not authorized")

    return response

def disable_proxies():
    """ Unset proxy environment variables to enable connecting to Check_MK-Server directly.
    This function unsets all environment variables employed by curl and requests.
    """
    for env_var in os.environ.keys():
        if env_var.lower() in ["http_proxy", "https_proxy", "all_proxy"]:
            os.environ.pop(env_var)

def certificate_dir():
    if os.path.exists("/var"): # Linux
        return "/var/lib/check_mk_agent/cas"
    else: # windows
        return os.path.join(config_file_dir(), "cas")


def get_all_certs_filepath():
    return "%s/cert_store.pem" % certificate_dir()


def update_ca_store(ca_files):
    if ca_files:
        if os.path.exists(certificate_dir()):
            for ca_file in os.listdir(certificate_dir()):
                os.remove(os.path.join(certificate_dir(), ca_file))
        else:
            os.makedirs(certificate_dir())

        vverbose("Updating the certificate store \"%s\"...\n" % get_all_certs_filepath())
        all_certs = "\n".join(ca_files)
        acf = open(get_all_certs_filepath(), "w")
        acf.write(all_certs)
        acf.close()
        verbose("Updated the certificate store \"%s\" with %d certificates\n" %
                            (get_all_certs_filepath(), len(ca_files)))

        return get_all_certs_filepath()

    return False


def gen_url(protocol, server, omd_site, rel_uri, username, password, secret):
    base_url = "%s://%s/%s/check_mk/" % (protocol, server, omd_site)
    if username:
        if password:
            url = make_uri(base_url + "login.py", [
                 ("_login", "1"),
                 ("_username", username),
                 ("_password", password),
                 ("_origtarget",  "/%s/check_mk/%s" % (omd_site, rel_uri))])
        else:
            url = "%s%s&_username=%s&_secret=%s" % (
                 base_url, rel_uri, urlencode(username), urlencode(secret))

    else:
        url = base_url + rel_uri

    return url


def get_url_requests(protocol, certificates, server, omd_site, rel_uri,
                     username=None, password=None, secret=None):
    import requests

    url = gen_url(protocol, server, omd_site, rel_uri, username, password, secret)

    vverbose("Fetching URL (using requests): %s\n" % remove_parameters_from_url(url))

    verify = update_ca_store(certificates)

    try:
        response = requests.get(url, verify=verify)

        if response.status_code != 200:
            raise Exception("Error while fetching deployment URL via requests: %s" %
                                                                response.status_code)

        response.raise_for_status()

        return response.content

    except Exception, e:
        if opt_debug:
            raise
        # request.get or raise_for_status may raise exceptions containing the url.
        # since the url would be embedded in the message, the remove_parameters_from_url method
        # isn't precise enough to get rid of the password.
        orig_message = "%s" % e
        raise type(e)(re.sub("_password=([^&]*)", "_password=...", orig_message))


def get_url_curl(protocol, certificates, server, omd_site, rel_uri,
                 username=None, password=None, secret=None):
    extra_curl_options = []

    verify = update_ca_store(certificates)
    if verify:
        extra_curl_options += [ "--cacert", verify ]
    else:
        extra_curl_options += [ "--insecure" ]

    url = gen_url(protocol, server, omd_site, rel_uri, username, password, secret)
    if username and password:
        extra_curl_options += ["--cookie", ""]

    command = ["curl", "--silent", "--tlsv1", "--include", "--location"] + extra_curl_options + [ url ]

    vverbose("Fetching URL (using curl): %s\n" % remove_parameters_from_url(url))
    raw_response, errors, exit_code = run_curl(command)

    if exit_code:
        vverbose("Cannot fetch URL. Command line was: %s\n" %
                    remove_parameters_from_url(subprocess.list2cmdline(command)))
        raise Exception("Cannot fetch deployment URL via curl: %s" % (
            curl_error_codes.get(exit_code, "Unknown curl error, exit code: %d" % exit_code)))

    # Split HTTP headers and body
    elements = re.split("HTTP\/[\d.]* ", raw_response)
    last_element = elements[-1]
    status_code = int(last_element[:3])
    content_length_match = re.search("Content-Length: ([0-9]*)", last_element)

    if status_code != 200:
        raise Exception("Error while fetching deployment URL via curl: Unexpected status code %s" % status_code)

    if content_length_match is not None:
        content_length = int(content_length_match.group(1))
        response = last_element[-content_length:]
    else:
        response = last_element.split("\r\n\r\n",1)[1]

    return response


def run_curl(command):
    # TODO: Removethe "--silent" option and handle stderr messages. They would
    # be helpful because they contain more details than the message resulting
    # from the exit code.
    p = subprocess.Popen(command, stdin=open(os.devnull), stdout=subprocess.PIPE, env=os.environ)
    raw_response, errors = p.communicate()
    exit_code = p.returncode

    return raw_response, errors, exit_code


def remove_parameters_from_url(urltext):
    return re.sub("\.py\?.*", ".py?...", urltext)

curl_error_codes = {
    1:   "Unsupported protocol. This build of curl has no support for this protocol.",
    2:   "Failed to initialize.",
    3:   "URL malformat. The syntax was not correct.",
    5:   "Couldn't resolve proxy. The given proxy host could not be resolved.",
    6:   "Couldn't resolve host. The given remote host was not resolved.",
    7:   "Failed to connect to host.",
    8:   "FTP weird server reply. The server sent data curl couldn't parse.",
    9:   "FTP access denied. The server denied login or denied access to the particular resource or directory you wanted to reach. Most often you tried to change to a directory that doesn't exist on the server.",
    11:   "FTP weird PASS reply. Curl couldn't parse the reply sent to the PASS request.",
    13:   "FTP weird PASV reply, Curl couldn't parse the reply sent to the PASV request.",
    14:   "FTP weird 227 format. Curl couldn't parse the 227-line the server sent.",
    15:   "FTP can't get host. Couldn't resolve the host IP we got in the 227-line.",
    17:   "FTP couldn't set binary. Couldn't change transfer method to binary.",
    18:   "Partial file. Only a part of the file was transferred.",
    19:   "FTP couldn't download/access the given file, the RETR (or similar) command failed.",
    21:   "FTP quote error. A quote command returned error from the server.",
    22:   "HTTP page not retrieved. The requested url was not found or returned another error with the HTTP error code being 400 or above. This return code only appears if -f/--fail is used.",
    23:   "Write error. Curl couldn't write data to a local filesystem or similar.",
    25:   "FTP couldn't STOR file. The server denied the STOR operation, used for FTP uploading.",
    26:   "Read error. Various reading problems.",
    27:   "Out of memory. A memory allocation request failed.",
    28:   "Operation timeout. The specified time-out period was reached according to the conditions.",
    30:   "FTP PORT failed. The PORT command failed. Not all FTP servers support the PORT command, try doing a transfer using PASV instead!",
    31:   "FTP couldn't use REST. The REST command failed. This command is used for resumed FTP transfers.",
    33:   "HTTP range error. The range 'command' didn't work.",
    34:   "HTTP post error. Internal post-request generation error.",
    35:   "SSL connect error. The SSL handshaking failed.",
    36:   "FTP bad download resume. Couldn't continue an earlier aborted download.",
    37:   "FILE couldn't read file. Failed to open the file. Permissions?",
    38:   "LDAP cannot bind. LDAP bind operation failed.",
    39:   "LDAP search failed.",
    41:   "Function not found. A required LDAP function was not found.",
    42:   "Aborted by callback. An application told curl to abort the operation.",
    43:   "Internal error. A function was called with a bad parameter.",
    45:   "Interface error. A specified outgoing interface could not be used.",
    47:   "Too many redirects. When following redirects, curl hit the maximum amount.",
    48:   "Unknown TELNET option specified.",
    49:   "Malformed telnet option.",
    51:   "The peer's SSL certificate or SSH MD5 fingerprint was not ok",
    52:   "The server didn't reply anything, which here is considered an error.",
    53:   "SSL crypto engine not found",
    54:   "Cannot set SSL crypto engine as default",
    55:   "Failed sending network data",
    56:   "Failure in receiving network data",
    58:   "Problem with the local certificate",
    59:   "Couldn't use specified SSL cipher",
    60:   "Peer certificate cannot be authenticated with known CA certificates",
    61:   "Unrecognized transfer encoding",
    62:   "Invalid LDAP URL",
    63:   "Maximum file size exceeded",
    64:   "Requested FTP SSL level failed",
    65:   "Sending the data requires a rewind that failed",
    66:   "Failed to initialise SSL Engine",
    67:   "User, password or similar was not accepted and curl failed to login",
    68:   "File not found on TFTP server",
    69:   "Permission problem on TFTP server",
    70:   "Out of disk space on TFTP server",
    71:   "Illegal TFTP operation",
    72:   "Unknown TFTP transfer ID",
    73:   "File already exists (TFTP)",
    74:   "No such user (TFTP)",
    75:   "Character conversion failed",
    76:   "Character conversion functions required",
    77:   "Problem with reading the SSL CA cert (path? access rights?)",
    78:   "The resource referenced in the URL does not exist",
    79:   "An unspecified error occurred during the SSH session",
    80:   "Failed to shut down the SSL connection",
    82:   "Could not load CRL file, missing or wrong format (added in 7.19.0)",
    83:   "Issuer check failed (added in 7.19.0)",
}


# Note: this is unicode safe (urllib isn't)
def make_uri(base_url, variables):
    return base_url + "?" + "&".join([
        "%s=%s" % (varname, urlencode(value))
        for (varname, value) in variables ])


def urlencode(value):
    if type(value) == unicode:
        value = value.encode("utf-8")
    elif value == None:
        return ""
    ret = ""
    for c in value:
        if c == " ":
            c = "+"
        elif ord(c) <= 32 or ord(c) > 127 or c in [ '#', '+', '"', "'", "=", "&", ":", "%", "[", "]" ]:
            c = "%%%02x" % ord(c)
        ret += c
    return ret


#.
#   .--Config-&-Status-----------------------------------------------------.
#   |   ____             __ _        ___   ____  _        _                |
#   |  / ___|___  _ __  / _(_) __ _ ( _ ) / ___|| |_ __ _| |_ _   _ ___    |
#   | | |   / _ \| '_ \| |_| |/ _` |/ _ \/\___ \| __/ _` | __| | | / __|   |
#   | | |__| (_) | | | |  _| | (_| | (_>  <___) | || (_| | |_| |_| \__ \   |
#   |  \____\___/|_| |_|_| |_|\__, |\___/\/____/ \__\__,_|\__|\__,_|___/   |
#   |                         |___/                                        |
#   +----------------------------------------------------------------------+
#   | Reading and saving of config file and deployment status              |
#   '----------------------------------------------------------------------'

def config_file_path():
    return os.path.join(config_file_dir(), "cmk-update-agent.cfg")


def agent_config_path():
    if os.name == "nt":
        # for windows, look for a config directory parallel to the plugin directory
        # we're running from.
        return os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(sys.executable)), "..", "config"))

    else:
        return default_agent_config_path


def config_file_dir():
    for dir_candidate in [
        os.getenv("MK_CONFDIR"),
        agent_config_path(),
        ".",
    ]:
        if dir_candidate and os.path.exists(dir_candidate):
            return dir_candidate

def read_config_file():
    return read_repr_file(config_file_path())


def read_state_file(catch_exc=True):
    try:
        return read_repr_file(state_file_path())
    except Exception:
        if catch_exc:
            # For this situation allow a failed read (e.g. syntax error).
            # The file is read again later when we know how to handle
            # exceptions correctly (agent reporting, command line output)
            return {}
        else:
            raise


def write_state_file(status):
    file_path = state_file_path()

    try:
        tmp_file = tempfile.NamedTemporaryFile("w", dir=os.path.dirname(file_path),
                                           prefix=".%s.new" % os.path.basename(file_path),
                                           delete=False)
        tmp_path = tmp_file.name
    except TypeError:
        # Manually create a temporary file if NamedTemporaryFile fails.
        # This is the case for Python versions < 2.6 without support for "delete" Keyword.
        tmp_path = file_path + ".new"
        tmp_file = file(tmp_path, mode="w")

    try:
        os.chmod(tmp_path, 0600)

        tmp_file.write("%r\n" % status)
        tmp_file.flush()
        os.fsync(tmp_file.fileno())
        tmp_file.close()

        shutil.move(tmp_path, file_path)

    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)

    vverbose("Saved deployment status to %s.\n" % state_file_path())


def update_deployment_state(updates):
    deployment_state = read_state_file()
    deployment_state.update(updates)

    try:
        write_state_file(deployment_state)
    except Exception, e:
        if opt_debug:
            raise
        raise Exception("Failed to write state to '%s': %s" % (state_file_path(), e))


def state_file_path():
    if os.name == "posix" and os.path.exists("/etc"): # Don't use e.g. C:\etc on Windows if existant!
        # Beware: On linux we must not use /etc/check_mk. This will be removed
        # by the agent on update.
        config_dir = "/etc"
    else:
        config_dir = config_file_dir()
    return os.path.join(config_dir, "cmk-update-agent.state")


def read_repr_file(path):
    if not os.path.exists(path):
        return {}

    try:
        value = eval(file(path).read())
        vverbose("Read %s.\n" % path)
        return value
    except Exception, e:
        if opt_debug:
            raise
        raise Exception("Cannot read file %s: %s" % (path, e))

#.
#   .--Helpers-------------------------------------------------------------.
#   |                  _   _      _                                        |
#   |                 | | | | ___| |_ __   ___ _ __ ___                    |
#   |                 | |_| |/ _ \ | '_ \ / _ \ '__/ __|                   |
#   |                 |  _  |  __/ | |_) |  __/ |  \__ \                   |
#   |                 |_| |_|\___|_| .__/ \___|_|  |___/                   |
#   |                              |_|                                     |
#   +----------------------------------------------------------------------+
#   |  Generic helper functions                                            |
#   '----------------------------------------------------------------------'

if sys.stdout.isatty() and not os.name == "nt":
    tty_bold      = '\033[1m'
    tty_normal    = '\033[0m'
    tty_red       = tty_bold + '\033[31m'
else:
    tty_bold = ""
    tty_normal = ""
    tty_red = ""


def error(text):
    sys.stderr.write(tty_red + "ERROR " + text + tty_normal + "\n")
    sys.stderr.flush()

    optional_log("ERROR: %s" % text)


def warn(text):
    sys.stderr.write(tty_bold + "WARNING " + text + tty_normal + "\n")
    sys.stderr.flush()

    optional_log("WARN: %s" % text)


def write_syslog(text):
    syslog.syslog(text)


def write_evtlog(handle, text):
    win32evtlog.ReportEvent(handle, win32evtlog.EVENTLOG_INFORMATION_TYPE,
                            0, 0, None, [text], None)


write_oslog = None


def init_logger():
    global write_oslog
    if syslog:
        syslog.openlog("cmk-update-agent")
        write_oslog = write_syslog

    elif win32evtlog:
        handle = win32evtlog.RegisterEventSource(None, "cmk-update-agent")
        write_oslog = lambda text: write_evtlog(handle, text)

    else:
        logdir = local_logdir()

        if logdir:
            f = open(os.path.join(logdir, "cmk-update-agent.log"), "w")
            write_oslog = lambda text: f.write(text)

        elif our_os_type() == "windows_msi":
            raise Exception("Could not find suitable directory for logging.")


def local_logdir():
    logdir = os.getenv("MK_LOGDIR", os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..", "log")))
    if os.path.exists(logdir):
        return logdir


def verbose(text):
    optional_log(text)
    if opt_verbose >= 1:
        sys.stderr.write(text)
        sys.stderr.flush()



def vverbose(text):
    optional_log(text)
    if opt_verbose >= 2:
        sys.stderr.write(text)
        sys.stderr.flush()


def optional_log(text):
    if write_oslog:
        text = "%s %s" % (time.strftime("%b %d %Y %H:%M:%S", time.localtime()), text)
        write_oslog(text)


def bail_out(reason):
    error(str(reason))
    sys.exit(1)


def read_line(prompt, echo=True):
    sys.stderr.write(prompt + " " + tty_bold)
    sys.stderr.flush()
    try:
        if echo:
            answer = sys.stdin.readline().rstrip()
        else:
            answer = getpass.getpass(prompt="")
    except:
        sys.stderr.write("\n")
        answer = None

    sys.stderr.write(tty_normal)
    if not answer:
        raise Exception("Aborted.")
    return answer


def our_os_type():
    if os.name == "nt":
        return "windows_msi"
    elif os.path.exists("/var/lib/dpkg/status"):
        return "linux_deb"
    elif os.path.exists("/var/lib/rpm") and (
        os.path.exists("/bin/rpm") or os.path.exists("/usr/bin/rpm")):
        return "linux_rpm"
    else:
        return "linux_tgz"



#.
#   .--Generic-------------------------------------------------------------.
#   |                   ____                      _                        |
#   |                  / ___| ___ _ __   ___ _ __(_) ___                   |
#   |                 | |  _ / _ \ '_ \ / _ \ '__| |/ __|                  |
#   |                 | |_| |  __/ | | |  __/ |  | | (__                   |
#   |                  \____|\___|_| |_|\___|_|  |_|\___|                  |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   |  Help and generic option parsing                                     |
#   '----------------------------------------------------------------------'

def usage():
    sys.stdout.write("""Usage: cmk-update-agent [register] [OPTIONS]

   GENERAL OPTIONS:

   -h, --help           Show this help
   -V, --version        Show the version of this program
   -v, --verbose        Enable verbose output, twice for more details
       --debug          Let Python exceptions come through (for debugging)


   REGISTRATION: Options for "cmk-update-agent register". Registration will
   negotiate a shared secret that authenticates the download of the agent
   for the specified host name in future.

   -s, --server SERVER  DNS name or IP address of update server
   -i, --site SITE      Name of Check_MK site on that server
   -p, --protocol P     Either http or https (default is https)
   -H, --hostname       Host name to fetch agent for
   -U, --user USER      User-ID of a user who is allowed to download the agent.
   -P, --password PW    Password of the user (in case of normal user)
   -S, --secret SECRET  Automation secret of that user (in case of automation user)


   UPDATE

   -u, --run-as-plugin  Behave like if called as agent plugin
   -G  --skip-signature Skip validation of package signature
   -r, --reinstall      Also update if package seems up-to-date
   -f, --force          Do --skip-signature and -r

""")

opt_verbose = 0
opt_debug = False

generic_short_options = "hvV"
generic_long_options = [ "help", "verbose", "debug", "version" ]

def parse_generic_options(opts):
    for o, _unused_a in opts:
        if o in [ '-v', '--verbose' ]:
            global opt_verbose
            opt_verbose += 1
        elif o == '--debug':
            global opt_debug
            opt_debug = True
        elif o in [ '-h', '--help' ]:
            usage()
            sys.exit(1)
        elif o in [ '-V', '--version' ]:
            sys.stdout.write("cmk-update-agent %s\n" % __version__)
            sys.exit(1)

def aquire_updater_lock():
    # TODO: tempfile path is different for command line and windows service
    #       its still possible that two agent updaters are running at the same time..

    lockfile_path = ""
    lockfile_path = os.path.join(tempfile.gettempdir(), "cmk-update-agent.pid")
    lockfile = file(lockfile_path, "w")
    try:
        if os.name == "posix":
            fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
        elif os.name == "nt":
            msvcrt.locking(lockfile.fileno(), msvcrt.LK_NBLCK, 1024)
    except IOError, e:
        if e.errno in [errno.EACCES, errno.EAGAIN]:
            bail_out("An instance of %s is already running on this system. Aborting..." % sys.argv[0])
        else:
            raise

    lockfile.write(str(os.getpid()))
    lockfile.flush()

    return lockfile, lockfile_path





#.
#   .--Windows-------------------------------------------------------------.
#   |            __        ___           _                                 |
#   |            \ \      / (_)_ __   __| | _____      _____               |
#   |             \ \ /\ / /| | '_ \ / _` |/ _ \ \ /\ / / __|              |
#   |              \ V  V / | | | | | (_| | (_) \ V  V /\__ \              |
#   |               \_/\_/  |_|_| |_|\__,_|\___/ \_/\_/ |___/              |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   | The agent updater is a PyInstaller one file executable on windows.   |
#   | There is need for some special handling of this situation.           |
#   '----------------------------------------------------------------------'

#   .--MEI-Cleanup---------------------------------------------------------.
#   |     __  __ _____ ___       ____ _                                    |
#   |    |  \/  | ____|_ _|     / ___| | ___  __ _ _ __  _   _ _ __        |
#   |    | |\/| |  _|  | |_____| |   | |/ _ \/ _` | '_ \| | | | '_ \       |
#   |    | |  | | |___ | |_____| |___| |  __/ (_| | | | | |_| | |_) |      |
#   |    |_|  |_|_____|___|     \____|_|\___|\__,_|_| |_|\__,_| .__/       |
#   |                                                         |_|          |
#   +----------------------------------------------------------------------+
# In case the program crashes or is killed in a hard way, the frozen binary .exe
# may leave temporary directories named "_MEI..." in the temporary path. Clean them
# up to prevent eating disk space over time.

####################################################################
############## DUPLICATE CODE WARNING ##############################
### This code is also used in the mk_logwatch frozen binary ########
### Any changes to this class should also be made in mk_logwatch ###
### In the bright future we will move this code into a library #####
####################################################################

class MEIFolderCleaner(object):
    def pid_running(self, pid):
        import ctypes
        kernel32 = ctypes.windll.kernel32
        SYNCHRONIZE = 0x100000

        process = kernel32.OpenProcess(SYNCHRONIZE, 0, pid)

        if process != 0:
            kernel32.CloseHandle(process)
            return True
        else:
            return False


    def find_and_remove_leftover_folders(self, hint_filenames):
        if not hasattr(sys, "frozen"):
            return

        import win32file

        base_path = tempfile.gettempdir()
        for f in os.listdir(base_path):
            try:
                path = os.path.join(base_path, f)

                if not os.path.isdir(path):
                    continue

                # Only care about directories related to our program
                invalid_dir = False
                for hint_filename in hint_filenames:
                    if not os.path.exists(os.path.join(path, hint_filename)):
                        invalid_dir = True
                        break
                if invalid_dir:
                    continue

                pyinstaller_tmp_path = win32file.GetLongPathName(sys._MEIPASS).lower() # pylint: disable=no-member
                if pyinstaller_tmp_path == path.lower():
                    continue # Skip our own directory

                # Extract the process id from the directory and check whether or not it is still
                # running. Don't delete directories of running processes!
                # The name of the temporary directories is "_MEI<PID><NR>". We try to extract the PID
                # by stripping of a single digit from the right. In the hope the NR is a single digit
                # in all relevant cases.
                pid = int(f[4:-1])
                if self.pid_running(pid):
                    continue

                verbose("Cleaning up abandoned tempdir '%s'" % path)
                shutil.rmtree(path)
            except Exception, e:
                error("Failed to cleanup abandoned tempdir '%s': %s" % (path, e))


def need_restart_from_tempdir():
    # Under windows, the update agent is unable to update itself, because a running binary cannot be replaced.
    # To "fix" this problem, the cmk-update-agent.exe is executed in a temporary directory. The windows agent
    # takes care that the agent is copied into the temp dir before its executed.
    # However, if this script is called manually from the command line in the plugins dir,
    # a copy is created and executed in the temp dir. Is this a hack? Most certainly...

    # Note this code block is only relevant when started from the command line
    # The update process MUST start in the temp dir
    # The sys.argv[0] MUST be an absolute path

    file_dir = os.path.dirname(sys.executable)
    return file_dir.endswith("plugins")


def restart_in_subprocess():
    binary_dir = os.path.dirname(sys.executable)
    binary_name = os.path.basename(sys.executable)

    target_path = os.path.abspath(os.path.join(binary_dir, "..", "temp", binary_name))
    try:
        shutil.copy(sys.executable, target_path)
    except IOError, exc:
        raise Exception("Could not copy temporary cmk-update-agent.exe to %s: %s" % (target_path, exc))

    # Base command
    command = [target_path]
    command.extend(sys.argv[1:])

    CREATE_NEW_CONSOLE       = subprocess.CREATE_NEW_CONSOLE
    CREATE_NEW_PROCESS_GROUP = subprocess.CREATE_NEW_PROCESS_GROUP

    subprocess.Popen(command, close_fds=True, creationflags=CREATE_NEW_PROCESS_GROUP)


def verify_file_permissions():
    vverbose("Check for correct access rights within Check_MK-Agent install directory...\n")

    install_dir = os.path.dirname(os.path.dirname(os.path.abspath(sys.executable)))
    if not "check_mk_agent.exe" in os.listdir(install_dir):
        vverbose("Could not determine Check_MK-Agent install directory. Aborting Check.\n")
        return

    try:
        check_users = ["system", "nt service\\trustedinstaller"]
        verify_permissions_for_users(install_dir, check_users)
    except Exception, exc:
        if opt_debug:
            raise
        # In this version, any permission exceptions are only informational
        vverbose("Error while checking file permissions: %s\n" % exc)


def verify_permissions_for_users(install_dir, users):
    global desired_access_rights
    desired_access_rights = (
        ntsecuritycon.FILE_GENERIC_READ
        | ntsecuritycon.FILE_GENERIC_WRITE
        | ntsecuritycon.FILE_GENERIC_EXECUTE
        | ntsecuritycon.DELETE
        | ntsecuritycon.FILE_DELETE_CHILD
    )
    desired_access_string = (
        "FILE_GENERIC_READ"
        " | FILE_GENERIC_WRITE"
        " | FILE_GENERIC_EXECUTE"
        " | DELETE"
        " | FILE_DELETE_CHILD"
    )

    wrong_permissions = {}
    for user in users:
        wrong_permissions[user] = 0

    vverbose(
        "Analyze permissions....\n"
        "(Note: This is done for diagnostic reasons only"
        " and does not interfere with normal program flow.\n"
        "You can most likely ignore the results.)\n"
    )
    optional_log("Tested permissions are %i\n" % desired_access_rights)
    optional_log("i.e. %s\n" % desired_access_string)

    for user in users:
        try:
            collect_wrong_permissions(wrong_permissions, install_dir, user)
        except Exception, exc:
            vverbose("Skipping check for user %s because of error: %s\n" % (user, exc))
            break
        optional_log(
            "...... Found %s files/directories with missing write permissions for user %s\n"
            % (wrong_permissions[user], user)
        )

    if sum(wrong_permissions.values()) > 0:
        vverbose(
            "Missing permissions on some files.\n"
            "You can find details about the permissions within the cmk-update-agent.log.\n"
        )
    else:
        vverbose("Looks good.\n")


def collect_wrong_permissions(wrong_permissions, install_dir, user):
    # do the walk seperately for each user. This is less effective but yields the advantage
    # that the check can still be performed for another user after it fails for one user
    for (walk_dir, current_subdirs, walk_files) in os.walk(install_dir):
        (dir_has_wrong_permission, files_have_wrong_permissions) = collect_permissions(walk_dir, walk_files, user)
        optional_log("=> Results for user %s in directory %s:\n" % (user, walk_dir))
        wrong_permissions[user] += \
            analyze_permission_check_outcomes(dir_has_wrong_permission, files_have_wrong_permissions)


def collect_permissions(check_dir, check_files, user):
    dir_is_wrong = not check_effective_rights(check_dir, user)
    files_are_wrong = {}

    for check_file in check_files:
        check_path = os.path.join(check_dir, check_file)
        files_are_wrong[check_path] = not check_effective_rights(check_path, user)

    return (dir_is_wrong, files_are_wrong)


def check_effective_rights(check_file, user):
    file_security = win32security.GetFileSecurity(check_file, win32security.DACL_SECURITY_INFORMATION)
    dacl = file_security.GetSecurityDescriptorDacl()
    user_sid = win32security.LookupAccountName("",user)[0]
    user_trustee = {
        "Identifier": user_sid,
        "TrusteeForm": win32security.TRUSTEE_IS_SID,
        "TrusteeType": win32security.TRUSTEE_IS_UNKNOWN,
        "MultipleTrustee": 0,
        "MultipleTrusteeOperation": 0
    }

    actual_access_rights = dacl.GetEffectiveRightsFromAcl(user_trustee)
    optional_log("Access mask for %s on %s: %i\n" % (user, check_file, actual_access_rights))

    return actual_access_rights & desired_access_rights == desired_access_rights


def analyze_permission_check_outcomes(dir_has_wrong_permission, files_have_wrong_permissions):
    wrong_permissions = 0
    if dir_has_wrong_permission:
        optional_log("\tlacking dirdctory permissions.\n")
        wrong_permissions += 1
    else:
        optional_log("\tDirectory permissions OK.\n")

    for wrong_file in [f for f in files_have_wrong_permissions if files_have_wrong_permissions[f]]:
        optional_log("\tlacking permissions on file %s.\n" % wrong_file)
        wrong_permissions += 1
    for right_file in [f for f in files_have_wrong_permissions if not files_have_wrong_permissions[f]]:
        optional_log("\tPermissions OK on file %s\n" % right_file)

    return wrong_permissions



def ensure_correct_capfile_installation():
    verbose("Checking correct plugins.cap installation\n")

    agent_dir = os.path.abspath(os.path.dirname((os.path.dirname(sys.executable))))
    cap = file(os.path.join(agent_dir, "plugins.cap"), "rb").read()

    os.chdir(agent_dir)
    capfiles_info = {"all_files": [], "missing_files": []}
    while cap:
        pathname, file_content, cap = _cap_next_entry(cap)
        capfiles_info["all_files"].append(pathname)
        if not os.path.exists(pathname):
            capfiles_info["missing_files"].append(pathname)
            warn("Unpack error. Missing file from plugins.cap: %s\n" % pathname)

    if capfiles_info["missing_files"]:
        warn("MSI capfile extraction failed. Starting extraction of plugins.cap\n")
        unpack_cap_file()


def unpack_cap_file():
    verbose("Unpacking plugins.cap\n")
    agent_dir = os.path.abspath(os.path.dirname((os.path.dirname(sys.executable))))
    cap = file(os.path.join(agent_dir, "plugins.cap"), "rb").read()
    os.chdir(agent_dir)

    all_files = []
    while cap:
        try:
            pathname, file_content, cap = _cap_next_entry(cap)
            dirname = os.path.dirname(pathname)
            verbose("Next file to extract %s\n" % pathname)
            if dirname and not os.path.exists(dirname):
                os.makedirs(dirname)
            file(pathname, "wb").write(file_content)
            all_files.append(os.path.join(agent_dir, pathname))
        except Exception, e:
            warn("Error while unpacking: %s, %s\n" % (pathname, e))

    uninstall_script = ["REM * Written by cmk-update-agent.exe",
                        "REM * If you want to uninstall the plugins which were installed during the",
                        "REM * last 'check_mk_agent.exe unpack' command, just execute this script","",""]

    all_files.append(os.path.join(agent_dir, "uninstall_plugins.bat"))
    for filepath in all_files:
        uninstall_script.append("del \"%s\"" % filepath.replace("/", "\\"))


    file(os.path.join(agent_dir, "uninstall_plugins.bat"), "w").write("\n".join(uninstall_script))


def _cap_next_entry(cap):
    pathname_length = ord(cap[0])
    cap = cap[1:]
    pathname = cap[:pathname_length]
    cap = cap[pathname_length:]
    filesize = _cap_get_filesize(cap[:4])
    cap = cap[4:]
    file_content = cap[:filesize]
    cap = cap[filesize:]
    return pathname, file_content, cap


def _cap_get_filesize(b):
    return ord(b[0]) + \
           (ord(b[1]) * 0x100)+ \
           (ord(b[2]) * 0x10000) + \
           (ord(b[3]) * 0x1000000)


#   .--Main----------------------------------------------------------------.
#   |                        __  __       _                                |
#   |                       |  \/  | __ _(_)_ __                           |
#   |                       | |\/| |/ _` | | '_ \                          |
#   |                       | |  | | (_| | | | | |                         |
#   |                       |_|  |_|\__,_|_|_| |_|                         |
#   |                                                                      |
#   +----------------------------------------------------------------------+
#   |  Main entry point, command line parsing, help                        |
#   '----------------------------------------------------------------------'

def main():
    os.unsetenv("LANG")

    lockfile, lockfile_path = aquire_updater_lock()
    restart_from_tempdir = False

    try:

        try:
            # This removes leftover folders which may be generated by crashing frozen binaries
            folder_cleaner = MEIFolderCleaner()
            folder_cleaner.find_and_remove_leftover_folders(hint_filenames = ["cmk-update-agent.exe.manifest"])

            # read config from files (not yet from command line)
            config = { "opsys" : our_os_type() }
            config.update(read_state_file())
            config.update(read_config_file()) # Can be overridden with options from cmdline

            if len(sys.argv) > 1 and sys.argv[1] == "register":
                main_register(config)
            else:
                if os.name == "nt" and need_restart_from_tempdir():
                    restart_from_tempdir = True
                else:
                    main_update(config)

        finally:
            lockfile.close()
            os.remove(lockfile_path)

            if restart_from_tempdir:
                restart_in_subprocess()

    except Exception, e:
        if opt_debug:
            raise
        bail_out(e)


if __name__ == "__main__":
    main()
