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

Implement draft 10 of the http2 spec #34

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
87a8149
Implement compatibility layer exposing pyOpenSSL as a Py3.3 ssl-like API
alekstorm Apr 3, 2014
0b75307
Implement draft 10 of the http2 spec, modulo meta-protocol concerns l…
alekstorm Apr 2, 2014
7a42e05
Implement new TLS options mandated by h2-10, including SNI support in…
alekstorm Apr 3, 2014
228362f
Add pyOpenSSL to test_requirements.txt
alekstorm Apr 3, 2014
da4e207
Refactor DATA frame padding calculations for clarity
alekstorm Apr 3, 2014
c750a32
Add PAD_HIGH,PAD_LOW flags to HEADERS and CONTINUATION frames
alekstorm Apr 3, 2014
79ef23e
Include DATA frame padding in flow control manager update
alekstorm Apr 3, 2014
5ce4ecd
Refactor ssl_compat attribute translation into a dictionary
alekstorm Apr 4, 2014
2d2490a
Make OpenSSL(TLSv1.2) and pyOpenSSL soft dependencies, add tests for …
alekstorm Apr 4, 2014
ce43c6d
Rename compat.handle_missing() to ignore_missing()
alekstorm Apr 4, 2014
2c5e53a
Fixup docstrings in ssl_compat functions lifted from stdlib
alekstorm Apr 4, 2014
2da1ba5
Factor ssl_compat.SSLSocket connection testing out into a property
alekstorm Apr 4, 2014
4a0cd6c
Check that the negotiated servername matches the requested one
alekstorm Apr 4, 2014
b72a3c4
Fixup use of get_connection() in test_hyper
alekstorm Apr 5, 2014
c2e9280
Revert non-TLS support, make pyOpenSSL a hard dependency
alekstorm Apr 5, 2014
5d5d61c
Revert change to HTTP20Adapter.get_connection() that made it scheme-d…
alekstorm Apr 5, 2014
6030a38
Revert changes to build matrix on Travis CI that tested pyOpenSSL ava…
alekstorm Apr 5, 2014
32572a6
Add ssl_compat.py to files omitted from coverage tests
alekstorm Apr 5, 2014
575a856
Fix import and Unicode-correctness issues in ssl_compat on Py3.3
alekstorm Apr 5, 2014
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
10 changes: 10 additions & 0 deletions hyper/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

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

Expand All @@ -17,7 +18,15 @@
#: Python 3.x?
is_py3 = (_ver[0] == 3)

@contextmanager
def handle_missing():
try:
yield
except (AttributeError, NotImplementedError): # pragma: no cover
pass

if is_py2:
import ssl_compat as ssl
from urlparse import urlparse

def to_byte(char):
Expand All @@ -32,6 +41,7 @@ def zlib_compressobj(level=6, method=zlib.DEFLATED, wbits=15, memlevel=8,
return zlib.compressobj(level, method, wbits, memlevel, strategy)

elif is_py3:
import ssl
from urllib.parse import urlparse

def to_byte(char):
Expand Down
2 changes: 1 addition & 1 deletion hyper/http20/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"""
from .hpack import Encoder, Decoder
from .stream import Stream
from .tls import wrap_socket
from .tls import NPN_PROTOCOL, wrap_socket
from .frame import (
DataFrame, HeadersFrame, SettingsFrame, Frame, WindowUpdateFrame,
GoAwayFrame
Expand Down
76 changes: 52 additions & 24 deletions hyper/http20/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
# The maximum length of a frame. Some frames have shorter maximum lengths.
FRAME_MAX_LEN = (2 ** 14) - 1

def _total_padding(high, low):
return high * 256 + low

Copy link
Member

Choose a reason for hiding this comment

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

Can we get a docstring on this, just to explain its purpose?

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 also probably more clearly expressed as a bitshift, e.g. (high << 8) + low.


class Frame(object):
"""
Expand Down Expand Up @@ -82,26 +85,51 @@ class DataFrame(Frame):
associated with a stream. One or more DATA frames are used, for instance,
to carry HTTP request or response payloads.
"""
defined_flags = [('END_STREAM', 0x01)]
defined_flags = [('END_STREAM', 0x01), ('END_SEGMENT', 0x02),
('PAD_LOW', 0x10), ('PAD_HIGH', 0x20)]

type = 0

def __init__(self, stream_id):
super(DataFrame, self).__init__(stream_id)

self.data = b''
self.low_padding = 0
self.high_padding = 0

# Data frames may not be stream 0.
if not self.stream_id:
raise ValueError()

def serialize(self):
data = self.build_frame_header(len(self.data))
padding_length = _total_padding(self.high_padding, self.low_padding)
data = self.build_frame_header(len(self.data) + self._padding_lengths + padding_length)
if 'PAD_LOW' in self.flags:
if 'PAD_HIGH' in self.flags:
data += struct.pack('!BB', self.high_padding, self.low_padding)
else:
data += struct.pack('!B', self.low_padding)
data += self.data
data += b'\0' * padding_length
return data

def parse_body(self, data):
self.data = data
padding_length = 0
if 'PAD_LOW' in self.flags:
if 'PAD_HIGH' in self.flags:
self.high_padding, self.low_padding = struct.unpack('!BB', data[:2])
padding_length = _total_padding(self.high_padding, self.low_padding)
else:
padding_length = self.low_padding = struct.unpack('!B', data[:1])[0]
self.data = data[self._padding_lengths:len(data)-padding_length]

@property
def _padding_lengths(self):
if 'PAD_LOW' in self.flags:
if 'PAD_HIGH' in self.flags:
return 2
return 1
return 0
Copy link
Member

Choose a reason for hiding this comment

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

Can we rename this property to something clearer? _padding_data_length might be better.



class PriorityFrame(Frame):
Expand Down Expand Up @@ -187,9 +215,8 @@ class SettingsFrame(Frame):
# attributes.
HEADER_TABLE_SIZE = 0x01
ENABLE_PUSH = 0x02
MAX_CONCURRENT_STREAMS = 0x04
INITIAL_WINDOW_SIZE = 0x07
FLOW_CONTROL_OPTIONS = 0x0A
MAX_CONCURRENT_STREAMS = 0x03
INITIAL_WINDOW_SIZE = 0x04

def __init__(self, stream_id):
super(SettingsFrame, self).__init__(stream_id)
Expand All @@ -202,18 +229,18 @@ def __init__(self, stream_id):

def serialize(self):
# Each setting consumes 8 bytes.
length = len(self.settings) * 8
length = len(self.settings) * 5
Copy link
Member

Choose a reason for hiding this comment

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

Update the comment? =)


data = self.build_frame_header(length)

for setting, value in self.settings.items():
data += struct.pack("!LL", setting & 0x00FFFFFF, value)
data += struct.pack("!BL", setting & 0xFF, value)

return data

def parse_body(self, data):
for i in range(0, len(data), 8):
name, value = struct.unpack("!LL", data[i:i+8])
for i in range(0, len(data), 5):
name, value = struct.unpack("!BL", data[i:i+5])
self.settings[name] = value


Expand Down Expand Up @@ -315,7 +342,7 @@ class WindowUpdateFrame(Frame):
can indirectly cause the propagation of flow control information toward the
original sender.
"""
type = 0x09
type = 0x08

def __init__(self, stream_id):
super(WindowUpdateFrame, self).__init__(stream_id)
Expand Down Expand Up @@ -386,21 +413,22 @@ class ContinuationFrame(DataFrame):
Much like the HEADERS frame, hyper treats this as an opaque data frame with
different flags and a different type.
"""
type = 0x0A
type = 0x09

defined_flags = [('END_HEADERS', 0x04)]


# A map of type byte to frame class.
FRAMES = {
0x00: DataFrame,
0x01: HeadersFrame,
0x02: PriorityFrame,
0x03: RstStreamFrame,
0x04: SettingsFrame,
0x05: PushPromiseFrame,
0x06: PingFrame,
0x07: GoAwayFrame,
0x09: WindowUpdateFrame,
0x0A: ContinuationFrame
}
_FRAME_CLASSES = [
DataFrame,
HeadersFrame,
PriorityFrame,
RstStreamFrame,
SettingsFrame,
PushPromiseFrame,
PingFrame,
GoAwayFrame,
WindowUpdateFrame,
ContinuationFrame,
]
FRAMES = {cls.type: cls for cls in _FRAME_CLASSES}
75 changes: 28 additions & 47 deletions hyper/http20/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,75 +5,56 @@

Contains the TLS/SSL logic for use in hyper.
"""
import ssl
import os.path as path

from ..compat import is_py3
from ..compat import handle_missing, ssl


# Right now we support draft 9.
SUPPORTED_PROTOCOLS = ['http/1.1', 'HTTP-draft-09/2.0']
NPN_PROTOCOL = 'h2-10'
SUPPORTED_NPN_PROTOCOLS = ['http/1.1', NPN_PROTOCOL]


# We have a singleton SSLContext object. There's no reason to be creating one
# per connection. We're using v23 right now until someone gives me a reason not
# to.
# per connection.
_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(sock, 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 ssl.HAS_SNI:
return _context.wrap_socket(socket, server_hostname=server_hostname)
if _context is None: # pragma: no cover
_context = _init_context()

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)
# the spec requires SNI support
ssl_sock = _context.wrap_socket(sock, server_hostname=server_hostname)
with handle_missing():
assert ssl_sock.selected_npn_protocol() == NPN_PROTOCOL
return ssl_sock


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

try:
context.set_npn_protocols(SUPPORTED_PROTOCOLS)
except (AttributeError, NotImplementedError): # pragma: no cover
pass
context.verify_mode = ssl.CERT_REQUIRED
# TODO This just verifies that the post-handshake servername matches the
# certificate, right? We need to also check that the returned servername
# matches the requested one... right?
context.check_hostname = True
Copy link
Member

Choose a reason for hiding this comment

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

Might be worth examining what Requests does here. =)


# We do our best to do better security
try:
context.options |= ssl.OP_NO_SSLv2
except AttributeError: # pragma: no cover
pass
with handle_missing():
context.set_npn_protocols(SUPPORTED_NPN_PROTOCOLS)

try:
context.options |= ssl.OP_NO_COMPRESSION
except AttributeError: # pragma: no cover
pass
# required by the spec
context.options |= ssl.OP_NO_COMPRESSION

return context
2 changes: 1 addition & 1 deletion hyper/httplib_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
except ImportError:
import httplib

import ssl
from .compat import ssl
from .http20.tls import wrap_socket

# If there's no NPN support, we're going to drop all support for HTTP/2.0.
Expand Down
Loading