From c95e738518418d3659e34fb198d455a1377d9bc7 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Wed, 13 Nov 2024 21:57:30 +0000 Subject: [PATCH] fix: No more BytesWarnings Fixes #1236. This patch makes all header operations operate on `bytes` and converts all headers and values to bytes before operation. With a follow up patch to `hpack` it should also increase efficiency as currently, `hpack` casts everything to a `str` first before converting back to bytes: https://github.com/python-hyper/hpack/blob/02afcab28ca56eb5259904fd414baa89e9f50266/src/hpack/hpack.py#L150-L151 --- src/h2/connection.py | 642 ++++++++++++++++-------------- src/h2/events.py | 94 +++-- src/h2/stream.py | 685 ++++++++++++++++++--------------- src/h2/utilities.py | 316 +++++++-------- test/test_invalid_headers.py | 493 ++++++++++-------------- test/test_utility_functions.py | 54 +-- tox.ini | 2 +- 7 files changed, 1154 insertions(+), 1132 deletions(-) diff --git a/src/h2/connection.py b/src/h2/connection.py index 25251e20a..513d68206 100644 --- a/src/h2/connection.py +++ b/src/h2/connection.py @@ -11,9 +11,18 @@ from hyperframe.exceptions import InvalidPaddingError from hyperframe.frame import ( - GoAwayFrame, WindowUpdateFrame, HeadersFrame, DataFrame, PingFrame, - PushPromiseFrame, SettingsFrame, RstStreamFrame, PriorityFrame, - ContinuationFrame, AltSvcFrame, ExtensionFrame + GoAwayFrame, + WindowUpdateFrame, + HeadersFrame, + DataFrame, + PingFrame, + PushPromiseFrame, + SettingsFrame, + RstStreamFrame, + PriorityFrame, + ContinuationFrame, + AltSvcFrame, + ExtensionFrame, ) from hpack.hpack import Encoder, Decoder from hpack.exceptions import HPACKError, OversizedHeaderListError @@ -21,19 +30,32 @@ from .config import H2Configuration from .errors import ErrorCodes, _error_code_from_int from .events import ( - WindowUpdated, RemoteSettingsChanged, PingReceived, PingAckReceived, - SettingsAcknowledged, ConnectionTerminated, PriorityUpdated, - AlternativeServiceAvailable, UnknownFrameReceived + WindowUpdated, + RemoteSettingsChanged, + PingReceived, + PingAckReceived, + SettingsAcknowledged, + ConnectionTerminated, + PriorityUpdated, + AlternativeServiceAvailable, + UnknownFrameReceived, ) from .exceptions import ( - ProtocolError, NoSuchStreamError, FlowControlError, FrameTooLargeError, - TooManyStreamsError, StreamClosedError, StreamIDTooLowError, - NoAvailableStreamIDError, RFC1122Error, DenialOfServiceError + ProtocolError, + NoSuchStreamError, + FlowControlError, + FrameTooLargeError, + TooManyStreamsError, + StreamClosedError, + StreamIDTooLowError, + NoAvailableStreamIDError, + RFC1122Error, + DenialOfServiceError, ) from .frame_buffer import FrameBuffer from .settings import Settings, SettingCodes from .stream import H2Stream, StreamClosedBy -from .utilities import SizeLimitDict, guard_increment_window +from .utilities import SizeLimitDict, guard_increment_window, utf8_encode_headers from .windows import WindowManager @@ -81,6 +103,7 @@ class H2ConnectionStateMachine: maintains very little state directly, instead focusing entirely on managing state transitions. """ + # For the purposes of this state machine we treat HEADERS and their # associated CONTINUATION frames as a single jumbo frame. The protocol # allows/requires this by preventing other frames from being interleved in @@ -93,121 +116,221 @@ class H2ConnectionStateMachine: _transitions = { # State: idle - (ConnectionState.IDLE, ConnectionInputs.SEND_HEADERS): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.IDLE, ConnectionInputs.RECV_HEADERS): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.IDLE, ConnectionInputs.SEND_SETTINGS): - (None, ConnectionState.IDLE), - (ConnectionState.IDLE, ConnectionInputs.RECV_SETTINGS): - (None, ConnectionState.IDLE), - (ConnectionState.IDLE, ConnectionInputs.SEND_WINDOW_UPDATE): - (None, ConnectionState.IDLE), - (ConnectionState.IDLE, ConnectionInputs.RECV_WINDOW_UPDATE): - (None, ConnectionState.IDLE), - (ConnectionState.IDLE, ConnectionInputs.SEND_PING): - (None, ConnectionState.IDLE), - (ConnectionState.IDLE, ConnectionInputs.RECV_PING): - (None, ConnectionState.IDLE), - (ConnectionState.IDLE, ConnectionInputs.SEND_GOAWAY): - (None, ConnectionState.CLOSED), - (ConnectionState.IDLE, ConnectionInputs.RECV_GOAWAY): - (None, ConnectionState.CLOSED), - (ConnectionState.IDLE, ConnectionInputs.SEND_PRIORITY): - (None, ConnectionState.IDLE), - (ConnectionState.IDLE, ConnectionInputs.RECV_PRIORITY): - (None, ConnectionState.IDLE), - (ConnectionState.IDLE, ConnectionInputs.SEND_ALTERNATIVE_SERVICE): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.IDLE, ConnectionInputs.RECV_ALTERNATIVE_SERVICE): - (None, ConnectionState.CLIENT_OPEN), - + (ConnectionState.IDLE, ConnectionInputs.SEND_HEADERS): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.IDLE, ConnectionInputs.RECV_HEADERS): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.IDLE, ConnectionInputs.SEND_SETTINGS): ( + None, + ConnectionState.IDLE, + ), + (ConnectionState.IDLE, ConnectionInputs.RECV_SETTINGS): ( + None, + ConnectionState.IDLE, + ), + (ConnectionState.IDLE, ConnectionInputs.SEND_WINDOW_UPDATE): ( + None, + ConnectionState.IDLE, + ), + (ConnectionState.IDLE, ConnectionInputs.RECV_WINDOW_UPDATE): ( + None, + ConnectionState.IDLE, + ), + (ConnectionState.IDLE, ConnectionInputs.SEND_PING): ( + None, + ConnectionState.IDLE, + ), + (ConnectionState.IDLE, ConnectionInputs.RECV_PING): ( + None, + ConnectionState.IDLE, + ), + (ConnectionState.IDLE, ConnectionInputs.SEND_GOAWAY): ( + None, + ConnectionState.CLOSED, + ), + (ConnectionState.IDLE, ConnectionInputs.RECV_GOAWAY): ( + None, + ConnectionState.CLOSED, + ), + (ConnectionState.IDLE, ConnectionInputs.SEND_PRIORITY): ( + None, + ConnectionState.IDLE, + ), + (ConnectionState.IDLE, ConnectionInputs.RECV_PRIORITY): ( + None, + ConnectionState.IDLE, + ), + (ConnectionState.IDLE, ConnectionInputs.SEND_ALTERNATIVE_SERVICE): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.IDLE, ConnectionInputs.RECV_ALTERNATIVE_SERVICE): ( + None, + ConnectionState.CLIENT_OPEN, + ), # State: open, client side. - (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_HEADERS): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_DATA): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_GOAWAY): - (None, ConnectionState.CLOSED), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_WINDOW_UPDATE): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_PING): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_SETTINGS): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_PRIORITY): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_HEADERS): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PUSH_PROMISE): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_DATA): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_GOAWAY): - (None, ConnectionState.CLOSED), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_WINDOW_UPDATE): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PING): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_SETTINGS): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_RST_STREAM): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_RST_STREAM): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PRIORITY): - (None, ConnectionState.CLIENT_OPEN), - (ConnectionState.CLIENT_OPEN, - ConnectionInputs.RECV_ALTERNATIVE_SERVICE): - (None, ConnectionState.CLIENT_OPEN), - + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_HEADERS): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_DATA): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_GOAWAY): ( + None, + ConnectionState.CLOSED, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_WINDOW_UPDATE): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_PING): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_SETTINGS): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_PRIORITY): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_HEADERS): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PUSH_PROMISE): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_DATA): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_GOAWAY): ( + None, + ConnectionState.CLOSED, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_WINDOW_UPDATE): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PING): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_SETTINGS): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.SEND_RST_STREAM): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_RST_STREAM): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_PRIORITY): ( + None, + ConnectionState.CLIENT_OPEN, + ), + (ConnectionState.CLIENT_OPEN, ConnectionInputs.RECV_ALTERNATIVE_SERVICE): ( + None, + ConnectionState.CLIENT_OPEN, + ), # State: open, server side. - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_HEADERS): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PUSH_PROMISE): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_DATA): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_GOAWAY): - (None, ConnectionState.CLOSED), - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_WINDOW_UPDATE): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PING): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_SETTINGS): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PRIORITY): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_HEADERS): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_DATA): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_GOAWAY): - (None, ConnectionState.CLOSED), - (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_WINDOW_UPDATE): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_PING): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_SETTINGS): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_PRIORITY): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_RST_STREAM): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_RST_STREAM): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, - ConnectionInputs.SEND_ALTERNATIVE_SERVICE): - (None, ConnectionState.SERVER_OPEN), - (ConnectionState.SERVER_OPEN, - ConnectionInputs.RECV_ALTERNATIVE_SERVICE): - (None, ConnectionState.SERVER_OPEN), - + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_HEADERS): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PUSH_PROMISE): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_DATA): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_GOAWAY): ( + None, + ConnectionState.CLOSED, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_WINDOW_UPDATE): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PING): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_SETTINGS): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_PRIORITY): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_HEADERS): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_DATA): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_GOAWAY): ( + None, + ConnectionState.CLOSED, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_WINDOW_UPDATE): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_PING): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_SETTINGS): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_PRIORITY): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_RST_STREAM): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_RST_STREAM): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.SEND_ALTERNATIVE_SERVICE): ( + None, + ConnectionState.SERVER_OPEN, + ), + (ConnectionState.SERVER_OPEN, ConnectionInputs.RECV_ALTERNATIVE_SERVICE): ( + None, + ConnectionState.SERVER_OPEN, + ), # State: closed - (ConnectionState.CLOSED, ConnectionInputs.SEND_GOAWAY): - (None, ConnectionState.CLOSED), - (ConnectionState.CLOSED, ConnectionInputs.RECV_GOAWAY): - (None, ConnectionState.CLOSED), + (ConnectionState.CLOSED, ConnectionInputs.SEND_GOAWAY): ( + None, + ConnectionState.CLOSED, + ), + (ConnectionState.CLOSED, ConnectionInputs.RECV_GOAWAY): ( + None, + ConnectionState.CLOSED, + ), } def __init__(self): @@ -225,9 +348,7 @@ def process_input(self, input_): except KeyError: old_state = self.state self.state = ConnectionState.CLOSED - raise ProtocolError( - "Invalid input %s in state %s" % (input_, old_state) - ) + raise ProtocolError("Invalid input %s in state %s" % (input_, old_state)) else: self.state = target_state if func is not None: # pragma: no cover @@ -264,6 +385,7 @@ class H2Connection: :type config: :class:`H2Configuration ` """ + # The initial maximum outbound frame size. This can be changed by receiving # a settings frame. DEFAULT_MAX_OUTBOUND_FRAME_SIZE = 65535 @@ -322,17 +444,14 @@ def __init__(self, config=None): client=self.config.client_side, initial_values={ SettingCodes.MAX_CONCURRENT_STREAMS: 100, - SettingCodes.MAX_HEADER_LIST_SIZE: - self.DEFAULT_MAX_HEADER_LIST_SIZE, - } + SettingCodes.MAX_HEADER_LIST_SIZE: self.DEFAULT_MAX_HEADER_LIST_SIZE, + }, ) self.remote_settings = Settings(client=not self.config.client_side) # The current value of the connection flow control windows on the # connection. - self.outbound_flow_control_window = ( - self.remote_settings.initial_window_size - ) + self.outbound_flow_control_window = self.remote_settings.initial_window_size #: The maximum size of a frame that can be emitted by this peer, in #: bytes. @@ -358,9 +477,7 @@ def __init__(self, config=None): # Also used to determine whether we should consider a frame received # while a stream is closed as either a stream error or a connection # error. - self._closed_streams = SizeLimitDict( - size_limit=self.MAX_CLOSED_STREAMS - ) + self._closed_streams = SizeLimitDict(size_limit=self.MAX_CLOSED_STREAMS) # The flow control window manager for the connection. self._inbound_flow_control_window_manager = WindowManager( @@ -380,13 +497,13 @@ def __init__(self, config=None): GoAwayFrame: self._receive_goaway_frame, ContinuationFrame: self._receive_naked_continuation, AltSvcFrame: self._receive_alt_svc_frame, - ExtensionFrame: self._receive_unknown_frame + ExtensionFrame: self._receive_unknown_frame, } def _prepare_for_sending(self, frames): if not frames: return - self._data_to_send += b''.join(f.serialize() for f in frames) + self._data_to_send += b"".join(f.serialize() for f in frames) assert all(f.body_len <= self.max_outbound_frame_size for f in frames) def _open_streams(self, remainder): @@ -446,28 +563,25 @@ def _begin_new_stream(self, stream_id, allowed_ids): :param stream_id: The ID of the stream to open. :param allowed_ids: What kind of stream ID is allowed. """ - self.config.logger.debug( - "Attempting to initiate stream ID %d", stream_id - ) + self.config.logger.debug("Attempting to initiate stream ID %d", stream_id) outbound = self._stream_id_is_outbound(stream_id) highest_stream_id = ( - self.highest_outbound_stream_id if outbound else - self.highest_inbound_stream_id + self.highest_outbound_stream_id + if outbound + else self.highest_inbound_stream_id ) if stream_id <= highest_stream_id: raise StreamIDTooLowError(stream_id, highest_stream_id) if (stream_id % 2) != int(allowed_ids): - raise ProtocolError( - "Invalid stream ID for peer." - ) + raise ProtocolError("Invalid stream ID for peer.") s = H2Stream( stream_id, config=self.config, inbound_window_size=self.local_settings.initial_window_size, - outbound_window_size=self.remote_settings.initial_window_size + outbound_window_size=self.remote_settings.initial_window_size, ) self.config.logger.debug("Stream ID %d created", stream_id) s.max_inbound_frame_size = self.max_inbound_frame_size @@ -491,16 +605,14 @@ def initiate_connection(self): self.config.logger.debug("Initializing connection") self.state_machine.process_input(ConnectionInputs.SEND_SETTINGS) if self.config.client_side: - preamble = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + preamble = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" else: - preamble = b'' + preamble = b"" f = SettingsFrame(0) for setting, value in self.local_settings.items(): f.settings[setting] = value - self.config.logger.debug( - "Send Settings frame: %s", self.local_settings - ) + self.config.logger.debug("Send Settings frame: %s", self.local_settings) self._data_to_send += preamble + f.serialize() @@ -566,7 +678,8 @@ def initiate_upgrade_connection(self, settings_header=None): # half-closed(local) for clients, half-closed(remote) for servers. # Additionally, we need to set up the Connection state machine. connection_input = ( - ConnectionInputs.SEND_HEADERS if self.config.client_side + ConnectionInputs.SEND_HEADERS + if self.config.client_side else ConnectionInputs.RECV_HEADERS ) self.config.logger.debug("Process input %s", connection_input) @@ -605,8 +718,9 @@ def _get_stream_by_id(self, stream_id): except KeyError: outbound = self._stream_id_is_outbound(stream_id) highest_stream_id = ( - self.highest_outbound_stream_id if outbound else - self.highest_inbound_stream_id + self.highest_outbound_stream_id + if outbound + else self.highest_inbound_stream_id ) if stream_id > highest_stream_id: @@ -642,17 +756,21 @@ def get_next_available_stream_id(self): next_stream_id = 1 if self.config.client_side else 2 else: next_stream_id = self.highest_outbound_stream_id + 2 - self.config.logger.debug( - "Next available stream ID %d", next_stream_id - ) + self.config.logger.debug("Next available stream ID %d", next_stream_id) if next_stream_id > self.HIGHEST_ALLOWED_STREAM_ID: raise NoAvailableStreamIDError("Exhausted allowed stream IDs") return next_stream_id - def send_headers(self, stream_id, headers, end_stream=False, - priority_weight=None, priority_depends_on=None, - priority_exclusive=None): + def send_headers( + self, + stream_id, + headers, + end_stream=False, + priority_weight=None, + priority_depends_on=None, + priority_exclusive=None, + ): """ Send headers on a given stream. @@ -750,32 +868,28 @@ def send_headers(self, stream_id, headers, end_stream=False, :returns: Nothing """ - self.config.logger.debug( - "Send headers on stream ID %d", stream_id - ) + self.config.logger.debug("Send headers on stream ID %d", stream_id) # Check we can open the stream. if stream_id not in self.streams: max_open_streams = self.remote_settings.max_concurrent_streams if (self.open_outbound_streams + 1) > max_open_streams: raise TooManyStreamsError( - "Max outbound streams is %d, %d open" % - (max_open_streams, self.open_outbound_streams) + "Max outbound streams is %d, %d open" + % (max_open_streams, self.open_outbound_streams) ) self.state_machine.process_input(ConnectionInputs.SEND_HEADERS) stream = self._get_or_create_stream( stream_id, AllowedStreamIDs(self.config.client_side) ) - frames = stream.send_headers( - headers, self.encoder, end_stream - ) + frames = stream.send_headers(headers, self.encoder, end_stream) # We may need to send priority information. priority_present = ( - (priority_weight is not None) or - (priority_depends_on is not None) or - (priority_exclusive is not None) + (priority_weight is not None) + or (priority_depends_on is not None) + or (priority_exclusive is not None) ) if priority_present: @@ -783,12 +897,9 @@ def send_headers(self, stream_id, headers, end_stream=False, raise RFC1122Error("Servers SHOULD NOT prioritize streams.") headers_frame = frames[0] - headers_frame.flags.add('PRIORITY') + headers_frame.flags.add("PRIORITY") frames[0] = _add_frame_priority( - headers_frame, - priority_weight, - priority_depends_on, - priority_exclusive + headers_frame, priority_weight, priority_depends_on, priority_exclusive ) self._prepare_for_sending(frames) @@ -844,13 +955,13 @@ def send_data(self, stream_id, data, end_stream=False, pad_length=None): if frame_size > self.local_flow_control_window(stream_id): raise FlowControlError( - "Cannot send %d bytes, flow control window is %d." % - (frame_size, self.local_flow_control_window(stream_id)) + "Cannot send %d bytes, flow control window is %d." + % (frame_size, self.local_flow_control_window(stream_id)) ) elif frame_size > self.max_outbound_frame_size: raise FrameTooLargeError( - "Cannot send frame size %d, max frame size is %d" % - (frame_size, self.max_outbound_frame_size) + "Cannot send frame size %d, max frame size is %d" + % (frame_size, self.max_outbound_frame_size) ) self.state_machine.process_input(ConnectionInputs.SEND_DATA) @@ -862,8 +973,7 @@ def send_data(self, stream_id, data, end_stream=False, pad_length=None): self.outbound_flow_control_window -= frame_size self.config.logger.debug( - "Outbound flow control window size is %d", - self.outbound_flow_control_window + "Outbound flow control window size is %d", self.outbound_flow_control_window ) assert self.outbound_flow_control_window >= 0 @@ -903,21 +1013,18 @@ def increment_flow_control_window(self, increment, stream_id=None): """ if not (1 <= increment <= self.MAX_WINDOW_INCREMENT): raise ValueError( - "Flow control increment must be between 1 and %d" % - self.MAX_WINDOW_INCREMENT + "Flow control increment must be between 1 and %d" + % self.MAX_WINDOW_INCREMENT ) self.state_machine.process_input(ConnectionInputs.SEND_WINDOW_UPDATE) if stream_id is not None: stream = self.streams[stream_id] - frames = stream.increase_flow_control_window( - increment - ) + frames = stream.increase_flow_control_window(increment) self.config.logger.debug( - "Increase stream ID %d flow control window by %d", - stream_id, increment + "Increase stream ID %d flow control window by %d", stream_id, increment ) else: self._inbound_flow_control_window_manager.window_opened(increment) @@ -953,9 +1060,7 @@ def push_stream(self, stream_id, promised_stream_id, request_headers): :class:`HeaderTuple ` objects. :returns: Nothing """ - self.config.logger.debug( - "Send Push Promise frame on stream ID %d", stream_id - ) + self.config.logger.debug("Send Push Promise frame on stream ID %d", stream_id) if not self.remote_settings.enable_push: raise ProtocolError("Remote peer has disabled stream push") @@ -971,11 +1076,10 @@ def push_stream(self, stream_id, promised_stream_id, request_headers): if (stream_id % 2) == 0: raise ProtocolError("Cannot recursively push streams.") - new_stream = self._begin_new_stream( - promised_stream_id, AllowedStreamIDs.EVEN - ) + new_stream = self._begin_new_stream(promised_stream_id, AllowedStreamIDs.EVEN) self.streams[promised_stream_id] = new_stream + request_headers = utf8_encode_headers(request_headers) frames = stream.push_stream_in_band( promised_stream_id, request_headers, self.encoder ) @@ -1023,9 +1127,7 @@ def reset_stream(self, stream_id, error_code=0): frames = stream.reset_stream(error_code) self._prepare_for_sending(frames) - def close_connection(self, error_code=0, additional_data=None, - last_stream_id=None): - + def close_connection(self, error_code=0, additional_data=None, last_stream_id=None): """ Close a connection, emitting a GOAWAY frame. @@ -1054,7 +1156,7 @@ def close_connection(self, error_code=0, additional_data=None, stream_id=0, last_stream_id=last_stream_id, error_code=error_code, - additional_data=(additional_data or b'') + additional_data=(additional_data or b""), ) self._prepare_for_sending([f]) @@ -1065,19 +1167,14 @@ def update_settings(self, new_settings): :param new_settings: A dictionary of {setting: new value} """ - self.config.logger.debug( - "Update connection settings to %s", new_settings - ) + self.config.logger.debug("Update connection settings to %s", new_settings) self.state_machine.process_input(ConnectionInputs.SEND_SETTINGS) self.local_settings.update(new_settings) s = SettingsFrame(0) s.settings = new_settings self._prepare_for_sending([s]) - def advertise_alternative_service(self, - field_value, - origin=None, - stream_id=None): + def advertise_alternative_service(self, field_value, origin=None, stream_id=None): """ Notify a client about an available Alternative Service. @@ -1137,9 +1234,7 @@ def advertise_alternative_service(self, if origin is not None and stream_id is not None: raise ValueError("Must not provide both origin and stream_id") - self.state_machine.process_input( - ConnectionInputs.SEND_ALTERNATIVE_SERVICE - ) + self.state_machine.process_input(ConnectionInputs.SEND_ALTERNATIVE_SERVICE) if origin is not None: # This ALTSVC is sent on stream zero. @@ -1153,8 +1248,7 @@ def advertise_alternative_service(self, self._prepare_for_sending(frames) - def prioritize(self, stream_id, weight=None, depends_on=None, - exclusive=None): + def prioritize(self, stream_id, weight=None, depends_on=None, exclusive=None): """ Notify a server about the priority of a stream. @@ -1220,9 +1314,7 @@ def prioritize(self, stream_id, weight=None, depends_on=None, if not self.config.client_side: raise RFC1122Error("Servers SHOULD NOT prioritize streams.") - self.state_machine.process_input( - ConnectionInputs.SEND_PRIORITY - ) + self.state_machine.process_input(ConnectionInputs.SEND_PRIORITY) frame = PriorityFrame(stream_id) frame = _add_frame_priority(frame, weight, depends_on, exclusive) @@ -1252,8 +1344,7 @@ def local_flow_control_window(self, stream_id): """ stream = self._get_stream_by_id(stream_id) return min( - self.outbound_flow_control_window, - stream.outbound_flow_control_window + self.outbound_flow_control_window, stream.outbound_flow_control_window ) def remote_flow_control_window(self, stream_id): @@ -1278,10 +1369,7 @@ def remote_flow_control_window(self, stream_id): :rtype: ``int`` """ stream = self._get_stream_by_id(stream_id) - return min( - self.inbound_flow_control_window, - stream.inbound_flow_control_window - ) + return min(self.inbound_flow_control_window, stream.inbound_flow_control_window) def acknowledge_received_data(self, acknowledged_size, stream_id): """ @@ -1303,12 +1391,12 @@ def acknowledge_received_data(self, acknowledged_size, stream_id): """ self.config.logger.debug( "Ack received data on stream ID %d with size %d", - stream_id, acknowledged_size + stream_id, + acknowledged_size, ) if stream_id <= 0: raise ValueError( - "Stream ID %d is not valid for acknowledge_received_data" % - stream_id + "Stream ID %d is not valid for acknowledge_received_data" % stream_id ) if acknowledged_size < 0: raise ValueError("Cannot acknowledge negative data") @@ -1331,9 +1419,7 @@ def acknowledge_received_data(self, acknowledged_size, stream_id): else: # No point incrementing the windows of closed streams. if stream.open: - frames.extend( - stream.acknowledge_received_data(acknowledged_size) - ) + frames.extend(stream.acknowledge_received_data(acknowledged_size)) self._prepare_for_sending(frames) @@ -1407,7 +1493,7 @@ def _acknowledge_settings(self): stream.max_outbound_frame_size = setting.new_value f = SettingsFrame(0) - f.flags.add('ACK') + f.flags.add("ACK") return [f] def _flow_control_change_from_settings(self, old_value, new_value): @@ -1424,8 +1510,7 @@ def _flow_control_change_from_settings(self, old_value, new_value): for stream in self.streams.values(): stream.outbound_flow_control_window = guard_increment_window( - stream.outbound_flow_control_window, - delta + stream.outbound_flow_control_window, delta ) def _inbound_flow_control_change_from_settings(self, old_value, new_value): @@ -1543,8 +1628,8 @@ def _receive_headers_frame(self, frame): max_open_streams = self.local_settings.max_concurrent_streams if (self.open_inbound_streams + 1) > max_open_streams: raise TooManyStreamsError( - "Max outbound streams is %d, %d open" % - (max_open_streams, self.open_outbound_streams) + "Max outbound streams is %d, %d open" + % (max_open_streams, self.open_outbound_streams) ) # Let's decode the headers. We handle headers as bytes internally up @@ -1552,19 +1637,15 @@ def _receive_headers_frame(self, frame): # convert them to unicode. headers = _decode_headers(self.decoder, frame.data) - events = self.state_machine.process_input( - ConnectionInputs.RECV_HEADERS - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_HEADERS) stream = self._get_or_create_stream( frame.stream_id, AllowedStreamIDs(not self.config.client_side) ) frames, stream_events = stream.receive_headers( - headers, - 'END_STREAM' in frame.flags, - self.config.header_encoding + headers, "END_STREAM" in frame.flags, self.config.header_encoding ) - if 'PRIORITY' in frame.flags: + if "PRIORITY" in frame.flags: p_frames, p_events = self._receive_priority_frame(frame) stream_events[0].priority_updated = p_events[0] stream_events.extend(p_events) @@ -1581,9 +1662,7 @@ def _receive_push_promise_frame(self, frame): pushed_headers = _decode_headers(self.decoder, frame.data) - events = self.state_machine.process_input( - ConnectionInputs.RECV_PUSH_PROMISE - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_PUSH_PROMISE) try: stream = self._get_stream_by_id(frame.stream_id) @@ -1597,8 +1676,10 @@ def _receive_push_promise_frame(self, frame): # PROTOCOL_ERROR: pushing a stream on a naturally closed stream is # a real problem because it creates a brand new stream that the # remote peer now believes exists. - if (self._stream_closed_by(frame.stream_id) == - StreamClosedBy.SEND_RST_STREAM): + if ( + self._stream_closed_by(frame.stream_id) + == StreamClosedBy.SEND_RST_STREAM + ): f = RstStreamFrame(frame.promised_stream_id) f.error_code = ErrorCodes.REFUSED_STREAM return [f], events @@ -1643,9 +1724,7 @@ def _handle_data_on_closed_stream(self, events, exc, frame): # the inbound flow window being 0. frames = [] conn_manager = self._inbound_flow_control_window_manager - conn_increment = conn_manager.process_bytes( - frame.flow_controlled_length - ) + conn_increment = conn_manager.process_bytes(frame.flow_controlled_length) if conn_increment: f = WindowUpdateFrame(0) f.window_increment = conn_increment @@ -1653,7 +1732,8 @@ def _handle_data_on_closed_stream(self, events, exc, frame): self.config.logger.debug( "Received DATA frame on closed stream %d - " "auto-emitted a WINDOW_UPDATE by %d", - frame.stream_id, conn_increment + frame.stream_id, + conn_increment, ) f = RstStreamFrame(exc.stream_id) f.error_code = exc.error_code @@ -1670,9 +1750,7 @@ def _receive_data_frame(self, frame): """ flow_controlled_length = frame.flow_controlled_length - events = self.state_machine.process_input( - ConnectionInputs.RECV_DATA - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_DATA) self._inbound_flow_control_window_manager.window_consumed( flow_controlled_length ) @@ -1680,9 +1758,7 @@ def _receive_data_frame(self, frame): try: stream = self._get_stream_by_id(frame.stream_id) frames, stream_events = stream.receive_data( - frame.data, - 'END_STREAM' in frame.flags, - flow_controlled_length + frame.data, "END_STREAM" in frame.flags, flow_controlled_length ) except StreamClosedError as e: # This stream is either marked as CLOSED or already gone from our @@ -1695,12 +1771,10 @@ def _receive_settings_frame(self, frame): """ Receive a SETTINGS frame on the connection. """ - events = self.state_machine.process_input( - ConnectionInputs.RECV_SETTINGS - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_SETTINGS) # This is an ack of the local settings. - if 'ACK' in frame.flags: + if "ACK" in frame.flags: changed_settings = self._local_settings_acked() ack_event = SettingsAcknowledged() ack_event.changed_settings = changed_settings @@ -1710,9 +1784,7 @@ def _receive_settings_frame(self, frame): # Add the new settings. self.remote_settings.update(frame.settings) events.append( - RemoteSettingsChanged.from_settings( - self.remote_settings, frame.settings - ) + RemoteSettingsChanged.from_settings(self.remote_settings, frame.settings) ) frames = self._acknowledge_settings() @@ -1725,9 +1797,7 @@ def _receive_window_update_frame(self, frame): # hyperframe will take care of validating the window_increment. # If we reach in here, we can assume a valid value. - events = self.state_machine.process_input( - ConnectionInputs.RECV_WINDOW_UPDATE - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_WINDOW_UPDATE) if frame.stream_id: try: @@ -1740,8 +1810,7 @@ def _receive_window_update_frame(self, frame): else: # Increment our local flow control window. self.outbound_flow_control_window = guard_increment_window( - self.outbound_flow_control_window, - frame.window_increment + self.outbound_flow_control_window, frame.window_increment ) # FIXME: Should we split this into one event per active stream? @@ -1757,19 +1826,17 @@ def _receive_ping_frame(self, frame): """ Receive a PING frame on the connection. """ - events = self.state_machine.process_input( - ConnectionInputs.RECV_PING - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_PING) flags = [] - if 'ACK' in frame.flags: + if "ACK" in frame.flags: evt = PingAckReceived() else: evt = PingReceived() # automatically ACK the PING with the same 'opaque data' f = PingFrame(0) - f.flags = {'ACK'} + f.flags = {"ACK"} f.opaque_data = frame.opaque_data flags.append(f) @@ -1782,9 +1849,7 @@ def _receive_rst_stream_frame(self, frame): """ Receive a RST_STREAM frame on the connection. """ - events = self.state_machine.process_input( - ConnectionInputs.RECV_RST_STREAM - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_RST_STREAM) try: stream = self._get_stream_by_id(frame.stream_id) except NoSuchStreamError: @@ -1800,9 +1865,7 @@ def _receive_priority_frame(self, frame): """ Receive a PRIORITY frame on the connection. """ - events = self.state_machine.process_input( - ConnectionInputs.RECV_PRIORITY - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_PRIORITY) event = PriorityUpdated() event.stream_id = frame.stream_id @@ -1815,9 +1878,7 @@ def _receive_priority_frame(self, frame): # A stream may not depend on itself. if event.depends_on == frame.stream_id: - raise ProtocolError( - "Stream %d may not depend on itself" % frame.stream_id - ) + raise ProtocolError("Stream %d may not depend on itself" % frame.stream_id) events.append(event) return [], events @@ -1826,9 +1887,7 @@ def _receive_goaway_frame(self, frame): """ Receive a GOAWAY frame on the connection. """ - events = self.state_machine.process_input( - ConnectionInputs.RECV_GOAWAY - ) + events = self.state_machine.process_input(ConnectionInputs.RECV_GOAWAY) # Clear the outbound data buffer: we cannot send further data now. self.clear_outbound_data_buffer() @@ -1837,8 +1896,9 @@ def _receive_goaway_frame(self, frame): new_event = ConnectionTerminated() new_event.error_code = _error_code_from_int(frame.error_code) new_event.last_stream_id = frame.last_stream_id - new_event.additional_data = (frame.additional_data - if frame.additional_data else None) + new_event.additional_data = ( + frame.additional_data if frame.additional_data else None + ) events.append(new_event) return [], events @@ -1950,7 +2010,7 @@ def _stream_id_is_outbound(self, stream_id): Returns ``True`` if the stream ID corresponds to an outbound stream (one initiated by this peer), returns ``False`` otherwise. """ - return (stream_id % 2 == int(self.config.client_side)) + return stream_id % 2 == int(self.config.client_side) def _stream_closed_by(self, stream_id): """ @@ -1973,7 +2033,8 @@ def _stream_is_closed_by_reset(self, stream_id): RST_STREAM frame. Returns ``False`` otherwise. """ return self._stream_closed_by(stream_id) in ( - StreamClosedBy.RECV_RST_STREAM, StreamClosedBy.SEND_RST_STREAM + StreamClosedBy.RECV_RST_STREAM, + StreamClosedBy.SEND_RST_STREAM, ) def _stream_is_closed_by_end(self, stream_id): @@ -1983,7 +2044,8 @@ def _stream_is_closed_by_end(self, stream_id): otherwise. """ return self._stream_closed_by(stream_id) in ( - StreamClosedBy.RECV_END_STREAM, StreamClosedBy.SEND_END_STREAM + StreamClosedBy.RECV_END_STREAM, + StreamClosedBy.SEND_END_STREAM, ) @@ -1999,16 +2061,12 @@ def _add_frame_priority(frame, weight=None, depends_on=None, exclusive=None): """ # A stream may not depend on itself. if depends_on == frame.stream_id: - raise ProtocolError( - "Stream %d may not depend on itself" % frame.stream_id - ) + raise ProtocolError("Stream %d may not depend on itself" % frame.stream_id) # Weight must be between 1 and 256. if weight is not None: if weight > 256 or weight < 1: - raise ProtocolError( - "Weight must be between 1 and 256, not %d" % weight - ) + raise ProtocolError("Weight must be between 1 and 256, not %d" % weight) else: # Weight is an integer between 1 and 256, but the byte only allows # 0 to 255: subtract one. diff --git a/src/h2/events.py b/src/h2/events.py index 66c3cff4a..fe635f172 100644 --- a/src/h2/events.py +++ b/src/h2/events.py @@ -18,6 +18,7 @@ class Event: """ Base class for h2 events. """ + pass @@ -34,6 +35,7 @@ class RequestReceived(Event): .. versionchanged:: 2.4.0 Added ``stream_ended`` and ``priority_updated`` properties. """ + def __init__(self): #: The Stream ID for the stream this request was made on. self.stream_id = None @@ -57,23 +59,25 @@ def __init__(self): def __repr__(self): return "" % ( - self.stream_id, self.headers + self.stream_id, + self.headers, ) class ResponseReceived(Event): """ - The ResponseReceived event is fired whenever response headers are received. - This event carries the HTTP headers for the given response and the stream - ID of the new stream. + The ResponseReceived event is fired whenever response headers are received. + This event carries the HTTP headers for the given response and the stream + ID of the new stream. - .. versionchanged:: 2.3.0 - Changed the type of ``headers`` to :class:`HeaderTuple - `. This has no effect on current users. + .. versionchanged:: 2.3.0 + Changed the type of ``headers`` to :class:`HeaderTuple + `. This has no effect on current users. - .. versionchanged:: 2.4.0 - Added ``stream_ended`` and ``priority_updated`` properties. + .. versionchanged:: 2.4.0 + Added ``stream_ended`` and ``priority_updated`` properties. """ + def __init__(self): #: The Stream ID for the stream this response was made on. self.stream_id = None @@ -97,7 +101,8 @@ def __init__(self): def __repr__(self): return "" % ( - self.stream_id, self.headers + self.stream_id, + self.headers, ) @@ -117,6 +122,7 @@ class TrailersReceived(Event): .. versionchanged:: 2.4.0 Added ``stream_ended`` and ``priority_updated`` properties. """ + def __init__(self): #: The Stream ID for the stream on which these trailers were received. self.stream_id = None @@ -139,7 +145,8 @@ def __init__(self): def __repr__(self): return "" % ( - self.stream_id, self.headers + self.stream_id, + self.headers, ) @@ -150,6 +157,7 @@ class _HeadersSent(Event): This is an internal event, used to determine validation steps on outgoing header blocks. """ + pass @@ -161,6 +169,7 @@ class _ResponseSent(_HeadersSent): This is an internal event, used to determine validation steps on outgoing header blocks. """ + pass @@ -172,6 +181,7 @@ class _RequestSent(_HeadersSent): This is an internal event, used to determine validation steps on outgoing header blocks. """ + pass @@ -185,6 +195,7 @@ class _TrailersSent(_HeadersSent): This is an internal event, used to determine validation steps on outgoing header blocks. """ + pass @@ -196,6 +207,7 @@ class _PushedRequestSent(_HeadersSent): This is an internal event, used to determine validation steps on outgoing header blocks. """ + pass @@ -221,6 +233,7 @@ class InformationalResponseReceived(Event): .. versionchanged:: 2.4.0 Added ``priority_updated`` property. """ + def __init__(self): #: The Stream ID for the stream this informational response was made #: on. @@ -238,7 +251,8 @@ def __init__(self): def __repr__(self): return "" % ( - self.stream_id, self.headers + self.stream_id, + self.headers, ) @@ -251,6 +265,7 @@ class DataReceived(Event): .. versionchanged:: 2.4.0 Added ``stream_ended`` property. """ + def __init__(self): #: The Stream ID for the stream this data was received on. self.stream_id = None @@ -275,7 +290,8 @@ def __repr__(self): return ( "" % ( + "data:%s>" + % ( self.stream_id, self.flow_controlled_length, _bytes_representation(self.data[:20]), @@ -291,6 +307,7 @@ class WindowUpdated(Event): the stream to which it applies (set to zero if the window update applies to the connection), and the delta in the window size. """ + def __init__(self): #: The Stream ID of the stream whose flow control window was changed. #: May be ``0`` if the connection window was changed. @@ -300,9 +317,7 @@ def __init__(self): self.delta = None def __repr__(self): - return "" % ( - self.stream_id, self.delta - ) + return "" % (self.stream_id, self.delta) class RemoteSettingsChanged(Event): @@ -325,6 +340,7 @@ class RemoteSettingsChanged(Event): This is no longer the case: h2 now automatically acknowledges them. """ + def __init__(self): #: A dictionary of setting byte to #: :class:`ChangedSetting `, representing @@ -364,14 +380,13 @@ class PingReceived(Event): .. versionadded:: 3.1.0 """ + def __init__(self): #: The data included on the ping. self.ping_data = None def __repr__(self): - return "" % ( - _bytes_representation(self.ping_data), - ) + return "" % (_bytes_representation(self.ping_data),) class PingAckReceived(Event): @@ -385,6 +400,7 @@ class PingAckReceived(Event): .. versionchanged:: 4.0.0 Removed deprecated but equivalent ``PingAcknowledged``. """ + def __init__(self): #: The data included on the ping. self.ping_data = None @@ -401,6 +417,7 @@ class StreamEnded(Event): party. The stream may not be fully closed if it has not been closed locally, but no further data or headers should be expected on that stream. """ + def __init__(self): #: The Stream ID of the stream that was closed. self.stream_id = None @@ -419,6 +436,7 @@ class StreamReset(Event): .. versionchanged:: 2.0.0 This event is now fired when h2 automatically resets a stream. """ + def __init__(self): #: The Stream ID of the stream that was reset. self.stream_id = None @@ -432,7 +450,9 @@ def __init__(self): def __repr__(self): return "" % ( - self.stream_id, self.error_code, self.remote_reset + self.stream_id, + self.error_code, + self.remote_reset, ) @@ -442,6 +462,7 @@ class PushedStreamReceived(Event): received from a remote peer. The event carries on it the new stream ID, the ID of the parent stream, and the request headers pushed by the remote peer. """ + def __init__(self): #: The Stream ID of the stream created by the push. self.pushed_stream_id = None @@ -455,7 +476,8 @@ def __init__(self): def __repr__(self): return ( "" % ( + "headers:%s>" + % ( self.pushed_stream_id, self.parent_stream_id, self.headers, @@ -470,6 +492,7 @@ class SettingsAcknowledged(Event): acknowedged, in the same format as :class:`h2.events.RemoteSettingsChanged`. """ + def __init__(self): #: A dictionary of setting byte to #: :class:`ChangedSetting `, representing @@ -492,6 +515,7 @@ class PriorityUpdated(Event): .. versionadded:: 2.0.0 """ + def __init__(self): #: The ID of the stream whose priority information is being updated. self.stream_id = None @@ -511,12 +535,8 @@ def __init__(self): def __repr__(self): return ( "" % ( - self.stream_id, - self.weight, - self.depends_on, - self.exclusive - ) + "exclusive:%s>" + % (self.stream_id, self.weight, self.depends_on, self.exclusive) ) @@ -526,6 +546,7 @@ class ConnectionTerminated(Event): the remote peer using a GOAWAY frame. Once received, no further action may be taken on the connection: a new connection must be established. """ + def __init__(self): #: The error code cited when tearing down the connection. Should be #: one of :class:`ErrorCodes `, but may not be if @@ -543,12 +564,13 @@ def __init__(self): def __repr__(self): return ( "" % ( + "additional_data:%s>" + % ( self.error_code, self.last_stream_id, _bytes_representation( - self.additional_data[:20] - if self.additional_data else None) + self.additional_data[:20] if self.additional_data else None + ), ) ) @@ -573,6 +595,7 @@ class AlternativeServiceAvailable(Event): .. versionadded:: 2.3.0 """ + def __init__(self): #: The origin to which the alternative service field value applies. #: This field is either supplied by the server directly, or inferred by @@ -589,11 +612,9 @@ def __init__(self): self.field_value = None def __repr__(self): - return ( - "" % ( - self.origin.decode('utf-8', 'ignore'), - self.field_value.decode('utf-8', 'ignore'), - ) + return "" % ( + self.origin.decode("utf-8", "replace"), + self.field_value.decode("utf-8", "replace"), ) @@ -611,6 +632,7 @@ class UnknownFrameReceived(Event): .. versionadded:: 2.7.0 """ + def __init__(self): #: The hyperframe Frame object that encapsulates the received frame. self.frame = None @@ -631,4 +653,4 @@ def _bytes_representation(data): if data is None: return None - return binascii.hexlify(data).decode('ascii') + return binascii.hexlify(data).decode("ascii") diff --git a/src/h2/stream.py b/src/h2/stream.py index 1c34dcd3e..fd142921c 100644 --- a/src/h2/stream.py +++ b/src/h2/stream.py @@ -8,24 +8,49 @@ from enum import Enum, IntEnum from hpack import HeaderTuple from hyperframe.frame import ( - HeadersFrame, ContinuationFrame, DataFrame, WindowUpdateFrame, - RstStreamFrame, PushPromiseFrame, AltSvcFrame + HeadersFrame, + ContinuationFrame, + DataFrame, + WindowUpdateFrame, + RstStreamFrame, + PushPromiseFrame, + AltSvcFrame, ) from .errors import ErrorCodes, _error_code_from_int from .events import ( - RequestReceived, ResponseReceived, DataReceived, WindowUpdated, - StreamEnded, PushedStreamReceived, StreamReset, TrailersReceived, - InformationalResponseReceived, AlternativeServiceAvailable, - _ResponseSent, _RequestSent, _TrailersSent, _PushedRequestSent + RequestReceived, + ResponseReceived, + DataReceived, + WindowUpdated, + StreamEnded, + PushedStreamReceived, + StreamReset, + TrailersReceived, + InformationalResponseReceived, + AlternativeServiceAvailable, + _ResponseSent, + _RequestSent, + _TrailersSent, + _PushedRequestSent, ) from .exceptions import ( - ProtocolError, StreamClosedError, InvalidBodyLengthError, FlowControlError + ProtocolError, + StreamClosedError, + InvalidBodyLengthError, + FlowControlError, ) from .utilities import ( - guard_increment_window, is_informational_response, authority_from_headers, - validate_headers, validate_outbound_headers, normalize_outbound_headers, - HeaderValidationFlags, extract_method_header, normalize_inbound_headers + guard_increment_window, + is_informational_response, + authority_from_headers, + utf8_encode_headers, + validate_headers, + validate_outbound_headers, + normalize_outbound_headers, + HeaderValidationFlags, + extract_method_header, + normalize_inbound_headers, ) from .windows import WindowManager @@ -90,6 +115,7 @@ class H2StreamStateMachine: :param stream_id: The stream ID of this stream. This is stored primarily for logging purposes. """ + def __init__(self, stream_id): self.state = StreamState.IDLE self.stream_id = stream_id @@ -118,9 +144,7 @@ def process_input(self, input_): except KeyError: old_state = self.state self.state = StreamState.CLOSED - raise ProtocolError( - "Invalid input %s in state %s" % (input_, old_state) - ) + raise ProtocolError("Invalid input %s in state %s" % (input_, old_state)) else: previous_state = self.state self.state = target_state @@ -459,9 +483,7 @@ def send_alt_svc(self, previous_state): # We should not send ALTSVC after we've sent response headers, as the # client may have disposed of its state. if self.headers_sent: - raise ProtocolError( - "Cannot send ALTSVC after sending response headers." - ) + raise ProtocolError("Cannot send ALTSVC after sending response headers.") return @@ -537,161 +559,264 @@ def send_alt_svc(self, previous_state): # invalid and immediately causes a transition to ``closed``. _transitions = { # State: idle - (StreamState.IDLE, StreamInputs.SEND_HEADERS): - (H2StreamStateMachine.request_sent, StreamState.OPEN), - (StreamState.IDLE, StreamInputs.RECV_HEADERS): - (H2StreamStateMachine.request_received, StreamState.OPEN), - (StreamState.IDLE, StreamInputs.RECV_DATA): - (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), - (StreamState.IDLE, StreamInputs.SEND_PUSH_PROMISE): - (H2StreamStateMachine.send_new_pushed_stream, - StreamState.RESERVED_LOCAL), - (StreamState.IDLE, StreamInputs.RECV_PUSH_PROMISE): - (H2StreamStateMachine.recv_new_pushed_stream, - StreamState.RESERVED_REMOTE), - (StreamState.IDLE, StreamInputs.RECV_ALTERNATIVE_SERVICE): - (None, StreamState.IDLE), - (StreamState.IDLE, StreamInputs.UPGRADE_CLIENT): - (H2StreamStateMachine.request_sent, StreamState.HALF_CLOSED_LOCAL), - (StreamState.IDLE, StreamInputs.UPGRADE_SERVER): - (H2StreamStateMachine.request_received, - StreamState.HALF_CLOSED_REMOTE), - + (StreamState.IDLE, StreamInputs.SEND_HEADERS): ( + H2StreamStateMachine.request_sent, + StreamState.OPEN, + ), + (StreamState.IDLE, StreamInputs.RECV_HEADERS): ( + H2StreamStateMachine.request_received, + StreamState.OPEN, + ), + (StreamState.IDLE, StreamInputs.RECV_DATA): ( + H2StreamStateMachine.reset_stream_on_error, + StreamState.CLOSED, + ), + (StreamState.IDLE, StreamInputs.SEND_PUSH_PROMISE): ( + H2StreamStateMachine.send_new_pushed_stream, + StreamState.RESERVED_LOCAL, + ), + (StreamState.IDLE, StreamInputs.RECV_PUSH_PROMISE): ( + H2StreamStateMachine.recv_new_pushed_stream, + StreamState.RESERVED_REMOTE, + ), + (StreamState.IDLE, StreamInputs.RECV_ALTERNATIVE_SERVICE): (None, StreamState.IDLE), + (StreamState.IDLE, StreamInputs.UPGRADE_CLIENT): ( + H2StreamStateMachine.request_sent, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.IDLE, StreamInputs.UPGRADE_SERVER): ( + H2StreamStateMachine.request_received, + StreamState.HALF_CLOSED_REMOTE, + ), # State: reserved local - (StreamState.RESERVED_LOCAL, StreamInputs.SEND_HEADERS): - (H2StreamStateMachine.response_sent, StreamState.HALF_CLOSED_REMOTE), - (StreamState.RESERVED_LOCAL, StreamInputs.RECV_DATA): - (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), - (StreamState.RESERVED_LOCAL, StreamInputs.SEND_WINDOW_UPDATE): - (None, StreamState.RESERVED_LOCAL), - (StreamState.RESERVED_LOCAL, StreamInputs.RECV_WINDOW_UPDATE): - (H2StreamStateMachine.window_updated, StreamState.RESERVED_LOCAL), - (StreamState.RESERVED_LOCAL, StreamInputs.SEND_RST_STREAM): - (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), - (StreamState.RESERVED_LOCAL, StreamInputs.RECV_RST_STREAM): - (H2StreamStateMachine.stream_reset, StreamState.CLOSED), - (StreamState.RESERVED_LOCAL, StreamInputs.SEND_ALTERNATIVE_SERVICE): - (H2StreamStateMachine.send_alt_svc, StreamState.RESERVED_LOCAL), - (StreamState.RESERVED_LOCAL, StreamInputs.RECV_ALTERNATIVE_SERVICE): - (None, StreamState.RESERVED_LOCAL), - + (StreamState.RESERVED_LOCAL, StreamInputs.SEND_HEADERS): ( + H2StreamStateMachine.response_sent, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.RESERVED_LOCAL, StreamInputs.RECV_DATA): ( + H2StreamStateMachine.reset_stream_on_error, + StreamState.CLOSED, + ), + (StreamState.RESERVED_LOCAL, StreamInputs.SEND_WINDOW_UPDATE): ( + None, + StreamState.RESERVED_LOCAL, + ), + (StreamState.RESERVED_LOCAL, StreamInputs.RECV_WINDOW_UPDATE): ( + H2StreamStateMachine.window_updated, + StreamState.RESERVED_LOCAL, + ), + (StreamState.RESERVED_LOCAL, StreamInputs.SEND_RST_STREAM): ( + H2StreamStateMachine.send_reset_stream, + StreamState.CLOSED, + ), + (StreamState.RESERVED_LOCAL, StreamInputs.RECV_RST_STREAM): ( + H2StreamStateMachine.stream_reset, + StreamState.CLOSED, + ), + (StreamState.RESERVED_LOCAL, StreamInputs.SEND_ALTERNATIVE_SERVICE): ( + H2StreamStateMachine.send_alt_svc, + StreamState.RESERVED_LOCAL, + ), + (StreamState.RESERVED_LOCAL, StreamInputs.RECV_ALTERNATIVE_SERVICE): ( + None, + StreamState.RESERVED_LOCAL, + ), # State: reserved remote - (StreamState.RESERVED_REMOTE, StreamInputs.RECV_HEADERS): - (H2StreamStateMachine.response_received, - StreamState.HALF_CLOSED_LOCAL), - (StreamState.RESERVED_REMOTE, StreamInputs.RECV_DATA): - (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), - (StreamState.RESERVED_REMOTE, StreamInputs.SEND_WINDOW_UPDATE): - (None, StreamState.RESERVED_REMOTE), - (StreamState.RESERVED_REMOTE, StreamInputs.RECV_WINDOW_UPDATE): - (H2StreamStateMachine.window_updated, StreamState.RESERVED_REMOTE), - (StreamState.RESERVED_REMOTE, StreamInputs.SEND_RST_STREAM): - (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), - (StreamState.RESERVED_REMOTE, StreamInputs.RECV_RST_STREAM): - (H2StreamStateMachine.stream_reset, StreamState.CLOSED), - (StreamState.RESERVED_REMOTE, StreamInputs.RECV_ALTERNATIVE_SERVICE): - (H2StreamStateMachine.recv_alt_svc, StreamState.RESERVED_REMOTE), - + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_HEADERS): ( + H2StreamStateMachine.response_received, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_DATA): ( + H2StreamStateMachine.reset_stream_on_error, + StreamState.CLOSED, + ), + (StreamState.RESERVED_REMOTE, StreamInputs.SEND_WINDOW_UPDATE): ( + None, + StreamState.RESERVED_REMOTE, + ), + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_WINDOW_UPDATE): ( + H2StreamStateMachine.window_updated, + StreamState.RESERVED_REMOTE, + ), + (StreamState.RESERVED_REMOTE, StreamInputs.SEND_RST_STREAM): ( + H2StreamStateMachine.send_reset_stream, + StreamState.CLOSED, + ), + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_RST_STREAM): ( + H2StreamStateMachine.stream_reset, + StreamState.CLOSED, + ), + (StreamState.RESERVED_REMOTE, StreamInputs.RECV_ALTERNATIVE_SERVICE): ( + H2StreamStateMachine.recv_alt_svc, + StreamState.RESERVED_REMOTE, + ), # State: open - (StreamState.OPEN, StreamInputs.SEND_HEADERS): - (H2StreamStateMachine.response_sent, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.RECV_HEADERS): - (H2StreamStateMachine.response_received, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.SEND_DATA): - (None, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.RECV_DATA): - (H2StreamStateMachine.data_received, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.SEND_END_STREAM): - (None, StreamState.HALF_CLOSED_LOCAL), - (StreamState.OPEN, StreamInputs.RECV_END_STREAM): - (H2StreamStateMachine.stream_half_closed, - StreamState.HALF_CLOSED_REMOTE), - (StreamState.OPEN, StreamInputs.SEND_WINDOW_UPDATE): - (None, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.RECV_WINDOW_UPDATE): - (H2StreamStateMachine.window_updated, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.SEND_RST_STREAM): - (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), - (StreamState.OPEN, StreamInputs.RECV_RST_STREAM): - (H2StreamStateMachine.stream_reset, StreamState.CLOSED), - (StreamState.OPEN, StreamInputs.SEND_PUSH_PROMISE): - (H2StreamStateMachine.send_push_promise, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.RECV_PUSH_PROMISE): - (H2StreamStateMachine.recv_push_promise, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.SEND_INFORMATIONAL_HEADERS): - (H2StreamStateMachine.send_informational_response, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.RECV_INFORMATIONAL_HEADERS): - (H2StreamStateMachine.recv_informational_response, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.SEND_ALTERNATIVE_SERVICE): - (H2StreamStateMachine.send_alt_svc, StreamState.OPEN), - (StreamState.OPEN, StreamInputs.RECV_ALTERNATIVE_SERVICE): - (H2StreamStateMachine.recv_alt_svc, StreamState.OPEN), - + (StreamState.OPEN, StreamInputs.SEND_HEADERS): ( + H2StreamStateMachine.response_sent, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.RECV_HEADERS): ( + H2StreamStateMachine.response_received, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.SEND_DATA): (None, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.RECV_DATA): ( + H2StreamStateMachine.data_received, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.SEND_END_STREAM): ( + None, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.OPEN, StreamInputs.RECV_END_STREAM): ( + H2StreamStateMachine.stream_half_closed, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.OPEN, StreamInputs.SEND_WINDOW_UPDATE): (None, StreamState.OPEN), + (StreamState.OPEN, StreamInputs.RECV_WINDOW_UPDATE): ( + H2StreamStateMachine.window_updated, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.SEND_RST_STREAM): ( + H2StreamStateMachine.send_reset_stream, + StreamState.CLOSED, + ), + (StreamState.OPEN, StreamInputs.RECV_RST_STREAM): ( + H2StreamStateMachine.stream_reset, + StreamState.CLOSED, + ), + (StreamState.OPEN, StreamInputs.SEND_PUSH_PROMISE): ( + H2StreamStateMachine.send_push_promise, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.RECV_PUSH_PROMISE): ( + H2StreamStateMachine.recv_push_promise, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.SEND_INFORMATIONAL_HEADERS): ( + H2StreamStateMachine.send_informational_response, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.RECV_INFORMATIONAL_HEADERS): ( + H2StreamStateMachine.recv_informational_response, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.SEND_ALTERNATIVE_SERVICE): ( + H2StreamStateMachine.send_alt_svc, + StreamState.OPEN, + ), + (StreamState.OPEN, StreamInputs.RECV_ALTERNATIVE_SERVICE): ( + H2StreamStateMachine.recv_alt_svc, + StreamState.OPEN, + ), # State: half-closed remote - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_HEADERS): - (H2StreamStateMachine.response_sent, StreamState.HALF_CLOSED_REMOTE), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_HEADERS): - (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_DATA): - (None, StreamState.HALF_CLOSED_REMOTE), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_DATA): - (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_END_STREAM): - (H2StreamStateMachine.send_end_stream, StreamState.CLOSED), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_WINDOW_UPDATE): - (None, StreamState.HALF_CLOSED_REMOTE), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_WINDOW_UPDATE): - (H2StreamStateMachine.window_updated, StreamState.HALF_CLOSED_REMOTE), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_RST_STREAM): - (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_RST_STREAM): - (H2StreamStateMachine.stream_reset, StreamState.CLOSED), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_PUSH_PROMISE): - (H2StreamStateMachine.send_push_promise, - StreamState.HALF_CLOSED_REMOTE), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_PUSH_PROMISE): - (H2StreamStateMachine.reset_stream_on_error, StreamState.CLOSED), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_INFORMATIONAL_HEADERS): - (H2StreamStateMachine.send_informational_response, - StreamState.HALF_CLOSED_REMOTE), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_ALTERNATIVE_SERVICE): - (H2StreamStateMachine.send_alt_svc, StreamState.HALF_CLOSED_REMOTE), - (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_ALTERNATIVE_SERVICE): - (H2StreamStateMachine.recv_alt_svc, StreamState.HALF_CLOSED_REMOTE), - + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_HEADERS): ( + H2StreamStateMachine.response_sent, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_HEADERS): ( + H2StreamStateMachine.reset_stream_on_error, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_DATA): ( + None, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_DATA): ( + H2StreamStateMachine.reset_stream_on_error, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_END_STREAM): ( + H2StreamStateMachine.send_end_stream, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_WINDOW_UPDATE): ( + None, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_WINDOW_UPDATE): ( + H2StreamStateMachine.window_updated, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_RST_STREAM): ( + H2StreamStateMachine.send_reset_stream, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_RST_STREAM): ( + H2StreamStateMachine.stream_reset, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_PUSH_PROMISE): ( + H2StreamStateMachine.send_push_promise, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_PUSH_PROMISE): ( + H2StreamStateMachine.reset_stream_on_error, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_INFORMATIONAL_HEADERS): ( + H2StreamStateMachine.send_informational_response, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.SEND_ALTERNATIVE_SERVICE): ( + H2StreamStateMachine.send_alt_svc, + StreamState.HALF_CLOSED_REMOTE, + ), + (StreamState.HALF_CLOSED_REMOTE, StreamInputs.RECV_ALTERNATIVE_SERVICE): ( + H2StreamStateMachine.recv_alt_svc, + StreamState.HALF_CLOSED_REMOTE, + ), # State: half-closed local - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_HEADERS): - (H2StreamStateMachine.response_received, - StreamState.HALF_CLOSED_LOCAL), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_DATA): - (H2StreamStateMachine.data_received, StreamState.HALF_CLOSED_LOCAL), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_END_STREAM): - (H2StreamStateMachine.stream_ended, StreamState.CLOSED), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_WINDOW_UPDATE): - (None, StreamState.HALF_CLOSED_LOCAL), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_WINDOW_UPDATE): - (H2StreamStateMachine.window_updated, StreamState.HALF_CLOSED_LOCAL), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_RST_STREAM): - (H2StreamStateMachine.send_reset_stream, StreamState.CLOSED), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_RST_STREAM): - (H2StreamStateMachine.stream_reset, StreamState.CLOSED), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_PUSH_PROMISE): - (H2StreamStateMachine.recv_push_promise, - StreamState.HALF_CLOSED_LOCAL), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_INFORMATIONAL_HEADERS): - (H2StreamStateMachine.recv_informational_response, - StreamState.HALF_CLOSED_LOCAL), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_ALTERNATIVE_SERVICE): - (H2StreamStateMachine.send_alt_svc, StreamState.HALF_CLOSED_LOCAL), - (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_ALTERNATIVE_SERVICE): - (H2StreamStateMachine.recv_alt_svc, StreamState.HALF_CLOSED_LOCAL), - + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_HEADERS): ( + H2StreamStateMachine.response_received, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_DATA): ( + H2StreamStateMachine.data_received, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_END_STREAM): ( + H2StreamStateMachine.stream_ended, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_WINDOW_UPDATE): ( + None, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_WINDOW_UPDATE): ( + H2StreamStateMachine.window_updated, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_RST_STREAM): ( + H2StreamStateMachine.send_reset_stream, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_RST_STREAM): ( + H2StreamStateMachine.stream_reset, + StreamState.CLOSED, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_PUSH_PROMISE): ( + H2StreamStateMachine.recv_push_promise, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_INFORMATIONAL_HEADERS): ( + H2StreamStateMachine.recv_informational_response, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.SEND_ALTERNATIVE_SERVICE): ( + H2StreamStateMachine.send_alt_svc, + StreamState.HALF_CLOSED_LOCAL, + ), + (StreamState.HALF_CLOSED_LOCAL, StreamInputs.RECV_ALTERNATIVE_SERVICE): ( + H2StreamStateMachine.recv_alt_svc, + StreamState.HALF_CLOSED_LOCAL, + ), # State: closed - (StreamState.CLOSED, StreamInputs.RECV_END_STREAM): - (None, StreamState.CLOSED), - (StreamState.CLOSED, StreamInputs.RECV_ALTERNATIVE_SERVICE): - (None, StreamState.CLOSED), - + (StreamState.CLOSED, StreamInputs.RECV_END_STREAM): (None, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.RECV_ALTERNATIVE_SERVICE): ( + None, + StreamState.CLOSED, + ), # RFC 7540 Section 5.1 defines how the end point should react when # receiving a frame on a closed stream with the following statements: # @@ -700,39 +825,52 @@ def send_alt_svc(self, previous_state): # > An endpoint that receives any frames after receiving a frame with the # > END_STREAM flag set MUST treat that as a connection error of type # > STREAM_CLOSED. - (StreamState.CLOSED, StreamInputs.RECV_HEADERS): - (H2StreamStateMachine.recv_on_closed_stream, StreamState.CLOSED), - (StreamState.CLOSED, StreamInputs.RECV_DATA): - (H2StreamStateMachine.recv_on_closed_stream, StreamState.CLOSED), - + (StreamState.CLOSED, StreamInputs.RECV_HEADERS): ( + H2StreamStateMachine.recv_on_closed_stream, + StreamState.CLOSED, + ), + (StreamState.CLOSED, StreamInputs.RECV_DATA): ( + H2StreamStateMachine.recv_on_closed_stream, + StreamState.CLOSED, + ), # > WINDOW_UPDATE or RST_STREAM frames can be received in this state # > for a short period after a DATA or HEADERS frame containing a # > END_STREAM flag is sent, as instructed in RFC 7540 Section 5.1. But we # > don't have access to a clock so we just always allow it. - (StreamState.CLOSED, StreamInputs.RECV_WINDOW_UPDATE): - (None, StreamState.CLOSED), - (StreamState.CLOSED, StreamInputs.RECV_RST_STREAM): - (None, StreamState.CLOSED), - + (StreamState.CLOSED, StreamInputs.RECV_WINDOW_UPDATE): (None, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.RECV_RST_STREAM): (None, StreamState.CLOSED), # > A receiver MUST treat the receipt of a PUSH_PROMISE on a stream that is # > neither "open" nor "half-closed (local)" as a connection error of type # > PROTOCOL_ERROR. - (StreamState.CLOSED, StreamInputs.RECV_PUSH_PROMISE): - (H2StreamStateMachine.recv_push_on_closed_stream, StreamState.CLOSED), - + (StreamState.CLOSED, StreamInputs.RECV_PUSH_PROMISE): ( + H2StreamStateMachine.recv_push_on_closed_stream, + StreamState.CLOSED, + ), # Also, users should be forbidden from sending on closed streams. - (StreamState.CLOSED, StreamInputs.SEND_HEADERS): - (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), - (StreamState.CLOSED, StreamInputs.SEND_PUSH_PROMISE): - (H2StreamStateMachine.send_push_on_closed_stream, StreamState.CLOSED), - (StreamState.CLOSED, StreamInputs.SEND_RST_STREAM): - (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), - (StreamState.CLOSED, StreamInputs.SEND_DATA): - (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), - (StreamState.CLOSED, StreamInputs.SEND_WINDOW_UPDATE): - (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), - (StreamState.CLOSED, StreamInputs.SEND_END_STREAM): - (H2StreamStateMachine.send_on_closed_stream, StreamState.CLOSED), + (StreamState.CLOSED, StreamInputs.SEND_HEADERS): ( + H2StreamStateMachine.send_on_closed_stream, + StreamState.CLOSED, + ), + (StreamState.CLOSED, StreamInputs.SEND_PUSH_PROMISE): ( + H2StreamStateMachine.send_push_on_closed_stream, + StreamState.CLOSED, + ), + (StreamState.CLOSED, StreamInputs.SEND_RST_STREAM): ( + H2StreamStateMachine.send_on_closed_stream, + StreamState.CLOSED, + ), + (StreamState.CLOSED, StreamInputs.SEND_DATA): ( + H2StreamStateMachine.send_on_closed_stream, + StreamState.CLOSED, + ), + (StreamState.CLOSED, StreamInputs.SEND_WINDOW_UPDATE): ( + H2StreamStateMachine.send_on_closed_stream, + StreamState.CLOSED, + ), + (StreamState.CLOSED, StreamInputs.SEND_END_STREAM): ( + H2StreamStateMachine.send_on_closed_stream, + StreamState.CLOSED, + ), } @@ -746,11 +884,8 @@ class H2Stream: Attempts to create frames that cannot be sent will raise a ``ProtocolError``. """ - def __init__(self, - stream_id, - config, - inbound_window_size, - outbound_window_size): + + def __init__(self, stream_id, config, inbound_window_size, outbound_window_size): self.state_machine = H2StreamStateMachine(stream_id) self.stream_id = stream_id self.max_outbound_frame_size = None @@ -778,7 +913,7 @@ def __repr__(self): return "<%s id:%d state:%r>" % ( type(self).__name__, self.stream_id, - self.state_machine.state + self.state_machine.state, ) @property @@ -828,8 +963,7 @@ def upgrade(self, client_side): assert self.stream_id == 1 input_ = ( - StreamInputs.UPGRADE_CLIENT if client_side - else StreamInputs.UPGRADE_SERVER + StreamInputs.UPGRADE_CLIENT if client_side else StreamInputs.UPGRADE_SERVER ) # This may return events, we deliberately don't want them. @@ -851,12 +985,11 @@ def send_headers(self, headers, encoder, end_stream=False): # we need to scan the header block to see if this is an informational # response. input_ = StreamInputs.SEND_HEADERS - if ((not self.state_machine.client) and - is_informational_response(headers)): + + headers = utf8_encode_headers(headers) + if (not self.state_machine.client) and is_informational_response(headers): if end_stream: - raise ProtocolError( - "Cannot set END_STREAM on informational responses." - ) + raise ProtocolError("Cannot set END_STREAM on informational responses.") input_ = StreamInputs.SEND_INFORMATIONAL_HEADERS @@ -864,15 +997,13 @@ def send_headers(self, headers, encoder, end_stream=False): hf = HeadersFrame(self.stream_id) hdr_validation_flags = self._build_hdr_validation_flags(events) - frames = self._build_headers_frames( - headers, encoder, hf, hdr_validation_flags - ) + frames = self._build_headers_frames(headers, encoder, hf, hdr_validation_flags) if end_stream: # Not a bug: the END_STREAM flag is valid on the initial HEADERS # frame, not the CONTINUATION frames that follow. self.state_machine.process_input(StreamInputs.SEND_END_STREAM) - frames[0].flags.add('END_STREAM') + frames[0].flags.add("END_STREAM") if self.state_machine.trailers_sent and not end_stream: raise ProtocolError("Trailers must have END_STREAM set.") @@ -896,16 +1027,12 @@ def push_stream_in_band(self, related_stream_id, headers, encoder): # Because encoding headers makes an irreversible change to the header # compression context, we make the state transition *first*. - events = self.state_machine.process_input( - StreamInputs.SEND_PUSH_PROMISE - ) + events = self.state_machine.process_input(StreamInputs.SEND_PUSH_PROMISE) ppf = PushPromiseFrame(self.stream_id) ppf.promised_stream_id = related_stream_id hdr_validation_flags = self._build_hdr_validation_flags(events) - frames = self._build_headers_frames( - headers, encoder, ppf, hdr_validation_flags - ) + frames = self._build_headers_frames(headers, encoder, ppf, hdr_validation_flags) return frames @@ -916,9 +1043,7 @@ def locally_pushed(self): state machine. """ # This does not trigger any events. - events = self.state_machine.process_input( - StreamInputs.SEND_PUSH_PROMISE - ) + events = self.state_machine.process_input(StreamInputs.SEND_PUSH_PROMISE) assert not events return [] @@ -938,9 +1063,9 @@ def send_data(self, data, end_stream=False, pad_length=None): df.data = data if end_stream: self.state_machine.process_input(StreamInputs.SEND_END_STREAM) - df.flags.add('END_STREAM') + df.flags.add("END_STREAM") if pad_length is not None: - df.flags.add('PADDED') + df.flags.add("PADDED") df.pad_length = pad_length # Subtract flow_controlled_length to account for possible padding @@ -957,7 +1082,7 @@ def end_stream(self): self.state_machine.process_input(StreamInputs.SEND_END_STREAM) df = DataFrame(self.stream_id) - df.flags.add('END_STREAM') + df.flags.add("END_STREAM") return [df] def advertise_alternative_service(self, field_value): @@ -978,8 +1103,7 @@ def increase_flow_control_window(self, increment): Increase the size of the flow control window for the remote side. """ self.config.logger.debug( - "Increase flow control window for %r by %d", - self, increment + "Increase flow control window for %r by %d", self, increment ) self.state_machine.process_input(StreamInputs.SEND_WINDOW_UPDATE) self._inbound_window_manager.window_opened(increment) @@ -988,22 +1112,18 @@ def increase_flow_control_window(self, increment): wuf.window_increment = increment return [wuf] - def receive_push_promise_in_band(self, - promised_stream_id, - headers, - header_encoding): + def receive_push_promise_in_band( + self, promised_stream_id, headers, header_encoding + ): """ Receives a push promise frame sent on this stream, pushing a remote stream. This is called on the stream that has the PUSH_PROMISE sent on it. """ self.config.logger.debug( - "Receive Push Promise on %r for remote stream %d", - self, promised_stream_id - ) - events = self.state_machine.process_input( - StreamInputs.RECV_PUSH_PROMISE + "Receive Push Promise on %r for remote stream %d", self, promised_stream_id ) + events = self.state_machine.process_input(StreamInputs.RECV_PUSH_PROMISE) events[0].pushed_stream_id = promised_stream_id hdr_validation_flags = self._build_hdr_validation_flags(events) @@ -1019,9 +1139,7 @@ def remotely_pushed(self, pushed_headers): updates the state machine. """ self.config.logger.debug("%r pushed by remote peer", self) - events = self.state_machine.process_input( - StreamInputs.RECV_PUSH_PROMISE - ) + events = self.state_machine.process_input(StreamInputs.RECV_PUSH_PROMISE) self._authority = authority_from_headers(pushed_headers) return [], events @@ -1031,9 +1149,7 @@ def receive_headers(self, headers, end_stream, header_encoding): """ if is_informational_response(headers): if end_stream: - raise ProtocolError( - "Cannot set END_STREAM on informational responses" - ) + raise ProtocolError("Cannot set END_STREAM on informational responses") input_ = StreamInputs.RECV_INFORMATIONAL_HEADERS else: input_ = StreamInputs.RECV_HEADERS @@ -1041,9 +1157,7 @@ def receive_headers(self, headers, end_stream, header_encoding): events = self.state_machine.process_input(input_) if end_stream: - es_events = self.state_machine.process_input( - StreamInputs.RECV_END_STREAM - ) + es_events = self.state_machine.process_input(StreamInputs.RECV_END_STREAM) events[0].stream_ended = es_events[0] events += es_events @@ -1065,16 +1179,17 @@ def receive_data(self, data, end_stream, flow_control_len): """ self.config.logger.debug( "Receive data on %r with end stream %s and flow control length " - "set to %d", self, end_stream, flow_control_len + "set to %d", + self, + end_stream, + flow_control_len, ) events = self.state_machine.process_input(StreamInputs.RECV_DATA) self._inbound_window_manager.window_consumed(flow_control_len) self._track_content_length(len(data), end_stream) if end_stream: - es_events = self.state_machine.process_input( - StreamInputs.RECV_END_STREAM - ) + es_events = self.state_machine.process_input(StreamInputs.RECV_END_STREAM) events[0].stream_ended = es_events[0] events.extend(es_events) @@ -1087,12 +1202,9 @@ def receive_window_update(self, increment): Handle a WINDOW_UPDATE increment. """ self.config.logger.debug( - "Receive Window Update on %r for increment of %d", - self, increment - ) - events = self.state_machine.process_input( - StreamInputs.RECV_WINDOW_UPDATE + "Receive Window Update on %r for increment of %d", self, increment ) + events = self.state_machine.process_input(StreamInputs.RECV_WINDOW_UPDATE) frames = [] # If we encounter a problem with incrementing the flow control window, @@ -1102,8 +1214,7 @@ def receive_window_update(self, increment): events[0].delta = increment try: self.outbound_flow_control_window = guard_increment_window( - self.outbound_flow_control_window, - increment + self.outbound_flow_control_window, increment ) except FlowControlError: # Ok, this is bad. We're going to need to perform a local @@ -1125,9 +1236,7 @@ def receive_continuation(self): transition the state of the stream, so we need to handle it. """ self.config.logger.debug("Receive Continuation frame on %r", self) - self.state_machine.process_input( - StreamInputs.RECV_CONTINUATION - ) + self.state_machine.process_input(StreamInputs.RECV_CONTINUATION) assert False, "Should not be reachable" def receive_alt_svc(self, frame): @@ -1135,17 +1244,13 @@ def receive_alt_svc(self, frame): An Alternative Service frame was received on the stream. This frame inherits the origin associated with this stream. """ - self.config.logger.debug( - "Receive Alternative Service frame on stream %r", self - ) + self.config.logger.debug("Receive Alternative Service frame on stream %r", self) # If the origin is present, RFC 7838 says we have to ignore it. if frame.origin: return [], [] - events = self.state_machine.process_input( - StreamInputs.RECV_ALTERNATIVE_SERVICE - ) + events = self.state_machine.process_input(StreamInputs.RECV_ALTERNATIVE_SERVICE) # There are lots of situations where we want to ignore the ALTSVC # frame. If we need to pay attention, we'll have an event and should @@ -1161,9 +1266,7 @@ def reset_stream(self, error_code=0): """ Close the stream locally. Reset the stream with an error code. """ - self.config.logger.debug( - "Local reset %r with error code: %d", self, error_code - ) + self.config.logger.debug("Local reset %r with error code: %d", self, error_code) self.state_machine.process_input(StreamInputs.SEND_RST_STREAM) rsf = RstStreamFrame(self.stream_id) @@ -1192,12 +1295,9 @@ def acknowledge_received_data(self, acknowledged_size): potentially return some WindowUpdate frames. """ self.config.logger.debug( - "Acknowledge received data with size %d on %r", - acknowledged_size, self - ) - increment = self._inbound_window_manager.process_bytes( - acknowledged_size + "Acknowledge received data with size %d on %r", acknowledged_size, self ) + increment = self._inbound_window_manager.process_bytes(acknowledged_size) if increment: f = WindowUpdateFrame(self.stream_id) f.window_increment = increment @@ -1210,16 +1310,9 @@ def _build_hdr_validation_flags(self, events): Constructs a set of header validation flags for use when normalizing and validating header blocks. """ - is_trailer = isinstance( - events[0], (_TrailersSent, TrailersReceived) - ) + is_trailer = isinstance(events[0], (_TrailersSent, TrailersReceived)) is_response_header = isinstance( - events[0], - ( - _ResponseSent, - ResponseReceived, - InformationalResponseReceived - ) + events[0], (_ResponseSent, ResponseReceived, InformationalResponseReceived) ) is_push_promise = isinstance( events[0], (PushedStreamReceived, _PushedRequestSent) @@ -1232,16 +1325,15 @@ def _build_hdr_validation_flags(self, events): is_push_promise=is_push_promise, ) - def _build_headers_frames(self, - headers, - encoder, - first_frame, - hdr_validation_flags): + def _build_headers_frames( + self, headers, encoder, first_frame, hdr_validation_flags + ): """ Helper method to build headers or push promise frames. """ # We need to lowercase the header names, and to ensure that secure # header fields are kept out of compression contexts. + if self.config.normalize_outbound_headers: # also we may want to split outbound cookies to improve # headers compression @@ -1251,9 +1343,7 @@ def _build_headers_frames(self, headers, hdr_validation_flags, should_split_outbound_cookies ) if self.config.validate_outbound_headers: - headers = validate_outbound_headers( - headers, hdr_validation_flags - ) + headers = validate_outbound_headers(headers, hdr_validation_flags) encoded_headers = encoder.encode(headers) @@ -1261,10 +1351,8 @@ def _build_headers_frames(self, # it only works right because we never send padded frames or priority # information on the frames. Revisit this if we do. header_blocks = [ - encoded_headers[i:i+self.max_outbound_frame_size] - for i in range( - 0, len(encoded_headers), self.max_outbound_frame_size - ) + encoded_headers[i : i + self.max_outbound_frame_size] + for i in range(0, len(encoded_headers), self.max_outbound_frame_size) ] frames = [] @@ -1276,22 +1364,19 @@ def _build_headers_frames(self, cf.data = block frames.append(cf) - frames[-1].flags.add('END_HEADERS') + frames[-1].flags.add("END_HEADERS") return frames - def _process_received_headers(self, - headers, - header_validation_flags, - header_encoding): + def _process_received_headers( + self, headers, header_validation_flags, header_encoding + ): """ When headers have been received from the remote peer, run a processing pipeline on them to transform them into the appropriate form for attaching to an event. """ if self.config.normalize_inbound_headers: - headers = normalize_inbound_headers( - headers, header_validation_flags - ) + headers = normalize_inbound_headers(headers, header_validation_flags) if self.config.validate_inbound_headers: headers = validate_headers(headers, header_validation_flags) @@ -1309,18 +1394,16 @@ def _initialize_content_length(self, headers): _expected_content_length field from it. It's not an error for no Content-Length header to be present. """ - if self.request_method == b'HEAD': + if self.request_method == b"HEAD": self._expected_content_length = 0 return for n, v in headers: - if n == b'content-length': + if n == b"content-length": try: self._expected_content_length = int(v, 10) except ValueError: - raise ProtocolError( - "Invalid content-length header: %s" % v - ) + raise ProtocolError(f"Invalid content-length header: {repr(v)}") return diff --git a/src/h2/utilities.py b/src/h2/utilities.py index 3a7bf6e07..a2a0f5d01 100644 --- a/src/h2/utilities.py +++ b/src/h2/utilities.py @@ -15,49 +15,60 @@ UPPER_RE = re.compile(b"[A-Z]") +SIGIL = ord(b":") + + # A set of headers that are hop-by-hop or connection-specific and thus # forbidden in HTTP/2. This list comes from RFC 7540 § 8.1.2.2. -CONNECTION_HEADERS = frozenset([ - b'connection', u'connection', - b'proxy-connection', u'proxy-connection', - b'keep-alive', u'keep-alive', - b'transfer-encoding', u'transfer-encoding', - b'upgrade', u'upgrade', -]) +CONNECTION_HEADERS = frozenset( + [ + b"connection", + b"proxy-connection", + b"keep-alive", + b"transfer-encoding", + b"upgrade", + ] +) -_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset([ - b':method', u':method', - b':scheme', u':scheme', - b':authority', u':authority', - b':path', u':path', - b':status', u':status', - b':protocol', u':protocol', -]) +_ALLOWED_PSEUDO_HEADER_FIELDS = frozenset( + [ + b":method", + b":scheme", + b":authority", + b":path", + b":status", + b":protocol", + ] +) -_SECURE_HEADERS = frozenset([ - # May have basic credentials which are vulnerable to dictionary attacks. - b'authorization', u'authorization', - b'proxy-authorization', u'proxy-authorization', -]) +_SECURE_HEADERS = frozenset( + [ + # May have basic credentials which are vulnerable to dictionary attacks. + b"authorization", + b"proxy-authorization", + ] +) -_REQUEST_ONLY_HEADERS = frozenset([ - b':scheme', u':scheme', - b':path', u':path', - b':authority', u':authority', - b':method', u':method', - b':protocol', u':protocol', -]) +_REQUEST_ONLY_HEADERS = frozenset( + [ + b":scheme", + b":path", + b":authority", + b":method", + b":protocol", + ] +) -_RESPONSE_ONLY_HEADERS = frozenset([b':status', u':status']) +_RESPONSE_ONLY_HEADERS = frozenset([b":status"]) # A Set of pseudo headers that are only valid if the method is # CONNECT, see RFC 8441 § 5 -_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b':protocol', u':protocol']) +_CONNECT_REQUEST_ONLY_HEADERS = frozenset([b":protocol"]) _WHITESPACE = frozenset(map(ord, whitespace)) @@ -84,7 +95,7 @@ def _secure_headers(headers, hdr_validation_flags): for header in headers: if header[0] in _SECURE_HEADERS: yield NeverIndexedHeaderTuple(*header) - elif header[0] in (b'cookie', u'cookie') and len(header[1]) < 20: + elif header[0] == b"cookie" and len(header[1]) < 20: yield NeverIndexedHeaderTuple(*header) else: yield header @@ -95,11 +106,8 @@ def extract_method_header(headers): Extracts the request method from the headers list. """ for k, v in headers: - if k in (b':method', u':method'): - if not isinstance(v, bytes): - return v.encode('utf-8') - else: - return v + if k == b":method": + return v def is_informational_response(headers): @@ -113,18 +121,12 @@ def is_informational_response(headers): :param headers: The HTTP/2 header block. :returns: A boolean indicating if this is an informational response. """ + status = b":status" + informational_start = ord(b"1") for n, v in headers: - if isinstance(n, bytes): - sigil = b':' - status = b':status' - informational_start = b'1' - else: - sigil = u':' - status = u':status' - informational_start = u'1' - # If we find a non-special header, we're done here: stop looping. - if not n.startswith(sigil): + + if n and n[0] != SIGIL: return False # This isn't the status header, bail. @@ -132,7 +134,7 @@ def is_informational_response(headers): continue # If the first digit is a 1, we've got informational headers. - return v.startswith(informational_start) + return v[0] == informational_start def guard_increment_window(current, increment): @@ -152,8 +154,8 @@ def guard_increment_window(current, increment): if new_size > LARGEST_FLOW_CONTROL_WINDOW: raise FlowControlError( - "May not increment flow control window past %d" % - LARGEST_FLOW_CONTROL_WINDOW + "May not increment flow control window past %d" + % LARGEST_FLOW_CONTROL_WINDOW ) return new_size @@ -164,7 +166,7 @@ def authority_from_headers(headers): Given a header set, searches for the authority header and returns the value. - Note that this doesn't terminate early, so should only be called if the + Note that this doesn't use indexing, so should only be called if the headers are for a client request. Otherwise, will loop over the entire header set, which is potentially unwise. @@ -173,11 +175,8 @@ def authority_from_headers(headers): :rtype: ``bytes`` or ``None``. """ for n, v in headers: - # This gets run against headers that come both from HPACK and from the - # user, so we may have unicode floating around in here. We only want - # bytes. - if n in (b':authority', u':authority'): - return v.encode('utf-8') if not isinstance(v, bytes) else v + if n == b":authority": + return v return None @@ -185,8 +184,8 @@ def authority_from_headers(headers): # Flags used by the validate_headers pipeline to determine which checks # should be applied to a given set of headers. HeaderValidationFlags = collections.namedtuple( - 'HeaderValidationFlags', - ['is_client', 'is_trailer', 'is_response_header', 'is_push_promise'] + "HeaderValidationFlags", + ["is_client", "is_trailer", "is_response_header", "is_push_promise"], ) @@ -206,27 +205,13 @@ def validate_headers(headers, hdr_validation_flags): # For example, we avoid tuple unpacking in loops because it represents a # fixed cost that we don't want to spend, instead indexing into the header # tuples. - headers = _reject_empty_header_names( - headers, hdr_validation_flags - ) - headers = _reject_uppercase_header_fields( - headers, hdr_validation_flags - ) - headers = _reject_surrounding_whitespace( - headers, hdr_validation_flags - ) - headers = _reject_te( - headers, hdr_validation_flags - ) - headers = _reject_connection_header( - headers, hdr_validation_flags - ) - headers = _reject_pseudo_header_fields( - headers, hdr_validation_flags - ) - headers = _check_host_authority_header( - headers, hdr_validation_flags - ) + headers = _reject_empty_header_names(headers, hdr_validation_flags) + headers = _reject_uppercase_header_fields(headers, hdr_validation_flags) + headers = _reject_surrounding_whitespace(headers, hdr_validation_flags) + headers = _reject_te(headers, hdr_validation_flags) + headers = _reject_connection_header(headers, hdr_validation_flags) + headers = _reject_pseudo_header_fields(headers, hdr_validation_flags) + headers = _check_host_authority_header(headers, hdr_validation_flags) headers = _check_path_header(headers, hdr_validation_flags) return headers @@ -252,8 +237,7 @@ def _reject_uppercase_header_fields(headers, hdr_validation_flags): """ for header in headers: if UPPER_RE.search(header[0]): - raise ProtocolError( - "Received uppercase header name %s." % header[0]) + raise ProtocolError(f"Received uppercase header name {repr(header[0])}.") yield header @@ -270,9 +254,11 @@ def _reject_surrounding_whitespace(headers, hdr_validation_flags): for header in headers: if header[0][0] in _WHITESPACE or header[0][-1] in _WHITESPACE: raise ProtocolError( - "Received header name surrounded by whitespace %r" % header[0]) - if header[1] and ((header[1][0] in _WHITESPACE) or - (header[1][-1] in _WHITESPACE)): + "Received header name surrounded by whitespace %r" % header[0] + ) + if header[1] and ( + (header[1][0] in _WHITESPACE) or (header[1][-1] in _WHITESPACE) + ): raise ProtocolError( "Received header value surrounded by whitespace %r" % header[1] ) @@ -285,12 +271,9 @@ def _reject_te(headers, hdr_validation_flags): its value is anything other than "trailers". """ for header in headers: - if header[0] in (b'te', u'te'): - if header[1].lower() not in (b'trailers', u'trailers'): - raise ProtocolError( - "Invalid value for TE header: %s" % - header[1] - ) + if header[0] == b"te": + if header[1].lower() != b"trailers": + raise ProtocolError(f"Invalid value for TE header: {repr(header[1])}") yield header @@ -303,32 +286,21 @@ def _reject_connection_header(headers, hdr_validation_flags): for header in headers: if header[0] in CONNECTION_HEADERS: raise ProtocolError( - "Connection-specific header field present: %s." % header[0] + f"Connection-specific header field present: {repr(header[0])}." ) yield header -def _custom_startswith(test_string, bytes_prefix, unicode_prefix): - """ - Given a string that might be a bytestring or a Unicode string, - return True if it starts with the appropriate prefix. - """ - if isinstance(test_string, bytes): - return test_string.startswith(bytes_prefix) - else: - return test_string.startswith(unicode_prefix) - - -def _assert_header_in_set(string_header, bytes_header, header_set): +def _assert_header_in_set(bytes_header, header_set): """ Given a set of header names, checks whether the string or byte version of the header name is present. Raises a Protocol error with the appropriate error if it's missing. """ - if not (string_header in header_set or bytes_header in header_set): + if bytes_header not in header_set: raise ProtocolError( - "Header block missing mandatory %s header" % string_header + f"Header block missing mandatory {repr(bytes_header)} header" ) @@ -345,30 +317,26 @@ def _reject_pseudo_header_fields(headers, hdr_validation_flags): method = None for header in headers: - if _custom_startswith(header[0], b':', u':'): + if header[0][0] == SIGIL: if header[0] in seen_pseudo_header_fields: raise ProtocolError( - "Received duplicate pseudo-header field %s" % header[0] + f"Received duplicate pseudo-header field {repr(header[0])}" ) seen_pseudo_header_fields.add(header[0]) if seen_regular_header: raise ProtocolError( - "Received pseudo-header field out of sequence: %s" % - header[0] + f"Received pseudo-header field out of sequence: {repr(header[0])}" ) if header[0] not in _ALLOWED_PSEUDO_HEADER_FIELDS: raise ProtocolError( - "Received custom pseudo-header field %s" % header[0] + f"Received custom pseudo-header field {repr(header[0])}" ) - if header[0] in (b':method', u':method'): - if not isinstance(header[1], bytes): - method = header[1].encode('utf-8') - else: - method = header[1] + if header[0] in b":method": + method = header[1] else: seen_regular_header = True @@ -381,18 +349,16 @@ def _reject_pseudo_header_fields(headers, hdr_validation_flags): ) -def _check_pseudo_header_field_acceptability(pseudo_headers, - method, - hdr_validation_flags): +def _check_pseudo_header_field_acceptability( + pseudo_headers, method, hdr_validation_flags +): """ Given the set of pseudo-headers present in a header block and the validation flags, confirms that RFC 7540 allows them. """ # Pseudo-header fields MUST NOT appear in trailers - RFC 7540 § 8.1.2.1 if hdr_validation_flags.is_trailer and pseudo_headers: - raise ProtocolError( - "Received pseudo-header in trailer %s" % pseudo_headers - ) + raise ProtocolError("Received pseudo-header in trailer %s" % pseudo_headers) # If ':status' pseudo-header is not there in a response header, reject it. # Similarly, if ':path', ':method', or ':scheme' are not there in a request @@ -401,32 +367,31 @@ def _check_pseudo_header_field_acceptability(pseudo_headers, # Relevant RFC section: RFC 7540 § 8.1.2.4 # https://tools.ietf.org/html/rfc7540#section-8.1.2.4 if hdr_validation_flags.is_response_header: - _assert_header_in_set(u':status', b':status', pseudo_headers) + _assert_header_in_set(b":status", pseudo_headers) invalid_response_headers = pseudo_headers & _REQUEST_ONLY_HEADERS if invalid_response_headers: raise ProtocolError( - "Encountered request-only headers %s" % - invalid_response_headers + "Encountered request-only headers %s" % invalid_response_headers ) - elif (not hdr_validation_flags.is_response_header and - not hdr_validation_flags.is_trailer): + elif ( + not hdr_validation_flags.is_response_header + and not hdr_validation_flags.is_trailer + ): # This is a request, so we need to have seen :path, :method, and # :scheme. - _assert_header_in_set(u':path', b':path', pseudo_headers) - _assert_header_in_set(u':method', b':method', pseudo_headers) - _assert_header_in_set(u':scheme', b':scheme', pseudo_headers) + _assert_header_in_set(b":path", pseudo_headers) + _assert_header_in_set(b":method", pseudo_headers) + _assert_header_in_set(b":scheme", pseudo_headers) invalid_request_headers = pseudo_headers & _RESPONSE_ONLY_HEADERS if invalid_request_headers: raise ProtocolError( - "Encountered response-only headers %s" % - invalid_request_headers + "Encountered response-only headers %s" % invalid_request_headers ) - if method != b'CONNECT': + if method != b"CONNECT": invalid_headers = pseudo_headers & _CONNECT_REQUEST_ONLY_HEADERS if invalid_headers: raise ProtocolError( - "Encountered connect-request-only headers %s" % - invalid_headers + "Encountered connect-request-only headers %s" % invalid_headers ) @@ -451,17 +416,17 @@ def _validate_host_authority_header(headers): host_header_val = None for header in headers: - if header[0] in (b':authority', u':authority'): + if header[0] == b":authority": authority_header_val = header[1] - elif header[0] in (b'host', u'host'): + elif header[0] == b"host": host_header_val = header[1] yield header # If we have not-None values for these variables, then we know we saw # the corresponding header. - authority_present = (authority_header_val is not None) - host_present = (host_header_val is not None) + authority_present = authority_header_val is not None + host_present = host_header_val is not None # It is an error for a request header block to contain neither # an :authority header nor a Host header. @@ -475,8 +440,7 @@ def _validate_host_authority_header(headers): if authority_header_val != host_header_val: raise ProtocolError( "Request header block has mismatched :authority and " - "Host headers: %r / %r" - % (authority_header_val, host_header_val) + "Host headers: %r / %r" % (authority_header_val, host_header_val) ) @@ -490,8 +454,7 @@ def _check_host_authority_header(headers, hdr_validation_flags): # blocks that aren't trailers, so skip this validation if this is a # response header or we're looking at trailer blocks. skip_validation = ( - hdr_validation_flags.is_response_header or - hdr_validation_flags.is_trailer + hdr_validation_flags.is_response_header or hdr_validation_flags.is_trailer ) if skip_validation: return headers @@ -504,9 +467,10 @@ def _check_path_header(headers, hdr_validation_flags): Raise a ProtocolError if a header block arrives or is sent that contains an empty :path header. """ + def inner(): for header in headers: - if header[0] in (b':path', u':path'): + if header[0] == b":path": if not header[1]: raise ProtocolError("An empty :path header is forbidden") @@ -516,8 +480,7 @@ def inner(): # blocks that aren't trailers, so skip this validation if this is a # response header or we're looking at trailer blocks. skip_validation = ( - hdr_validation_flags.is_response_header or - hdr_validation_flags.is_trailer + hdr_validation_flags.is_response_header or hdr_validation_flags.is_trailer ) if skip_validation: return headers @@ -525,6 +488,27 @@ def inner(): return inner() +def _to_bytes(v): + return v if isinstance(v, bytes) else v.encode("utf-8") + + +def utf8_encode_headers(headers): + """ + Given an iterable of header two-tuples, rebuilds that iterable with the + header names and values encoded as utf-8 bytes. This generator produces + tuples that preserve the original type of the header tuple for tuple and + any ``HeaderTuple``. + """ + return [ + ( + header.__class__(_to_bytes(header[0]), _to_bytes(header[1])) + if isinstance(header, HeaderTuple) + else (_to_bytes(header[0]), _to_bytes(header[1])) + ) + for header in headers + ] + + def _lowercase_header_names(headers, hdr_validation_flags): """ Given an iterable of header two-tuples, rebuilds that iterable with the @@ -571,8 +555,7 @@ def _check_sent_host_authority_header(headers, hdr_validation_flags): # blocks that aren't trailers, so skip this validation if this is a # response header or we're looking at trailer blocks. skip_validation = ( - hdr_validation_flags.is_response_header or - hdr_validation_flags.is_trailer + hdr_validation_flags.is_response_header or hdr_validation_flags.is_trailer ) if skip_validation: return headers @@ -594,13 +577,13 @@ def _combine_cookie_fields(headers, hdr_validation_flags): # logic and make them never-indexed. cookies = [] for header in headers: - if header[0] == b'cookie': + if header[0] == b"cookie": cookies.append(header[1]) else: yield header if cookies: - cookie_val = b'; '.join(cookies) - yield NeverIndexedHeaderTuple(b'cookie', cookie_val) + cookie_val = b"; ".join(cookies) + yield NeverIndexedHeaderTuple(b"cookie", cookie_val) def _split_outbound_cookie_fields(headers, hdr_validation_flags): @@ -612,22 +595,19 @@ def _split_outbound_cookie_fields(headers, hdr_validation_flags): inbound. """ for header in headers: - if header[0] in (b'cookie', 'cookie'): - needle = b'; ' if isinstance(header[0], bytes) else '; ' - - if needle in header[1]: - for cookie_val in header[1].split(needle): - if isinstance(header, HeaderTuple): - yield header.__class__(header[0], cookie_val) - else: - yield header[0], cookie_val - else: - yield header + if header[0] == b"cookie": + for cookie_val in header[1].split(b"; "): + if isinstance(header, HeaderTuple): + yield header.__class__(header[0], cookie_val) + else: + yield header[0], cookie_val else: yield header -def normalize_outbound_headers(headers, hdr_validation_flags, should_split_outbound_cookies): +def normalize_outbound_headers( + headers, hdr_validation_flags, should_split_outbound_cookies +): """ Normalizes a header sequence that we are about to send. @@ -663,18 +643,10 @@ def validate_outbound_headers(headers, hdr_validation_flags): :param headers: The HTTP header set. :param hdr_validation_flags: An instance of HeaderValidationFlags. """ - headers = _reject_te( - headers, hdr_validation_flags - ) - headers = _reject_connection_header( - headers, hdr_validation_flags - ) - headers = _reject_pseudo_header_fields( - headers, hdr_validation_flags - ) - headers = _check_sent_host_authority_header( - headers, hdr_validation_flags - ) + headers = _reject_te(headers, hdr_validation_flags) + headers = _reject_connection_header(headers, hdr_validation_flags) + headers = _reject_pseudo_header_fields(headers, hdr_validation_flags) + headers = _check_sent_host_authority_header(headers, hdr_validation_flags) headers = _check_path_header(headers, hdr_validation_flags) return headers diff --git a/test/test_invalid_headers.py b/test/test_invalid_headers.py index 165183e28..22b4cbdf7 100644 --- a/test/test_invalid_headers.py +++ b/test/test_invalid_headers.py @@ -30,37 +30,37 @@ class TestInvalidFrameSequences(object): """ Invalid header sequences cause ProtocolErrors to be thrown when received. """ + base_request_headers = [ - (':authority', 'example.com'), - (':path', '/'), - (':scheme', 'https'), - (':method', 'GET'), - ('user-agent', 'someua/0.0.1'), + (":authority", "example.com"), + (":path", "/"), + (":scheme", "https"), + (":method", "GET"), + ("user-agent", "someua/0.0.1"), ] invalid_header_blocks = [ - base_request_headers + [('Uppercase', 'name')], - base_request_headers + [(':late', 'pseudo-header')], - [(':path', 'duplicate-pseudo-header')] + base_request_headers, - base_request_headers + [('connection', 'close')], - base_request_headers + [('proxy-connection', 'close')], - base_request_headers + [('keep-alive', 'close')], - base_request_headers + [('transfer-encoding', 'gzip')], - base_request_headers + [('upgrade', 'super-protocol/1.1')], - base_request_headers + [('te', 'chunked')], - base_request_headers + [('host', 'notexample.com')], - base_request_headers + [(' name', 'name with leading space')], - base_request_headers + [('name ', 'name with trailing space')], - base_request_headers + [('name', ' value with leading space')], - base_request_headers + [('name', 'value with trailing space ')], - [header for header in base_request_headers - if header[0] != ':authority'], - [(':protocol', 'websocket')] + base_request_headers, + base_request_headers + [("Uppercase", "name")], + base_request_headers + [(":late", "pseudo-header")], + [(":path", "duplicate-pseudo-header")] + base_request_headers, + base_request_headers + [("connection", "close")], + base_request_headers + [("proxy-connection", "close")], + base_request_headers + [("keep-alive", "close")], + base_request_headers + [("transfer-encoding", "gzip")], + base_request_headers + [("upgrade", "super-protocol/1.1")], + base_request_headers + [("te", "chunked")], + base_request_headers + [("host", "notexample.com")], + base_request_headers + [(" name", "name with leading space")], + base_request_headers + [("name ", "name with trailing space")], + base_request_headers + [("name", " value with leading space")], + base_request_headers + [("name", "value with trailing space ")], + [header for header in base_request_headers if header[0] != ":authority"], + [(":protocol", "websocket")] + base_request_headers, ] server_config = h2.config.H2Configuration( - client_side=False, header_encoding='utf-8' + client_side=False, header_encoding="utf-8" ) - @pytest.mark.parametrize('headers', invalid_header_blocks) + @pytest.mark.parametrize("headers", invalid_header_blocks) def test_headers_event(self, frame_factory, headers): """ Test invalid headers are rejected with PROTOCOL_ERROR. @@ -80,7 +80,7 @@ def test_headers_event(self, frame_factory, headers): ) assert c.data_to_send() == expected_frame.serialize() - @pytest.mark.parametrize('headers', invalid_header_blocks) + @pytest.mark.parametrize("headers", invalid_header_blocks) def test_push_promise_event(self, frame_factory, headers): """ If a PUSH_PROMISE header frame is received with an invalid header block @@ -88,15 +88,11 @@ def test_push_promise_event(self, frame_factory, headers): """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers( - stream_id=1, headers=self.base_request_headers, end_stream=True - ) + c.send_headers(stream_id=1, headers=self.base_request_headers, end_stream=True) c.clear_outbound_data_buffer() f = frame_factory.build_push_promise_frame( - stream_id=1, - promised_stream_id=2, - headers=headers + stream_id=1, promised_stream_id=2, headers=headers ) data = f.serialize() @@ -108,29 +104,23 @@ def test_push_promise_event(self, frame_factory, headers): ) assert c.data_to_send() == expected_frame.serialize() - @pytest.mark.parametrize('headers', invalid_header_blocks) + @pytest.mark.parametrize("headers", invalid_header_blocks) def test_push_promise_skipping_validation(self, frame_factory, headers): """ If we have ``validate_inbound_headers`` disabled, then invalid header blocks in push promise frames are allowed to pass. """ config = h2.config.H2Configuration( - client_side=True, - validate_inbound_headers=False, - header_encoding='utf-8' + client_side=True, validate_inbound_headers=False, header_encoding="utf-8" ) c = h2.connection.H2Connection(config=config) c.initiate_connection() - c.send_headers( - stream_id=1, headers=self.base_request_headers, end_stream=True - ) + c.send_headers(stream_id=1, headers=self.base_request_headers, end_stream=True) c.clear_outbound_data_buffer() f = frame_factory.build_push_promise_frame( - stream_id=1, - promised_stream_id=2, - headers=headers + stream_id=1, promised_stream_id=2, headers=headers ) data = f.serialize() @@ -139,16 +129,14 @@ def test_push_promise_skipping_validation(self, frame_factory, headers): pp_event = events[0] assert pp_event.headers == headers - @pytest.mark.parametrize('headers', invalid_header_blocks) + @pytest.mark.parametrize("headers", invalid_header_blocks) def test_headers_event_skipping_validation(self, frame_factory, headers): """ If we have ``validate_inbound_headers`` disabled, then all of these invalid header blocks are allowed to pass. """ config = h2.config.H2Configuration( - client_side=False, - validate_inbound_headers=False, - header_encoding='utf-8' + client_side=False, validate_inbound_headers=False, header_encoding="utf-8" ) c = h2.connection.H2Connection(config=config) @@ -166,9 +154,7 @@ def test_te_trailers_is_valid(self, frame_factory): """ `te: trailers` is allowed by the filter. """ - headers = ( - self.base_request_headers + [('te', 'trailers')] - ) + headers = self.base_request_headers + [("te", "trailers")] c = h2.connection.H2Connection(config=self.server_config) c.receive_data(frame_factory.preamble()) @@ -185,15 +171,13 @@ def test_pseudo_headers_rejected_in_trailer(self, frame_factory): """ Ensure we reject pseudo headers included in trailers """ - trailers = [(':path', '/'), ('extra', 'value')] + trailers = [(":path", "/"), ("extra", "value")] c = h2.connection.H2Connection(config=self.server_config) c.receive_data(frame_factory.preamble()) c.clear_outbound_data_buffer() - header_frame = frame_factory.build_headers_frame( - self.base_request_headers - ) + header_frame = frame_factory.build_headers_frame(self.base_request_headers) trailer_frame = frame_factory.build_headers_frame( trailers, flags=["END_STREAM"] ) @@ -218,33 +202,33 @@ class TestSendingInvalidFrameSequences(object): Trying to send invalid header sequences cause ProtocolErrors to be thrown. """ + base_request_headers = [ - (':authority', 'example.com'), - (':path', '/'), - (':scheme', 'https'), - (':method', 'GET'), - ('user-agent', 'someua/0.0.1'), + (":authority", "example.com"), + (":path", "/"), + (":scheme", "https"), + (":method", "GET"), + ("user-agent", "someua/0.0.1"), ] invalid_header_blocks = [ - base_request_headers + [(':late', 'pseudo-header')], - [(':path', 'duplicate-pseudo-header')] + base_request_headers, - base_request_headers + [('te', 'chunked')], - base_request_headers + [('host', 'notexample.com')], - [header for header in base_request_headers - if header[0] != ':authority'], + base_request_headers + [(":late", "pseudo-header")], + [(":path", "duplicate-pseudo-header")] + base_request_headers, + base_request_headers + [("te", "chunked")], + base_request_headers + [("host", "notexample.com")], + [header for header in base_request_headers if header[0] != ":authority"], ] strippable_header_blocks = [ - base_request_headers + [('connection', 'close')], - base_request_headers + [('proxy-connection', 'close')], - base_request_headers + [('keep-alive', 'close')], - base_request_headers + [('transfer-encoding', 'gzip')], - base_request_headers + [('upgrade', 'super-protocol/1.1')] + base_request_headers + [("connection", "close")], + base_request_headers + [("proxy-connection", "close")], + base_request_headers + [("keep-alive", "close")], + base_request_headers + [("transfer-encoding", "gzip")], + base_request_headers + [("upgrade", "super-protocol/1.1")], ] all_header_blocks = invalid_header_blocks + strippable_header_blocks server_config = h2.config.H2Configuration(client_side=False) - @pytest.mark.parametrize('headers', invalid_header_blocks) + @pytest.mark.parametrize("headers", invalid_header_blocks) def test_headers_event(self, frame_factory, headers): """ Test sending invalid headers raise a ProtocolError. @@ -257,7 +241,7 @@ def test_headers_event(self, frame_factory, headers): with pytest.raises(h2.exceptions.ProtocolError): c.send_headers(1, headers) - @pytest.mark.parametrize('headers', invalid_header_blocks) + @pytest.mark.parametrize("headers", invalid_header_blocks) def test_send_push_promise(self, frame_factory, headers): """ Sending invalid headers in a push promise raises a ProtocolError. @@ -266,27 +250,21 @@ def test_send_push_promise(self, frame_factory, headers): c.initiate_connection() c.receive_data(frame_factory.preamble()) - header_frame = frame_factory.build_headers_frame( - self.base_request_headers - ) + header_frame = frame_factory.build_headers_frame(self.base_request_headers) c.receive_data(header_frame.serialize()) # Clear the data, then try to send a push promise. c.clear_outbound_data_buffer() with pytest.raises(h2.exceptions.ProtocolError): - c.push_stream( - stream_id=1, promised_stream_id=2, request_headers=headers - ) + c.push_stream(stream_id=1, promised_stream_id=2, request_headers=headers) - @pytest.mark.parametrize('headers', all_header_blocks) + @pytest.mark.parametrize("headers", all_header_blocks) def test_headers_event_skipping_validation(self, frame_factory, headers): """ If we have ``validate_outbound_headers`` disabled, then all of these invalid header blocks are allowed to pass. """ - config = h2.config.H2Configuration( - validate_outbound_headers=False - ) + config = h2.config.H2Configuration(validate_outbound_headers=False) c = h2.connection.H2Connection(config=config) c.initiate_connection() @@ -296,11 +274,13 @@ def test_headers_event_skipping_validation(self, frame_factory, headers): c.send_headers(1, headers) # Ensure headers are still normalized. - norm_headers = h2.utilities.normalize_outbound_headers(headers, None, False) + norm_headers = h2.utilities.normalize_outbound_headers( + h2.utilities.utf8_encode_headers(headers), None, False + ) f = frame_factory.build_headers_frame(norm_headers) assert c.data_to_send() == f.serialize() - @pytest.mark.parametrize('headers', all_header_blocks) + @pytest.mark.parametrize("headers", all_header_blocks) def test_push_promise_skipping_validation(self, frame_factory, headers): """ If we have ``validate_outbound_headers`` disabled, then all of these @@ -315,34 +295,31 @@ def test_push_promise_skipping_validation(self, frame_factory, headers): c.initiate_connection() c.receive_data(frame_factory.preamble()) - header_frame = frame_factory.build_headers_frame( - self.base_request_headers - ) + header_frame = frame_factory.build_headers_frame(self.base_request_headers) c.receive_data(header_frame.serialize()) # Create push promise frame with normalized headers. frame_factory.refresh_encoder() - norm_headers = h2.utilities.normalize_outbound_headers(headers, None, False) + norm_headers = h2.utilities.normalize_outbound_headers( + h2.utilities.utf8_encode_headers(headers), None, False + ) pp_frame = frame_factory.build_push_promise_frame( stream_id=1, promised_stream_id=2, headers=norm_headers ) # Clear the data, then send a push promise. c.clear_outbound_data_buffer() - c.push_stream( - stream_id=1, promised_stream_id=2, request_headers=headers - ) + c.push_stream(stream_id=1, promised_stream_id=2, request_headers=headers) assert c.data_to_send() == pp_frame.serialize() - @pytest.mark.parametrize('headers', all_header_blocks) + @pytest.mark.parametrize("headers", all_header_blocks) def test_headers_event_skip_normalization(self, frame_factory, headers): """ If we have ``normalize_outbound_headers`` disabled, then all of these invalid header blocks are sent through unmodified. """ config = h2.config.H2Configuration( - validate_outbound_headers=False, - normalize_outbound_headers=False + validate_outbound_headers=False, normalize_outbound_headers=False ) c = h2.connection.H2Connection(config=config) @@ -358,7 +335,7 @@ def test_headers_event_skip_normalization(self, frame_factory, headers): c.send_headers(1, headers) assert c.data_to_send() == f.serialize() - @pytest.mark.parametrize('headers', all_header_blocks) + @pytest.mark.parametrize("headers", all_header_blocks) def test_push_promise_skip_normalization(self, frame_factory, headers): """ If we have ``normalize_outbound_headers`` disabled, then all of these @@ -374,9 +351,7 @@ def test_push_promise_skip_normalization(self, frame_factory, headers): c.initiate_connection() c.receive_data(frame_factory.preamble()) - header_frame = frame_factory.build_headers_frame( - self.base_request_headers - ) + header_frame = frame_factory.build_headers_frame(self.base_request_headers) c.receive_data(header_frame.serialize()) frame_factory.refresh_encoder() @@ -386,12 +361,10 @@ def test_push_promise_skip_normalization(self, frame_factory, headers): # Clear the data, then send a push promise. c.clear_outbound_data_buffer() - c.push_stream( - stream_id=1, promised_stream_id=2, request_headers=headers - ) + c.push_stream(stream_id=1, promised_stream_id=2, request_headers=headers) assert c.data_to_send() == pp_frame.serialize() - @pytest.mark.parametrize('headers', strippable_header_blocks) + @pytest.mark.parametrize("headers", strippable_header_blocks) def test_strippable_headers(self, frame_factory, headers): """ Test connection related headers are removed before sending. @@ -416,9 +389,10 @@ class TestFilter(object): HTTP/2 and so may never hit the function, but it's worth validating that it behaves as expected anyway. """ + validation_functions = [ h2.utilities.validate_headers, - h2.utilities.validate_outbound_headers + h2.utilities.validate_outbound_headers, ] hdr_validation_combos = [ @@ -431,146 +405,106 @@ class TestFilter(object): ] hdr_validation_response_headers = [ - flags for flags in hdr_validation_combos - if flags.is_response_header + flags for flags in hdr_validation_combos if flags.is_response_header ] hdr_validation_request_headers_no_trailer = [ - flags for flags in hdr_validation_combos + flags + for flags in hdr_validation_combos if not (flags.is_trailer or flags.is_response_header) ] invalid_request_header_blocks_bytes = ( # First, missing :method ( - (b':authority', b'google.com'), - (b':path', b'/'), - (b':scheme', b'https'), + (b":authority", b"google.com"), + (b":path", b"/"), + (b":scheme", b"https"), ), # Next, missing :path ( - (b':authority', b'google.com'), - (b':method', b'GET'), - (b':scheme', b'https'), + (b":authority", b"google.com"), + (b":method", b"GET"), + (b":scheme", b"https"), ), # Next, missing :scheme ( - (b':authority', b'google.com'), - (b':method', b'GET'), - (b':path', b'/'), + (b":authority", b"google.com"), + (b":method", b"GET"), + (b":path", b"/"), ), # Finally, path present but empty. ( - (b':authority', b'google.com'), - (b':method', b'GET'), - (b':scheme', b'https'), - (b':path', b''), - ), - ) - invalid_request_header_blocks_unicode = ( - # First, missing :method - ( - (':authority', 'google.com'), - (':path', '/'), - (':scheme', 'https'), - ), - # Next, missing :path - ( - (':authority', 'google.com'), - (':method', 'GET'), - (':scheme', 'https'), - ), - # Next, missing :scheme - ( - (':authority', 'google.com'), - (':method', 'GET'), - (':path', '/'), - ), - # Finally, path present but empty. - ( - (':authority', 'google.com'), - (':method', 'GET'), - (':scheme', 'https'), - (':path', ''), + (b":authority", b"google.com"), + (b":method", b"GET"), + (b":scheme", b"https"), + (b":path", b""), ), ) # All headers that are forbidden from either request or response blocks. - forbidden_request_headers_bytes = (b':status',) - forbidden_request_headers_unicode = (':status',) - forbidden_response_headers_bytes = ( - b':path', b':scheme', b':authority', b':method' - ) - forbidden_response_headers_unicode = ( - ':path', ':scheme', ':authority', ':method' - ) + forbidden_request_headers_bytes = (b":status",) + forbidden_response_headers_bytes = (b":path", b":scheme", b":authority", b":method") - @pytest.mark.parametrize('validation_function', validation_functions) - @pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos) + @pytest.mark.parametrize("validation_function", validation_functions) + @pytest.mark.parametrize("hdr_validation_flags", hdr_validation_combos) @given(headers=HEADERS_STRATEGY) - def test_range_of_acceptable_outputs(self, - headers, - validation_function, - hdr_validation_flags): + def test_range_of_acceptable_outputs( + self, headers, validation_function, hdr_validation_flags + ): """ The header validation functions either return the data unchanged or throw a ProtocolError. """ try: - assert headers == list(validation_function( - headers, hdr_validation_flags)) + assert headers == list(validation_function(headers, hdr_validation_flags)) except h2.exceptions.ProtocolError: assert True - @pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos) + @pytest.mark.parametrize("hdr_validation_flags", hdr_validation_combos) def test_invalid_pseudo_headers(self, hdr_validation_flags): - headers = [(b':custom', b'value')] + headers = [(b":custom", b"value")] with pytest.raises(h2.exceptions.ProtocolError): list(h2.utilities.validate_headers(headers, hdr_validation_flags)) - @pytest.mark.parametrize('validation_function', validation_functions) + @pytest.mark.parametrize("validation_function", validation_functions) @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_request_headers_no_trailer + "hdr_validation_flags", hdr_validation_request_headers_no_trailer ) - def test_matching_authority_host_headers(self, - validation_function, - hdr_validation_flags): + def test_matching_authority_host_headers( + self, validation_function, hdr_validation_flags + ): """ If a header block has :authority and Host headers and they match, the headers should pass through unchanged. """ headers = [ - (b':authority', b'example.com'), - (b':path', b'/'), - (b':scheme', b'https'), - (b':method', b'GET'), - (b'host', b'example.com'), + (b":authority", b"example.com"), + (b":path", b"/"), + (b":scheme", b"https"), + (b":method", b"GET"), + (b"host", b"example.com"), ] - assert headers == list(h2.utilities.validate_headers( - headers, hdr_validation_flags - )) + assert headers == list( + h2.utilities.validate_headers(headers, hdr_validation_flags) + ) - @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_response_headers - ) + @pytest.mark.parametrize("hdr_validation_flags", hdr_validation_response_headers) def test_response_header_without_status(self, hdr_validation_flags): - headers = [(b'content-length', b'42')] + headers = [(b"content-length", b"42")] with pytest.raises(h2.exceptions.ProtocolError): list(h2.utilities.validate_headers(headers, hdr_validation_flags)) @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_request_headers_no_trailer + "hdr_validation_flags", hdr_validation_request_headers_no_trailer ) @pytest.mark.parametrize( - 'header_block', - ( - invalid_request_header_blocks_bytes + - invalid_request_header_blocks_unicode - ) + "header_block", + (invalid_request_header_blocks_bytes), ) - def test_outbound_req_header_missing_pseudo_headers(self, - hdr_validation_flags, - header_block): + def test_outbound_req_header_missing_pseudo_headers( + self, hdr_validation_flags, header_block + ): with pytest.raises(h2.exceptions.ProtocolError): list( h2.utilities.validate_outbound_headers( @@ -579,131 +513,113 @@ def test_outbound_req_header_missing_pseudo_headers(self, ) @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_request_headers_no_trailer - ) - @pytest.mark.parametrize( - 'header_block', invalid_request_header_blocks_bytes + "hdr_validation_flags", hdr_validation_request_headers_no_trailer ) - def test_inbound_req_header_missing_pseudo_headers(self, - hdr_validation_flags, - header_block): + @pytest.mark.parametrize("header_block", invalid_request_header_blocks_bytes) + def test_inbound_req_header_missing_pseudo_headers( + self, hdr_validation_flags, header_block + ): with pytest.raises(h2.exceptions.ProtocolError): - list( - h2.utilities.validate_headers( - header_block, hdr_validation_flags - ) - ) + list(h2.utilities.validate_headers(header_block, hdr_validation_flags)) @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_request_headers_no_trailer + "hdr_validation_flags", hdr_validation_request_headers_no_trailer ) @pytest.mark.parametrize( - 'invalid_header', - forbidden_request_headers_bytes + forbidden_request_headers_unicode + "invalid_header", + forbidden_request_headers_bytes, ) - def test_outbound_req_header_extra_pseudo_headers(self, - hdr_validation_flags, - invalid_header): + def test_outbound_req_header_extra_pseudo_headers( + self, hdr_validation_flags, invalid_header + ): """ Outbound request header blocks containing the forbidden request headers fail validation. """ headers = [ - (b':path', b'/'), - (b':scheme', b'https'), - (b':authority', b'google.com'), - (b':method', b'GET'), + (b":path", b"/"), + (b":scheme", b"https"), + (b":authority", b"google.com"), + (b":method", b"GET"), ] - headers.append((invalid_header, b'some value')) + headers.append((invalid_header, b"some value")) with pytest.raises(h2.exceptions.ProtocolError): - list( - h2.utilities.validate_outbound_headers( - headers, hdr_validation_flags - ) - ) + list(h2.utilities.validate_outbound_headers(headers, hdr_validation_flags)) @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_request_headers_no_trailer + "hdr_validation_flags", hdr_validation_request_headers_no_trailer ) - @pytest.mark.parametrize( - 'invalid_header', - forbidden_request_headers_bytes - ) - def test_inbound_req_header_extra_pseudo_headers(self, - hdr_validation_flags, - invalid_header): + @pytest.mark.parametrize("invalid_header", forbidden_request_headers_bytes) + def test_inbound_req_header_extra_pseudo_headers( + self, hdr_validation_flags, invalid_header + ): """ Inbound request header blocks containing the forbidden request headers fail validation. """ headers = [ - (b':path', b'/'), - (b':scheme', b'https'), - (b':authority', b'google.com'), - (b':method', b'GET'), + (b":path", b"/"), + (b":scheme", b"https"), + (b":authority", b"google.com"), + (b":method", b"GET"), ] - headers.append((invalid_header, b'some value')) + headers.append((invalid_header, b"some value")) with pytest.raises(h2.exceptions.ProtocolError): list(h2.utilities.validate_headers(headers, hdr_validation_flags)) + @pytest.mark.parametrize("hdr_validation_flags", hdr_validation_response_headers) @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_response_headers + "invalid_header", + forbidden_response_headers_bytes, ) - @pytest.mark.parametrize( - 'invalid_header', - forbidden_response_headers_bytes + forbidden_response_headers_unicode - ) - def test_outbound_resp_header_extra_pseudo_headers(self, - hdr_validation_flags, - invalid_header): + def test_outbound_resp_header_extra_pseudo_headers( + self, hdr_validation_flags, invalid_header + ): """ Outbound response header blocks containing the forbidden response headers fail validation. """ - headers = [(b':status', b'200')] - headers.append((invalid_header, b'some value')) + headers = [(b":status", b"200")] + headers.append((invalid_header, b"some value")) with pytest.raises(h2.exceptions.ProtocolError): - list( - h2.utilities.validate_outbound_headers( - headers, hdr_validation_flags - ) - ) + list(h2.utilities.validate_outbound_headers(headers, hdr_validation_flags)) - @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_response_headers - ) - @pytest.mark.parametrize( - 'invalid_header', - forbidden_response_headers_bytes - ) - def test_inbound_resp_header_extra_pseudo_headers(self, - hdr_validation_flags, - invalid_header): + @pytest.mark.parametrize("hdr_validation_flags", hdr_validation_response_headers) + @pytest.mark.parametrize("invalid_header", forbidden_response_headers_bytes) + def test_inbound_resp_header_extra_pseudo_headers( + self, hdr_validation_flags, invalid_header + ): """ Inbound response header blocks containing the forbidden response headers fail validation. """ - headers = [(b':status', b'200')] - headers.append((invalid_header, b'some value')) + headers = [(b":status", b"200")] + headers.append((invalid_header, b"some value")) with pytest.raises(h2.exceptions.ProtocolError): list(h2.utilities.validate_headers(headers, hdr_validation_flags)) - @pytest.mark.parametrize('hdr_validation_flags', hdr_validation_combos) + @pytest.mark.parametrize("hdr_validation_flags", hdr_validation_combos) def test_inbound_header_name_length(self, hdr_validation_flags): with pytest.raises(h2.exceptions.ProtocolError): - list(h2.utilities.validate_headers([(b'', b'foobar')], hdr_validation_flags)) + list( + h2.utilities.validate_headers([(b"", b"foobar")], hdr_validation_flags) + ) def test_inbound_header_name_length_full_frame_decode(self, frame_factory): f = frame_factory.build_headers_frame([]) f.data = b"\x00\x00\x05\x00\x00\x00\x00\x04" data = f.serialize() - c = h2.connection.H2Connection(config=h2.config.H2Configuration(client_side=False)) + c = h2.connection.H2Connection( + config=h2.config.H2Configuration(client_side=False) + ) c.initiate_connection() c.receive_data(frame_factory.preamble()) c.clear_outbound_data_buffer() - with pytest.raises(h2.exceptions.ProtocolError, match="Received header name with zero length."): + with pytest.raises( + h2.exceptions.ProtocolError, match="Received header name with zero length." + ): c.receive_data(data) @@ -712,15 +628,16 @@ class TestOversizedHeaders(object): Tests that oversized header blocks are correctly rejected. This replicates the "HPACK Bomb" attack, and confirms that we're resistant against it. """ + request_header_block = [ - (b':method', b'GET'), - (b':authority', b'example.com'), - (b':scheme', b'https'), - (b':path', b'/'), + (b":method", b"GET"), + (b":authority", b"example.com"), + (b":scheme", b"https"), + (b":path", b"/"), ] response_header_block = [ - (b':status', b'200'), + (b":status", b"200"), ] # The first header block contains a single header that fills the header @@ -729,14 +646,14 @@ class TestOversizedHeaders(object): # table. It must come last, so that it evicts all other headers. # This block must be appended to either a request or response block. first_header_block = [ - (b'a', b'a' * 4063), + (b"a", b"a" * 4063), ] # The second header "block" is actually a custom HEADERS frame body that # simply repeatedly refers to the first entry for 16kB. Each byte has the # high bit set (0x80), and then uses the remaining 7 bits to encode the # number 62 (0x3e), leading to a repeat of the byte 0xbe. - second_header_block = b'\xbe' * 2**14 + second_header_block = b"\xbe" * 2**14 server_config = h2.config.H2Configuration(client_side=False) @@ -758,7 +675,7 @@ def test_hpack_bomb_request(self, frame_factory): # Build the attack payload. attack_frame = hyperframe.frame.HeadersFrame(stream_id=3) attack_frame.data = self.second_header_block - attack_frame.flags.add('END_HEADERS') + attack_frame.flags.add("END_HEADERS") data = attack_frame.serialize() with pytest.raises(h2.exceptions.DenialOfServiceError): @@ -776,12 +693,8 @@ def test_hpack_bomb_response(self, frame_factory): """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers( - stream_id=1, headers=self.request_header_block - ) - c.send_headers( - stream_id=3, headers=self.request_header_block - ) + c.send_headers(stream_id=1, headers=self.request_header_block) + c.send_headers(stream_id=3, headers=self.request_header_block) c.clear_outbound_data_buffer() f = frame_factory.build_headers_frame( @@ -793,7 +706,7 @@ def test_hpack_bomb_response(self, frame_factory): # Build the attack payload. attack_frame = hyperframe.frame.HeadersFrame(stream_id=3) attack_frame.data = self.second_header_block - attack_frame.flags.add('END_HEADERS') + attack_frame.flags.add("END_HEADERS") data = attack_frame.serialize() with pytest.raises(h2.exceptions.DenialOfServiceError): @@ -811,9 +724,7 @@ def test_hpack_bomb_push(self, frame_factory): """ c = h2.connection.H2Connection() c.initiate_connection() - c.send_headers( - stream_id=1, headers=self.request_header_block - ) + c.send_headers(stream_id=1, headers=self.request_header_block) c.clear_outbound_data_buffer() f = frame_factory.build_headers_frame( @@ -827,7 +738,7 @@ def test_hpack_bomb_push(self, frame_factory): attack_frame = hyperframe.frame.PushPromiseFrame(stream_id=3) attack_frame.promised_stream_id = 2 attack_frame.data = self.second_header_block[:-4] - attack_frame.flags.add('END_HEADERS') + attack_frame.flags.add("END_HEADERS") data = attack_frame.serialize() with pytest.raises(h2.exceptions.DenialOfServiceError): @@ -849,8 +760,7 @@ def test_reject_headers_when_list_size_shrunk(self, frame_factory): # Receive the first request, which causes no problem. f = frame_factory.build_headers_frame( - stream_id=1, - headers=self.request_header_block + stream_id=1, headers=self.request_header_block ) data = f.serialize() c.receive_data(data) @@ -860,8 +770,7 @@ def test_reject_headers_when_list_size_shrunk(self, frame_factory): c.update_settings({h2.settings.SettingCodes.MAX_HEADER_LIST_SIZE: 50}) c.clear_outbound_data_buffer() f = frame_factory.build_headers_frame( - stream_id=3, - headers=self.request_header_block + stream_id=3, headers=self.request_header_block ) data = f.serialize() c.receive_data(data) @@ -873,8 +782,7 @@ def test_reject_headers_when_list_size_shrunk(self, frame_factory): # Now a third request comes in. This explodes. f = frame_factory.build_headers_frame( - stream_id=5, - headers=self.request_header_block + stream_id=5, headers=self.request_header_block ) data = f.serialize() @@ -897,8 +805,7 @@ def test_reject_headers_when_table_size_shrunk(self, frame_factory): # Receive the first request, which causes no problem. f = frame_factory.build_headers_frame( - stream_id=1, - headers=self.request_header_block + stream_id=1, headers=self.request_header_block ) data = f.serialize() c.receive_data(data) @@ -908,8 +815,7 @@ def test_reject_headers_when_table_size_shrunk(self, frame_factory): c.update_settings({h2.settings.SettingCodes.HEADER_TABLE_SIZE: 128}) c.clear_outbound_data_buffer() f = frame_factory.build_headers_frame( - stream_id=3, - headers=self.request_header_block + stream_id=3, headers=self.request_header_block ) data = f.serialize() c.receive_data(data) @@ -922,8 +828,7 @@ def test_reject_headers_when_table_size_shrunk(self, frame_factory): # Now a third request comes in. This explodes, as it does not contain # a dynamic table size update. f = frame_factory.build_headers_frame( - stream_id=5, - headers=self.request_header_block + stream_id=5, headers=self.request_header_block ) data = f.serialize() @@ -946,8 +851,7 @@ def test_reject_headers_exceeding_table_size(self, frame_factory): # Receive the first request, which causes no problem. f = frame_factory.build_headers_frame( - stream_id=1, - headers=self.request_header_block + stream_id=1, headers=self.request_header_block ) data = f.serialize() c.receive_data(data) @@ -956,8 +860,7 @@ def test_reject_headers_exceeding_table_size(self, frame_factory): # This explodes. frame_factory.change_table_size(c.local_settings.header_table_size + 1) f = frame_factory.build_headers_frame( - stream_id=5, - headers=self.request_header_block + stream_id=5, headers=self.request_header_block ) data = f.serialize() diff --git a/test/test_utility_functions.py b/test/test_utility_functions.py index c6578df35..579e145df 100644 --- a/test/test_utility_functions.py +++ b/test/test_utility_functions.py @@ -19,16 +19,14 @@ class TestGetNextAvailableStreamID(object): """ Tests for the ``H2Connection.get_next_available_stream_id`` method. """ + example_request_headers = [ - (':authority', 'example.com'), - (':path', '/'), - (':scheme', 'https'), - (':method', 'GET'), - ] - example_response_headers = [ - (':status', '200'), - ('server', 'fake-serv/0.1.0') + (":authority", "example.com"), + (":path", "/"), + (":scheme", "https"), + (":method", "GET"), ] + example_response_headers = [(":status", "200"), ("server", "fake-serv/0.1.0")] server_config = h2.config.H2Configuration(client_side=False) def test_returns_correct_sequence_for_clients(self, frame_factory): @@ -55,12 +53,12 @@ def test_returns_correct_sequence_for_clients(self, frame_factory): c.send_headers( stream_id=stream_id, headers=self.example_request_headers, - end_stream=True + end_stream=True, ) f = frame_factory.build_headers_frame( headers=self.example_response_headers, stream_id=stream_id, - flags=['END_STREAM'], + flags=["END_STREAM"], ) c.receive_data(f.serialize()) c.clear_outbound_data_buffer() @@ -71,7 +69,7 @@ def test_returns_correct_sequence_for_clients(self, frame_factory): c.send_headers( stream_id=last_client_id, headers=self.example_request_headers, - end_stream=True + end_stream=True, ) with pytest.raises(h2.exceptions.NoAvailableStreamIDError): @@ -93,9 +91,7 @@ def test_returns_correct_sequence_for_servers(self, frame_factory): c = h2.connection.H2Connection(config=self.server_config) c.initiate_connection() c.receive_data(frame_factory.preamble()) - f = frame_factory.build_headers_frame( - headers=self.example_request_headers - ) + f = frame_factory.build_headers_frame(headers=self.example_request_headers) c.receive_data(f.serialize()) initial_sequence = range(2, 2**13, 2) @@ -107,12 +103,12 @@ def test_returns_correct_sequence_for_servers(self, frame_factory): c.push_stream( stream_id=1, promised_stream_id=stream_id, - request_headers=self.example_request_headers + request_headers=self.example_request_headers, ) c.send_headers( stream_id=stream_id, headers=self.example_response_headers, - end_stream=True + end_stream=True, ) c.clear_outbound_data_buffer() @@ -141,10 +137,7 @@ def test_does_not_increment_without_stream_send(self): assert first_stream_id == second_stream_id - c.send_headers( - stream_id=first_stream_id, - headers=self.example_request_headers - ) + c.send_headers(stream_id=first_stream_id, headers=self.example_request_headers) third_stream_id = c.get_next_available_stream_id() assert third_stream_id == (first_stream_id + 2) @@ -152,24 +145,15 @@ def test_does_not_increment_without_stream_send(self): class TestExtractHeader(object): - example_request_headers = [ - (u':authority', u'example.com'), - (u':path', u'/'), - (u':scheme', u'https'), - (u':method', u'GET'), - ] example_headers_with_bytes = [ - (b':authority', b'example.com'), - (b':path', b'/'), - (b':scheme', b'https'), - (b':method', b'GET'), + (b":authority", b"example.com"), + (b":path", b"/"), + (b":scheme", b"https"), + (b":method", b"GET"), ] - @pytest.mark.parametrize( - 'headers', [example_request_headers, example_headers_with_bytes] - ) - def test_extract_header_method(self, headers): - assert extract_method_header(headers) == b'GET' + def test_extract_header_method(self): + assert extract_method_header(self.example_headers_with_bytes) == b"GET" def test_size_limit_dict_limit(): diff --git a/tox.ini b/tox.ini index eaf0a4360..6d8ddc563 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ deps = pytest-xdist>=2.0.0,<3 hypothesis>=5.5,<7 commands = - pytest --cov-report=xml --cov-report=term --cov=h2 {posargs} + python -bb -m pytest --cov-report=xml --cov-report=term --cov=h2 {posargs} [testenv:pypy3] # temporarily disable coverage testing on PyPy due to performance problems