Source code

Revision control

Copy as Markdown

Other Tools

# -*- coding: utf-8 -*-
"""
test_invalid_frame_sequences.py
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module contains tests that use invalid frame sequences, and validates that
they fail appropriately.
"""
import pytest
import h2.config
import h2.connection
import h2.errors
import h2.events
import h2.exceptions
class TestInvalidFrameSequences(object):
"""
Invalid frame sequences, either sent or received, cause ProtocolErrors to
be thrown.
"""
example_request_headers = [
(':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)
client_config = h2.config.H2Configuration(client_side=True)
def test_cannot_send_on_closed_stream(self):
"""
When we've closed a stream locally, we cannot send further data.
"""
c = h2.connection.H2Connection()
c.initiate_connection()
c.send_headers(1, self.example_request_headers, end_stream=True)
with pytest.raises(h2.exceptions.ProtocolError):
c.send_data(1, b'some data')
def test_missing_preamble_errors(self):
"""
Server side connections require the preamble.
"""
c = h2.connection.H2Connection(config=self.server_config)
encoded_headers_frame = (
b'\x00\x00\r\x01\x04\x00\x00\x00\x01'
b'A\x88/\x91\xd3]\x05\\\x87\xa7\x84\x87\x82'
)
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(encoded_headers_frame)
def test_server_connections_reject_even_streams(self, frame_factory):
"""
Servers do not allow clients to initiate even-numbered streams.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_headers_frame(
self.example_request_headers, stream_id=2
)
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(f.serialize())
def test_clients_reject_odd_stream_pushes(self, frame_factory):
"""
Clients do not allow servers to push odd numbered streams.
"""
c = h2.connection.H2Connection()
c.initiate_connection()
c.send_headers(1, self.example_request_headers, end_stream=True)
f = frame_factory.build_push_promise_frame(
stream_id=1,
headers=self.example_request_headers,
promised_stream_id=3
)
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(f.serialize())
def test_can_handle_frames_with_invalid_padding(self, frame_factory):
"""
Frames with invalid padding cause connection teardown.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_headers_frame(self.example_request_headers)
c.receive_data(f.serialize())
c.clear_outbound_data_buffer()
invalid_data_frame = (
b'\x00\x00\x05\x00\x0b\x00\x00\x00\x01\x06\x54\x65\x73\x74'
)
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(invalid_data_frame)
expected_frame = frame_factory.build_goaway_frame(
last_stream_id=1, error_code=1
)
assert c.data_to_send() == expected_frame.serialize()
def test_receiving_frames_with_insufficent_size(self, frame_factory):
"""
Frames with not enough data cause connection teardown.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
c.clear_outbound_data_buffer()
invalid_window_update_frame = (
b'\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x02'
)
with pytest.raises(h2.exceptions.FrameDataMissingError):
c.receive_data(invalid_window_update_frame)
expected_frame = frame_factory.build_goaway_frame(
last_stream_id=0, error_code=h2.errors.ErrorCodes.FRAME_SIZE_ERROR
)
assert c.data_to_send() == expected_frame.serialize()
def test_reject_data_on_closed_streams(self, frame_factory):
"""
When a stream is not open to the remote peer, we reject receiving data
frames from them.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_headers_frame(
self.example_request_headers,
flags=['END_STREAM']
)
c.receive_data(f.serialize())
c.clear_outbound_data_buffer()
bad_frame = frame_factory.build_data_frame(
data=b'some data'
)
c.receive_data(bad_frame.serialize())
expected = frame_factory.build_rst_stream_frame(
stream_id=1,
error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
).serialize()
assert c.data_to_send() == expected
def test_unexpected_continuation_on_closed_stream(self, frame_factory):
"""
CONTINUATION frames received on closed streams cause connection errors
of type PROTOCOL_ERROR.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_headers_frame(
self.example_request_headers,
flags=['END_STREAM']
)
c.receive_data(f.serialize())
c.clear_outbound_data_buffer()
bad_frame = frame_factory.build_continuation_frame(
header_block=b'hello'
)
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(bad_frame.serialize())
expected_frame = frame_factory.build_goaway_frame(
error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR,
last_stream_id=1
)
assert c.data_to_send() == expected_frame.serialize()
def test_prevent_continuation_dos(self, frame_factory):
"""
Receiving too many CONTINUATION frames in one block causes a protocol
error.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_headers_frame(
self.example_request_headers,
)
f.flags = {'END_STREAM'}
c.receive_data(f.serialize())
c.clear_outbound_data_buffer()
# Send 63 additional frames.
for _ in range(0, 63):
extra_frame = frame_factory.build_continuation_frame(
header_block=b'hello'
)
c.receive_data(extra_frame.serialize())
# The final continuation frame should cause a protocol error.
extra_frame = frame_factory.build_continuation_frame(
header_block=b'hello'
)
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(extra_frame.serialize())
expected_frame = frame_factory.build_goaway_frame(
last_stream_id=0,
error_code=0x1,
)
assert c.data_to_send() == expected_frame.serialize()
# These settings are a bit annoyingly anonymous, but trust me, they're bad.
@pytest.mark.parametrize(
"settings",
[
{0x2: 5},
{0x4: 2**31},
{0x5: 5},
{0x5: 2**24},
]
)
def test_reject_invalid_settings_values(self, frame_factory, settings):
"""
When a SETTINGS frame is received with invalid settings values it
causes connection teardown with the appropriate error code.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_settings_frame(settings=settings)
with pytest.raises(h2.exceptions.InvalidSettingsValueError) as e:
c.receive_data(f.serialize())
assert e.value.error_code == (
h2.errors.ErrorCodes.FLOW_CONTROL_ERROR if 0x4 in settings else
h2.errors.ErrorCodes.PROTOCOL_ERROR
)
def test_invalid_frame_headers_are_protocol_errors(self, frame_factory):
"""
When invalid frame headers are received they cause ProtocolErrors to be
raised.
"""
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
)
# Do some annoying bit twiddling here: the stream ID is currently set
# to '1', change it to '0'. Grab the first 9 bytes (the frame header),
# replace any instances of the byte '\x01', and then graft it onto the
# remaining bytes.
frame_data = f.serialize()
frame_data = frame_data[:9].replace(b'\x01', b'\x00') + frame_data[9:]
with pytest.raises(h2.exceptions.ProtocolError) as e:
c.receive_data(frame_data)
assert "Received frame with invalid header" in str(e.value)
def test_data_before_headers(self, frame_factory):
"""
When data frames are received before headers
they cause ProtocolErrors to be raised.
"""
c = h2.connection.H2Connection(config=self.client_config)
c.initiate_connection()
# transition stream into OPEN
c.send_headers(1, self.example_request_headers)
f = frame_factory.build_data_frame(b"hello")
with pytest.raises(h2.exceptions.ProtocolError) as e:
c.receive_data(f.serialize())
assert "cannot receive data before headers" in str(e.value)
def test_get_stream_reset_event_on_auto_reset(self, frame_factory):
"""
When hyper-h2 resets a stream automatically, a StreamReset event fires.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_headers_frame(
self.example_request_headers,
flags=['END_STREAM']
)
c.receive_data(f.serialize())
c.clear_outbound_data_buffer()
bad_frame = frame_factory.build_data_frame(
data=b'some data'
)
events = c.receive_data(bad_frame.serialize())
expected = frame_factory.build_rst_stream_frame(
stream_id=1,
error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
).serialize()
assert c.data_to_send() == expected
assert len(events) == 1
event = events[0]
assert isinstance(event, h2.events.StreamReset)
assert event.stream_id == 1
assert event.error_code == h2.errors.ErrorCodes.STREAM_CLOSED
assert not event.remote_reset
def test_one_one_stream_reset(self, frame_factory):
"""
When hyper-h2 resets a stream automatically, a StreamReset event fires,
but only for the first reset: the others are silent.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_headers_frame(
self.example_request_headers,
flags=['END_STREAM']
)
c.receive_data(f.serialize())
c.clear_outbound_data_buffer()
bad_frame = frame_factory.build_data_frame(
data=b'some data'
)
# Receive 5 frames.
events = c.receive_data(bad_frame.serialize() * 5)
expected = frame_factory.build_rst_stream_frame(
stream_id=1,
error_code=h2.errors.ErrorCodes.STREAM_CLOSED,
).serialize()
assert c.data_to_send() == expected * 5
assert len(events) == 1
event = events[0]
assert isinstance(event, h2.events.StreamReset)
assert event.stream_id == 1
assert event.error_code == h2.errors.ErrorCodes.STREAM_CLOSED
assert not event.remote_reset
@pytest.mark.parametrize('value', ['', 'twelve'])
def test_error_on_invalid_content_length(self, frame_factory, value):
"""
When an invalid content-length is received, a ProtocolError is thrown.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
c.clear_outbound_data_buffer()
f = frame_factory.build_headers_frame(
stream_id=1,
headers=self.example_request_headers + [('content-length', value)]
)
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(f.serialize())
expected_frame = frame_factory.build_goaway_frame(
last_stream_id=1,
error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
)
assert c.data_to_send() == expected_frame.serialize()
def test_invalid_header_data_protocol_error(self, frame_factory):
"""
If an invalid header block is received, we raise a ProtocolError.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
c.clear_outbound_data_buffer()
f = frame_factory.build_headers_frame(
stream_id=1,
headers=self.example_request_headers
)
f.data = b'\x00\x00\x00\x00'
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(f.serialize())
expected_frame = frame_factory.build_goaway_frame(
last_stream_id=0,
error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
)
assert c.data_to_send() == expected_frame.serialize()
def test_invalid_push_promise_data_protocol_error(self, frame_factory):
"""
If an invalid header block is received on a PUSH_PROMISE, we raise a
ProtocolError.
"""
c = h2.connection.H2Connection()
c.initiate_connection()
c.send_headers(stream_id=1, headers=self.example_request_headers)
c.clear_outbound_data_buffer()
f = frame_factory.build_push_promise_frame(
stream_id=1,
promised_stream_id=2,
headers=self.example_request_headers
)
f.data = b'\x00\x00\x00\x00'
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(f.serialize())
expected_frame = frame_factory.build_goaway_frame(
last_stream_id=0,
error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
)
assert c.data_to_send() == expected_frame.serialize()
def test_cannot_receive_push_on_pushed_stream(self, frame_factory):
"""
If a PUSH_PROMISE frame is received with the parent stream ID being a
pushed stream, this is rejected with a PROTOCOL_ERROR.
"""
c = h2.connection.H2Connection()
c.initiate_connection()
c.send_headers(
stream_id=1,
headers=self.example_request_headers,
end_stream=True
)
f1 = frame_factory.build_push_promise_frame(
stream_id=1,
promised_stream_id=2,
headers=self.example_request_headers,
)
f2 = frame_factory.build_headers_frame(
stream_id=2,
headers=self.example_response_headers,
)
c.receive_data(f1.serialize() + f2.serialize())
c.clear_outbound_data_buffer()
f = frame_factory.build_push_promise_frame(
stream_id=2,
promised_stream_id=4,
headers=self.example_request_headers,
)
with pytest.raises(h2.exceptions.ProtocolError):
c.receive_data(f.serialize())
expected_frame = frame_factory.build_goaway_frame(
last_stream_id=2,
error_code=h2.errors.ErrorCodes.PROTOCOL_ERROR
)
assert c.data_to_send() == expected_frame.serialize()
def test_cannot_send_push_on_pushed_stream(self, frame_factory):
"""
If a user tries to send a PUSH_PROMISE frame with the parent stream ID
being a pushed stream, this is rejected with a PROTOCOL_ERROR.
"""
c = h2.connection.H2Connection(config=self.server_config)
c.initiate_connection()
c.receive_data(frame_factory.preamble())
f = frame_factory.build_headers_frame(
stream_id=1, headers=self.example_request_headers
)
c.receive_data(f.serialize())
c.push_stream(
stream_id=1,
promised_stream_id=2,
request_headers=self.example_request_headers
)
c.send_headers(stream_id=2, headers=self.example_response_headers)
with pytest.raises(h2.exceptions.ProtocolError):
c.push_stream(
stream_id=2,
promised_stream_id=4,
request_headers=self.example_request_headers
)