Index: trunk/SConstruct
===================================================================
--- trunk/SConstruct	(revision 39)
+++ trunk/SConstruct	(revision 40)
@@ -65,8 +65,8 @@
 #   env.AddPostAction(libinstall, 'ldconfig')
 
-if sys.version_info[0] == 2:
-   install_items.append('bletchley-python2.log')
-   env.Command('bletchley-python2.log', Glob('lib/bletchley/*.py')+Glob('lib/bletchley/CBC/*.py'),
-               "python bletchley-distutils install --root=/%s | tee bletchley-python2.log" % destdir)
+#if sys.version_info[0] == 2:
+#   install_items.append('bletchley-python2.log')
+#   env.Command('bletchley-python2.log', Glob('lib/bletchley/*.py')+Glob('lib/bletchley/CBC/*.py'),
+#               "python bletchley-distutils install --root=/%s | tee bletchley-python2.log" % destdir)
 
 python_path = os.popen('which python3').read()
Index: trunk/bin/bletchley-analyze
===================================================================
--- trunk/bin/bletchley-analyze	(revision 39)
+++ trunk/bin/bletchley-analyze	(revision 40)
@@ -1,5 +1,5 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-# Requires Python 2.7+
+# Requires Python 3+
 
 '''
@@ -54,9 +54,9 @@
     sys.exit(0)
 
-input_file = sys.stdin
+input_file = sys.stdin.buffer
 if options.input_file is not None:
-    input_file = file(options.input_file, 'rb')
+    input_file = open(options.input_file, 'rb')
 
-blobs = input_file.read().rstrip('\n').replace('\r', '').split('\n')
+blobs = input_file.read().rstrip(b'\n').replace(b'\r', b'').split(b'\n')
 #print(repr(blobs))
 
@@ -81,9 +81,9 @@
     line_size = 64
     group_size *= 2 # two hex digits per byte
-    hex_blobs = map(binascii.b2a_hex, blobs)
+    hex_blobs = [binascii.b2a_hex(b) for b in blobs]
     color_map = buffertools.blockWiseColorMap(group_size, hex_blobs)
 
     for k in range(0,len(hex_blobs)):
-        hex = hex_blobs[k]
+        hex = hex_blobs[k].decode()
         
         for i in range(0,len(hex),line_size):
@@ -98,5 +98,5 @@
        
             line += '| '
-            line += repr(blobs[k][i/2:(i+line_size)/2])
+            line += repr(blobs[k][i//2:(i+line_size)//2])
         
             print(line)
@@ -136,5 +136,4 @@
         encoding = specified_encodings.pop(0)
 
-
     blobs_to_print = blobs[:options.output_lines]
     print('First %d Values:' % len(blobs_to_print))
Index: trunk/bin/bletchley-decode
===================================================================
--- trunk/bin/bletchley-decode	(revision 39)
+++ trunk/bin/bletchley-decode	(revision 40)
@@ -1,5 +1,5 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-# Requires Python 2.7+
+# Requires Python 3+
 
 '''
@@ -45,10 +45,10 @@
     sys.exit(0)
 
-input_file = sys.stdin
+input_file = sys.stdin.buffer
 if options.input_file is not None:
-    input_file = file(options.input_file, 'rb')
+    input_file = open(options.input_file, 'rb')
 
-blob = input_file.read()
+blob = input_file.read().rstrip(b'\r\n')
 
 specified_encodings = options.encoding_chain.split(',')
-sys.stdout.write(blobtools.decodeChain(specified_encodings, blob))
+sys.stdout.buffer.write(blobtools.decodeChain(specified_encodings, blob))
Index: trunk/bin/bletchley-encode
===================================================================
--- trunk/bin/bletchley-encode	(revision 39)
+++ trunk/bin/bletchley-encode	(revision 40)
@@ -1,5 +1,5 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
-# Requires Python 2.7+
+# Requires Python 3+
 
 '''
@@ -46,5 +46,5 @@
     sys.exit(0)
 
-input_file = sys.stdin
+input_file = sys.stdin.buffer
 if options.input_file is not None:
     input_file = file(options.input_file, 'rb')
@@ -55,4 +55,4 @@
 specified_encodings.reverse()
 # XXX: report invalid encodings
-sys.stdout.write(blobtools.encodeChain(specified_encodings, blob))
-sys.stdout.write('\n')
+sys.stdout.buffer.write(blobtools.encodeChain(specified_encodings, blob))
+sys.stdout.buffer.write(b'\n')
Index: trunk/bin/bletchley-http2py
===================================================================
--- trunk/bin/bletchley-http2py	(revision 39)
+++ trunk/bin/bletchley-http2py	(revision 40)
@@ -1,10 +1,11 @@
-#!/usr/bin/env python
-
-# Requires Python 2.7+
+#!/usr/bin/env python3
+
+# Requires Python 3+
 
 '''
-This script reads a raw HTTP request from stdin and writes to stdout
-a Python script.  The generated script sends the same (or a very similar) 
-request using the httplib/http.client libraries.
+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 standard httplib/http.client library, or optionally
+using the more user friendly python-requests library.
 
 Certainly if you have a raw request, you could simply send it via TCP
@@ -34,10 +35,4 @@
 import sys
 import argparse
-try:
-    from lxml import etree
-except:
-    sys.stderr.write('ERROR: Could not import lxml module.  Ensure it is installed.\n')
-    sys.stderr.write('       Under Debian, the package name is "python-lxml"\n.')
-    sys.exit(1)
 
 parser = argparse.ArgumentParser(
@@ -48,19 +43,12 @@
     ' For more information, see: http://code.google.com/p/bletchley/wiki/Overview')
 parser.add_argument(
-    'requestfile', type=file, nargs='?', default=sys.stdin, 
+    'requestfile', type=open, nargs='?', default=sys.stdin, 
     help='A file containing an HTTP request.  Defaults to stdin if omitted.')
 parser.add_argument(
-    '--burp', action='store_true', help='Input file is a XML export from Burp.'
-    ' (First request in file is used.)')
+    '--requests', action='store_true', help='Generate a script that uses the'
+    ' python-requests module rather than httplib/http.client (experimental).')
+
 args = parser.parse_args()
-
-if args.burp:
-    safe_parser = etree.ETCompatXMLParser(resolve_entities=False)
-    root = etree.parse(args.requestfile, parser=safe_parser)
-    input_req = root.xpath('/items/item/request')[0].text
-    root = None
-else:
-    input_req = args.requestfile.read()
-
+input_req = args.requestfile.read()
 
 
@@ -79,4 +67,5 @@
 port = 80
 use_ssl = False
+protocol = 'http'
 
 headers = []
@@ -85,5 +74,5 @@
         break
     # Handle header line continuations
-    if l[0] == '\t':
+    if l[0] in ' \t':
         if len(headers) == 0:
             continue
@@ -108,20 +97,80 @@
             if port == 443:
                 use_ssl = True
+                protocol = 'https'
         else:
             host = value
 
 
-print('''#!/usr/bin/env python
+formatted_body = '\n            '.join([repr(body[i:i+40].encode()) for i in range(0,len(body),40)])
+if formatted_body == '':
+    formatted_body = "b''"
+
+
+if args.requests:
+    print('''#!/usr/bin/env python3
 
 import sys
-# function with either Python 2.7 or 3.x
 try:
-    import http.client as httpc
+    import requests
 except:
-    import httplib as httpc
+    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.exit(1)
+
+
+# TODO: ensure the host, port, and SSL settings are correct.
+host = %s
+port = %s
+protocol = %s
+''' % (repr(host),repr(port),repr(protocol)))
+
+    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()
+# TODO: use "data" to supply any parameters to be included in the request
+def sendRequest(session, data=None):
+    method = %s
+    path = %s
+    headers = %s
+    url = "%%s://%%s:%%d%%s" %% (protocol,host,port,path)
+    body = (%s)
+
+    return session.request(method, url, headers=headers, data=body)
+    ''' % (repr(method), repr(path), repr(headers), formatted_body))
+
+    print('''    
+
+def fetch(data):
+    global session
+    ret_val = None
+
+    # 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
+    response = sendRequest(session, data)
+    print(response.headers)
+    print(repr(response.content))
+
+    return ret_val
+
+data = ''
+fetch(data)
 ''')
 
 
-print('''
+
+else:
+    print('''#!/usr/bin/env python3
+
+import sys
+import http.client as httpc
+
+
 # TODO: ensure the host, port, and SSL settings are correct.
 host = %s
@@ -130,9 +179,5 @@
 ''' % (repr(host),repr(port),repr(use_ssl)))
 
-chunked_body = '\n            '.join([repr(body[i:i+40]) for i in range(0,len(body),40)])
-if chunked_body == '':
-    chunked_body = "''"
-
-print('''
+    print('''
 # TODO: use "data" to supply any parameters to be included in the request
 def sendRequest(connection, data=None):
@@ -142,14 +187,14 @@
     
     connection.putrequest(method, path)
-    ''' % (repr(method), repr(path), chunked_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('''    
+    ''' % (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))
@@ -161,5 +206,4 @@
 
 def newConnection():
-    connection = None
     if use_ssl:
         return httpc.HTTPSConnection(host, port)
Index: trunk/lib/bletchley/blobtools.py
===================================================================
--- trunk/lib/bletchley/blobtools.py	(revision 39)
+++ trunk/lib/bletchley/blobtools.py	(revision 40)
@@ -22,15 +22,56 @@
 import base64
 import binascii
-import urllib
 import fractions
 import operator
 import functools
 import itertools
-import buffertools
+from . import buffertools
+
+
+# urllib.parse's functions are not well suited for encoding/decoding
+# bytes or managing encoded case 
+def _percentEncode(binary, plus=False, upper=True):
+    fmt = "%%%.2X"
+    if upper:
+        fmt = "%%%.2x"
+
+    ret_val = b''
+    for c in binary:
+        if c not in b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789':
+            ret_val += (fmt % c).encode('ascii')
+        elif plus and (c == 20):
+            ret_val += b'+'
+        else:
+            ret_val += c
+    
+    return ret_val
+
+
+def _percentDecode(binary, plus=False):
+    ret_val = b''
+    if plus:
+        binary = binary.replace(b'+', b' ')
+    if binary == b'':
+        return b''
+    chunks = binary.split(b'%')
+    if binary[0] == 0x25:
+        chunks = chunks[1:]
+
+    for chunk in chunks:
+        if len(chunk) < 2:
+            return None
+        try:
+            ret_val += bytes([int(chunk[0:2], 16)]) + chunk[2:]
+        except:
+            print(repr(chunk))
+            return None
+            
+    return ret_val
+
 
 # abstract class
 class DataEncoding(object):
-    charset = frozenset('')
-    extraneous_chars = ''
+    charset = frozenset(b'')
+    extraneous_chars = b''
     dialect = None
     name = None
@@ -65,49 +106,49 @@
         super(base64Encoding, self).__init__(dialect)
         if dialect.startswith('rfc3548'):
-            self.c62 = '+'
-            self.c63 = '/'
-            self.pad = '='
+            self.c62 = b'+'
+            self.c63 = b'/'
+            self.pad = b'='
         elif dialect.startswith('filename'):
-            self.c62 = '+'
-            self.c63 = '-'
-            self.pad = '='
+            self.c62 = b'+'
+            self.c63 = b'-'
+            self.pad = b'='
         elif dialect.startswith('url1'):
-            self.c62 = '-'
-            self.c63 = '_'
-            self.pad = '='
+            self.c62 = b'-'
+            self.c63 = b'_'
+            self.pad = b'='
         elif dialect.startswith('url2'):
-            self.c62 = '-'
-            self.c63 = '_'
-            self.pad = '.'
+            self.c62 = b'-'
+            self.c63 = b'_'
+            self.pad = b'.'
         elif dialect.startswith('url3'):
-            self.c62 = '_'
-            self.c63 = '-'
-            self.pad = '.'
+            self.c62 = b'_'
+            self.c63 = b'-'
+            self.pad = b'.'
         elif dialect.startswith('url4'):
-            self.c62 = '-'
-            self.c63 = '_'
-            self.pad = '!'
+            self.c62 = b'-'
+            self.c63 = b'_'
+            self.pad = b'!'
         elif dialect.startswith('url5'):
-            self.c62 = '+'
-            self.c63 = '/'
-            self.pad = '$'
+            self.c62 = b'+'
+            self.c63 = b'/'
+            self.pad = b'$'
         elif dialect.startswith('otkurl'):
-            self.c62 = '-'
-            self.c63 = '_'
-            self.pad = '*'
+            self.c62 = b'-'
+            self.c63 = b'_'
+            self.pad = b'*'
         elif dialect.startswith('xmlnmtoken'):
-            self.c62 = '.'
-            self.c63 = '-'
-            self.pad = '='
+            self.c62 = b'.'
+            self.c63 = b'-'
+            self.pad = b'='
         elif dialect.startswith('xmlname'):
-            self.c62 = '_'
-            self.c63 = ':'
-            self.pad = '='
+            self.c62 = b'_'
+            self.c63 = b':'
+            self.pad = b'='
         
         if 'newline' in dialect:
-            self.extraneous_chars = '\r\n'
-
-        self.charset = frozenset('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-                                 +'abcdefghijklmnopqrstuvwxyz0123456789'
+            self.extraneous_chars = b'\r\n'
+
+        self.charset = frozenset(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+                                 +b'abcdefghijklmnopqrstuvwxyz0123456789'
                                  +self.c62+self.c63+self.pad+self.extraneous_chars)
 
@@ -120,5 +161,5 @@
     def extraTests(self, blob):
         for c in self.extraneous_chars:
-            blob = blob.replace(c, '')
+            blob = blob.replace(bytes([c]), b'')
 
         nopad = blob.rstrip(self.pad)
@@ -137,5 +178,5 @@
     def decode(self, blob):
         for c in self.extraneous_chars:
-            blob = blob.replace(c, '')
+            blob = blob.replace(bytes(c), b'')
 
         if self.dialect.endswith('nopad'):
@@ -150,5 +191,5 @@
 
         if not self.dialect.startswith('rfc3548'):
-            table = string.maketrans(self.c62+self.c63+self.pad, '+/=')
+            table = string.maketrans(self.c62+self.c63+self.pad, b'+/=')
             blob = blob.translate(table)
 
@@ -160,5 +201,5 @@
 
         if not self.dialect.startswith('rfc3548'):
-            table = string.maketrans('+/=', self.c62+self.c63+self.pad)
+            table = string.maketrans(b'+/=', self.c62+self.c63+self.pad)
             ret_val = ret_val.translate(table)
 
@@ -174,10 +215,10 @@
         super(base32Encoding, self).__init__(dialect)
         if dialect.startswith('rfc3548upper'):
-            self.pad = '='
-            self.charset = frozenset('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'+self.pad)
+            self.pad = b'='
+            self.charset = frozenset(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'+self.pad)
 
         elif dialect.startswith('rfc3548lower'):
-            self.pad = '='
-            self.charset = frozenset('abcdefghijklmnopqrstuvwxyz234567'+self.pad)
+            self.pad = b'='
+            self.charset = frozenset(b'abcdefghijklmnopqrstuvwxyz234567'+self.pad)
 
     def _guessPadLength(self, nopad_len):
@@ -203,5 +244,5 @@
         if self.dialect.endswith('nopad'):
             if self.pad in blob:
-                raise Exception("Unpadded base64 string contains pad character")
+                raise Exception("Unpadded base32 string contains pad character")
 
             padlen = self._guessPadLength(len(blob))
@@ -233,9 +274,9 @@
         super(hexEncoding, self).__init__(dialect)
         if 'mixed' in dialect:
-            self.charset = frozenset('ABCDEFabcdef0123456789')
+            self.charset = frozenset(b'ABCDEFabcdef0123456789')
         elif 'upper' in dialect:
-            self.charset = frozenset('ABCDEF0123456789')            
+            self.charset = frozenset(b'ABCDEF0123456789')            
         elif 'lower' in dialect:
-            self.charset = frozenset('abcdef0123456789')
+            self.charset = frozenset(b'abcdef0123456789')
 
 
@@ -261,12 +302,12 @@
         self.charset = None
         if 'mixed' in dialect:
-            self.hexchars = frozenset('ABCDEFabcdef0123456789')
+            self.hexchars = frozenset(b'ABCDEFabcdef0123456789')
         elif 'upper' in dialect:
-            self.hexchars = frozenset('ABCDEF0123456789')            
+            self.hexchars = frozenset(b'ABCDEF0123456789')            
         elif 'lower' in dialect:
-            self.hexchars = frozenset('abcdef0123456789')
+            self.hexchars = frozenset(b'abcdef0123456789')
 
     def extraTests(self, blob):
-        chunks = blob.split('%')
+        chunks = blob.split(b'%')
         if len(chunks) < 2:
             return None
@@ -279,15 +320,18 @@
 
     def decode(self, blob):
+        plus = False
         if 'plus' in self.dialect:
-            return urllib.unquote(blob)
-        else:
-            return urllib.unquote_plus(blob)
-
-    # XXX: should technically produce quoted digits in same upper/lower case
+            plus = True
+        return _percentDecode(blob, plus=plus)
+
     def encode(self, blob):
+        upper = True
+        plus = False
         if 'plus' in self.dialect:
-            return urllib.quote(blob, '')
-        else:
-            return urllib.quote_plus(blob, '')
+            plus = True
+        if 'lower' in self.dialect:
+            upper = False
+
+        return _percentEncode(blob, plus=plus, upper=upper)
 
 
@@ -336,5 +380,5 @@
 
 def supportedEncodings():
-    e = encodings.keys()
+    e = list(encodings.keys())
     e.sort()
     return e
@@ -380,8 +424,8 @@
 
 def decodeAll(encoding, blobs):
-    return map(encodings[encoding].decode, blobs)
+    return [encodings[encoding].decode(b) for b in blobs]
 
 def encodeAll(encoding, blobs):
-    return map(encodings[encoding].encode, blobs)
+    return [encodings[encoding].encode(b) for b in blobs]
 
 def decodeChain(decoding_chain, blob):
@@ -412,5 +456,5 @@
 
 
-allTrue = functools.partial(reduce, (lambda x,y: x and y))
+allTrue = functools.partial(functools.reduce, (lambda x,y: x and y))
 
 def checkCommonBlocksizes(lengths):
Index: trunk/lib/bletchley/buffertools.py
===================================================================
--- trunk/lib/bletchley/buffertools.py	(revision 39)
+++ trunk/lib/bletchley/buffertools.py	(revision 40)
@@ -75,5 +75,5 @@
 
     colors = {}
-    for block,count in block_counts.iteritems():
+    for block,count in block_counts.items():
         if count == 1:
             # mask needed for portability
