-
Notifications
You must be signed in to change notification settings - Fork 193
Implement Python 2.7 support #33
Changes from 1 commit
513e57e
0d3a4f5
bca22f4
6c82103
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
language: python | ||
python: | ||
- "2.7" | ||
- "3.3" | ||
|
||
install: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
) | ||
|
@@ -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) | ||
|
@@ -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' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha, understood. |
||
|
||
if huffman: | ||
name = self.huffman_coder.encode(name) | ||
|
@@ -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): | ||
""" | ||
|
@@ -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): | ||
|
@@ -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:]) | ||
|
@@ -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. | ||
|
@@ -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. | ||
|
||
|
@@ -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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, this stuff could be moved to |
||
|
||
|
||
def _pad_binary(bin_str, req_len=8): | ||
|
@@ -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))) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
if IS_PY3:
def _to_byte(num):
return num
else:
def _to_byte(num):
return ord(num) Then we can return this to a |
||
padded_bin_string_list = map(_pad_binary, unpadded_bin_string_list) | ||
bitwise_message = "".join(padded_bin_string_list) | ||
return bitwise_message | ||
|
@@ -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: | ||
|
@@ -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 | ||
|
@@ -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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'] | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably fix this. ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
|
||
def _init_context(): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
||
|
@@ -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] | ||
|
||
|
@@ -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 | ||
|
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -101,4 +126,3 @@ def tear_down(self): | |
Tears down the testing thread. | ||
""" | ||
self.server_thread.join(0.1) | ||
|
There was a problem hiding this comment.
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.