diff --git a/h2/events.py b/h2/events.py index 7a3db8ed2..d52b8ff38 100644 --- a/h2/events.py +++ b/h2/events.py @@ -146,6 +146,17 @@ class _HeadersSent(object): pass +class _ResponseHeadersSent(_HeadersSent): + """ + The _ResponseHeadersSent event is fired whenever response headers are sent + on a stream. + + This is an internal event, used to determine validation steps on + outgoing header blocks. + """ + pass + + class _TrailersSent(_HeadersSent): """ The _TrailersSent event is fired whenever trailers are sent on a diff --git a/h2/stream.py b/h2/stream.py index 940849304..ac8b05e33 100644 --- a/h2/stream.py +++ b/h2/stream.py @@ -19,7 +19,7 @@ RequestReceived, ResponseReceived, DataReceived, WindowUpdated, StreamEnded, PushedStreamReceived, StreamReset, TrailersReceived, InformationalResponseReceived, AlternativeServiceAvailable, - _HeadersSent, _TrailersSent + _HeadersSent, _ResponseHeadersSent, _TrailersSent ) from .exceptions import ( ProtocolError, StreamClosedError, InvalidBodyLengthError @@ -146,7 +146,7 @@ def response_sent(self, previous_state): if self.client is True or self.client is None: raise ProtocolError("Client cannot send responses.") self.headers_sent = True - event = _HeadersSent() + event = _ResponseHeadersSent() else: assert not self.trailers_sent self.trailers_sent = True @@ -1040,18 +1040,26 @@ def _build_hdr_validation_flags(self, events): is_trailer = isinstance( events[0], (_TrailersSent, TrailersReceived) ) + is_response_header = isinstance( + events[0], (_ResponseHeadersSent, ResponseReceived) + ) except IndexError: # Some state changes don't emit an internal event (for example, # sending a push promise). We *always* emit an event for trailers, # so the absence of an event means this definitely isn't a trailer. + # Similarly, we also emit an event whenever response headers are + # sent or received. So absence of those events means this is not an + # response header either. # # TODO: Find any places where we don't emit anything, and emit # an internal event, so we can do away with this branch. is_trailer = False + is_response_header = False return HeaderValidationFlags( is_client=self.state_machine.client, - is_trailer=is_trailer + is_trailer=is_trailer, + is_response_header=is_response_header ) def _build_headers_frames(self, diff --git a/h2/utilities.py b/h2/utilities.py index f0f019685..c8aacd2da 100644 --- a/h2/utilities.py +++ b/h2/utilities.py @@ -171,7 +171,7 @@ def authority_from_headers(headers): # should be applied to a given set of headers. HeaderValidationFlags = collections.namedtuple( 'HeaderValidationFlags', - ['is_client', 'is_trailer'] + ['is_client', 'is_trailer', 'is_response_header'] ) @@ -331,6 +331,19 @@ def _reject_pseudo_header_fields(headers, hdr_validation_flags): seen_pseudo_header_fields ) + # If ':status' pseudo-header is not there in a response header, reject it + # 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: + seen_status_field = ( + b':status' in seen_pseudo_header_fields or + u':status' in seen_pseudo_header_fields + ) + if not seen_status_field: + raise ProtocolError( + "Response header block does not have a :status header" + ) + def _validate_host_authority_header(headers): """ diff --git a/test/test_basic_logic.py b/test/test_basic_logic.py index 1664c2408..caa1303a2 100644 --- a/test/test_basic_logic.py +++ b/test/test_basic_logic.py @@ -605,7 +605,7 @@ def test_reject_trailers_not_ending_stream(self, frame_factory): c = h2.connection.H2Connection() c.initiate_connection() c.send_headers(1, self.example_request_headers) - f = frame_factory.build_headers_frame(self.example_request_headers) + f = frame_factory.build_headers_frame(self.example_response_headers) c.receive_data(f.serialize()) # Send in trailers. diff --git a/test/test_invalid_headers.py b/test/test_invalid_headers.py index 4d323810b..b819f9ac7 100644 --- a/test/test_invalid_headers.py +++ b/test/test_invalid_headers.py @@ -6,6 +6,8 @@ This module contains tests that use invalid header blocks, and validates that they fail appropriately. """ +import itertools + import pytest import h2.connection @@ -239,21 +241,22 @@ class TestFilter(object): ] hdr_validation_combos = [ - h2.utilities.HeaderValidationFlags(is_client, is_trailer) - for is_client, is_trailer in [ - (True, True), - (True, False), - (False, True), - (False, False) - ] + h2.utilities.HeaderValidationFlags( + is_client, is_trailer, is_response_header + ) + for is_client, is_trailer, is_response_header in ( + itertools.product([True, False], repeat=3) + ) ] - hdr_validation_no_trailers = [ - h2.utilities.HeaderValidationFlags(is_client, is_trailer) - for is_client, is_trailer in [ - (True, False), - (False, False) - ] + hdr_validation_response_headers = [ + 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 + if not (flags.is_trailer or flags.is_response_header) ] @pytest.mark.parametrize('validation_function', validation_functions) @@ -281,7 +284,7 @@ def test_invalid_pseudo_headers(self, hdr_validation_flags): @pytest.mark.parametrize('validation_function', validation_functions) @pytest.mark.parametrize( - 'hdr_validation_flags', hdr_validation_no_trailers + 'hdr_validation_flags', hdr_validation_request_headers_no_trailer ) def test_matching_authority_host_headers(self, validation_function, @@ -298,7 +301,16 @@ def test_matching_authority_host_headers(self, (b'host', b'example.com'), ] assert headers == h2.utilities.validate_headers( - headers, hdr_validation_flags) + headers, hdr_validation_flags + ) + + @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')] + with pytest.raises(h2.exceptions.ProtocolError): + h2.utilities.validate_headers(headers, hdr_validation_flags) class TestOversizedHeaders(object):