diff --git a/framework/deproxy_client.py b/framework/deproxy_client.py index fe25feee1..11da7527b 100644 --- a/framework/deproxy_client.py +++ b/framework/deproxy_client.py @@ -4,13 +4,21 @@ from io import StringIO import h2.connection -from h2.events import DataReceived, ResponseReceived, StreamEnded, TrailersReceived +from h2.events import ( + ConnectionTerminated, + DataReceived, + ResponseReceived, + SettingsAcknowledged, + StreamEnded, + TrailersReceived, +) +from h2.settings import SettingCodes, Settings from hpack import Encoder from helpers import deproxy, selfproxy, stateful, tf_cfg __author__ = "Tempesta Technologies, Inc." -__copyright__ = "Copyright (C) 2018-2022 Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2018-2023 Tempesta Technologies, Inc." __license__ = "GPL2" @@ -357,6 +365,7 @@ def encode(self, headers, huffman=True): class DeproxyClientH2(DeproxyClient): last_response: deproxy.H2Response + h2_connection: h2.connection.H2Connection def __init__(self, *args, **kwargs): DeproxyClient.__init__(self, *args, **kwargs) @@ -364,6 +373,7 @@ def __init__(self, *args, **kwargs): self.h2_connection = None self.stream_id = 1 self.active_responses = {} + self.ack_settings = False def make_requests(self, requests): for request in requests: @@ -383,8 +393,6 @@ def make_request(self, request: tuple or list or str, end_stream=True, huffman=T self.h2_connection = h2.connection.H2Connection() self.h2_connection.encoder = self.encoder self.h2_connection.initiate_connection() - if self.selfproxy_present: - self.update_selfproxy() self.h2_connection.encoder.huffman = huffman @@ -417,6 +425,80 @@ def make_request(self, request: tuple or list or str, end_stream=True, huffman=T self.stream_id += 2 self.valid_req_num += 1 + def update_initial_settings( + self, + header_table_size: int = None, + enable_push: int = None, + max_concurrent_stream: int = None, + initial_window_size: int = None, + max_frame_size: int = None, + max_header_list_size: int = None, + ) -> None: + """Update initial SETTINGS frame and add preamble + SETTINGS frame in `data_to_send`.""" + if not self.h2_connection: + self.h2_connection = h2.connection.H2Connection() + self.h2_connection.encoder = self.encoder + + new_settings = self.__generate_new_settings( + header_table_size, + enable_push, + max_concurrent_stream, + initial_window_size, + max_frame_size, + max_header_list_size, + ) + + # if settings is empty, we should not change them + if new_settings: + self.h2_connection.local_settings = Settings(initial_values=new_settings) + self.h2_connection.local_settings.update(new_settings) + + self.h2_connection.initiate_connection() + + def send_settings_frame( + self, + header_table_size: int = None, + enable_push: int = None, + max_concurrent_stream: int = None, + initial_window_size: int = None, + max_frame_size: int = None, + max_header_list_size: int = None, + ) -> None: + self.ack_settings = False + + new_settings = self.__generate_new_settings( + header_table_size, + enable_push, + max_concurrent_stream, + initial_window_size, + max_frame_size, + max_header_list_size, + ) + + self.h2_connection.update_settings(new_settings) + + self.send_bytes(data=self.h2_connection.data_to_send()) + self.h2_connection.clear_outbound_data_buffer() + + def wait_for_ack_settings(self, timeout=5): + """Wait SETTINGS frame with ack flag.""" + if self.state != stateful.STATE_STARTED: + return False + + t0 = time.time() + while not self.ack_settings: + t = time.time() + if t - t0 > timeout: + return False + time.sleep(0.01) + return True + + def send_bytes(self, data: bytes, expect_response=False): + self.request_buffers.append(data) + self.nrreq += 1 + if expect_response: + self.valid_req_num += 1 + def handle_read(self): self.response_buffer = self.recv(deproxy.MAX_MESSAGE_SIZE) if not self.response_buffer: @@ -426,7 +508,6 @@ def handle_read(self): tf_cfg.dbg(5, self.response_buffer) try: - method = self.methods[self.nrresp] events = self.h2_connection.receive_data(self.response_buffer) for event in events: if isinstance(event, ResponseReceived): @@ -440,7 +521,7 @@ def handle_read(self): else: response = deproxy.H2Response( headers + "\r\n", - method=method, + method=self.methods[self.nrresp], body_parsing=False, keep_original_data=self.keep_original_data, ) @@ -464,6 +545,12 @@ def handle_read(self): return self.receive_response(response) self.nrresp += 1 + elif isinstance(event, ConnectionTerminated): + self.error_codes.append(event.error_code) + elif isinstance(event, SettingsAcknowledged): + self.ack_settings = True + # TODO should be changed by issue #358 + self.handle_read() # TODO should be changed by issue #358 else: self.handle_read() @@ -513,3 +600,27 @@ def __headers_to_string(self, headers): def __binary_headers_to_string(self, headers): return "".join(["%s: %s\r\n" % (h.decode(), v.decode()) for h, v in headers]) + + @staticmethod + def __generate_new_settings( + header_table_size: int = None, + enable_push: int = None, + max_concurrent_stream: int = None, + initial_window_size: int = None, + max_frame_size: int = None, + max_header_list_size: int = None, + ) -> dict: + new_settings = dict() + if header_table_size is not None: + new_settings[SettingCodes.HEADER_TABLE_SIZE] = header_table_size + if enable_push is not None: + new_settings[SettingCodes.ENABLE_PUSH] = header_table_size + if max_concurrent_stream is not None: + new_settings[SettingCodes.MAX_CONCURRENT_STREAMS] = max_concurrent_stream + if initial_window_size is not None: + new_settings[SettingCodes.INITIAL_WINDOW_SIZE] = initial_window_size + if max_frame_size is not None: + new_settings[SettingCodes.MAX_FRAME_SIZE] = max_frame_size + if max_header_list_size is not None: + new_settings[SettingCodes.MAX_HEADER_LIST_SIZE] = max_header_list_size + return new_settings diff --git a/helpers/deproxy.py b/helpers/deproxy.py index 9b38d0781..4cbce0e71 100644 --- a/helpers/deproxy.py +++ b/helpers/deproxy.py @@ -32,7 +32,7 @@ from . import error, stateful, tempesta, tf_cfg __author__ = "Tempesta Technologies, Inc." -__copyright__ = "Copyright (C) 2017-2022 Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2017-2023 Tempesta Technologies, Inc." __license__ = "GPL2" # ------------------------------------------------------------------------------- @@ -741,6 +741,7 @@ def __init__( self.stop_procedures = [self.__stop_client] self.conn_is_closed = True self.bind_addr = bind_addr + self.error_codes = [] def __stop_client(self): tf_cfg.dbg(4, "\tStop deproxy client") @@ -821,7 +822,8 @@ def handle_write(self): self.request_buffer = self.request_buffer[sent:] def handle_error(self): - _, v, _ = sys.exc_info() + type_error, v, _ = sys.exc_info() + self.error_codes.append(type_error) if type(v) == ParseError or type(v) == AssertionError: raise v elif type(v) == ssl.SSLWantReadError: diff --git a/http2_general/__init__.py b/http2_general/__init__.py index e1c2e83c8..284514851 100644 --- a/http2_general/__init__.py +++ b/http2_general/__init__.py @@ -1,3 +1,14 @@ -__all__ = ["test_h2_specs", "test_h2_perf", "test_h2_ping", "test_h2_frame", "test_h2_hpack"] +__all__ = [ + "test_h2_specs", + "test_h2_perf", + "test_h2_ping", + "test_h2_frame", + "test_h2_hpack", + "test_flow_control_window", + "test_h2_streams", + "test_h2_headers", + "test_h2_sticky_cookie", + "test_h2_max_frame_size", +] # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/http2_general/helpers.py b/http2_general/helpers.py new file mode 100644 index 000000000..dd68e4ea0 --- /dev/null +++ b/http2_general/helpers.py @@ -0,0 +1,71 @@ +__author__ = "Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2023 Tempesta Technologies, Inc." +__license__ = "GPL2" + +from framework import deproxy_client, tester + + +class H2Base(tester.TempestaTest): + backends = [ + { + "id": "deproxy", + "type": "deproxy", + "port": "8000", + "response": "static", + "response_content": ( + "HTTP/1.1 200 OK\r\n" + + "Date: test\r\n" + + "Server: debian\r\n" + + "Content-Length: 0\r\n\r\n" + ), + } + ] + + tempesta = { + "config": """ + listen 443 proto=h2; + server ${server_ip}:8000; + tls_certificate ${tempesta_workdir}/tempesta.crt; + tls_certificate_key ${tempesta_workdir}/tempesta.key; + tls_match_any_server_name; + + block_action attack reply; + block_action error reply; + """ + } + + clients = [ + { + "id": "deproxy", + "type": "deproxy_h2", + "addr": "${tempesta_ip}", + "port": "443", + "ssl": True, + }, + ] + + post_request = [ + (":authority", "example.com"), + (":path", "/"), + (":scheme", "https"), + (":method", "POST"), + ] + + get_request = [ + (":authority", "example.com"), + (":path", "/"), + (":scheme", "https"), + (":method", "GET"), + ] + + def initiate_h2_connection(self, client: deproxy_client.DeproxyClientH2): + # add preamble + settings frame with default variable into data_to_send + client.update_initial_settings() + # send preamble + settings frame to Tempesta + client.send_bytes(client.h2_connection.data_to_send()) + client.h2_connection.clear_outbound_data_buffer() + + self.assertTrue( + client.wait_for_ack_settings(), + "Tempesta foes not returns SETTINGS frame with ACK flag.", + ) diff --git a/http2_general/test_flow_control_window.py b/http2_general/test_flow_control_window.py new file mode 100644 index 000000000..4f6ee2072 --- /dev/null +++ b/http2_general/test_flow_control_window.py @@ -0,0 +1,41 @@ +"""Functional tests for flow control window.""" + +__author__ = "Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2023 Tempesta Technologies, Inc." +__license__ = "GPL2" + +from h2.exceptions import FlowControlError + +from http2_general.helpers import H2Base + + +class TestFlowControl(H2Base): + def test_flow_control_window_for_stream(self): + """ + Client sets SETTINGS_INITIAL_WINDOW_SIZE = 1k bytes and backend returns response + with 2k bytes body. + Tempesta must forward DATA frame with 1k bytes and wait WindowUpdate from client. + """ + self.start_all_services() + client = self.get_client("deproxy") + server = self.get_server("deproxy") + server.set_response( + "HTTP/1.1 200 OK\r\n" + + "Date: test\r\n" + + "Server: debian\r\n" + + "Content-Length: 2000\r\n\r\n" + + ("x" * 2000) + ) + + client.update_initial_settings(initial_window_size=1000) + client.make_request(self.post_request) + client.wait_for_response(3) + + self.assertNotIn( + FlowControlError, client.error_codes, "Tempesta ignored flow control window for stream." + ) + self.assertFalse(client.connection_is_closed()) + self.assertEqual(client.last_response.status, "200", "Status code mismatch.") + self.assertEqual( + len(client.last_response.body), 2000, "Tempesta did not return full response body." + ) diff --git a/http2_general/test_h2_frame.py b/http2_general/test_h2_frame.py index d60de7e64..e44665150 100644 --- a/http2_general/test_h2_frame.py +++ b/http2_general/test_h2_frame.py @@ -1,53 +1,15 @@ """Functional tests for h2 frames.""" __author__ = "Tempesta Technologies, Inc." -__copyright__ = "Copyright (C) 2022 Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2023 Tempesta Technologies, Inc." __license__ = "GPL2" - -from framework import tester +from framework import deproxy_client from helpers import checks_for_tests as checks +from http2_general.helpers import H2Base -class TestH2Frame(tester.TempestaTest): - - backends = [ - { - "id": "deproxy", - "type": "deproxy", - "port": "8000", - "response": "static", - "response_content": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", - } - ] - - clients = [ - { - "id": "deproxy", - "type": "deproxy_h2", - "addr": "${tempesta_ip}", - "port": "443", - "ssl": True, - }, - ] - - tempesta = { - "config": """ - listen 443 proto=h2; - server ${server_ip}:8000; - tls_certificate ${tempesta_workdir}/tempesta.crt; - tls_certificate_key ${tempesta_workdir}/tempesta.key; - tls_match_any_server_name; - """ - } - - request_headers = [ - (":authority", "debian"), - (":path", "/"), - (":scheme", "https"), - (":method", "POST"), - ] - +class TestH2Frame(H2Base): def test_data_framing(self): """Send many 1 byte frames in request.""" self.start_all_services() @@ -55,7 +17,7 @@ def test_data_framing(self): deproxy_cl.parsing = False request_body = "x" * 100 - deproxy_cl.make_request(request=self.request_headers, end_stream=False) + deproxy_cl.make_request(request=self.post_request, end_stream=False) for byte in request_body[:-1]: deproxy_cl.make_request(request=byte, end_stream=False) deproxy_cl.make_request(request=request_body[-1], end_stream=True) @@ -71,7 +33,7 @@ def test_empty_last_data_frame(self): deproxy_cl.parsing = False request_body = "123" - deproxy_cl.make_request(request=self.request_headers, end_stream=False) + deproxy_cl.make_request(request=self.post_request, end_stream=False) deproxy_cl.make_request(request=request_body, end_stream=False) deproxy_cl.make_request(request="", end_stream=True) @@ -86,7 +48,7 @@ def test_empty_data_frame(self): deproxy_cl.parsing = False request_body = "123" - deproxy_cl.make_request(request=self.request_headers, end_stream=False) + deproxy_cl.make_request(request=self.post_request, end_stream=False) deproxy_cl.make_request(request="", end_stream=False) deproxy_cl.make_request(request=request_body, end_stream=True) @@ -99,7 +61,7 @@ def test_tcp_framing_for_request_headers(self): self.start_all_services() client.parsing = False - client.make_request(self.request_headers) + client.make_request(self.post_request) self.__assert_test(client=client, request_body="", request_number=1) @@ -113,7 +75,7 @@ def test_tcp_framing_for_request(self): for chunk_size in chunk_sizes: with self.subTest(chunk_size=chunk_size): client.segment_size = chunk_size - client.make_request(self.request_headers, False) + client.make_request(self.post_request, False) request_body = "0123456789" client.make_request(request_body, True) @@ -124,6 +86,70 @@ def test_tcp_framing_for_request(self): request_number=chunk_sizes.index(chunk_size) + 1, ) + def test_settings_frame(self): + """ + Create tls connection and send preamble + correct settings frame. + Tempesta must accept settings and return settings + ack settings frames. + Then client send ack settings frame and Tempesta must correctly accept it. + """ + self.start_all_services(client=True) + + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + # initiate_connection() generates preamble + settings frame with default variables + self.initiate_h2_connection(client) + + # send empty setting frame with ack flag. + client.send_bytes(client.h2_connection.data_to_send()) + client.h2_connection.clear_outbound_data_buffer() + + # send header frame after exchanging settings and make sure + # that connection is open. + client.send_request(self.post_request, "200") + + def test_window_update_frame(self): + """Tempesta must handle WindowUpdate frame.""" + self.start_all_services(client=True) + + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + # add preamble + settings frame with SETTING_INITIAL_WINDOW_SIZE = 65535 + client.update_initial_settings() + + # send preamble + settings frame + client.send_bytes(client.h2_connection.data_to_send()) + client.h2_connection.clear_outbound_data_buffer() + self.assertTrue(client.wait_for_ack_settings()) + + # send WindowUpdate frame with window size increment = 5000 + client.h2_connection.increment_flow_control_window(5000) + client.send_bytes(client.h2_connection.data_to_send()) + client.h2_connection.clear_outbound_data_buffer() + + # send header frame after sending WindowUpdate and make sure + # that connection is working correctly. + client.send_request(self.get_request, "200") + self.assertFalse(client.connection_is_closed()) + + def test_continuation_frame(self): + """Tempesta must handle CONTINUATION frame.""" + self.start_all_services() + + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + client.update_initial_settings() + client.send_bytes(client.h2_connection.data_to_send()) + client.h2_connection.clear_outbound_data_buffer() + + # H2Connection separates headers to HEADERS + CONTINUATION frames + # if they are larger than 16384 bytes + client.send_request( + request=self.get_request + [("qwerty", "x" * 5000) for _ in range(4)], + expected_status_code="200", + ) + + self.assertFalse(client.connection_is_closed()) + def __assert_test(self, client, request_body: str, request_number: int): server = self.get_server("deproxy") @@ -138,7 +164,7 @@ def __assert_test(self, client, request_body: str, request_number: int): srv_msg_forwarded=request_number, ) error_msg = "Malformed request from Tempesta." - self.assertEqual(server.last_request.method, self.request_headers[3][1], error_msg) - self.assertEqual(server.last_request.headers["host"], self.request_headers[0][1], error_msg) - self.assertEqual(server.last_request.uri, self.request_headers[1][1], error_msg) + self.assertEqual(server.last_request.method, self.post_request[3][1], error_msg) + self.assertEqual(server.last_request.headers["host"], self.post_request[0][1], error_msg) + self.assertEqual(server.last_request.uri, self.post_request[1][1], error_msg) self.assertEqual(server.last_request.body, request_body) diff --git a/http2_general/test_h2_headers.py b/http2_general/test_h2_headers.py index 456492e6c..64bb70ecf 100644 --- a/http2_general/test_h2_headers.py +++ b/http2_general/test_h2_headers.py @@ -4,7 +4,8 @@ analises its return code. """ -from framework import tester +from framework import deproxy_client, tester +from http2_general.helpers import H2Base __author__ = "Tempesta Technologies, Inc." __copyright__ = "Copyright (C) 2023 Tempesta Technologies, Inc." @@ -82,31 +83,7 @@ """ -class HeadersParsing(tester.TempestaTest): - clients = [ - { - "id": "deproxy", - "type": "deproxy_h2", - "addr": "${tempesta_ip}", - "port": "443", - "ssl": True, - } - ] - - backends = [ - { - "id": "deproxy", - "type": "deproxy", - "port": "8000", - "response": "static", - "response_content": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", - } - ] - - tempesta = { - "config": TEMPESTA_CONFIG % "", - } - +class HeadersParsing(H2Base): def test_small_header_in_request(self): """Request with small header name length completes successfully.""" self.start_all_services() @@ -116,13 +93,7 @@ def test_small_header_in_request(self): for length in range(1, 5): header = "x" * length client.send_request( - [ - (":authority", "localhost"), - (":path", "/"), - (":scheme", "https"), - (":method", "GET"), - (header, "test"), - ], + self.get_request + [(header, "test")], "200", ) @@ -134,38 +105,124 @@ def test_capitalized_header_in_request(self): client.parsing = False client.send_request( ( - [ - (":authority", "localhost"), - (":path", "/"), - (":scheme", "https"), - (":method", "POST"), - ("Content-Length", "3"), - ], + self.post_request + [("Content-Length", "3")], "123", ), "400", ) - def test_chunked_header_in_request(self): - """The request must be treated as malformed. RFC 7540 8.2.2""" + +class TestPseudoHeaders(H2Base): + def test_invalid_pseudo_header(self): + """ + Endpoints MUST NOT generate pseudo-header fields other than those defined in this document. + RFC 9113 8.3 + """ + self.__test([(":content-length", "0")]) + + def test_duplicate_pseudo_header(self): + """ + The same pseudo-header field name MUST NOT appear more than once in a field block. + A field block for an HTTP request or response that contains a repeated pseudo-header + field name MUST be treated as malformed. + RFC 9113 8.3 + """ + self.__test([(":path", "/")]) + + def test_status_header_in_request(self): + """ + Pseudo-header fields defined for responses MUST NOT appear in requests. + RFC 9113 8.3 + """ + self.__test([(":status", "200")]) + + def test_regular_header_before_pseudo_header(self): + """ + All pseudo-header fields MUST appear in a field block before all regular field lines. + RFC 9113 8.3 + """ + self.post_request = [ + (":authority", "example.com"), + (":path", "/"), + (":scheme", "https"), + ] + self.__test([("content-length", "0"), (":method", "POST")]) + + def __test(self, optional_header: list): self.start_all_services() client = self.get_client("deproxy") client.parsing = False + client.send_request( - ( - [ - (":authority", "localhost"), - (":path", "/"), - (":scheme", "https"), - (":method", "POST"), - ("transfer-encoding", "chunked"), - ], - "3\r\n123\r\n0\r\n\r\n", - ), + self.post_request + optional_header, "400", ) + self.assertTrue(client.connection_is_closed()) + + +class TestConnectionHeaders(H2Base): + def __test_request(self, header: tuple): + """ + An endpoint MUST NOT generate an HTTP/2 message containing connection-specific + header fields. Any message containing connection-specific header fields MUST be treated + as malformed. + RFC 9113 8.2.2 + """ + self.start_all_services() + client = self.get_client("deproxy") + client.parsing = False + + client.send_request(self.post_request + [header], "400") + self.assertTrue(client.connection_is_closed()) + + def __test_response(self, header: tuple): + """ + An intermediary transforming an HTTP/1.x message to HTTP/2 MUST remove connection-specific + header fields or their messages will be treated by other HTTP/2 endpoints as malformed. + RFC 9113 8.2.2 + """ + self.start_all_services() + client = self.get_client("deproxy") + server = self.get_server("deproxy") + client.parsing = False + + server.set_response( + "HTTP/1.1 200 OK\r\n" + + "Date: test\r\n" + + "Server: debian\r\n" + + f"{header[0].capitalize()}: {header[1]}\r\n" + + "Content-Length: 0\r\n\r\n" + ) + + client.send_request(self.post_request, "200") + self.assertNotIn(header, client.last_response.headers.headers) + + def test_connection_header_in_request(self): + self.__test_request(header=("connection", "keep-alive")) + + def test_keep_alive_header_in_request(self): + self.__test_request(header=("keep-alive", "timeout=5, max=10")) + + def test_proxy_connection_header_in_request(self): + self.__test_request(header=("proxy-connection", "keep-alive")) + + def test_upgrade_header_in_request(self): + self.__test_request(header=("upgrade", "websocket")) + + def test_connection_header_in_response(self): + self.__test_response(header=("connection", "keep-alive")) + + def test_keep_alive_header_in_response(self): + self.__test_response(header=("keep-alive", "timeout=5, max=10")) + + def test_proxy_connection_header_in_response(self): + self.__test_response(header=("proxy-connection", "keep-alive")) + + def test_upgrade_header_in_response(self): + self.__test_response(header=("upgrade", "websocket")) + class CurlTestBase(tester.TempestaTest): @@ -328,7 +385,7 @@ def test(self, served_from_cache=True): for line in lines: if line.startswith("< set-cookie:"): setcookie_count += 1 - self.assertTrue(len(line.split(','))==1, "Wrong separator") + self.assertTrue(len(line.split(",")) == 1, "Wrong separator") self.assertTrue(setcookie_count == 3, "Set-Cookie headers quantity mismatch") diff --git a/http2_general/test_h2_hpack.py b/http2_general/test_h2_hpack.py index b074942a4..db753c085 100755 --- a/http2_general/test_h2_hpack.py +++ b/http2_general/test_h2_hpack.py @@ -7,44 +7,18 @@ import time from ssl import SSLWantWriteError +from h2.connection import AllowedStreamIDs, ConnectionInputs +from h2.errors import ErrorCodes from h2.exceptions import ProtocolError +from h2.stream import StreamInputs from hpack import HeaderTuple, NeverIndexedHeaderTuple from hyperframe.frame import HeadersFrame -from framework import deproxy_client, tester - - -class TestHpack(tester.TempestaTest): - backends = [ - { - "id": "deproxy", - "type": "deproxy", - "port": "8000", - "response": "static", - "response_content": "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n", - } - ] - - clients = [ - { - "id": "deproxy", - "type": "deproxy_h2", - "addr": "${tempesta_ip}", - "port": "443", - "ssl": True, - }, - ] - - tempesta = { - "config": """ - listen 443 proto=h2; - server ${server_ip}:8000; - tls_certificate ${tempesta_workdir}/tempesta.crt; - tls_certificate_key ${tempesta_workdir}/tempesta.key; - tls_match_any_server_name; - """ - } +from framework import deproxy_client +from http2_general.helpers import H2Base + +class TestHpack(H2Base): def test_static_table(self): """ Send request with headers from static table. @@ -95,19 +69,263 @@ def test_disable_huffman(self): client = self.get_client("deproxy") client.make_request( - [ - (":authority", "example.com"), - (":path", "/"), - (":scheme", "https"), - (":method", "GET"), - ("host", "example.com"), - ], + self.post_request + [("host", "example.com")], end_stream=True, huffman=False, ) self.assertTrue(client.wait_for_response()) self.assertEqual(client.last_response.status, "200") + def test_settings_header_table_size(self): + """ + Client sets non-default value for SETTINGS_HEADER_TABLE_SIZE. + Tempesta must not encode headers larger than set size. + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + server = self.get_server("deproxy") + + new_table_size = 512 + client.update_initial_settings(header_table_size=new_table_size) + + header = "x" * new_table_size * 2 + server.set_response( + "HTTP/1.1 200 OK\r\n" + "Server: Debian\r\n" + "Date: test\r\n" + f"x: {header}\r\n" + "Content-Length: 0\r\n" + "\r\n" + ) + + client.request_buffers.append(client.h2_connection.data_to_send()) + client.nrreq += 1 + + client.wait_for_ack_settings() + + # Tempesta must not save large header in dynamic table. + client.send_request(request=self.post_request, expected_status_code="200") + + # Client received large header as plain text. + client.send_request(request=self.post_request, expected_status_code="200") + self.assertNotIn( + b"2.0 tempesta_fw (Tempesta FW pre-0.7.0)", + client.response_buffer, + "Tempesta does not encode via header as expected.", + ) + self.assertIn( + header.encode(), + client.response_buffer, + "Tempesta encode large header, but HEADER_TABLE_SIZE smaller than this header.", + ) + + def test_rewrite_dynamic_table_for_request(self): + """ + "Before a new entry is added to the dynamic table, entries are evicted + from the end of the dynamic table until the size of the dynamic table + is less than or equal to (maximum size - new entry size) or until the + table is empty." + RFC 7541 4.4 + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + client.parsing = False + + headers = [ + HeaderTuple(":path", "/"), + HeaderTuple(":scheme", "https"), + HeaderTuple(":method", "POST"), + ] + # send request with max size header in dynamic table + # Tempesta MUST write header to table + first_indexed_header = [HeaderTuple("a", "a" * 4063)] + client.send_request( + request=( + headers + + [NeverIndexedHeaderTuple(":authority", "localhost")] + + first_indexed_header + ), + expected_status_code="200", + ) + # send request with new incremental header + # Tempesta MUST rewrite header to dynamic table. + # Dynamic table does not have header from first request. + second_indexed_header = [HeaderTuple("x", "x")] + client.send_request( + request=( + headers + + [NeverIndexedHeaderTuple(":authority", "localhost")] + + second_indexed_header + ), + expected_status_code="200", + ) + + # We generate new stream with link to first index in dynamic table + stream_id = 5 + stream = client.h2_connection._begin_new_stream( + stream_id, AllowedStreamIDs(client.h2_connection.config.client_side) + ) + stream.state_machine.process_input(StreamInputs.SEND_HEADERS) + + client.methods.append("POST") + client.request_buffers.append( + # \xbe - link to first index in dynamic table + b"\x00\x00\x0c\x01\x05\x00\x00\x00\x05\x11\x86\xa0\xe4\x1d\x13\x9d\t\x84\x87\x83\xbe" + ) + # increment counter to call handle_write method + client.nrreq += 1 + client.valid_req_num += 1 + self.assertTrue(client.wait_for_response()) + + # Last forwarded request from Tempesta MUST have second indexed header + server = self.get_server("deproxy") + self.assertEqual(3, len(server.requests)) + self.assertIn(second_indexed_header[0], server.last_request.headers.items()) + self.assertNotIn(first_indexed_header[0], server.last_request.headers.items()) + + def test_rewrite_dynamic_table_for_response(self): + """ + "Before a new entry is added to the dynamic table, entries are evicted + from the end of the dynamic table until the size of the dynamic table + is less than or equal to (maximum size - new entry size) or until the + table is empty." + RFC 7541 4.4 + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + server = self.get_server("deproxy") + client.parsing = False + + # Tempesta rewrites headers in dynamic table and saves 4064 bytes header last. + server.set_response( + "HTTP/1.1 200 OK\r\n" + f"qwerty: {'x' * 4058}\r\n" + "Content-Length: 0\r\n" + "Date: test\r\n" + "\r\n" + ) + + client.send_request(request=self.get_request, expected_status_code="200") + + # Second request must contain all response headers as new indexed field + # because they will be rewritten in table in cycle. + client.send_request(request=self.get_request, expected_status_code="200") + + for header in ( + b"2.0 tempesta_fw (Tempesta FW pre-0.7.0)", # Via header + b"Tempesta FW/pre-0.7.0", # Server header + b"test", # Date header + b"x" * 4058, # optional header + ): + self.assertIn( + header, + client.response_buffer, + "Tempesta does not encode via header as expected.", + ) + + def test_clearing_dynamic_table(self): + """ + "an attempt to add an entry larger than the maximum size causes the table + to be emptied of all existing entries and results in an empty table." + RFC 7541 4.4 + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + client.parsing = False + + client.send_request( + # Tempesta save header with 1k bytes in dynamic table. + request=(self.get_request + [HeaderTuple("a", "a" * 1000)]), + expected_status_code="200", + ) + + client.send_request( + # Tempesta MUST clear dynamic table + # because new indexed header is larger than 4096 bytes + request=(self.get_request + [HeaderTuple("a", "a" * 6000)]), + expected_status_code="200", + ) + + # We generate new stream with link to first index in dynamic table + stream_id = 5 + stream = client.h2_connection._begin_new_stream( + stream_id, AllowedStreamIDs(client.h2_connection.config.client_side) + ) + stream.state_machine.process_input(StreamInputs.SEND_HEADERS) + + client.methods.append("POST") + client.request_buffers.append( + # \xbe - link to first index in table + b"\x00\x00\x0c\x01\x05\x00\x00\x00\x05\x11\x86\xa0\xe4\x1d\x13\x9d\t\x84\x87\x83\xbe" + ) + # increment counter to call handle_write method + client.nrreq += 1 + client.valid_req_num += 1 + + self.assertTrue(client.wait_for_response()) + self.assertEqual(client.last_response.status, "400", "HTTP response status codes mismatch.") + + def test_clearing_dynamic_table_with_settings_frame(self): + """ + "A change in the maximum size of the dynamic table is signaled via + a dynamic table size update. + This mechanism can be used to completely clear entries from the dynamic table by setting + a maximum size of 0, which can subsequently be restored." + RFC 7541 4.2 + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + # Tempesta forwards response with via header and saves it in dynamic table. + client.send_request(request=self.post_request, expected_status_code="200") + self.assertIn(b"2.0 tempesta_fw (Tempesta FW pre-0.7.0)", client.response_buffer) + + # Tempesta forwards header from dynamic table. Via header is indexed. + client.send_request(request=self.post_request, expected_status_code="200") + self.assertNotIn(b"2.0 tempesta_fw (Tempesta FW pre-0.7.0)", client.response_buffer) + + # Tempesta MUST clear dynamic table when receive SETTINGS_HEADER_TABLE_SIZE = 0 + client.send_settings_frame(header_table_size=0) + self.assertTrue(client.wait_for_ack_settings()) + + client.send_settings_frame(header_table_size=4096) + self.assertTrue(client.wait_for_ack_settings()) + + # Tempesta MUST saves via header in dynamic table again. Via header is indexed again. + client.send_request(request=self.post_request, expected_status_code="200") + self.assertIn(b"2.0 tempesta_fw (Tempesta FW pre-0.7.0)", client.response_buffer) + + # Tempesta forwards header from dynamic table again. + client.send_request(request=self.post_request, expected_status_code="200") + self.assertNotIn(b"2.0 tempesta_fw (Tempesta FW pre-0.7.0)", client.response_buffer) + + def test_settings_header_table_stress(self): + client, server = self.__setup_settings_header_table_tests() + + for new_table_size in range(128, 0, -1): + header = "x" * new_table_size * 2 + server.set_response( + "HTTP/1.1 200 OK\r\n" + "Server: Debian\r\n" + "Date: test\r\n" + f"x: {header}\r\n" + "Content-Length: 0\r\n" + "\r\n" + ) + self.__change_header_table_size_and_send_request(client, new_table_size, header) + + for new_table_size in range(0, 128, 1): + header = "x" * new_table_size * 2 + server.set_response( + "HTTP/1.1 200 OK\r\n" + "Server: Debian\r\n" + "Date: test\r\n" + f"x: {header}\r\n" + "Content-Length: 0\r\n" + "\r\n" + ) + self.__change_header_table_size_and_send_request(client, new_table_size, header) + def test_hpack_bomb(self): """ A HPACK bomb request causes the connection to be torn down with the @@ -124,13 +342,7 @@ def test_hpack_bomb(self): client.stop() client.start() client.make_request( - request=[ - HeaderTuple(":authority", "example.com"), - HeaderTuple(":path", "/"), - HeaderTuple(":scheme", "https"), - HeaderTuple(":method", "POST"), - HeaderTuple(b"a", b"a" * 4063), - ], + request=self.post_request + [HeaderTuple(b"a", b"a" * 4063)], end_stream=False, ) @@ -156,3 +368,109 @@ def test_hpack_bomb(self): with self.assertRaises(ProtocolError): client.stream_id = 1 client.make_request(request="asd", end_stream=True) + + def __setup_settings_header_table_tests(self): + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + server = self.get_server("deproxy") + + client.update_initial_settings() + client.send_bytes(client.h2_connection.data_to_send()) + client.wait_for_ack_settings() + + return client, server + + def __change_header_table_size_and_send_request(self, client, new_table_size, header): + client.send_settings_frame(header_table_size=new_table_size) + client.wait_for_ack_settings() + + client.send_request(request=self.post_request, expected_status_code="200") + client.send_request(request=self.post_request, expected_status_code="200") + + self.assertIn( + header.encode(), + client.response_buffer, + "Tempesta encode large header, but HEADER_TABLE_SIZE smaller than this header.", + ) + + +class TestFramePayloadLength(H2Base): + """ + Additionally, an endpoint MAY use any applicable error code when it detects + an error condition; a generic error code (such as PROTOCOL_ERROR or INTERNAL_ERROR) + can always be used in place of more specific error codes. + RFC 9113 5.4 + """ + + @staticmethod + def __make_request(client, data: bytes): + # Create stream for H2Connection to escape error + client.h2_connection.state_machine.process_input(ConnectionInputs.SEND_HEADERS) + stream = client.h2_connection._get_or_create_stream( + client.stream_id, AllowedStreamIDs(client.h2_connection.config.client_side) + ) + stream.state_machine.process_input(StreamInputs.SEND_HEADERS) + stream.state_machine.process_input(StreamInputs.SEND_END_STREAM) + + # add method in list to escape IndexError + client.methods.append("POST") + client.send_bytes(data, True) + client.wait_for_response(1) + + def test_small_frame_payload_length(self): + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + # Tempesta and deproxy must save headers in dynamic table. + client.send_request(self.get_request + [("asd", "qwe")], "200") + + # Tempesta return 200 response because extra bytes will be ignored. + self.__make_request( + client, + # header count - 5, headers - 8. + b"\x00\x00\x05\x01\x05\x00\x00\x00\x03\xbf\x84\x87\x82\xbe\xbe\xbe\xbe", + ) + + client.stream_id += 2 + client.make_request(self.get_request) + client.wait_for_response(0.5) + + # Client will be blocked because Tempesta received extra bytes + self.assertTrue(client.connection_is_closed()) + self.assertIn(ErrorCodes.FRAME_SIZE_ERROR, client.error_codes) + + def test_large_frame_payload_length(self): + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + # Tempesta and deproxy must save headers in dynamic table. + client.send_request(self.get_request + [("asd", "qwe")], "200") + + # Tempesta does not return response because it does not receive all bytes. + # Therefore, client must not wait for response. + client.valid_req_num = 0 + self.__make_request( + client, + # header count - 7, headers - 5. + b"\x00\x00\x07\x01\x05\x00\x00\x00\x03\xbf\x84\x87\x82\xbe", + ) + + client.stream_id += 2 + client.send_request(self.get_request, "400") + + self.assertTrue(client.connection_is_closed()) + self.assertIn(ErrorCodes.PROTOCOL_ERROR, client.error_codes) + + def test_invalid_data(self): + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + client.update_initial_settings() + client.send_bytes(client.h2_connection.data_to_send()) + + # send headers frame with stream_id = 1, header count = 3 + # and headers bytes - \x09\x02\x00 (invalid bytes) + self.__make_request(client, b"\x00\x00\x03\x01\x05\x00\x00\x00\x01\x09\x02\x00") + + self.assertEqual(client.last_response.status, "400") + self.assertTrue(client.connection_is_closed()) diff --git a/http2_general/test_h2_max_frame_size.py b/http2_general/test_h2_max_frame_size.py new file mode 100644 index 000000000..4d4b9432f --- /dev/null +++ b/http2_general/test_h2_max_frame_size.py @@ -0,0 +1,103 @@ +__author__ = "Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2023 Tempesta Technologies, Inc." +__license__ = "GPL2" + +from framework import deproxy_client, deproxy_server +from http2_general.helpers import H2Base + + +class TestMaxFrameSize(H2Base): + def test_large_data_frame_in_response(self): + """ + Tempesta must separate response body because it is larger than SETTINGS_MAX_FRAME_SIZE. + Client must receive several DATA frames. + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + server: deproxy_server.StaticDeproxyServer = self.get_server("deproxy") + + client.update_initial_settings(max_frame_size=16384) + + response_body = "x" * 20000 + server.set_response( + "HTTP/1.1 200 OK\r\n" + "Date: test\r\n" + "Server: deproxy\r\n" + f"Content-Length: {len(response_body)}\r\n\r\n" + response_body + ) + + # H2Connection has SETTINGS_MAX_FRAME_SIZE = 16384 in local config therefore, + # client does not receive response if Tempesta send DATA frame larger than 16384 + client.send_request(self.get_request, "200") + self.assertEqual(len(client.last_response.body), len(response_body)) + + def test_large_headers_frame_in_response(self): + """ + Tempesta must separate response headers to HEADERS and CONTINUATION frames because + it is larger than SETTINGS_MAX_FRAME_SIZE. + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + server: deproxy_server.StaticDeproxyServer = self.get_server("deproxy") + + client.update_initial_settings(max_frame_size=16384) + + large_header = ("qwerty", "x" * 17000) + server.set_response( + "HTTP/1.1 200 OK\r\n" + "Date: test\r\n" + "Server: deproxy\r\n" + f"{large_header[0]}: {large_header[1]}\r\n" + "Content-Length: 0\r\n\r\n" + ) + + # H2Connection has SETTINGS_MAX_FRAME_SIZE = 16384 in local config therefore, + # client does not receive response if Tempesta send HEADERS frame larger than 16384 + client.send_request(self.post_request, "200") + self.assertIsNotNone(client.last_response.headers.get(large_header[0])) + self.assertEqual( + len(client.last_response.headers.get(large_header[0])), len(large_header[1]) + ) + + def test_headers_frame_is_large_than_max_frame_size(self): + """ + An endpoint MUST send an error code of FRAME_SIZE_ERROR if + a frame exceeds the size defined in SETTINGS_MAX_FRAME_SIZE. + RFC 9113 4.2 + """ + self.start_all_services() + + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + client.update_initial_settings() + # We set SETTINGS_MAX_FRAME_SIZE = 20000 that H2Connection does not raise error, + # but Tempesta has default SETTINGS_MAX_FRAME_SIZE = 16384. + client.h2_connection.max_outbound_frame_size = 20000 + + request = self.post_request + request.append(("qwerty", "x" * 17000)) + + client.make_request(request=request, end_stream=True, huffman=False) + self.assertFalse(client.wait_for_response(1)) + self.assertTrue(client.connection_is_closed()) + + def test_data_frame_is_large_than_max_frame_size(self): + """ + An endpoint MUST send an error code of FRAME_SIZE_ERROR if + a frame exceeds the size defined in SETTINGS_MAX_FRAME_SIZE. + RFC 9113 4.2 + """ + self.start_all_services() + + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + + client.update_initial_settings() + # We set SETTINGS_MAX_FRAME_SIZE = 20000 that H2Connection does not raise error, + # but Tempesta has default SETTINGS_MAX_FRAME_SIZE = 16384. + client.h2_connection.max_outbound_frame_size = 20000 + + client.make_request( + request=(self.post_request, "x" * 18000), end_stream=True, huffman=False + ) + self.assertFalse(client.wait_for_response(1)) + self.assertTrue(client.connection_is_closed()) diff --git a/http2_general/test_h2_streams.py b/http2_general/test_h2_streams.py new file mode 100644 index 000000000..72a90e1a6 --- /dev/null +++ b/http2_general/test_h2_streams.py @@ -0,0 +1,133 @@ +"""Functional tests for h2 streams.""" + +__author__ = "Tempesta Technologies, Inc." +__copyright__ = "Copyright (C) 2023 Tempesta Technologies, Inc." +__license__ = "GPL2" + +from h2.connection import AllowedStreamIDs +from h2.errors import ErrorCodes +from h2.stream import StreamInputs + +from framework import deproxy_client +from http2_general.helpers import H2Base + + +class TestH2Stream(H2Base): + def test_max_concurrent_stream(self): + """ + An endpoint that receives a HEADERS frame that causes its advertised concurrent + stream limit to be exceeded MUST treat this as a stream error + of type PROTOCOL_ERROR or REFUSED_STREAM. + RFC 9113 5.1.2 + """ + self.start_all_services() + client = self.get_client("deproxy") + + # TODO need change after fix issue #1394 + max_streams = 128 + + for _ in range(max_streams): + client.make_request(request=self.post_request, end_stream=False) + client.stream_id += 2 + + client.make_request(request=self.post_request, end_stream=True) + client.wait_for_response(1) + + self.assertIn(ErrorCodes.PROTOCOL_ERROR or ErrorCodes.REFUSED_STREAM, client.error_codes) + + def test_reuse_stream_id(self): + """ + Stream identifiers cannot be reused. + + An endpoint that receives an unexpected stream identifier MUST + respond with a connection error of type PROTOCOL_ERROR. + RFC 9113 5.1.1 + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + self.initiate_h2_connection(client) + + # send headers frame with stream_id = 1 + client.send_request(self.post_request, "200") + # send headers frame with stream_id = 1 again. + client.send_bytes( + data=b"\x00\x00\n\x01\x05\x00\x00\x00\x01A\x85\x90\xb1\x98u\x7f\x84\x87\x83", + expect_response=True, + ) + client.wait_for_response(1) + + client.send_request(self.post_request, "200") + + self.assertIn(ErrorCodes.PROTOCOL_ERROR, client.error_codes) + + def test_headers_frame_with_zero_stream_id(self): + """ + The identifier of a newly established stream MUST be numerically greater + than all streams that the initiating endpoint has opened or reserved. + + An endpoint that receives an unexpected stream identifier MUST + respond with a connection error of type PROTOCOL_ERROR. + RFC 9113 5.1.1 + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + # add preamble + settings frame with default variable into data_to_send + self.initiate_h2_connection(client) + # send headers frame with stream_id = 0. + client.send_bytes( + b"\x00\x00\n\x01\x05\x00\x00\x00\x00A\x85\x90\xb1\x98u\x7f\x84\x87\x83", + expect_response=True, + ) + client.wait_for_response(1) + + self.assertIn(ErrorCodes.PROTOCOL_ERROR, client.error_codes) + + def test_request_with_even_numbered_stream_id(self): + """ + Streams initiated by a client MUST use odd-numbered stream identifiers. + + An endpoint that receives an unexpected stream identifier MUST + respond with a connection error of type PROTOCOL_ERROR. + RFC 9113 5.1.1 + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + self.initiate_h2_connection(client) + # send headers frame with stream_id = 2. + client.send_bytes( + b"\x00\x00\n\x01\x05\x00\x00\x00\x02A\x85\x90\xb1\x98u\x7f\x84\x87\x83", + expect_response=True, + ) + client.wait_for_response(1) + + self.assertIn(ErrorCodes.PROTOCOL_ERROR, client.error_codes) + + def test_request_with_large_stream_id(self): + """ + stream id >= 0x7fffffff (2**31-1). + + A reserved 1-bit field. The semantics of this bit are undefined, + and the bit MUST remain unset (0x00) when sending and MUST be ignored when receiving. + RFC 9113 4.2 + """ + self.start_all_services() + client: deproxy_client.DeproxyClientH2 = self.get_client("deproxy") + self.initiate_h2_connection(client) + + # Create stream that H2Connection object does not raise error. + # We are creating stream with id = 2 ** 31 - 1 because Tempesta must return response + # in stream with id = 2 ** 31 - 1, but request will be made in stream with id = 2 ** 32 - 1 + stream = client.h2_connection._begin_new_stream( + (2**31 - 1), AllowedStreamIDs(client.h2_connection.config.client_side) + ) + stream.state_machine.process_input(StreamInputs.SEND_HEADERS) + # add request method that avoid error in handle_read + client.methods.append("POST") + # send headers frame with stream_id = 0xffffffff (2**32-1). + client.send_bytes( + b"\x00\x00\n\x01\x05\xff\xff\xff\xffA\x85\x90\xb1\x98u\x7f\x84\x87\x83", + expect_response=True, + ) + + self.assertTrue(client.wait_for_response()) + self.assertEqual(client.last_response.status, "200") diff --git a/tests_disabled.json b/tests_disabled.json index e3a9d2cd6..e39968f19 100644 --- a/tests_disabled.json +++ b/tests_disabled.json @@ -444,6 +444,58 @@ { "name": "access_log.test_access_log_h2.FrangTest", "reason": "Disabled by issue #1781" + }, + { + "name": "http2_general.test_flow_control_window", + "reason": "Disabled by issue #1394" + }, + { + "name": "http2_general.test_h2_streams.TestH2Stream.test_max_concurrent_stream", + "reason": "Disabled by issue #1346" + }, + { + "name": "http2_general.test_h2_hpack.TestHpack.test_clearing_dynamic_table_with_settings_frame", + "reason": "Disabled by issue #1793" + }, + { + "name": "http2_general.test_h2_streams.TestH2Stream.test_reuse_stream_id", + "reason": "Disabled by issue #1800" + }, + { + "name": "http2_general.test_h2_headers.TestConnectionHeaders.test_proxy_connection_header_in_request", + "reason": "Disabled by issue #1805" + }, + { + "name": "http2_general.test_h2_headers.TestConnectionHeaders.test_upgrade_header_in_request", + "reason": "Disabled by issue #1805" + }, + { + "name": "http2_general.test_h2_headers.TestConnectionHeaders.test_proxy_connection_header_in_response", + "reason": "Disabled by issue #1805" + }, + { + "name": "http2_general.test_h2_headers.TestConnectionHeaders.test_upgrade_header_in_response", + "reason": "Disabled by issue #1805" + }, + { + "name": "http2_general.test_h2_hpack.TestHpack.test_rewrite_dynamic_table_for_response", + "reason": "Disabled by issue #1792" + }, + { + "name": "http2_general.test_h2_hpack.TestHpack.test_settings_header_table_size", + "reason": "Disabled by PR #1798" + }, + { + "name": "http2_general.test_h2_hpack.TestHpack.test_settings_header_table_stress", + "reason": "Disabled by issue #1792" + }, + { + "name": "http2_general.test_h2_hpack.TestFramePayloadLength.test_large_frame_payload_length", + "reason": "Disabled by PR #1798" + }, + { + "name": "http2_general.test_h2_max_frame_size.TestMaxFrameSize.test_large_headers_frame_in_response", + "reason": "Disabled by issue #1103" } ] }