#!/usr/bin/env python
try:
    import http.server as server
except ImportError:
    import BaseHTTPServer as server
import getopt
import re
import sys

try:
    from urllib.parse import urlparse, urlencode
    from urllib.request import urlopen, Request
    from urllib.error import HTTPError, URLError
    from urllib.request import (
        HTTPHandler,
        HTTPSHandler,
        HTTPRedirectHandler,
        ProxyHandler,
        build_opener,
        install_opener,
    )
except ImportError:
    from urlparse import urlparse
    from urllib import urlencode
    from urllib2 import (
        urlopen,
        Request,
        HTTPError,
        HTTPHandler,
        HTTPSHandler,
        HTTPRedirectHandler,
        ProxyHandler,
        build_opener,
        install_opener,
        URLError,
    )
################## HEADER ###################################

#
# Traceroute-like HTTP scanner
# Using the "Max-Forwards" header
# RFC 2616 - HTTP/1.1 - Section 14.31
# RFC 3261 - SIP - Section 8.1.1.6
#

#
# By Nicolas Gregoire (nicolas.gregoire@agarri.fr)
#
# 0.5 : First public release
# 0.4 : Private release, looking for bugs - More heuristics
# 0.3 : A lot more options - More verbosity levels - Some heuristics
#
# By Julien Cayssol (tools@aqwz.com)
#
# 0.2 : Add extract of headers
# 0.1 : Initial version
#

#
# Heuristics :
# - Status Codes :
# 	- HTTP Status Code == 502
# 	- HTTP Status Code == 483
# - Specific data in body or headers :
# 	- X-Forwarded-For in body when using TRACE
# 	- Via or X-Via in headers
# - Differences between hops :
# 	- HTTP Status Codes
# 	- Server headers
# 	- Content-Type headers
# 	- Via headers
# 	- HTML titles
# 	- HTML <address> tags
# 	- X-Forwarded-For values when using TRACE
#

############## GLOBAL VARIABLES ###################################

global_data = {
    "StatusCode": {},
    "Server": {},
    "Content-Type": {},
    "Title": {},
    "Address": {},
    "X-Fwd": {},
    "Via": {},
}
score = 0
verbosity = 0
scheme = "http"
host = "127.0.0.1"
port = "80"
path = "/"
method = "TRACE"
body_content = None
max_fwds = 3
userAgent = (
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.2) Gecko/20060502 Firefox/1.5.0.2"
)
contentType = "text/html"

############## FUNCTIONS ###################################


# Pretty printing
def zprint(string, flag="=="):
    print("[" + flag + "] " + string)


# Increment the heuristic score
def inc_score():
    global score
    score = score + 1
    if verbosity:
        zprint("Score : " + str(score), "!!")


# Help
def showUsage():
    print(
        "Usage : "
        + sys.argv[0]
        + " [-h] [-m method] [-s scheme] [-t target] [-p port] [-P path] [-v 0|1|2] [-f forwards]"
    )
    print("\t[-h] Help (this text)")
    print('\t[-m] HTTP Method : default is "TRACE"')
    print('\t[-s] Scheme : default is "http"')
    print('\t[-t] Target host : default is "127.0.0.1"')
    print('\t[-p] Port : default is "80"')
    print('\t[-P] Path : default is "/"')
    print('\t[-f] Max # of forwards : default is "3"')
    print("\t[-v] Verbosity : 0 = default, 1 = verbose, 2 = debug")
    print("Examples :")
    print(sys.argv[0] + " -t www.example.org")
    print("  => TRACE /")
    print(sys.argv[0] + " -t www.example.org -m GET -s https -p 443 -v 1")
    print("  => GET / on a SSL host")
    print(sys.argv[0] + " -t www.example.org -m POST -P /axis2/checkacc -v 2 -f 5")
    print("  => Debug mode on a specific end-point")
    sys.exit(1)


# Parse CLI args
def getArguments():
    try:
        if len(sys.argv) < 2:
            zprint('No arguments ? Probably a bad choice. Use "-h" ...', "!!")
            sys.exit(1)
        optlist, list = getopt.getopt(sys.argv[1:], "hm:s:t:p:P:v:f:")
    except getopt.GetoptError:
        showUsage()
    for opt in optlist:
        if opt[0] == "-h":
            showUsage()
        if opt[0] == "-m":
            global method
            method = opt[1]
        if opt[0] == "-s":
            global scheme
            scheme = opt[1]
        if opt[0] == "-t":
            global host
            host = opt[1]
        if opt[0] == "-p":
            global port
            port = opt[1]
        if opt[0] == "-P":
            global path
            path = opt[1]
        if opt[0] == "-v":
            global verbosity
            verbosity = int(opt[1])
        if opt[0] == "-f":
            global max_fwds
            max_fwds = int(opt[1])


# Extract some interesting data from the headers
def analyse_headers(data):

    if verbosity:
        zprint("Analyzing headers", "**")

    wanted_headers = [
        "Server",
        "Via",
        "X-Via",
        "Set-Cookie",
        "X-Forwarded-For",
        "Content-Type",
        "Content-Length",
        "Last-Modified",
        "Location",
        "Date",
    ]

    for h_name in wanted_headers:
        h_value = data.get(h_name)
        if h_value != None:
            # Print the value
            if verbosity:
                zprint(h_value, h_name)
            # Add it to the global structure if needed
            if h_name == "Server" or h_name == "Content-Type":
                global_data[h_name][hop] = h_value
            # Some heuristics
            if h_name == "Via" or h_name == "X-Via":
                zprint('"Via" header : Probably a reverse proxy', "++")
                global_data["Via"][hop] = h_value
                inc_score()


# Extract some interesting data from the body
def analyse_body(data):

    if verbosity:
        zprint("Analyzing body", "**")

    wanted_patterns = [
        "<title>(.*)</title>",
        "<address>(.*)</address>",
        "Reason: <strong>(.*)</strong>",
        "X-Forwarded-For: (.*)",
    ]

    for p_name in wanted_patterns:
        # Case insensitive search
        p_value = re.search(p_name, str(data), re.IGNORECASE)
        if p_value != None:
            # Only the 1st group, without newlines
            value = p_value.groups()[0].strip("\r\n")
            if verbosity:
                zprint(value, p_name)
            # Add it to the global structure if needed
            if p_name == "<title>(.*)</title>":
                global_data["Title"][hop] = value
            if p_name == "<address>(.*)</address>":
                global_data["Address"][hop] = value
            # Some heuristics
            if re.search("X-Forwarded-For:", p_name):
                global_data["X-Fwd"][hop] = value
                if method == "TRACE":
                    zprint(
                        '"X-Forwarded-For" in body when using TRACE : Probably a reverse proxy',
                        "++",
                    )
                    inc_score()


# Analyse the data returned by urllib2.*open()
def debug_and_parse(data):

    # Get data
    headers = data.info()
    body = data.read()

    # Debug
    if verbosity == 2:
        zprint(str(headers), "DEBUG HEADERS")
        zprint(str(body), "DEBUG BODY")

    # Extract some interesting info
    codes = server.BaseHTTPRequestHandler.responses
    global_data["StatusCode"][hop] = str(data.code) + " " + codes[data.code][0]
    analyse_headers(headers)
    analyse_body(body)


############## SCAN ###################################

# Init
getArguments()

# Current target
url = scheme + "://" + host + ":" + port + path
zprint("Target URL : " + url)
zprint("Used method : " + method)
zprint("Max number of hops : " + str(max_fwds))

# Scan
for hop in range(0, max_fwds):

    # Create the request object
    request = Request(url)
    request.get_method = lambda: method
    request.data = body_content
    request.add_header("Content-Type", contentType)
    request.add_header("User-agent", userAgent)

    # Add the 'Max-Forwards' header
    request.add_header("Max-Forwards", hop)
    if verbosity:
        print("-" * 80)
        zprint(
            'Current value of "Max-Forwards" = ' + str(hop) + " [" + "-" * 20 + "]",
            "-" * 19,
        )
        print("-" * 80)

    try:
        # Do the HTTP request
        opener = build_opener(HTTPHandler)
        result = opener.open(request)

        # Found something
        if verbosity:
            zprint("Status Code => HTTP 200: OK", "**")

        # Analyse it
        debug_and_parse(result)

    # Not a 200 OK
    except HTTPError as e:
        if verbosity:
            zprint("Status Code => " + str(e), "**")
        # Some heuristics
        if e.code == 502:
            zprint("HTTP 502 : Probably a reverse proxy", "++")
            inc_score()
        if e.code == 483:
            zprint("HTTP 483 : Probably a reverse proxy (SIP ?)", "++")
            inc_score()

        # Analyse it
        debug_and_parse(e)

    # Network problem
    except URLError as e:
        zprint("Network problem !", "!!")
        zprint("Reason : " + str(e.reason), "!!")
        break

############## REPORT ###################################

print("-" * 80)
zprint("Heuristic Report [" + "-" * 31 + "]", "-" * 27)
print("-" * 80)

# For each key
for k in list(global_data.keys()):
    string = k + ":\n"
    previous = "Undef"
    # For each hop
    ok = 0
    for i in range(0, max_fwds):
        # Try this key
        try:
            current = global_data[k][i]
            # We got a value !
            ok = 1
        except KeyError:
            current = "Undef"

        # Some heuristics
        if previous != current and i > 0:
            inc_score()

        # Then add it to the current string
        string = string + "\tHop #" + str(i) + " : " + current + "\n"
        previous = current

    # Display this key only if values were found
    if ok:
        print(string)

# Final score
if score == 0:
    zprint("No reverse proxy", "--")
else:
    zprint("Found a reverse proxy, score is " + str(score), "++")
