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 all commits
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
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
[run]
omit=hyper/httplib_compat.py
omit =
hyper/compat.py
hyper/httplib_compat.py
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
4 changes: 4 additions & 0 deletions CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ In chronological order:
- Sriram Ganesan (@elricL)

- Implemented the Huffman encoding/decoding logic.

- Alek Storm (@alekstorm)

- Implemented Python 2.7 support.
4 changes: 4 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
import pytest
import os
import json
import sys

if sys.version_info[0] == 2:
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.
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
43 changes: 43 additions & 0 deletions hyper/compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""
hyper/compat
~~~~~~~~~

Normalizes the Python 2/3 API for internal use.
"""
import sys
import zlib

# Syntax sugar.
_ver = sys.version_info

#: Python 2.x?
is_py2 = (_ver[0] == 2)

#: Python 3.x?
is_py3 = (_ver[0] == 3)

if is_py2:
from urlparse import urlparse

def to_byte(char):
return ord(char)

def decode_hex(b):
return b.decode('hex')

# The standard zlib.compressobj() accepts only positional arguments.
def zlib_compressobj(level=6, method=zlib.DEFLATED, wbits=15, memlevel=8,
strategy=zlib.Z_DEFAULT_STRATEGY):
return zlib.compressobj(level, method, wbits, memlevel, strategy)

elif is_py3:
from urllib.parse import urlparse

def to_byte(char):
return char

def decode_hex(b):
return bytes.fromhex(b)

zlib_compressobj = zlib.compressobj
2 changes: 1 addition & 1 deletion hyper/contrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
HTTPAdapter = object

from hyper import HTTP20Connection
from urllib.parse import urlparse
from hyper.compat import urlparse

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
24 changes: 13 additions & 11 deletions hyper/http20/hpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import collections
import logging

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

number = data[index] & mask
number = to_byte(data[index]) & mask

if (number == max_number):

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

if next_byte >= 128:
number += (next_byte - 128) * multiple(index)
Expand Down Expand Up @@ -407,15 +408,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 +429,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 +450,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 +573,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 = to_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 +680,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 = to_byte(data[0])

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

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

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

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

# Updated the total consumed length.
Expand Down
20 changes: 11 additions & 9 deletions hyper/http20/huffman.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
An implementation of a bitwise prefix tree specially built for decoding
Huffman-coded content where we already know the Huffman table.
"""
from ..compat import to_byte, decode_hex
from .exceptions import HPACKDecodingError


def _pad_binary(bin_str, req_len=8):
"""
Given a binary string (returned by bin()), pad it to a full byte length.
Expand All @@ -21,7 +21,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(to_byte(c)) for c in hex_string)
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 +60,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,9 +103,10 @@ 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:
bin_int_len = self.huffman_code_list_lengths[letter]
bin_int = self.huffman_code_list[letter] & (2 ** (bin_int_len + 1) - 1)
for char in bytes_to_encode:
byte = to_byte(char)
bin_int_len = self.huffman_code_list_lengths[byte]
bin_int = self.huffman_code_list[byte] & (2 ** (bin_int_len + 1) - 1)
final_num <<= bin_int_len
final_num |= bin_int
final_int_len += bin_int_len
Expand All @@ -115,10 +116,11 @@ def encode(self, bytes_to_encode):
final_num <<= bits_to_be_padded
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:]
# Convert the number to hex and strip off the leading '0x' and the
# trailing 'L', if present.
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)
38 changes: 25 additions & 13 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 ..compat import is_py3


# Right now we support draft 9.
SUPPORTED_PROTOCOLS = ['http/1.1', 'HTTP-draft-09/2.0']
Expand All @@ -18,35 +20,45 @@
# to.
_context = None

# Exposed here so it can be monkey-patched in integration tests.
_verify_mode = ssl.CERT_REQUIRED


# Work out where our certificates are.
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: # pragma: no cover
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: # pragma: no cover
def wrap_socket(socket, server_hostname):
return ssl.wrap_socket(socket, ssl_version=ssl.PROTOCOL_SSLv23,
ca_certs=cert_loc, cert_reqs=_verify_mode)


def _init_context():
def _init_context(): # pragma: no cover
"""
Creates the singleton SSLContext we use.
"""
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.set_default_verify_paths()
context.load_verify_locations(cafile=cert_loc)
context.verify_mode = ssl.CERT_REQUIRED
context.verify_mode = _verify_mode

try:
context.set_npn_protocols(SUPPORTED_PROTOCOLS)
Expand Down
Loading