Repository: abhinavsingh/proxy.py
Pull request #2 (2015-02-25 11:58:17)

python3 support


Patch:
diff --git a/proxy.py b/proxy.py
old mode 100644
new mode 100755
index 23a7fce..ef43c32
--- a/proxy.py
+++ b/proxy.py
@@ -17,9 +17,9 @@ __author_email__ = 'mailsforabhinav@gmail.com'
 __homepage__ = 'https://github.com/abhinavsingh/proxy.py'
 __license__ = 'BSD'
 
+import sys
 import multiprocessing
 import datetime
-import urlparse
 import argparse
 import logging
 import socket
@@ -27,7 +27,37 @@ import select
 
 logger = logging.getLogger(__name__)
 
-CRLF, COLON, SP = '\r\n', ':', ' '
+# True if we are running on Python 3.
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+    text_type = str
+    binary_type = bytes
+    from urllib import parse as urlparse
+else:
+    text_type = unicode
+    binary_type = str
+    import urlparse
+
+
+def text_(s, encoding='utf-8', errors='strict'):
+    """ If ``s`` is an instance of ``binary_type``, return
+    ``s.decode(encoding, errors)``, otherwise return ``s``"""
+    if isinstance(s, binary_type):
+        return s.decode(encoding, errors)
+    return s  # pragma: no cover
+
+
+def bytes_(s, encoding='utf-8', errors='strict'):
+    """ If ``s`` is an instance of ``text_type``, return
+    ``s.encode(encoding, errors)``, otherwise return ``s``"""
+    if isinstance(s, text_type):  # pragma: no cover
+        return s.encode(encoding, errors)
+    return s
+
+version = bytes_(__version__)
+
+CRLF, COLON, SP = b'\r\n', b':', b' '
 
 HTTP_REQUEST_PARSER = 1
 HTTP_RESPONSE_PARSER = 2
@@ -43,13 +73,14 @@ CHUNK_PARSER_STATE_WAITING_FOR_SIZE = 1
 CHUNK_PARSER_STATE_WAITING_FOR_DATA = 2
 CHUNK_PARSER_STATE_COMPLETE = 3
 
+
 class ChunkParser(object):
     """HTTP chunked encoding response parser."""
     
     def __init__(self):
         self.state = CHUNK_PARSER_STATE_WAITING_FOR_SIZE
-        self.body = ''
-        self.chunk = ''
+        self.body = b''
+        self.chunk = b''
         self.size = None
     
     def parse(self, data):
@@ -72,7 +103,7 @@ class ChunkParser(object):
                     self.state = CHUNK_PARSER_STATE_COMPLETE
                 else:
                     self.state = CHUNK_PARSER_STATE_WAITING_FOR_SIZE
-                self.chunk = ''
+                self.chunk = b''
                 self.size = None
         return len(data) > 0, data
 
@@ -83,8 +114,8 @@ class HttpParser(object):
         self.state = HTTP_PARSER_STATE_INITIALIZED
         self.type = type if type else HTTP_REQUEST_PARSER
         
-        self.raw = ''
-        self.buffer = ''
+        self.raw = b''
+        self.buffer = b''
         
         self.headers = dict()
         self.body = None
@@ -100,7 +131,7 @@ class HttpParser(object):
     def parse(self, data):
         self.raw += data
         data = self.buffer + data
-        self.buffer = ''
+        self.buffer = b''
         
         more = True if len(data) > 0 else False
         while more: 
@@ -109,16 +140,16 @@ class HttpParser(object):
     
     def process(self, data):
         if self.state >= HTTP_PARSER_STATE_HEADERS_COMPLETE and \
-        (self.method == "POST" or self.type == HTTP_RESPONSE_PARSER):
+        (self.method == b"POST" or self.type == HTTP_RESPONSE_PARSER):
             if not self.body:
-                self.body = ''
-            
-            if 'content-length' in self.headers:
+                self.body = b''
+
+            if b'content-length' in self.headers:
                 self.state = HTTP_PARSER_STATE_RCVING_BODY
                 self.body += data
-                if len(self.body) >= int(self.headers['content-length'][1]):
+                if len(self.body) >= int(self.headers[b'content-length'][1]):
                     self.state = HTTP_PARSER_STATE_COMPLETE
-            elif 'transfer-encoding' in self.headers and self.headers['transfer-encoding'][1].lower() == 'chunked':
+            elif b'transfer-encoding' in self.headers and self.headers[b'transfer-encoding'][1].lower() == b'chunked':
                 if not self.chunker:
                     self.chunker = ChunkParser()
                 self.chunker.parse(data)
@@ -126,7 +157,7 @@ class HttpParser(object):
                     self.body = self.chunker.body
                     self.state = HTTP_PARSER_STATE_COMPLETE
             
-            return False, ''
+            return False, b''
         
         line, data = HttpParser.split(data)
         if line == False: return line, data
@@ -138,7 +169,7 @@ class HttpParser(object):
         
         if self.state == HTTP_PARSER_STATE_HEADERS_COMPLETE and \
         self.type == HTTP_REQUEST_PARSER and \
-        not self.method == "POST" and \
+        not self.method == b"POST" and \
         self.raw.endswith(CRLF*2):
             self.state = HTTP_PARSER_STATE_COMPLETE
         
@@ -153,7 +184,7 @@ class HttpParser(object):
         else:
             self.version = line[0]
             self.code = line[1]
-            self.reason = ' '.join(line[2:])
+            self.reason = b' '.join(line[2:])
         self.state = HTTP_PARSER_STATE_LINE_RCVD
     
     def process_header(self, data):
@@ -171,19 +202,19 @@ class HttpParser(object):
     
     def build_url(self):
         if not self.url:
-            return '/None'
+            return b'/None'
         
         url = self.url.path
-        if url == '': url = '/'
-        if not self.url.query == '': url += '?' + self.url.query
-        if not self.url.fragment == '': url += '#' + self.url.fragment
+        if url == b'': url = b'/'
+        if not self.url.query == b'': url += b'?' + self.url.query
+        if not self.url.fragment == b'': url += b'#' + self.url.fragment
         return url
     
     def build_header(self, k, v):
-        return '%s: %s%s' % (k, v, CRLF)
+        return k + b": " + v + CRLF
     
     def build(self, del_headers=None, add_headers=None):
-        req = '%s %s %s' % (self.method, self.build_url(), self.version)
+        req = b" ".join([self.method, self.build_url(), self.version])
         req += CRLF
         
         if not del_headers: del_headers = []
@@ -213,7 +244,7 @@ class Connection(object):
     """TCP server/client connection abstraction."""
     
     def __init__(self, what):
-        self.buffer = ''
+        self.buffer = b''
         self.closed = False
         self.what = what # server or client
     
@@ -254,7 +285,7 @@ class Server(Connection):
     """Establish connection to destination server."""
     
     def __init__(self, host, port):
-        super(Server, self).__init__('server')
+        super(Server, self).__init__(b'server')
         self.addr = (host, int(port))
     
     def connect(self):
@@ -265,7 +296,7 @@ class Client(Connection):
     """Accepted client connection."""
     
     def __init__(self, conn, addr):
-        super(Client, self).__init__('client')
+        super(Client, self).__init__(b'client')
         self.conn = conn
         self.addr = addr
 
@@ -301,8 +332,8 @@ class Proxy(multiprocessing.Process):
         self.response = HttpParser(HTTP_RESPONSE_PARSER)
         
         self.connection_established_pkt = CRLF.join([
-            'HTTP/1.1 200 Connection established',
-            'Proxy-agent: proxy.py v%s' % __version__,
+            b'HTTP/1.1 200 Connection established',
+            b'Proxy-agent: proxy.py v' + version,
             CRLF
         ])
     
@@ -332,7 +363,7 @@ class Proxy(multiprocessing.Process):
         if self.request.state == HTTP_PARSER_STATE_COMPLETE:
             logger.debug('request parser is in state complete')
             
-            if self.request.method == "CONNECT":
+            if self.request.method == b"CONNECT":
                 host, port = self.request.url.path.split(COLON)
             elif self.request.url:
                 host, port = self.request.url.hostname, self.request.url.port if self.request.url.port else 80
@@ -349,20 +380,20 @@ class Proxy(multiprocessing.Process):
             # for http connect methods (https requests)
             # queue appropriate response for client 
             # notifying about established connection
-            if self.request.method == "CONNECT":
+            if self.request.method == b"CONNECT":
                 self.client.queue(self.connection_established_pkt)
             # for usual http requests, re-build request packet
             # and queue for the server with appropriate headers
             else:
                 self.server.queue(self.request.build(
-                    del_headers=['proxy-connection', 'connection', 'keep-alive'], 
-                    add_headers=[('Connection', 'Close')]
+                    del_headers=[b'proxy-connection', b'connection', b'keep-alive'], 
+                    add_headers=[(b'Connection', b'Close')]
                 ))
     
     def _process_response(self, data):
         # parse incoming response packet
         # only for non-https requests
-        if not self.request.method == "CONNECT":
+        if not self.request.method == b"CONNECT":
             self.response.parse(data)
         
         # queue data for client
@@ -370,7 +401,7 @@ class Proxy(multiprocessing.Process):
     
     def _access_log(self):
         host, port = self.server.addr if self.server else (None, None)
-        if self.request.method == "CONNECT":
+        if self.request.method == b"CONNECT":
             logger.info("%s:%s - %s %s:%s" % (self.client.addr[0], self.client.addr[1], self.request.method, host, port))
         else:
             logger.info("%s:%s - %s %s:%s%s - %s %s - %s bytes" % (self.client.addr[0], self.client.addr[1], self.request.method, host, port, self.request.build_url(), self.response.code, self.response.reason, len(self.response.raw)))
@@ -417,12 +448,12 @@ class Proxy(multiprocessing.Process):
             except ProxyConnectionFailed as e:
                 logger.exception(e)
                 self.client.queue(CRLF.join([
-                    'HTTP/1.1 502 Bad Gateway',
-                    'Proxy-agent: proxy.py v%s' % __version__,
-                    'Content-Length: 11',
-                    'Connection: close',
+                    b'HTTP/1.1 502 Bad Gateway',
+                    b'Proxy-agent: proxy.py v' + version,
+                    b'Content-Length: 11',
+                    b'Connection: close',
                     CRLF
-                ]) + 'Bad Gateway')
+                ]) + b'Bad Gateway')
                 self.client.flush()
                 return True
         
diff --git a/setup.py b/setup.py
index 5339f0e..5a4bda5 100644
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,7 @@ setup(
     url                 = proxy.__homepage__,
     license             = proxy.__license__,
     py_modules          = ['proxy'],
-    scripts             = ['proxy.py'],
+    scripts             = ['proxy/__init__.py'],
     install_requires    = [],
     classifiers         = classifiers
 )


Test patch:
diff --git a/tests.py b/tests.py
index 32d7ec4..dabd485 100644
--- a/tests.py
+++ b/tests.py
@@ -1,6 +1,8 @@
 import unittest
 import proxy
 from proxy import *
+from proxy import bytes_, text_
+
 
 class TestChunkParser(unittest.TestCase):
 
@@ -8,19 +10,19 @@ class TestChunkParser(unittest.TestCase):
         self.parser = ChunkParser()
 
     def test_chunk_parse(self):
-        self.parser.parse(''.join([
-            '4\r\n',
-            'Wiki\r\n',
-            '5\r\n',
-            'pedia\r\n',
-            'E\r\n',
-            ' in\r\n\r\nchunks.\r\n',
-            '0\r\n',
-            '\r\n'
+        self.parser.parse(b''.join([
+            b'4\r\n',
+            b'Wiki\r\n',
+            b'5\r\n',
+            b'pedia\r\n',
+            b'E\r\n',
+            b' in\r\n\r\nchunks.\r\n',
+            b'0\r\n',
+            b'\r\n'
         ]))
-        self.assertEqual(self.parser.chunk, '')
+        self.assertEqual(self.parser.chunk, b'')
         self.assertEqual(self.parser.size, None)
-        self.assertEqual(self.parser.body, 'Wikipedia in\r\n\r\nchunks.')
+        self.assertEqual(self.parser.body, b'Wikipedia in\r\n\r\nchunks.')
         self.assertEqual(self.parser.state, CHUNK_PARSER_STATE_COMPLETE)
 
 class TestHttpParser(unittest.TestCase):
@@ -29,26 +31,26 @@ class TestHttpParser(unittest.TestCase):
         self.parser = HttpParser()
 
     def test_get_full_parse(self):
-        raw = CRLF.join([
+        raw = text_(CRLF, encoding="utf-8").join([
             "GET %s HTTP/1.1",
             "Host: %s",
-            CRLF
+            text_(CRLF, encoding="utf-8")
         ])
-        self.parser.parse(raw % ('https://example.com/path/dir/?a=b&c=d#p=q', 'example.com'))
-        self.assertEqual(self.parser.build_url(), '/path/dir/?a=b&c=d#p=q')
-        self.assertEqual(self.parser.method, "GET")
-        self.assertEqual(self.parser.url.hostname, "example.com")
+        self.parser.parse(bytes_(raw % ('https://example.com/path/dir/?a=b&c=d#p=q', 'example.com')))
+        self.assertEqual(self.parser.build_url(), b'/path/dir/?a=b&c=d#p=q')
+        self.assertEqual(self.parser.method, b"GET")
+        self.assertEqual(self.parser.url.hostname, b"example.com")
         self.assertEqual(self.parser.url.port, None)
-        self.assertEqual(self.parser.version, "HTTP/1.1")
+        self.assertEqual(self.parser.version, b"HTTP/1.1")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_COMPLETE)
-        self.assertDictContainsSubset({'host':('Host', 'example.com')}, self.parser.headers)
-        self.assertEqual(raw % ('/path/dir/?a=b&c=d#p=q', 'example.com'), self.parser.build(del_headers=['host'], add_headers=[('Host', 'example.com')]))
+        self.assertDictContainsSubset({b'host': (b'Host', b'example.com')}, self.parser.headers)
+        self.assertEqual(bytes_(raw % ('/path/dir/?a=b&c=d#p=q', 'example.com')), self.parser.build(del_headers=[b'host'], add_headers=[(b'Host', b'example.com')]))
 
     def test_build_url_none(self):
-        self.assertEqual(self.parser.build_url(), '/None')
+        self.assertEqual(self.parser.build_url(), b'/None')
 
     def test_line_rcvd_to_rcving_headers_state_change(self):
-        self.parser.parse("GET http://localhost HTTP/1.1")
+        self.parser.parse(b"GET http://localhost HTTP/1.1")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_INITIALIZED)
         self.parser.parse(CRLF)
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_LINE_RCVD)
@@ -57,7 +59,7 @@ class TestHttpParser(unittest.TestCase):
 
     def test_get_partial_parse1(self):
         self.parser.parse(CRLF.join([
-            "GET http://localhost:8080 HTTP/1.1"
+            b"GET http://localhost:8080 HTTP/1.1"
         ]))
         self.assertEqual(self.parser.method, None)
         self.assertEqual(self.parser.url, None)
@@ -65,77 +67,77 @@ class TestHttpParser(unittest.TestCase):
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_INITIALIZED)
 
         self.parser.parse(CRLF)
-        self.assertEqual(self.parser.method, "GET")
-        self.assertEqual(self.parser.url.hostname, "localhost")
+        self.assertEqual(self.parser.method, b"GET")
+        self.assertEqual(self.parser.url.hostname, b"localhost")
         self.assertEqual(self.parser.url.port, 8080)
-        self.assertEqual(self.parser.version, "HTTP/1.1")
+        self.assertEqual(self.parser.version, b"HTTP/1.1")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_LINE_RCVD)
 
-        self.parser.parse("Host: localhost:8080")
+        self.parser.parse(b"Host: localhost:8080")
         self.assertDictEqual(self.parser.headers, dict())
-        self.assertEqual(self.parser.buffer, "Host: localhost:8080")
+        self.assertEqual(self.parser.buffer, b"Host: localhost:8080")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_LINE_RCVD)
 
         self.parser.parse(CRLF*2)
-        self.assertDictContainsSubset({'host':('Host', 'localhost:8080')}, self.parser.headers)
+        self.assertDictContainsSubset({b'host':(b'Host', b'localhost:8080')}, self.parser.headers)
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_COMPLETE)
 
     def test_get_partial_parse2(self):
         self.parser.parse(CRLF.join([
-            "GET http://localhost:8080 HTTP/1.1",
-            "Host: "
+            b"GET http://localhost:8080 HTTP/1.1",
+            b"Host: "
         ]))
-        self.assertEqual(self.parser.method, "GET")
-        self.assertEqual(self.parser.url.hostname, "localhost")
+        self.assertEqual(self.parser.method, b"GET")
+        self.assertEqual(self.parser.url.hostname, b"localhost")
         self.assertEqual(self.parser.url.port, 8080)
-        self.assertEqual(self.parser.version, "HTTP/1.1")
-        self.assertEqual(self.parser.buffer, "Host: ")
+        self.assertEqual(self.parser.version, b"HTTP/1.1")
+        self.assertEqual(self.parser.buffer, b"Host: ")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_LINE_RCVD)
 
-        self.parser.parse("localhost:8080%s" % CRLF)
-        self.assertDictContainsSubset({'host': ('Host', 'localhost:8080')}, self.parser.headers)
-        self.assertEqual(self.parser.buffer, "")
+        self.parser.parse(b"localhost:8080" + CRLF)
+        self.assertDictContainsSubset({b'host': (b'Host', b'localhost:8080')}, self.parser.headers)
+        self.assertEqual(self.parser.buffer, b"")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_RCVING_HEADERS)
 
-        self.parser.parse("Content-Type: text/plain%s" % CRLF)
-        self.assertEqual(self.parser.buffer, "")
-        self.assertDictContainsSubset({'content-type': ('Content-Type', 'text/plain')}, self.parser.headers)
+        self.parser.parse(b"Content-Type: text/plain" + CRLF)
+        self.assertEqual(self.parser.buffer, b"")
+        self.assertDictContainsSubset({b'content-type': (b'Content-Type', b'text/plain')}, self.parser.headers)
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_RCVING_HEADERS)
 
         self.parser.parse(CRLF)
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_COMPLETE)
 
     def test_post_full_parse(self):
-        raw = CRLF.join([
+        raw = text_(CRLF).join([
             "POST %s HTTP/1.1",
             "Host: localhost",
             "Content-Length: 7",
-            "Content-Type: application/x-www-form-urlencoded%s" % CRLF,
+            "Content-Type: application/x-www-form-urlencoded" + text_(CRLF, encoding="utf-8"),
             "a=b&c=d"
         ])
-        self.parser.parse(raw % 'http://localhost')
-        self.assertEqual(self.parser.method, "POST")
-        self.assertEqual(self.parser.url.hostname, "localhost")
+        self.parser.parse(bytes_(raw % 'http://localhost'))
+        self.assertEqual(self.parser.method, b"POST")
+        self.assertEqual(self.parser.url.hostname, b"localhost")
         self.assertEqual(self.parser.url.port, None)
-        self.assertEqual(self.parser.version, "HTTP/1.1")
-        self.assertDictContainsSubset({'content-type': ('Content-Type', 'application/x-www-form-urlencoded')}, self.parser.headers)
-        self.assertDictContainsSubset({'content-length': ('Content-Length', '7')}, self.parser.headers)
-        self.assertEqual(self.parser.body, "a=b&c=d")
-        self.assertEqual(self.parser.buffer, "")
+        self.assertEqual(self.parser.version, b"HTTP/1.1")
+        self.assertDictContainsSubset({b'content-type': (b'Content-Type', b'application/x-www-form-urlencoded')}, self.parser.headers)
+        self.assertDictContainsSubset({b'content-length': (b'Content-Length', b'7')}, self.parser.headers)
+        self.assertEqual(self.parser.body, b"a=b&c=d")
+        self.assertEqual(self.parser.buffer, b"")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_COMPLETE)
         self.assertEqual(len(self.parser.build()), len(raw % '/'))
 
     def test_post_partial_parse(self):
         self.parser.parse(CRLF.join([
-            "POST http://localhost HTTP/1.1",
-            "Host: localhost",
-            "Content-Length: 7",
-            "Content-Type: application/x-www-form-urlencoded"
+            b"POST http://localhost HTTP/1.1",
+            b"Host: localhost",
+            b"Content-Length: 7",
+            b"Content-Type: application/x-www-form-urlencoded"
         ]))
-        self.assertEqual(self.parser.method, "POST")
-        self.assertEqual(self.parser.url.hostname, "localhost")
+        self.assertEqual(self.parser.method, b"POST")
+        self.assertEqual(self.parser.url.hostname, b"localhost")
         self.assertEqual(self.parser.url.port, None)
-        self.assertEqual(self.parser.version, "HTTP/1.1")
+        self.assertEqual(self.parser.version, b"HTTP/1.1")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_RCVING_HEADERS)
 
         self.parser.parse(CRLF)
@@ -144,86 +146,86 @@ class TestHttpParser(unittest.TestCase):
         self.parser.parse(CRLF)
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_HEADERS_COMPLETE)
 
-        self.parser.parse("a=b")
+        self.parser.parse(b"a=b")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_RCVING_BODY)
-        self.assertEqual(self.parser.body, "a=b")
-        self.assertEqual(self.parser.buffer, "")
+        self.assertEqual(self.parser.body, b"a=b")
+        self.assertEqual(self.parser.buffer, b"")
 
-        self.parser.parse("&c=d")
+        self.parser.parse(b"&c=d")
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_COMPLETE)
-        self.assertEqual(self.parser.body, "a=b&c=d")
-        self.assertEqual(self.parser.buffer, "")
+        self.assertEqual(self.parser.body, b"a=b&c=d")
+        self.assertEqual(self.parser.buffer, b"")
 
     def test_response_parse(self):
         self.parser.type = HTTP_RESPONSE_PARSER
-        self.parser.parse(''.join([
-            'HTTP/1.1 301 Moved Permanently\r\n',
-            'Location: http://www.google.com/\r\n',
-            'Content-Type: text/html; charset=UTF-8\r\n',
-            'Date: Wed, 22 May 2013 14:07:29 GMT\r\n',
-            'Expires: Fri, 21 Jun 2013 14:07:29 GMT\r\n',
-            'Cache-Control: public, max-age=2592000\r\n',
-            'Server: gws\r\n',
-            'Content-Length: 219\r\n',
-            'X-XSS-Protection: 1; mode=block\r\n',
-            'X-Frame-Options: SAMEORIGIN\r\n\r\n',
-            '<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>301 Moved</TITLE></HEAD>',
-            '<BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.com/">here</A>.\r\n</BODY></HTML>\r\n'
+        self.parser.parse(b''.join([
+            b'HTTP/1.1 301 Moved Permanently\r\n',
+            b'Location: http://www.google.com/\r\n',
+            b'Content-Type: text/html; charset=UTF-8\r\n',
+            b'Date: Wed, 22 May 2013 14:07:29 GMT\r\n',
+            b'Expires: Fri, 21 Jun 2013 14:07:29 GMT\r\n',
+            b'Cache-Control: public, max-age=2592000\r\n',
+            b'Server: gws\r\n',
+            b'Content-Length: 219\r\n',
+            b'X-XSS-Protection: 1; mode=block\r\n',
+            b'X-Frame-Options: SAMEORIGIN\r\n\r\n',
+            b'<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>301 Moved</TITLE></HEAD>',
+            b'<BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.com/">here</A>.\r\n</BODY></HTML>\r\n'
         ]))
-        self.assertEqual(self.parser.code, '301')
-        self.assertEqual(self.parser.reason, 'Moved Permanently')
-        self.assertEqual(self.parser.version, 'HTTP/1.1')
-        self.assertEqual(self.parser.body, '<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.com/">here</A>.\r\n</BODY></HTML>\r\n')
-        self.assertDictContainsSubset({'content-length': ('Content-Length', '219')}, self.parser.headers)
+        self.assertEqual(self.parser.code, b'301')
+        self.assertEqual(self.parser.reason, b'Moved Permanently')
+        self.assertEqual(self.parser.version, b'HTTP/1.1')
+        self.assertEqual(self.parser.body, b'<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>301 Moved</TITLE></HEAD><BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.com/">here</A>.\r\n</BODY></HTML>\r\n')
+        self.assertDictContainsSubset({b'content-length': (b'Content-Length', b'219')}, self.parser.headers)
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_COMPLETE)
 
     def test_response_partial_parse(self):
         self.parser.type = HTTP_RESPONSE_PARSER
-        self.parser.parse(''.join([
-            'HTTP/1.1 301 Moved Permanently\r\n',
-            'Location: http://www.google.com/\r\n',
-            'Content-Type: text/html; charset=UTF-8\r\n',
-            'Date: Wed, 22 May 2013 14:07:29 GMT\r\n',
-            'Expires: Fri, 21 Jun 2013 14:07:29 GMT\r\n',
-            'Cache-Control: public, max-age=2592000\r\n',
-            'Server: gws\r\n',
-            'Content-Length: 219\r\n',
-            'X-XSS-Protection: 1; mode=block\r\n',
-            'X-Frame-Options: SAMEORIGIN\r\n'
+        self.parser.parse(b''.join([
+            b'HTTP/1.1 301 Moved Permanently\r\n',
+            b'Location: http://www.google.com/\r\n',
+            b'Content-Type: text/html; charset=UTF-8\r\n',
+            b'Date: Wed, 22 May 2013 14:07:29 GMT\r\n',
+            b'Expires: Fri, 21 Jun 2013 14:07:29 GMT\r\n',
+            b'Cache-Control: public, max-age=2592000\r\n',
+            b'Server: gws\r\n',
+            b'Content-Length: 219\r\n',
+            b'X-XSS-Protection: 1; mode=block\r\n',
+            b'X-Frame-Options: SAMEORIGIN\r\n'
         ]))
-        self.assertDictContainsSubset({'x-frame-options': ('X-Frame-Options', 'SAMEORIGIN')}, self.parser.headers)
+        self.assertDictContainsSubset({b'x-frame-options': (b'X-Frame-Options', b'SAMEORIGIN')}, self.parser.headers)
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_RCVING_HEADERS)
-        self.parser.parse('\r\n')
+        self.parser.parse(b'\r\n')
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_HEADERS_COMPLETE)
-        self.parser.parse('<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>301 Moved</TITLE></HEAD>')
+        self.parser.parse(b'<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">\n<TITLE>301 Moved</TITLE></HEAD>')
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_RCVING_BODY)
-        self.parser.parse('<BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.com/">here</A>.\r\n</BODY></HTML>\r\n')
+        self.parser.parse(b'<BODY>\n<H1>301 Moved</H1>\nThe document has moved\n<A HREF="http://www.google.com/">here</A>.\r\n</BODY></HTML>\r\n')
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_COMPLETE)
 
     def test_chunked_response_parse(self):
         self.parser.type = HTTP_RESPONSE_PARSER
-        self.parser.parse(''.join([
-            'HTTP/1.1 200 OK\r\n',
-            'Content-Type: application/json\r\n',
-            'Date: Wed, 22 May 2013 15:08:15 GMT\r\n',
-            'Server: gunicorn/0.16.1\r\n',
-            'transfer-encoding: chunked\r\n',
-            'Connection: keep-alive\r\n\r\n',
-            '4\r\n',
-            'Wiki\r\n',
-            '5\r\n',
-            'pedia\r\n',
-            'E\r\n',
-            ' in\r\n\r\nchunks.\r\n',
-            '0\r\n',
-            '\r\n'
+        self.parser.parse(b''.join([
+            b'HTTP/1.1 200 OK\r\n',
+            b'Content-Type: application/json\r\n',
+            b'Date: Wed, 22 May 2013 15:08:15 GMT\r\n',
+            b'Server: gunicorn/0.16.1\r\n',
+            b'transfer-encoding: chunked\r\n',
+            b'Connection: keep-alive\r\n\r\n',
+            b'4\r\n',
+            b'Wiki\r\n',
+            b'5\r\n',
+            b'pedia\r\n',
+            b'E\r\n',
+            b' in\r\n\r\nchunks.\r\n',
+            b'0\r\n',
+            b'\r\n'
         ]))
-        self.assertEqual(self.parser.body, 'Wikipedia in\r\n\r\nchunks.')
+        self.assertEqual(self.parser.body, b'Wikipedia in\r\n\r\nchunks.')
         self.assertEqual(self.parser.state, HTTP_PARSER_STATE_COMPLETE)
 
 class MockConnection(object):
     
-    def __init__(self, buffer=''):
+    def __init__(self, buffer=b''):
         self.buffer = buffer
     
     def recv(self, bytes=8192):
@@ -245,21 +247,21 @@ class TestProxy(unittest.TestCase):
         self.proxy = Proxy(Client(self._conn, self._addr))
 
     def test_http_get(self):
-        self.proxy.client.conn.queue("GET http://httpbin.org/get HTTP/1.1%s" % CRLF)
+        self.proxy.client.conn.queue(b"GET http://httpbin.org/get HTTP/1.1" + CRLF)
         self.proxy._process_request(self.proxy.client.recv())
         self.assertNotEqual(self.proxy.request.state, HTTP_PARSER_STATE_COMPLETE)
 
         self.proxy.client.conn.queue(CRLF.join([
-            "User-Agent: curl/7.27.0",
-            "Host: httpbin.org",
-            "Accept: */*",
-            "Proxy-Connection: Keep-Alive",
+            b"User-Agent: curl/7.27.0",
+            b"Host: httpbin.org",
+            b"Accept: */*",
+            b"Proxy-Connection: Keep-Alive",
             CRLF
         ]))
         
         self.proxy._process_request(self.proxy.client.recv())
         self.assertEqual(self.proxy.request.state, HTTP_PARSER_STATE_COMPLETE)
-        self.assertEqual(self.proxy.server.addr, ("httpbin.org", 80))
+        self.assertEqual(self.proxy.server.addr, (b"httpbin.org", 80))
         
         self.proxy.server.flush()
         self.assertEqual(self.proxy.server.buffer_size(), 0)
@@ -276,10 +278,10 @@ class TestProxy(unittest.TestCase):
 
     def test_https_get(self):
         self.proxy.client.conn.queue(CRLF.join([
-            "CONNECT httpbin.org:80 HTTP/1.1",
-            "Host: httpbin.org:80",
-            "User-Agent: curl/7.27.0",
-            "Proxy-Connection: Keep-Alive",
+            b"CONNECT httpbin.org:80 HTTP/1.1",
+            b"Host: httpbin.org:80",
+            b"User-Agent: curl/7.27.0",
+            b"Proxy-Connection: Keep-Alive",
             CRLF
         ]))
         self.proxy._process_request(self.proxy.client.recv())
@@ -295,9 +297,9 @@ class TestProxy(unittest.TestCase):
         self.assertEqual(self.proxy.client.buffer_size(), 0)
         
         self.proxy.client.conn.queue(CRLF.join([
-            "GET /user-agent HTTP/1.1",
-            "Host: httpbin.org",
-            "User-Agent: curl/7.27.0",
+            b"GET /user-agent HTTP/1.1",
+            b"Host: httpbin.org",
+            b"User-Agent: curl/7.27.0",
             CRLF
         ]))
         self.proxy._process_request(self.proxy.client.recv())
@@ -318,8 +320,8 @@ class TestProxy(unittest.TestCase):
     def test_proxy_connection_failed(self):
         with self.assertRaises(ProxyConnectionFailed):
             self.proxy._process_request(CRLF.join([
-                "GET http://unknown.domain HTTP/1.1",
-                "Host: unknown.domain",
+                b"GET http://unknown.domain HTTP/1.1",
+                b"Host: unknown.domain",
                 CRLF
             ]))