diff --git a/HISTORY.rst b/HISTORY.rst index 18188bc82..229d42ff0 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -7,6 +7,11 @@ Release History API Changes (Backward-Compatible) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Added new ``UnknownFrameReceived`` event that fires when unknown extension + frames have been received. This only fires when using hyperframe 5.0 or + later: earlier versions of hyperframe cause us to silently ignore extension + frames. + Bugfixes ~~~~~~~~ diff --git a/docs/source/api.rst b/docs/source/api.rst index 56ca1213a..46e8a0faa 100644 --- a/docs/source/api.rst +++ b/docs/source/api.rst @@ -77,6 +77,9 @@ Events .. autoclass:: h2.events.AlternativeServiceAvailable :members: +.. autoclass:: h2.events.UnknownFrameReceived + :members: + Exceptions ---------- diff --git a/h2/connection.py b/h2/connection.py index 5f9ccf84e..6a0d65ca4 100644 --- a/h2/connection.py +++ b/h2/connection.py @@ -23,13 +23,12 @@ from .events import ( WindowUpdated, RemoteSettingsChanged, PingAcknowledged, SettingsAcknowledged, ConnectionTerminated, PriorityUpdated, - AlternativeServiceAvailable, + AlternativeServiceAvailable, UnknownFrameReceived ) from .exceptions import ( ProtocolError, NoSuchStreamError, FlowControlError, FrameTooLargeError, TooManyStreamsError, StreamClosedError, StreamIDTooLowError, - NoAvailableStreamIDError, UnsupportedFrameError, RFC1122Error, - DenialOfServiceError + NoAvailableStreamIDError, RFC1122Error, DenialOfServiceError ) from .frame_buffer import FrameBuffer from .settings import Settings, SettingCodes @@ -46,6 +45,15 @@ class OversizedHeaderListError(Exception): pass +try: + from hyperframe.frame import ExtensionFrame +except ImportError: # Platform-specific: Hyperframe < 5.0.0 + # If the frame doesn't exist, that's just fine: we'll define it ourselves + # and the method will just never be called. + class ExtensionFrame(object): + pass + + class ConnectionState(Enum): IDLE = 0 CLIENT_OPEN = 1 @@ -404,6 +412,7 @@ def __init__(self, client_side=True, header_encoding='utf-8', config=None): GoAwayFrame: self._receive_goaway_frame, ContinuationFrame: self._receive_naked_continuation, AltSvcFrame: self._receive_alt_svc_frame, + ExtensionFrame: self._receive_unknown_frame } def _prepare_for_sending(self, frames): @@ -1575,10 +1584,6 @@ def _receive_frame(self, frame): # Closed implicitly, also a connection error, but of type # PROTOCOL_ERROR. raise - except KeyError as e: # pragma: no cover - # We don't have a function for handling this frame. Let's call this - # a PROTOCOL_ERROR and exit. - raise UnsupportedFrameError("Unexpected frame: %s" % frame) else: self._prepare_for_sending(frames) @@ -1922,6 +1927,23 @@ def _receive_alt_svc_frame(self, frame): return frames, events + def _receive_unknown_frame(self, frame): + """ + We have received a frame that we do not understand. This is almost + certainly an extension frame, though it's impossible to be entirely + sure. + + RFC 7540 ยง 5.5 says that we MUST ignore unknown frame types: so we + do. We do notify the user that we received one, however. + """ + # All we do here is log. + self.config.logger.debug( + "Received unknown extension frame (ID %d)", frame.stream_id + ) + event = UnknownFrameReceived() + event.frame = frame + return [], [event] + def _local_settings_acked(self): """ Handle the local settings being ACKed, update internal state. diff --git a/h2/events.py b/h2/events.py index 55f202183..ff3ec3df3 100644 --- a/h2/events.py +++ b/h2/events.py @@ -574,6 +574,28 @@ def __repr__(self): ) +class UnknownFrameReceived(Event): + """ + The UnknownFrameReceived event is fired when the remote peer sends a frame + that hyper-h2 does not understand. This occurs primarily when the remote + peer is employing HTTP/2 extensions that hyper-h2 doesn't know anything + about. + + RFC 7540 requires that HTTP/2 implementations ignore these frames. hyper-h2 + does so. However, this event is fired to allow implementations to perform + special processing on those frames if needed (e.g. if the implementation + is capable of handling the frame itself). + + .. versionadded:: 2.7.0 + """ + def __init__(self): + #: The hyperframe Frame object that encapsulates the received frame. + self.frame = None + + def __repr__(self): + return "" + + def _bytes_representation(data): """ Converts a bytestring into something that is safe to print on all Python diff --git a/h2/frame_buffer.py b/h2/frame_buffer.py index cfa319860..bc1f7ca4e 100644 --- a/h2/frame_buffer.py +++ b/h2/frame_buffer.py @@ -65,11 +65,13 @@ def _parse_frame_header(self, data): """ try: frame, length = Frame.parse_frame_header(data[:9]) - except UnknownFrameError as e: + except UnknownFrameError as e: # Platform-specific: Hyperframe < 5.0 # Here we do something a bit odd. We want to consume the frame data # as consistently as possible, but we also don't ever want to yield # None. Instead, we make sure that, if there is no frame, we # recurse into ourselves. + # This can only happen now on older versions of hyperframe. + # TODO: Remove in 3.0 length = e.length frame = None except ValueError as e: diff --git a/setup.py b/setup.py index eeda7d9f5..2ab40c699 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ 'Programming Language :: Python :: Implementation :: PyPy', ], install_requires=[ - 'hyperframe>=3.1, <5, !=4.0.0', + 'hyperframe>=3.1, <6, !=4.0.0', 'hpack>=2.2, <3', ], extras_require={ diff --git a/test/test_basic_logic.py b/test/test_basic_logic.py index e9aee2669..ac34f760b 100644 --- a/test/test_basic_logic.py +++ b/test/test_basic_logic.py @@ -1574,8 +1574,10 @@ def test_unknown_frames_are_ignored(self, frame_factory, frame_id): f.type = frame_id events = c.receive_data(f.serialize()) - assert not events assert not c.data_to_send() + assert len(events) == 1 + assert isinstance(events[0], h2.events.UnknownFrameReceived) + assert isinstance(events[0].frame, hyperframe.frame.ExtensionFrame) def test_can_send_goaway_repeatedly(self, frame_factory): """ diff --git a/test/test_events.py b/test/test_events.py index a4cbf809d..a40744908 100644 --- a/test/test_events.py +++ b/test/test_events.py @@ -323,6 +323,13 @@ def test_alternativeserviceavailable_repr(self): 'field_value:h2=":8000"; ma=60>' ) + def test_unknownframereceived_repr(self): + """ + UnknownFrameReceived has a useful debug representation. + """ + e = h2.events.UnknownFrameReceived() + assert repr(e) == '' + def all_events(): """