Skip to content
This repository has been archived by the owner on Jan 13, 2021. It is now read-only.

Implement Python 2.7 support #33

Merged
merged 4 commits into from
Apr 3, 2014
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
language: python
python:
- "2.7"
- "3.3"

install:
Expand Down
5 changes: 5 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
import os
import json

import hyper

if not hyper.http20.util.IS_PY3:
from codecs import open

# This pair of generator expressions are pretty lame, but building lists is a
# bad idea as I plan to have a substantial number of tests here.
story_directories = (
Expand Down
6 changes: 3 additions & 3 deletions hyper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
from .http20.connection import HTTP20Connection
from .http20.response import HTTP20Response

# Throw import errors on Python 2.
# Throw import errors on Python <2.7 and 3.0-3.2.
import sys as _sys
if _sys.version_info[0] < 3 or _sys.version_info[1] < 3:
raise ImportError("hyper only supports Python 3.3 or higher.")
if _sys.version_info < (2,7) or (3,0) <= _sys.version_info < (3,3):
raise ImportError("hyper only supports Python 2.7 and Python 3.3 or higher.")

__all__ = [HTTP20Response, HTTP20Connection]

Expand Down
7 changes: 6 additions & 1 deletion hyper/contrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
HTTPAdapter = object

from hyper import HTTP20Connection
from urllib.parse import urlparse
from hyper.http20.util import IS_PY3

if IS_PY3:
from urllib.parse import urlparse
else:
from urlparse import urlparse
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For things like this we should steal the requests approach, which is to have a file called compat that pastes over the differences. That way we can avoid having this horrible conditional import logic at the top of all our files.


class HTTP20Adapter(HTTPAdapter):
"""
Expand Down
3 changes: 1 addition & 2 deletions hyper/http20/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class HTTP20Connection(object):
:class:`FlowControlManager <hyper.http20.window.FlowControlManager>`
will be used.
"""
def __init__(self, host, port=None, *, window_manager=None, **kwargs):
def __init__(self, host, port=None, window_manager=None, **kwargs):
"""
Creates an HTTP/2.0 connection to a specific server.
"""
Expand Down Expand Up @@ -160,7 +160,6 @@ def connect(self):
if self._sock is None:
sock = socket.create_connection((self.host, self.port), 5)
sock = wrap_socket(sock, self.host)
assert sock.selected_npn_protocol() == 'HTTP-draft-09/2.0'
self._sock = sock

# We need to send the connection header immediately on this
Expand Down
25 changes: 13 additions & 12 deletions hyper/http20/hpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import collections
import logging

from .huffman import HuffmanDecoder, HuffmanEncoder
from .huffman import _get_byte, HuffmanDecoder, HuffmanEncoder
from hyper.http20.huffman_constants import (
REQUEST_CODES, REQUEST_CODES_LENGTH, RESPONSE_CODES, RESPONSE_CODES_LENGTH
)
Expand Down Expand Up @@ -55,13 +55,13 @@ def decode_integer(data, prefix_bits):
mask = 0xFF >> (8 - prefix_bits)
index = 0

number = data[index] & mask
number = _get_byte(data, index) & mask

if (number == max_number):

while True:
index += 1
next_byte = data[index]
next_byte = _get_byte(data, index)

if next_byte >= 128:
number += (next_byte - 128) * multiple(index)
Expand Down Expand Up @@ -407,15 +407,15 @@ def _encode_indexed(self, index):
"""
field = encode_integer(index, 7)
field[0] = field[0] | 0x80 # we set the top bit
return field
return bytes(field)

def _encode_literal(self, name, value, indexing, huffman=False):
"""
Encodes a header with a literal name and literal value. If ``indexing``
is True, the header will be added to the header table: otherwise it
will not.
"""
prefix = bytes([0x00 if indexing else 0x40])
prefix = b'\x00' if indexing else b'\x40'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just plain better than what I was doing before, I was clearly exhausted when I wrote that line of code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, understood.


if huffman:
name = self.huffman_coder.encode(name)
Expand All @@ -428,7 +428,7 @@ def _encode_literal(self, name, value, indexing, huffman=False):
name_len[0] |= 0x80
value_len[0] |= 0x80

return b''.join([prefix, name_len, name, value_len, value])
return b''.join([prefix, bytes(name_len), name, bytes(value_len), value])

def _encode_indexed_literal(self, index, value, indexing, huffman=False):
"""
Expand All @@ -449,7 +449,7 @@ def _encode_indexed_literal(self, index, value, indexing, huffman=False):
if huffman:
value_len[0] |= 0x80

return b''.join([name, value_len, value])
return b''.join([bytes(name), bytes(value_len), value])


class Decoder(object):
Expand Down Expand Up @@ -572,11 +572,12 @@ def decode(self, data):
while current_index < data_len:
# Work out what kind of header we're decoding.
# If the high bit is 1, it's an indexed field.
indexed = bool(data[current_index] & 0x80)
current = _get_byte(data, current_index)
indexed = bool(current & 0x80)

# Otherwise, if the second-highest bit is 1 it's a field that
# doesn't alter the header table.
literal_no_index = bool(data[current_index] & 0x40)
literal_no_index = bool(current & 0x40)

if indexed:
header, consumed = self._decode_indexed(data[current_index:])
Expand Down Expand Up @@ -678,7 +679,7 @@ def _decode_literal(self, data, should_index):

# If the low six bits of the first byte are nonzero, the header
# name is indexed.
first_byte = data[0]
first_byte = _get_byte(data, 0)

if first_byte & 0x3F:
# Indexed header name.
Expand All @@ -701,7 +702,7 @@ def _decode_literal(self, data, should_index):
length, consumed = decode_integer(data, 7)
name = data[consumed:consumed + length]

if data[0] & 0x80:
if _get_byte(data, 0) & 0x80:
name = self.huffman_coder.decode(name)
total_consumed = consumed + length + 1 # Since we moved forward 1.

Expand All @@ -711,7 +712,7 @@ def _decode_literal(self, data, should_index):
length, consumed = decode_integer(data, 7)
value = data[consumed:consumed + length]

if data[0] & 0x80:
if _get_byte(data, 0) & 0x80:
value = self.huffman_coder.decode(value)

# Updated the total consumed length.
Expand Down
25 changes: 20 additions & 5 deletions hyper/http20/huffman.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,20 @@
Huffman-coded content where we already know the Huffman table.
"""
from .exceptions import HPACKDecodingError
from .util import IS_PY3

if IS_PY3:
def _get_byte(b, idx):
return b[idx]

def _decode_hex(b):
return bytes.fromhex(b)
else:
def _get_byte(b, idx):
return ord(b[idx])

def _decode_hex(b):
return b.decode('hex')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this stuff could be moved to compat and then we can unconditionally import.



def _pad_binary(bin_str, req_len=8):
Expand All @@ -21,7 +35,7 @@ def _hex_to_bin_str(hex_string):
Given a Python bytestring, returns a string representing those bytes in
unicode form.
"""
unpadded_bin_string_list = map(bin, hex_string)
unpadded_bin_string_list = (bin(_get_byte(hex_string, i)) for i in range(len(hex_string)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

range(len(hex_string)) makes me sad. We can rewrite _get_byte to be _to_byte like so:

if IS_PY3:
    def _to_byte(num):
        return num
else:
    def _to_byte(num):
        return ord(num)

Then we can return this to a map call (or at least a nicer generator comprehension). Should clean up some of the hpack.py code as well.

padded_bin_string_list = map(_pad_binary, unpadded_bin_string_list)
bitwise_message = "".join(padded_bin_string_list)
return bitwise_message
Expand Down Expand Up @@ -60,7 +74,7 @@ def decode(self, encoded_string):
"""
number = _hex_to_bin_str(encoded_string)
cur_node = self.root
decoded_message = []
decoded_message = bytearray()

try:
for digit in number:
Expand Down Expand Up @@ -103,7 +117,8 @@ def encode(self, bytes_to_encode):
# Turn each byte into its huffman code. These codes aren't necessarily
# octet aligned, so keep track of how far through an octet we are. To
# handle this cleanly, just use a single giant integer.
for letter in bytes_to_encode:
for i in range(len(bytes_to_encode)):
letter = _get_byte(bytes_to_encode, i)
bin_int_len = self.huffman_code_list_lengths[letter]
bin_int = self.huffman_code_list[letter] & (2 ** (bin_int_len + 1) - 1)
final_num <<= bin_int_len
Expand All @@ -116,9 +131,9 @@ def encode(self, bytes_to_encode):
final_num |= (1 << (bits_to_be_padded)) - 1

# Convert the number to hex and strip off the leading '0x'
final_num = hex(final_num)[2:]
final_num = hex(final_num)[2:].rstrip('L')

# If this is odd, prepend a zero.
final_num = '0' + final_num if len(final_num) % 2 != 0 else final_num

return bytes.fromhex(final_num)
return _decode_hex(final_num)
31 changes: 20 additions & 11 deletions hyper/http20/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import ssl
import os.path as path

from .util import IS_PY3


# Right now we support draft 9.
SUPPORTED_PROTOCOLS = ['http/1.1', 'HTTP-draft-09/2.0']
Expand All @@ -23,20 +25,27 @@
cert_loc = path.join(path.dirname(__file__), '..', 'certs.pem')


def wrap_socket(socket, server_hostname):
"""
A vastly simplified SSL wrapping function. We'll probably extend this to
do more things later.
"""
global _context
if IS_PY3:
def wrap_socket(socket, server_hostname):
"""
A vastly simplified SSL wrapping function. We'll probably extend this to
do more things later.
"""
global _context

if _context is None: # pragma: no cover
_context = _init_context()
if _context is None: # pragma: no cover
_context = _init_context()

if ssl.HAS_SNI:
return _context.wrap_socket(socket, server_hostname=server_hostname)
if ssl.HAS_SNI:
return _context.wrap_socket(socket, server_hostname=server_hostname)

return _context.wrap_socket(socket) # pragma: no cover
wrapped = _context.wrap_socket(socket) # pragma: no cover
assert wrapped.selected_npn_protocol() == 'HTTP-draft-09/2.0'
return wrapped
else:
def wrap_socket(socket, server_hostname):
return ssl.wrap_socket(socket, ssl_version=ssl.PROTOCOL_SSLv23,
ca_certs=cert_loc, cert_reqs=ssl.CERT_NONE) # FIXME CERT_REQUIRED
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably fix this. ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gah I can't believe I forgot about this - I meant to mention it right in the PR description. With CERT_REQUIRED, cert validation fails in the tests, since it doesn't recognize the CA, despite passing the path to certs.pem as the ca_certs parameter. Is the test server certificate self-signed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is. You'll note in the integration tests for 3.3 we patch the SSLContext to avoid this problem: might be worth doing it for 2.7 as well.



def _init_context():
Expand Down
3 changes: 3 additions & 0 deletions hyper/http20/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

Utility functions for use with hyper.
"""
import sys
IS_PY3 = sys.version_info[0] == 3

def get_from_key_value_set(kvset, key, default=None):
"""
Returns a value from a key-value set, or the default if the value isn't
Expand Down
40 changes: 32 additions & 8 deletions test/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
from hyper.http20.huffman_constants import (
RESPONSE_CODES, RESPONSE_CODES_LENGTH
)
from hyper.http20.util import IS_PY3

class SocketServerThread(threading.Thread):
class _SocketServerThreadBase(threading.Thread):
"""
This method stolen wholesale from shazow/urllib3.

Expand All @@ -32,22 +33,18 @@ class SocketServerThread(threading.Thread):
:param ready_event: Event which gets set when the socket handler is
ready to receive requests.
"""
def __init__(self, socket_handler, host='localhost', port=8081,
ready_event=None):
def __init__(self, socket_handler, host='localhost', ready_event=None):
threading.Thread.__init__(self)

self.socket_handler = socket_handler
self.host = host
self.ready_event = ready_event
self.cxt = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
self.cxt.set_npn_protocols(['HTTP-draft-09/2.0'])
self.cxt.load_cert_chain(certfile='test/certs/server.crt', keyfile='test/certs/server.key')

def _start_server(self):
sock = socket.socket(socket.AF_INET6)
if sys.platform != 'win32':
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock = self.cxt.wrap_socket(sock, server_side=True)
sock = self._wrap_socket(sock)
sock.bind((self.host, 0))
self.port = sock.getsockname()[1]

Expand All @@ -60,10 +57,38 @@ def _start_server(self):
self.socket_handler(sock)
sock.close()

def _wrap_socket(self, sock):
raise NotImplementedError()

def run(self):
self.server = self._start_server()


class _SocketServerThreadPy2(_SocketServerThreadBase):
def _wrap_socket(self, sock):
return ssl.wrap_socket(sock, server_side=True,
certfile='test/certs/server.crt',
keyfile='test/certs/server.key')


class _SocketServerThreadPy3(_SocketServerThreadBase):
def __init__(self, socket_handler, host='localhost', ready_event=None):
super().__init__(socket_handler, host, ready_event)
self.cxt = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
self.cxt.set_npn_protocols(['HTTP-draft-09/2.0'])
self.cxt.load_cert_chain(certfile='test/certs/server.crt',
keyfile='test/certs/server.key')

def _wrap_socket(self, sock):
return self.cxt.wrap_socket(sock, server_side=True)


if IS_PY3:
SocketServerThread = _SocketServerThreadPy3
else:
SocketServerThread = _SocketServerThreadPy2


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stuff obviously doesn't need to move, it's a good solution to the problem. =)

class SocketLevelTest(object):
"""
A test-class that defines a few helper methods for running socket-level
Expand Down Expand Up @@ -101,4 +126,3 @@ def tear_down(self):
Tears down the testing thread.
"""
self.server_thread.join(0.1)

Loading