Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ Protocol Errors
:show-inheritance:
:members:

.. autoclass:: h2.exceptions.InvalidBodyLengthError
:show-inheritance:
:members:


HTTP/2 Error Codes
------------------
Expand Down
17 changes: 17 additions & 0 deletions h2/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,20 @@ class InvalidSettingsValueError(ProtocolError, ValueError):
def __init__(self, msg, error_code):
super(InvalidSettingsValueError, self).__init__(msg)
self.error_code = error_code


class InvalidBodyLengthError(ProtocolError):
"""
The remote peer sent more or less data that the Content-Length header
indicated.

.. versionadded:: 2.0.0
"""
def __init__(self, expected, actual):
self.expected_length = expected
self.actual_length = actual

def __str__(self):
return "InvalidBodyLengthError: Expected %d bytes, received %d" % (
self.expected_length, self.actual_length
)
45 changes: 44 additions & 1 deletion h2/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
StreamEnded, PushedStreamReceived, StreamReset, TrailersReceived,
PriorityUpdated,
)
from .exceptions import ProtocolError, StreamClosedError
from .exceptions import (
ProtocolError, StreamClosedError, InvalidBodyLengthError
)
from .utilities import guard_increment_window


Expand Down Expand Up @@ -553,6 +555,12 @@ def __init__(self, stream_id):
self.outbound_flow_control_window = 65535
self.inbound_flow_control_window = 65535

# The expected content length, if any.
self._expected_content_length = None

# The actual received content length. Always tracked.
self._actual_content_length = 0

@property
def open(self):
"""
Expand Down Expand Up @@ -707,6 +715,8 @@ def receive_headers(self, headers, end_stream):
StreamInputs.RECV_END_STREAM
)

self._initialize_content_length(headers)

if isinstance(events[0], TrailersReceived):
if not end_stream:
raise ProtocolError("Trailers must have END_STREAM set")
Expand All @@ -720,6 +730,7 @@ def receive_data(self, data, end_stream, flow_control_len):
"""
events = self.state_machine.process_input(StreamInputs.RECV_DATA)
self.inbound_flow_control_window -= flow_control_len
self._track_content_length(len(data), end_stream)

if end_stream:
events += self.state_machine.process_input(
Expand Down Expand Up @@ -828,3 +839,35 @@ def _build_headers_frames(self,

frames[-1].flags.add('END_HEADERS')
return frames

def _initialize_content_length(self, headers):
"""
Checks the headers for a content-length header and initializes the
_expected_content_length field from it. It's not an error for no
Content-Length header to be present.
"""
for n, v in headers:
if n == 'content-length':
self._expected_content_length = int(v, 10)
return

def _track_content_length(self, length, end_stream):
"""
Update the expected content length in response to data being received.
Validates that the appropriate amount of data is sent. Always updates
the received data, but only validates the length against the
content-length header if one was sent.

:param length: The length of the body chunk received.
:param end_stream: If this is the last body chunk received.
"""
self._actual_content_length += length
actual = self._actual_content_length
expected = self._expected_content_length

if expected is not None:
if expected < actual:
raise InvalidBodyLengthError(expected, actual)

if end_stream and expected != actual:
raise InvalidBodyLengthError(expected, actual)
134 changes: 134 additions & 0 deletions test/test_invalid_content_lengths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
"""
test_invalid_content_lengths.py
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

This module contains tests that use invalid content lengths, and validates that
they fail appropriately.
"""
import pytest

import h2.connection
import h2.errors
import h2.events
import h2.exceptions


class TestInvalidContentLengths(object):
"""
Hyper-h2 raises Protocol Errors when the content-length sent by a remote
peer is not valid.
"""
example_request_headers = [
(':authority', 'example.com'),
(':path', '/'),
(':scheme', 'https'),
(':method', 'POST'),
('content-length', '15'),
]
example_response_headers = [
(':status', '200'),
('server', 'fake-serv/0.1.0')
]

def test_too_much_data(self, frame_factory):
"""
Remote peers sending data in excess of content-length causes Protocol
Errors.
"""
c = h2.connection.H2Connection(client_side=False)
c.initiate_connection()
c.receive_data(frame_factory.preamble())

headers = frame_factory.build_headers_frame(
headers=self.example_request_headers
)
first_data = frame_factory.build_data_frame(data=b'\x01'*15)
c.receive_data(headers.serialize() + first_data.serialize())
c.clear_outbound_data_buffer()

second_data = frame_factory.build_data_frame(data=b'\x01')
with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp:
c.receive_data(second_data.serialize())

assert exp.value.expected_length == 15
assert exp.value.actual_length == 16
assert str(exp.value) == (
"InvalidBodyLengthError: Expected 15 bytes, received 16"
)

expected_frame = frame_factory.build_goaway_frame(
last_stream_id=1,
error_code=h2.errors.PROTOCOL_ERROR,
)
assert c.data_to_send() == expected_frame.serialize()

def test_insufficient_data(self, frame_factory):
"""
Remote peers sending less data than content-length causes Protocol
Errors.
"""
c = h2.connection.H2Connection(client_side=False)
c.initiate_connection()
c.receive_data(frame_factory.preamble())

headers = frame_factory.build_headers_frame(
headers=self.example_request_headers
)
first_data = frame_factory.build_data_frame(data=b'\x01'*13)
c.receive_data(headers.serialize() + first_data.serialize())
c.clear_outbound_data_buffer()

second_data = frame_factory.build_data_frame(
data=b'\x01',
flags=['END_STREAM'],
)
with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp:
c.receive_data(second_data.serialize())

assert exp.value.expected_length == 15
assert exp.value.actual_length == 14
assert str(exp.value) == (
"InvalidBodyLengthError: Expected 15 bytes, received 14"
)

expected_frame = frame_factory.build_goaway_frame(
last_stream_id=1,
error_code=h2.errors.PROTOCOL_ERROR,
)
assert c.data_to_send() == expected_frame.serialize()

def test_insufficient_data_empty_frame(self, frame_factory):
"""
Remote peers sending less data than content-length where the last data
frame is empty causes Protocol Errors.
"""
c = h2.connection.H2Connection(client_side=False)
c.initiate_connection()
c.receive_data(frame_factory.preamble())

headers = frame_factory.build_headers_frame(
headers=self.example_request_headers
)
first_data = frame_factory.build_data_frame(data=b'\x01'*14)
c.receive_data(headers.serialize() + first_data.serialize())
c.clear_outbound_data_buffer()

second_data = frame_factory.build_data_frame(
data=b'',
flags=['END_STREAM'],
)
with pytest.raises(h2.exceptions.InvalidBodyLengthError) as exp:
c.receive_data(second_data.serialize())

assert exp.value.expected_length == 15
assert exp.value.actual_length == 14
assert str(exp.value) == (
"InvalidBodyLengthError: Expected 15 bytes, received 14"
)

expected_frame = frame_factory.build_goaway_frame(
last_stream_id=1,
error_code=h2.errors.PROTOCOL_ERROR,
)
assert c.data_to_send() == expected_frame.serialize()