#!/usr/bin/env python
##############################################################################
# Copyright (c) Members of the EGEE Collaboration. 2015.
# See http://www.eu-egee.org/partners/ for details on the copyright
# holders.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS
# OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
##############################################################################
#
# NAME :        check_webdav
#
# DESCRIPTION : Checks the webdav interface of an endpoint
#
# AUTHORS :     Georgios Bitzes <georgios.bitzes@cern.ch>
#
##############################################################################

from __future__ import print_function, division, absolute_import

import os
import sys
import time
import pycurl
import functools
import argparse
import StringIO
import random
import traceback
import urlparse
import urllib
import cgi
import subprocess
import re
import tempfile

__version__ = "0.4.5"

__MAX_RND__ = 99
__TEST_FILENAME__ = "test_webdav_access_"
__TEST_FILE__ = "This is a test file generated by the nagios probe check-webdav from package nagios-plugins-webdav. It is safe to delete.\n"


# Verbosity
V_MINIMAL  = 0
V_EXTENDED = 1
V_DEBUG    = 2
V_ALL      = 3

EX_OK              = 0
EX_WARNING         = 1
EX_CRITICAL        = 2
EX_UNKNOWN         = 3

def ensure_safe_path(path):
    # Before running a dangerous operation (ie delete), make sure the path contains __TEST_FILENAME__
    # If not, something terribly wrong is going on. This should never trigger under any circumstances.

    if path.count(__TEST_FILENAME__) <= 0:
        raise Exception("ERROR: Refusing to touch something which does not contain {0}".format(__TEST_FILENAME__))

    if path.endswith("/"):
        raise Exception("ERROR: Refusing to touch a directory")

def http_status_to_explanation(status):
    if status == 404:
        return "404 file/path not found (are you using an incorrect path?)"
    elif status == 403:
        return "403 forbidden (are you using a valid certificate and path?)"
    elif status == 401:
        return "401 unauthorized (are you using a valid certificate and path?)"
    elif status == 500:
        return "500 server error"
    elif status == 507:
        return "507 insufficient storage"
    elif status == 0:
        return "connection failure (service down? SSL not configured properly?)"
    return "status {0}".format(status)

def add_prefix(string, prefix):
    """Returns a new string with the prefix string added before each line"""
    if string is None: return None

    ss = StringIO.StringIO()
    for line in string.splitlines():
        ss.write(prefix + line + "\n")
    return ss.getvalue()

def get_vo(proxy):
    """Returns the VO of the provided proxy"""
    process = subprocess.Popen(["voms-proxy-info", "-file", proxy, "-vo"],
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate()
    return stdout.strip()

credentials = ""
def get_credentials_information(proxy):
    """Returns verbose information about the given proxy"""
    process = subprocess.Popen(["voms-proxy-info", "-file", proxy, "-all"],
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate()
    return stdout

def getargs():
    class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawTextHelpFormatter):
        pass

    helptext = "Use either --uri or (--hostname, --port, and --path) to specify the target."

    parser = argparse.ArgumentParser(formatter_class=CustomFormatter,
                                     description="Checks the WebDAV capabilities of an endpoint.\n" + helptext,
                                     epilog="The script will return the following exit code:\n"
                                     "\t{0}, if all tests passed\n".format(EX_OK) +
                                     "\t{0}, if all tests passed but some were over the warning threshold\n".format(EX_WARNING) +
                                     "\t{0}, if some tests failed\n".format(EX_CRITICAL) +
                                     "\t{0}, if status is unknown\n".format(EX_UNKNOWN))

    # group = parser.add_mutually_exclusive_group(required=True)
    parser.add_argument('-H', '--hostname', type=str, help="The hostname to query\n")
    parser.add_argument('-u', '--uri', type=str, help="The target URI to test")

    parser.add_argument('-E', '--proxy', type=str, required=True, help="The path to the proxy certificate to use\n")
    parser.add_argument('-p', '--port', type=int, default=443, help="The server port to use\n")
    parser.add_argument('-P', '--path', type=str, help="The target server path to test\n")
    parser.add_argument('-v', '--verbose', action='count', default=0,
                        help="Verbosity levels:\n"
                        " -v   -> display some information about every test\n"
                        " -vv  -> show detailed information about every test\n"
                        " -vvv -> print server responses and headers, only for debugging\n")
    parser.add_argument('--capath', type=str, default="/etc/grid-security/certificates",
                        help="The certificate store to verify host credentials\n")
    parser.add_argument('-w', '--warning', type=int, default=10, help="Sets the warning threshold (seconds)\n")
    parser.add_argument('-t', '--timeout', type=int, default=100, help="Sets the timeout threshold (seconds)\n")
    parser.add_argument('-m', '--metric', type=str, help="The metric prefix under which to publish the results in Nagios.\n"
                        "Results will not be published if it isn't specified.")
    parser.add_argument('-f', '--fqan', type=str, default="", help="The FQAN suffix to append to the end of the Nagios service name.\n")
    parser.add_argument('--version', action='version', version=str(__version__))

    parser.add_argument('-c', '--no-crls', action='store_true', help="Copies the capath to a temporary location (given by --tempcapath),\n"
                        "while excluding Certificate Revocation Lists (all *.r0 files).\n"
                        "Necessary because of a bug in NSS that sometimes results in 'curl: (35) SSL connect error'")
    parser.add_argument('--tempcapath', type=str, default="/tmp/check-webdav_tempcapath",
                        help="The location to which the capath should be copied.\nOnly applies in case --no-crls is given.")
    parser.add_argument('--fixed-content-length', action='store_true', help="Do not use chunked encoding during uploads, but rather a fixed Content-Length")
    parser.add_argument('--dynafed', action='store_true', help="Use this flag when testing against a dynafed instance")
    parser.add_argument('--sanitized-query-params', nargs='+', default=["token", "dpmtoken", "dcache-http-uuid"])
    parser.add_argument('--no-cleanup', action='store_true', help="Do not perform a file cleanup at the end of the run")
    parser.add_argument('--wait-after-put', type=int, default=2, help="Wait this number of seconds after doing a PUT. \nRequired for certain"
                                                                      " overloaded servers, it takes a while until the file is available,\n"
                                                                      "otherwise you get an error. ")

    args = parser.parse_args()

    if args.hostname is None and args.uri is None:
        parser.error("neither --hostname nor --uri was given. " + helptext)

    if args.hostname is not None and args.path is None and args.uri is None:
        parser.error("no path specified. Using --path is mandatory with --hostname. " + helptext)

    # both --uri and --hostname were given.. make sure they match
    # Necessary because most nagios scripts are called with -H in
    # addition to all other arguments.
    if args.hostname is not None and args.uri is not None:
        uri = urlparse.urlparse(args.uri)
        if uri.hostname != args.hostname:
            parser.error("hostname in --uri does not match --hostname")

    if args.uri is None:
        args.uri = "https://{0}:{1}/{2}/".format(args.hostname, args.port, args.path)
    if not args.uri.endswith("/"):
        args.uri += "/"

    return args

def log(level, message, threshold=None):
    if level <= threshold:
        print(message, end="")

class SingleTest:
    """A class to help run a single test and produce the corresponding TestResult"""
    def __init__(self, name, description, method, args, expected=range(200, 300), critical=True):
        self.name = name
        self.description = description
        self.method = method
        self.args = args
        self.expected = expected
        self.critical = critical
    def finalize(self, result):
        result.setparams(self.name, self.description, expected_status=self.expected, critical=self.critical)
        result.finalize()
    def run(self):
        result = self.method(*self.args)
        self.finalize(result)
        return result
    def skip(self, msg):
        result = TestResult(0)
        result.diagnostic(msg)
        self.finalize(result)
        return result

class TestResult(object):
    """A class to store and display the results from a single test run"""
    STATE_NOT_RUN = 1
    STATE_RUN     = 2
    STATE_FAILED  = 3

    def __init__(self, warnthres):
        self.state = TestResult.STATE_NOT_RUN
        self.warnthres = warnthres

        self.messages = []
        self.history = None
        self.target = None
        self.ipaddr = None

        self.status = 0
        self.elapsed = 0
    def set(self, status, elapsed):
        self.status = status
        self.elapsed = elapsed
        if self.state == TestResult.STATE_NOT_RUN:
            self.state = TestResult.STATE_RUN

    def failure(self, msg):
        self.diagnostic(msg)
        self.state = TestResult.STATE_FAILED

    def sethistory(self, history):
        self.history = history

    def diagnostic(self, msg):
        self.messages.append(msg)

    def setparams(self, name, description, expected_status, critical):
        self.name = name
        self.description = description
        self.short_name = name.lower().replace(" ", "_").replace("-", "_")
        self.expected_status = expected_status
        self.critical = critical

    def finalize(self):
        if self.state != TestResult.STATE_NOT_RUN and self.elapsed >= self.warnthres:
            self.diagnostic("Test is slow - took more than {0} seconds".format(self.warnthres))

        self.outcome = self.determine_outcome()

    def is_slow(self):
        if self.elapsed > self.warnthres:
            return True
        return False

    def determine_outcome(self):
        if self.state == TestResult.STATE_NOT_RUN:
            return EX_UNKNOWN
        if self.state == TestResult.STATE_FAILED:
            return EX_CRITICAL

        assert self.state == TestResult.STATE_RUN
        if self.status not in self.expected_status:
            return EX_CRITICAL
        elif self.is_slow():
            return EX_WARNING
        else:
            return EX_OK

    def performance_data(self):
        # If a test fails, make nagios show an empty graph
        if self.outcome == EX_CRITICAL or self.outcome == EX_UNKNOWN or self.status not in self.expected_status:
            return ""
        return "{0}={1:.2f}s".format(self.short_name, self.elapsed)

    def string_outcome(self):
        if self.outcome == EX_CRITICAL:
            if self.critical:
                return "CRITICAL"
            else:
                return "FAILURE - non critical"
        elif self.outcome == EX_WARNING:
            return "SLOW"
        elif self.outcome == EX_OK:
            return "OK"
        elif self.outcome == EX_UNKNOWN:
            return "UNKNOWN"

    def short_output(self):
        s = self.string_outcome()
        out = "{0}: {1}".format(s, self.description)
        if self.outcome == EX_CRITICAL and self.critical:
            out += " - {0}".format(http_status_to_explanation(self.status))
        return out

    def long_output(self, printHTML=False):
        if printHTML:
            indent = "&nbsp;"*4
        else:
            indent = " "*4

        output = StringIO.StringIO()
        s = self.string_outcome()

        output.write("{0:29} {1:5} ({2:<10} sec)   {3}\n".format(self.name, self.status, self.elapsed, s))

        if self.target:
            output.write(add_prefix("Target: " + self.target + "\n", indent))

        if self.ipaddr:
            output.write(add_prefix("Last resolved IP: " + self.ipaddr + "\n", indent))

        for i in self.messages:
            output.write(add_prefix(i,indent))

        if self.outcome == EX_CRITICAL or self.outcome == EX_WARNING:
            output.write(add_prefix("For guidance on troubleshooting a failing endpoint, please have a look at https://twiki.cern.ch/twiki/bin/view/LCG/HTTPTFSAMProbe", indent))
            output.write(add_prefix("Credentials:\n", indent))
            output.write(add_prefix(credentials, indent*2))

        if (self.outcome != EX_OK) and self.history:
            output.write(add_prefix("Server response: {0}\n".format(http_status_to_explanation(self.status)), indent))
            output.write(add_prefix("Debugging information: the history of all performed requests and "
                                    "received responses during the execution of this test follows.\n", indent))
            if printHTML:
                output.write(add_prefix(cgi.escape(self.history), indent))
            else:
                output.write(add_prefix(self.history, indent))

        return output.getvalue()

def get_tls_ciphers(target):
    """Returns a list of ciphers that the endpoint supports"""
    uri = urlparse.urlparse(target)
    port = uri.port
    if not port: port = 443

    # create temporary servicesdb file for nmap which contains the desired port
    tmp = tempfile.mkstemp()[1]
    with open(tmp, "w") as f:
        f.write("https {0}/tcp 0.1".format(port))

    process = subprocess.Popen(["nmap", "-Pn", "--script", "ssl-enum-ciphers",
                                "--servicedb", tmp, "--port-ratio", "0", uri.hostname],
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    stdout, stderr = process.communicate()
    os.remove(tmp)

    ciphers = {}
    parsing = False
    protocol = None
    for line in stdout.split("\n"):
        if not line.startswith("|"): continue
        line = line[1:].strip()

        if line.startswith("TLSv"):
            protocol = line
        elif line.lower().startswith("ciphers"):
            parsing = True
        elif line.lower().startswith("compressors"):
            parsing = False
        elif line.startswith("TLS_") and parsing and protocol is not None:
            if protocol not in ciphers:
                ciphers[protocol] = []
            ciphers[protocol].append(line.split(' ')[0])

    return ciphers

def is_cipher_blacklisted(cipher):
    blacklist = ["_RC4_", "_NULL_"]
    for b in blacklist:
        if b in cipher:
            return True
    return False

def test_tls_ciphers(target):
    start = time.time()
    ciphers = get_tls_ciphers(target)
    end = time.time()

    # if an endpoint only provides blacklisted ciphers, this test will fail
    failure = True
    for protocol in ciphers:
        for cipher in ciphers[protocol]:
            if not is_cipher_blacklisted(cipher):
                failure = False

    # much higher time limit
    result = TestResult(600)
    result.set("", end-start)

    # test failed? print list of ciphers
    if failure:
        result.failure("Error - endpoint only accepts blacklisted ciphers.\nModern platforms (recent ubuntu) and clients (chrome, firefox) may have trouble connecting.")
        diagnostic = StringIO.StringIO()
        diagnostic.write("List of ciphers that the endpoint supports\n")
        for protocol in ciphers:
            diagnostic.write(protocol + "\n")
            for cipher in ciphers[protocol]:
                diagnostic.write("  " + cipher + "\n")

        result.diagnostic(diagnostic.getvalue())
    return result

class Tester(object):
    def __init__(self, args):
        self.args = args
        self.path = args.uri

    def log(self, level, message):
        if level <= self.args.verbose:
            print(message, end="")

    def determine_exit_code(self, results):
        slow = [x.is_slow() for x in results]
        outcomes = [x.outcome for x in results if x.critical]

        if outcomes.count(EX_CRITICAL) > 0:
            return EX_CRITICAL
        if outcomes.count(EX_WARNING) > 0:
            return EX_WARNING
        if True in slow:
            return EX_WARNING

        return EX_OK

    def print_results(self, results):
        """Print all necessary output to stdout"""
        if self.global_outcome == EX_OK:
            self.log(V_MINIMAL, "OK: the HTTP interface is usable")
        elif self.global_outcome == EX_WARNING:
            slow = [x for x in results if x.is_slow()]
            firstwarn = slow[0].name if len(slow) > 0 else '---'
            self.log(V_MINIMAL, "WARNING: {0} tests are slow, starting with '{1}'".format(len(slow), firstwarn))
        elif self.global_outcome == EX_CRITICAL:
            criticals = [x for x in results if x.outcome == EX_CRITICAL]
            firstcrit = criticals[0].name if len(criticals) > 0 else '---'
            self.log(V_MINIMAL, "CRITICAL: {0} tests failed, starting with '{1}'".format(len(criticals), firstcrit))
        elif self.global_outcome == EX_UNKNOWN:
            self.log(V_MINIMAL, "UNKNOWN: an unknown error occured")

        # publish performance data, used by nagios in making graphs
        self.log(V_EXTENDED, " | ")
        for result in results:
            self.log(V_EXTENDED, result.performance_data())
            self.log(V_EXTENDED, "  ")
        self.log(V_DEBUG, "\n")

        # if running inside nagios, escape all HTML
        printHTML = "NAGIOS_ARG1" in os.environ

        for result in results:
            longout = result.long_output(printHTML=printHTML)
            # prevent nagios from going insane if there's a pipe in the HTML page output
            if printHTML: longout = longout.replace("|", "--pipe symbol--")
            self.log(V_DEBUG, longout)

        self.log(V_MINIMAL, "\n")

    def publish_results(self, results):
        """Publish the passive test results to Nagios."""
        publish = []
        for r in results:
            item = {}

            outcome = r.outcome
            if r.outcome == EX_CRITICAL and not r.critical:
                outcome = EX_WARNING

            item["status"] = outcome
            item["host"] = urlparse.urlparse(self.args.uri).hostname
            item["details"] = r.long_output(printHTML=True)
            item["summary"] = r.short_output()
            item["service"] = self.args.metric + "-" + r.short_name.upper() + "-" + self.args.fqan
            publish.append(item)

        try:
            import gridmon.nagios.nagios
            gridmon.nagios.nagios.publishPassiveResult(publish)
        except Exception, e:
            self.log(V_DEBUG, "Error: could not publish passive result, an exception occured\n")
            self.log(V_DEBUG, "Exception text:  " + str(e) + "\n")

    def run_dynafed_tests(self, target1, target2):
        """Run all 'put-succeeded-with-dynafed' tests and return the results"""
        results = []
        tests = [
            SingleTest("FILE GET", "GET file", self.get, [target1, __TEST_FILE__]),
            SingleTest("FILE OPTIONS", "OPTIONS on file", self.options, [target1]),
            SingleTest("FILE HEAD", "HEAD on file", self.head, [target1]),
            SingleTest("FILE HEAD on non-existent", "HEAD on a path that does not exist, should return 404",
                       self.head, [target2], expected=[404]),
            SingleTest("FILE PROPFIND", "PROPFIND on file", self.propfind, [target1]),
            SingleTest("FILE DELETE", "DELETE on file", self.delete, [target1]),
            SingleTest("FILE DELETE on non-existent", "DELETE on a file that does not exist, should return 404",
                       self.delete, [target2], expected=[404], critical=False)
        ]

        for t in tests: results.append(t.run())
        return results

    def run_put_ok_tests(self, target1, target2):
        """Run all 'put-succeeded' tests and return the results"""
        results = []
        tests = [
            SingleTest("FILE GET", "GET file", self.get, [target1, __TEST_FILE__]),
            SingleTest("FILE OPTIONS", "OPTIONS on file", self.options, [target1]),
            SingleTest("FILE MOVE", "MOVE file to another path", self.move, [target1, target2]),
            SingleTest("FILE HEAD", "HEAD on file", self.head, [target2]),
            SingleTest("FILE HEAD on non-existent", "HEAD on a path that does not exist, should return 404",
                       self.head, [target1], expected=[404]),
            SingleTest("FILE PROPFIND", "PROPFIND on file", self.propfind, [target2]),
            SingleTest("FILE DELETE", "DELETE on file", self.delete, [target2]),
            SingleTest("FILE DELETE on non-existent", "DELETE on a file that does not exist, should return 404",
                       self.delete, [target1], expected=[404], critical=False)
        ]

        for t in tests: results.append(t.run())
        return results

    def run_put_failed_tests(self):
        """Run all 'put-failed' tests and return the results"""
        results = []

        # check if the standard file exists
        standard_file = self.path + "{0}_HTTPTFtest.txt".format(get_vo(self.args.proxy).upper())
        get = SingleTest("FILE GET", "GET file", self.get, [standard_file])
        results.append(get.run())
        getfailed = results[-1].outcome == EX_CRITICAL or results[-1].outcome == EX_UNKNOWN

        readtests = [
            SingleTest("FILE OPTIONS", "OPTIONS on file", self.options, [standard_file]),
            SingleTest("FILE HEAD", "HEAD on file", self.head, [standard_file]),
            SingleTest("FILE HEAD on non-existent", "HEAD on a path that does not exist, should return 404",
                       self.head,[standard_file+"does_not_exist"], expected=[404]),
            SingleTest("FILE PROPFIND", "PROPFIND on file", self.propfind, [standard_file]),
        ]

        for t in readtests:
            if not getfailed:
                results.append(t.run())
            else:
                results.append(t.skip("Standard file not found, skipping"))

        skipped = [
            SingleTest("FILE MOVE", "MOVE file to another path", None, None),
            SingleTest("FILE DELETE", "DELETE on file", None, None),
            SingleTest("FILE DELETE on non-existent", "DELETE on a file that does not exist, should return 404",
                        None, None)
        ]

        for t in skipped: results.append(t.skip("PUT failed, skipping"))
        return results

    def test(self):
        results = []

        target1 = self.path + __TEST_FILENAME__ + str(random.randint(0, __MAX_RND__))
        target2 = target1 + "_moved"

        # declare read tests
        readtests = [
            SingleTest("TLS CIPHERS", "Verifies the server provides a reasonable set of TLS ciphers",
                        test_tls_ciphers, [self.path], expected=[""], critical=False),
            SingleTest("DIR HEAD", "HEAD on dir", self.head, [self.path]),
            SingleTest("DIR GET", "GET on dir", self.get, [self.path, None])
        ]

        # run read tests
        for t in readtests:
            results.append(t.run())

        # initial PUT - do I have write access to the server?
        if self.args.fixed_content_length:
            put = SingleTest("FILE PUT", "PUT a file", self.putFixedString, [target1], critical=False)
        else:
            put = SingleTest("FILE PUT", "PUT a file (chunked encoding)", self.putChunked, [target1], critical=False)

        results.append(put.run())
        putok = results[-1].status >= 200 and results[-1].status < 300

        if putok:
            time.sleep(self.args.wait_after_put)

            if not self.args.dynafed:
                results += self.run_put_ok_tests(target1, target2)
            else:
                results += self.run_dynafed_tests(target1, target2)
        else:
            results += self.run_put_failed_tests()

        # print all output
        self.global_outcome = self.determine_exit_code(results)
        self.print_results(results)

        # do a cleanup, remove any files that might have been left over from a previous failed run
        if not self.args.no_cleanup:
            self.cleanup(self.path)

        # publish results to nagios
        if self.args.metric:
            self.publish_results(results)

        return self.global_outcome

def sanitize(query, param):
    if param in query:
        query[param] = "sanitized"

class CurlTester(Tester):
    def __init__(self, args):
        Tester.__init__(self, args)
    def getcurl(self, target):
        args = self.args
        curl = pycurl.Curl()
        curl.setopt(pycurl.CONNECTTIMEOUT, args.timeout)
        curl.setopt(pycurl.TIMEOUT, args.timeout)
        curl.setopt(pycurl.SSL_VERIFYPEER, 1)
        curl.setopt(pycurl.SSL_VERIFYHOST, 2)
        curl.setopt(pycurl.SSLVERSION, pycurl.SSLVERSION_TLSv1)

        curl.setopt(pycurl.FOLLOWLOCATION, 1)

        curl.setopt(pycurl.CAINFO, args.proxy)
        curl.setopt(pycurl.SSLCERT, args.proxy)
        curl.setopt(pycurl.CAPATH, args.capath)
        curl.__mytarget  = target # pycurl does not support getting the URL back, so let's improvise
        curl.setopt(pycurl.URL, target)

        self.responseBody = StringIO.StringIO()
        curl.setopt(pycurl.WRITEFUNCTION, self.responseBody.write)

        self.responseHeader = StringIO.StringIO()
        curl.setopt(pycurl.HEADERFUNCTION, self.responseHeader.write)

        self.history = StringIO.StringIO()
        def debugfunction(self, msgtype, msg):
            # request
            if msgtype == 2 or msgtype == 4:
                self.history.write(add_prefix(msg, "=> "))
            # response
            if msgtype == 1 or msgtype == 3:
                self.history.write(add_prefix(msg, "<= "))

        curl.setopt(curl.VERBOSE, 1)
        curl.setopt(pycurl.DEBUGFUNCTION, lambda x, y: debugfunction(self, x, y))

        return curl

    def putFixedString(self, target):
        # Do a put with a fixed string, which sends a fixed Content-Length.
        # Useful for servers which may not support chunked encoding.
        curl = self.getcurl(target)
        curl.setopt(pycurl.POSTFIELDS, __TEST_FILE__)
        curl.setopt(pycurl.CUSTOMREQUEST, "PUT")
        curl.setopt(pycurl.POST, 1)
        curl.setopt(pycurl.HTTPHEADER, ["Content-Type:", "Expect: 100-continue"])
        return self.perform(curl)

    def putChunked(self, target):
        curl = self.getcurl(target)

        # Sometimes, curl needs to rewind the sent data. The human way to deal
        # with this is to set SEEKFUNCTION, but pycurl on SL6 does not
        # yet support it.
        # Repeat reader rewinds itself automatically once it reaches the end.
        class RepeatReader:
            def __init__(self, data):
                self.pos = 0
                self.data = data
            def read(self, size):
                ret = self.data[self.pos:self.pos+size]
                self.pos=self.pos+size

                # rewind?
                if len(ret) == 0:
                    self.pos = 0

                return ret

        curl.setopt(pycurl.UPLOAD, 1)
        repeater = RepeatReader(__TEST_FILE__)
        curl.setopt(pycurl.READFUNCTION, repeater.read)

        # This is a pretty terrible hack: lib/transfer.c
        # in the curl C source checks for the existence of
        # POSTFIELDS - if it finds it, it thinks there is no need
        # to rewind the data.
        #
        # POSTFIELDS is usually what you want to send in the body
        # of a request, but since we're doing an UPLOAD, it's ignored.
        #
        # Without this, curl sees there's no SEEKFUNCTION defined
        # and throws an error when it's unable to rewind the data.
        # In our case, the read function rewinds itself automatically.
        curl.setopt(pycurl.POSTFIELDS, "irrelevant string")

        # Here is what the non-insane way to do this would look like - please
        # change as soon as SEEKFUNCTION is available on all platforms.

        # stringio = StringIO.StringIO(__TEST_FILE__)
        # curl.setopt(pycurl.READFUNCTION, stringio.read)
        # curl.setopt(pycurl.SEEKFUNCTION, stringio.seek)

        # There's also the possibility to use POSTFIELDS and manually set the verb to PUT,
        # but this does not send a 100-Continue.

        # curl.setopt(pycurl.CUSTOMREQUEST, "PUT")
        # curl.setopt(pycurl.POSTFIELDS, __TEST_FILE__)

        return self.perform(curl)
    def delete(self, target):
        # safety check: make sure we aren't deleting anything important
        ensure_safe_path(target)

        curl = self.getcurl(target)
        curl.setopt(pycurl.CUSTOMREQUEST, "DELETE")
        return self.perform(curl)
    def options(self, target):
        curl = self.getcurl(target)
        curl.setopt(pycurl.CUSTOMREQUEST, "OPTIONS")
        result = self.perform(curl)

        allows = None
        for line in self.responseHeader.getvalue().split("\n"):
            if line.lower().startswith("allow:") or line.lower().startswith("access-control-allow-methods"):
                allows = line

        if allows is None:
            result.failure("ERROR: Could not find Allow response")
        else:
            result.diagnostic(allows)

        return result
    def move(self, source, destination):
        ensure_safe_path(source)
        ensure_safe_path(destination)

        curl = self.getcurl(source)
        curl.setopt(pycurl.CUSTOMREQUEST, "MOVE")
        curl.setopt(pycurl.HTTPHEADER, ["Destination: {0}".format(destination)])
        return self.perform(curl)
    def head(self, target):
        curl = self.getcurl(target)
        curl.setopt(pycurl.NOBODY, 1)
        return self.perform(curl)
    def propfind(self, target):
        curl = self.getcurl(target)
        curl.setopt(pycurl.CUSTOMREQUEST, "PROPFIND")
        curl.setopt(pycurl.HTTPHEADER, ["Depth: 1"])
        curl.setopt(pycurl.POSTFIELDS, '<?xml version="1.0"?><a:propfind xmlns:a="DAV:"><a:prop></a:prop></a:propfind>')
        return self.perform(curl)
    def get(self, target, contents=None):
        curl = self.getcurl(target)
        result = self.perform(curl)
        if contents is not None and self.responseBody.getvalue() != contents:
            result.failure("ERROR: File contents are wrong!")
            result.diagnostic("CONTENTS: " + self.responseBody.getvalue())
        return result
    # clean all residual testfiles in target directory that might have been
    # left by previous runs of this script.
    # Prevents accumulation of testfiles
    def cleanup(self, target):
        print("Looking for left-over testfiles from previous failed runs..")
        self.propfind(target)
        xml = self.responseBody.getvalue()
        uri = urlparse.urlparse(target)

        iter = re.finditer(re.escape("<") + "[Dd]" + re.escape(":href>")
               + "(" + re.escape(uri.path) + __TEST_FILENAME__ + "\d+(_moved)?" + ")"
               + re.escape("</") + "[Dd]" + re.escape(":href>"), xml)
        for match in iter:
            path = match.group(1)
            newuri = uri._replace(path=path)
            print("Cleanup - deleting left-over testfile: " + newuri.geturl())
            self.delete(newuri.geturl())

        print("All done")
    def perform(self, curl):
        """Return a TestResult object with information about what happened during the request"""
        result = TestResult(self.args.warning)
        try:
            curl.perform()

            status = curl.getinfo(pycurl.HTTP_CODE)
            elapsed = curl.getinfo(pycurl.TOTAL_TIME)
            result.set(status, elapsed)
        except Exception, e:
            result.failure("Curl exception: " + str(e))

        effective = curl.getinfo(pycurl.EFFECTIVE_URL)

        # Print redirected URL after sanitizing sensitive token fields
        if effective != curl.__mytarget:
            url_parts = list(urlparse.urlparse(effective))
            query = urlparse.parse_qs(url_parts[4])

            for param in self.args.sanitized_query_params:
                sanitize(query, param)

            url_parts[4] = urllib.urlencode(query)
            redir = urlparse.urlunparse(url_parts)
            result.diagnostic("Redirection - effective URL: " + redir)

        result.sethistory(self.history.getvalue())
        result.target = curl.__mytarget
        result.ipaddr = curl.getinfo(pycurl.PRIMARY_IP)
        return result

def prepare_custom_capath(capath, tempcapath):
    DEVNULL = open(os.devnull, 'wb')
    process = subprocess.Popen(["rsync", "--archive", "--exclude", "*.r0", capath + "/", tempcapath],
                               stdout=DEVNULL, stderr=subprocess.STDOUT)
    ret = process.wait()
    # assert ret == 0

def main():
    try:
        args = getargs()

        if args.no_crls:
            prepare_custom_capath(args.capath, args.tempcapath)
            args.capath = args.tempcapath

        global credentials
        credentials = get_credentials_information(args.proxy)

        tester = CurlTester(args)
        return tester.test()
    except Exception, e:
        print("An unknown error occured")
        print(traceback.format_exc())
        return EX_UNKNOWN
if __name__ == "__main__":
    sys.exit(main())
