#!/usr/bin/env python3
#-*- mode: Python;-*-
#
# Requires Python 3+

'''
This script reads a raw HTTP request and writes to stdout a Python
script.  The generated script sends the same (or a very similar) 
request using the Requests library, or optionally, the built-in 
http.client library.

Certainly if you have a raw request, you could simply send it via TCP
sockets, but if for some reason the server behaves oddly with flow control,
insists on using gzip/deflate encoding, insists on using chunked encoding,
or any number of other annoying things, then using an HTTP library is a 
lot more convenient.  This script attempts to make the conversion from a
raw HTTP request to HTTP library calls easy for pentesting automation.


Copyright (C) 2011-2013 Virtual Security Research, LLC
Copyright (C) 2014-2017 Blindspot Security LLC
Author: Timothy D. Morgan

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Lesser General Public License, version 3,
 as published by the Free Software Foundation.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
'''

import sys
import argparse
import pprint
import urllib.parse

bopen = lambda f: open(f, 'rb')

parser = argparse.ArgumentParser(
    description='A script which accepts an HTTP request and prints out a'
    ' generated Python script which sends a similar request.  This is useful'
    ' when one wants to automate sending a large number of requests to a'
    ' particular page or application.'
    ' For more information, see: http://code.google.com/p/bletchley/wiki/Overview')
parser.add_argument(
    'requestfile', type=bopen, nargs='?', default=sys.stdin.buffer, 
    help='A file containing an HTTP request.  Defaults to stdin if omitted.')
group = parser.add_mutually_exclusive_group()
group.add_argument('--requests', action='store_true',
                   help='Generate a script that uses the Requests module'
                        ' rather than http.client (default).')
group.add_argument('--native', action='store_false', dest='requests',
                   help='Generate a script that uses Pythons built-in http.client'
                        ' rather than the Requests module.')
parser.set_defaults(requests=True)

args = parser.parse_args()
input_req = args.requestfile.read()


if b'\r\n\r\n' in input_req:
    raw_headers,body = input_req.split(b'\r\n\r\n', 1)
elif b'\n\n' in input_req:
    raw_headers,body = input_req.split(b'\n\n', 1)
else:
    raw_headers = input_req
    body = b''

raw_headers = raw_headers.decode('utf-8')

header_lines = raw_headers.split('\n')
method,path,version = header_lines[0].split(' ', 2)

host = 'TODO'
port = None
protocol = None

headers = []
for l in header_lines[1:]:
    if len(l) < 1: 
        break
    # Handle header line continuations
    if l[0] in ' \t':
        if len(headers) == 0:
            continue
        name,values = headers[-1]
        values.append(l.lstrip('\t'))
        headers[-1] = (name,values)
        continue

    name,value = l.split(':',1)
    value = value.lstrip(' ').rstrip('\r')

    # Skip headers that have to do with transfer encodings, connection longevity, and caching
    # XXX: maybe add these back as commented-out headers to the output?
    if name.lower() not in ['accept','accept-language',
                            'accept-encoding','accept-charset',
                            'connection', 'keep-alive', 'host', 
                            'content-length', 'proxy-connection',
                            'if-none-match']:
        headers.append((name,[value]))

    if name.lower() == 'host':
        if ':' in value:
            host,port = value.split(':',1)
            port = int(port, 10)
            if port == 443:
                protocol = 'https'
        else:
            host = value

    # Attempt to guess the port and protocol from the referer header, since
    # often it is the same site.  Defer to the host header though, if the
    # info is there.
    elif name.lower() == 'referer':
        rurl = urllib.parse.urlparse(value)
        if rurl.netloc == host:
            if rurl.scheme == 'https' and protocol == None:
                protocol = 'https'
            if rurl.port != None and port == None:
                port = rurl.port

if protocol == None:
    protocol = 'http'
if port == None:
    if protocol == 'https':
        port = 443
    else:
        port = 80


# XXX: use pprint
formatted_body = '\n            '.join([repr(body[i:i+40]) for i in range(0,len(body),40)])
if formatted_body == '':
    formatted_body = "b''"


print('''#!/usr/bin/env python3
# This script was generated by bletchley-http2py
# See the "TODO" comments below for places to edit your request as needed for your situation.

import sys
from bletchley import blobtools,buffertools
from bletchley import chosenct
from bletchley.CBC import *

# TODO: ensure the host, port, and protocol settings are correct.
host = %s
port = %s
protocol = %s

def decode(token):
    # TODO: Perhaps you needs something like this?
    #       (See 'bletchley-decode -e ?' for a list of encodings)
    # return blobtools.decodeChain(['percent/mixed','base64/rfc3548'], token)
    return token


def encode(binary):
    # TODO: Perhaps you needs something like this?
    # return blobtools.encodeChain(['base64/rfc3548', 'percent/mixed'], binary)
    return binary
''' % (repr(host),repr(port),repr(protocol)))

if args.requests:
    print('''
try:
    import requests
    import urllib3
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except:
    sys.stderr.write('ERROR: Could not import requests module.  Ensure it is installed.\\n')
    sys.stderr.write('       Under Debian, the package name is "python3-requests"\\n.')
    sys.stderr.write('       Alternatively, re-generate this script using the --native option.\\n.')
    sys.exit(1)
''')

    headers = dict(headers)
    # XXX: We don't currently support exactly formatted header
    #      continuations with python requests, but this should be
    #      semantically equivalent.
    for h in headers.keys():
        headers[h] = ' '.join(headers[h])

    print('''
session = requests.Session()
def sendRequest(session, data=None):
    data = data.decode('utf-8')
    # TODO: Replace the token you wish to target in this request with the "data" variable. 
    #       Then specify the starting value for that token at the end of this script.
    method = %s
    path = %s
    headers = %s
    url = "%%s://%%s:%%d%%s" %% (protocol,host,port,path)
    body = (%s)

    # Set verify=True if you want to validate the server cert
    return session.request(method, url, headers=headers, data=body, allow_redirects=False, verify=False)
    ''' % (repr(method), repr(path),
           pprint.pformat(headers, width=80-14).replace('\n','\n'+' '*14),
           formatted_body))

    print('''    

def processResponse(data, other=None):
    global session
    ret_val = None
    response = sendRequest(session, encode(data))

    # TODO: customize code here to retrieve what you need from the response(s)
    # For information on the response object's interface, see:
    #   http://docs.python-requests.org/en/latest/api/#requests.Response

    # These are useful for debugging, but once your response processing is working,
    # remove them so it isn't so verbose.
    print(response.status_code, file=sys.stderr)
    print(response.headers, file=sys.stderr)
    print(repr(response.content), file=sys.stderr)

    # You may need to return either true/false or a string/bytes object here 
    # (which is derived from the response) depending on your test
    return ret_val
''')


else:
    print('''
import http.client

def sendRequest(connection, data=None):
    data = data.decode('utf-8')
    # TODO: use "data" below, wherever your token normally appears
    method = %s
    path = %s
    body = (%s)
    
    connection.putrequest(method, path)
    ''' % (repr(method), repr(path), formatted_body))

    for name,values in headers:
        if len(values) > 1:
            continuations = ','.join([repr(v) for v in values[1:]])
            print('''    connection.putheader(%s, %s, %s)''' % (repr(name),repr(values[0]),continuations))
        else:
            print('''    connection.putheader(%s, %s)''' % (repr(name),repr(values[0])))

    print('''    
    if len(body) > 0:
        connection.putheader('Content-Length', len(body))
    connection.endheaders()
    connection.send(body)
    
    return connection.getresponse()


def newConnection():
    global protocol
    if protocol == 'https':
        return http.client.HTTPSConnection(host, port)
    else:
        return http.client.HTTPConnection(host, port)


def processResponse(data, other=None):
    ret_val = False
    connection = newConnection()
    response = sendRequest(connection, encode(data))

    # TODO: customize code here to retrieve what you need from the response(s)
    # For information on the response object's interface, see:
    #   http://docs.python.org/library/httplib.html#httpresponse-objects

    # These are useful for debugging, but once your response processing is working,
    # remove them so it isn't so verbose.
    print(response.status, file=sys.stderr)
    print(response.getheaders(), file=sys.stderr)
    print(repr(response.read()), file=sys.stderr)

    connection.close()
    # You may need to return either true/false or a string/bytes object here 
    # (which is derived from the response) depending on your test
    return ret_val
''')


print('''
token = b'TODO: paste your encoded ciphertext here (typically moved from the sendRequest function)'
ciphertext = decode(token)

# TODO: Use this to verify you get the response you expect.  
#       Once everything is working, use the commented code below to conduct specific attacks.
print(processResponse(ciphertext), file=sys.stderr)


# Padding Oracle Attacks 
# poa = POA(processResponse, {block size}, ciphertext, iv=None, threads=1, log_file=sys.stderr)
# print(poa.probe_padding()) # sanity check
# print(poa.decrypt())


# Byte-by-byte probing of ciphertext
#   Maybe start with this as a fast but gentle probe:
# result = chosenct.probe_bytes(processResponse, ciphertext, [1,128], max_threads=2)
#   This is more in-depth (every bit of each byte) and more threads
# result = chosenct.probe_bytes(processResponse, ciphertext, [1,2,4,8,16,32,64,128], max_threads=5)
#   Yet more intensive (every byte value against every byte):
# result = chosenct.probe_bytes(processResponse, ciphertext, list(range(1,256)), max_threads=8)
# 
# print(result.toHTML())
''')
