This repository has been archived by the owner on Jan 13, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 193
Implement draft 10 of the http2 spec #34
Closed
Closed
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 0b75307
Implement draft 10 of the http2 spec, modulo meta-protocol concerns l…
alekstorm 7a42e05
Implement new TLS options mandated by h2-10, including SNI support in…
alekstorm 228362f
Add pyOpenSSL to test_requirements.txt
alekstorm da4e207
Refactor DATA frame padding calculations for clarity
alekstorm c750a32
Add PAD_HIGH,PAD_LOW flags to HEADERS and CONTINUATION frames
alekstorm 79ef23e
Include DATA frame padding in flow control manager update
alekstorm 5ce4ecd
Refactor ssl_compat attribute translation into a dictionary
alekstorm 2d2490a
Make OpenSSL(TLSv1.2) and pyOpenSSL soft dependencies, add tests for …
alekstorm ce43c6d
Rename compat.handle_missing() to ignore_missing()
alekstorm 2c5e53a
Fixup docstrings in ssl_compat functions lifted from stdlib
alekstorm 2da1ba5
Factor ssl_compat.SSLSocket connection testing out into a property
alekstorm 4a0cd6c
Check that the negotiated servername matches the requested one
alekstorm b72a3c4
Fixup use of get_connection() in test_hyper
alekstorm c2e9280
Revert non-TLS support, make pyOpenSSL a hard dependency
alekstorm 5d5d61c
Revert change to HTTP20Adapter.get_connection() that made it scheme-d…
alekstorm 6030a38
Revert changes to build matrix on Travis CI that tested pyOpenSSL ava…
alekstorm 32572a6
Add ssl_compat.py to files omitted from coverage tests
alekstorm 575a856
Fix import and Unicode-correctness issues in ssl_compat on Py3.3
alekstorm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
||
class Frame(object): | ||
""" | ||
|
@@ -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 | ||
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. Can we rename this property to something clearer? |
||
|
||
|
||
class PriorityFrame(Frame): | ||
|
@@ -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) | ||
|
@@ -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 | ||
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. 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 | ||
|
||
|
||
|
@@ -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) | ||
|
@@ -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} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
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. 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Can we get a docstring on this, just to explain its purpose?
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.
This is also probably more clearly expressed as a bitshift, e.g.
(high << 8) + low
.