Skip to content

Commit

Permalink
Merge pull request #81 from python-hyper/haikuginger-add-unknown-frame
Browse files Browse the repository at this point in the history
Add support for extension frames.
  • Loading branch information
Lukasa committed Mar 7, 2017
2 parents 4247785 + 70cbe76 commit 4b1fc30
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 8 deletions.
9 changes: 9 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
Release History
===============

5.0.0dev0
---------

**Backwards Incompatible API Changes**

- Added support for unknown extension frames. These will be returned in the new
``ExtensionFrame`` object. The flag information for these frames is persisted
in ``flag_byte`` if needed.

4.0.2 (2017-02-20)
------------------

Expand Down
4 changes: 4 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ you need is not present!
:show-inheritance:
:members:

.. autoclass:: hyperframe.frame.ExtensionFrame
:show-inheritance:
:members:

.. autodata:: hyperframe.frame.FRAMES

Exceptions
Expand Down
72 changes: 68 additions & 4 deletions hyperframe/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,21 @@ def __repr__(self):
)

@staticmethod
def parse_frame_header(header):
def parse_frame_header(header, strict=False):
"""
Takes a 9-byte frame header and returns a tuple of the appropriate
Frame object and the length that needs to be read from the socket.
This populates the flags field, and determines how long the body is.
:param strict: Whether to raise an exception when encountering a frame
not defined by spec and implemented by hyperframe.
:raises hyperframe.exceptions.UnknownFrameError: If a frame of unknown
type is received.
.. versionchanged:: 5.0.0
Added :param:`strict` to accommodate :class:`ExtensionFrame`
"""
try:
fields = _STRUCT_HBBBL.unpack(header)
Expand All @@ -109,10 +115,13 @@ def parse_frame_header(header):
flags = fields[3]
stream_id = fields[4] & 0x7FFFFFFF

if type not in FRAMES:
raise UnknownFrameError(type, length)
try:
frame = FRAMES[type](stream_id)
except KeyError:
if strict:
raise UnknownFrameError(type, length)
frame = ExtensionFrame(type=type, stream_id=stream_id)

frame = FRAMES[type](stream_id)
frame.parse_flags(flags)
return (frame, length)

Expand Down Expand Up @@ -736,6 +745,61 @@ def parse_body(self, data):
self.body_len = len(data)


class ExtensionFrame(Frame):
"""
ExtensionFrame is used to wrap frames which are not natively interpretable
by hyperframe.
Although certain byte prefixes are ordained by specification to have
certain contextual meanings, frames with other prefixes are not prohibited,
and may be used to communicate arbitrary meaning between HTTP/2 peers.
Thus, hyperframe, rather than raising an exception when such a frame is
encountered, wraps it in a generic frame to be properly acted upon by
upstream consumers which might have additional context on how to use it.
.. versionadded:: 5.0.0
"""

stream_association = _STREAM_ASSOC_EITHER

def __init__(self, type, stream_id, **kwargs):
super(ExtensionFrame, self).__init__(stream_id, **kwargs)
self.type = type
self.flag_byte = None

def parse_flags(self, flag_byte):
"""
For extension frames, we parse the flags by just storing a flag byte.
"""
self.flag_byte = flag_byte

def parse_body(self, data):
self.body = data.tobytes()
self.body_len = len(data)

def serialize(self):
"""
A broad override of the serialize method that ensures that the data
comes back out exactly as it came in. This should not be used in most
user code: it exists only as a helper method if frames need to be
reconstituted.
"""
# Build the frame header.
# First, get the flags.
flags = self.flag_byte

header = _STRUCT_HBBBL.pack(
(self.body_len >> 8) & 0xFFFF, # Length spread over top 24 bits
self.body_len & 0xFF,
self.type,
flags,
self.stream_id & 0x7FFFFFFF # Stream ID is 32 bits.
)

return header + self.body


_FRAME_CLASSES = [
DataFrame,
HeadersFrame,
Expand Down
40 changes: 36 additions & 4 deletions test/test_frames.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from hyperframe.frame import (
Frame, Flags, DataFrame, PriorityFrame, RstStreamFrame, SettingsFrame,
PushPromiseFrame, PingFrame, GoAwayFrame, WindowUpdateFrame, HeadersFrame,
ContinuationFrame, AltSvcFrame
ContinuationFrame, AltSvcFrame, ExtensionFrame
)
from hyperframe.exceptions import (
UnknownFrameError, InvalidPaddingError, InvalidFrameError
Expand Down Expand Up @@ -35,10 +35,12 @@ def test_base_frame_cant_parse_body(self):
with pytest.raises(NotImplementedError):
f.parse_body(data)

def test_parse_frame_header_unknown_type(self):
def test_parse_frame_header_unknown_type_strict(self):
with pytest.raises(UnknownFrameError) as excinfo:
Frame.parse_frame_header(b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01')

Frame.parse_frame_header(
b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01',
strict=True
)
exception = excinfo.value
assert exception.frame_type == 0xFF
assert exception.length == 0x59
Expand All @@ -53,6 +55,36 @@ def test_parse_frame_header_ignore_first_bit_of_stream_id(self):

assert f.stream_id == 0

def test_parse_frame_header_unknown_type(self):
f, l = Frame.parse_frame_header(
b'\x00\x00\x59\xFF\x00\x00\x00\x00\x01'
)
assert f.type == 0xFF
assert l == 0x59
assert isinstance(f, ExtensionFrame)
assert f.stream_id == 1

def test_flags_are_persisted(self):
f, l = Frame.parse_frame_header(
b'\x00\x00\x59\xFF\x09\x00\x00\x00\x01'
)
assert f.type == 0xFF
assert l == 0x59
assert f.flag_byte == 0x09

def test_parse_body_unknown_type(self):
f = decode_frame(
b'\x00\x00\x0C\xFF\x00\x00\x00\x00\x01hello world!'
)
assert f.body == b'hello world!'
assert f.body_len == 12
assert f.stream_id == 1

def test_can_round_trip_unknown_frames(self):
frame_data = b'\x00\x00\x0C\xFF\x00\x00\x00\x00\x01hello world!'
f = decode_frame(frame_data)
assert f.serialize() == frame_data

def test_repr(self, monkeypatch):
f = Frame(stream_id=0)
monkeypatch.setattr(Frame, "serialize_body", lambda _: b"body")
Expand Down

0 comments on commit 4b1fc30

Please sign in to comment.