-
Notifications
You must be signed in to change notification settings - Fork 5
/
test_h2_frame.py
341 lines (268 loc) · 13.3 KB
/
test_h2_frame.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
"""Functional tests for h2 frames."""
__author__ = "Tempesta Technologies, Inc."
__copyright__ = "Copyright (C) 2023 Tempesta Technologies, Inc."
__license__ = "GPL2"
from h2.errors import ErrorCodes
from h2.exceptions import StreamClosedError
from framework import deproxy_client
from helpers import checks_for_tests as checks
from http2_general.helpers import H2Base
class TestH2Frame(H2Base):
def test_data_framing(self):
"""Send many 1 byte frames in request."""
self.start_all_services()
deproxy_cl = self.get_client("deproxy")
deproxy_cl.parsing = False
request_body = "x" * 100
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)
self.__assert_test(client=deproxy_cl, request_body=request_body, request_number=1)
def test_empty_last_data_frame(self):
"""
Send request with empty last data frame. It is valid request. RFC 9113 6.9.1.
"""
self.start_all_services()
deproxy_cl = self.get_client("deproxy")
deproxy_cl.parsing = False
request_body = "123"
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)
self.__assert_test(client=deproxy_cl, request_body=request_body, request_number=1)
def test_empty_data_frame(self):
"""
Send request with empty data frame. It is valid request. RFC 9113 10.5.
"""
self.start_all_services()
deproxy_cl = self.get_client("deproxy")
deproxy_cl.parsing = False
request_body = "123"
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)
self.__assert_test(client=deproxy_cl, request_body=request_body, request_number=1)
def test_tcp_framing_for_request_headers(self):
"""Client sends PRI+SETTING+HEADERS frames by 1-byte chunks."""
client = self.get_client("deproxy")
client.segment_size = 1
self.start_all_services()
client.parsing = False
client.make_request(self.post_request)
self.__assert_test(client=client, request_body="", request_number=1)
def test_tcp_framing_for_request(self):
"""Client sends request by n-byte chunks."""
client = self.get_client("deproxy")
self.start_all_services()
client.parsing = False
chunk_sizes = [1, 2, 3, 4, 8, 16]
for chunk_size in chunk_sizes:
with self.subTest(chunk_size=chunk_size):
client.segment_size = chunk_size
client.make_request(self.post_request, False)
request_body = "0123456789"
client.make_request(request_body, True)
self.__assert_test(
client=client,
request_body=request_body,
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 test_rst_frame_in_request(self):
"""
Tempesta must handle RST_STREAM frame and close stream but other streams MUST work.
"""
client = self.get_client("deproxy")
self.start_all_services()
self.initiate_h2_connection(client)
# client opens streams with id 1, 3 and does not close them
client.make_request(request=self.post_request, end_stream=False)
client.stream_id = 3
client.make_request(request=self.post_request, end_stream=False)
# client send RST_STREAM frame with NO_ERROR code in stream 1 and
# Tempesta closes it for itself.
client.h2_connection.reset_stream(stream_id=1, error_code=0)
client.send_bytes(client.h2_connection.data_to_send())
# Client send DATA frame in stream 3 and it MUST receive response
client.send_request("qwe", "200")
# Tempesta allows creating new streams.
client.stream_id = 5
client.send_request(self.post_request, "200")
self.assertFalse(
client.connection_is_closed(), "Tempesta closed connection after receiving RST_STREAM."
)
def test_rst_frame_in_response(self):
"""
When Tempesta returns RST_STREAM:
- open streams must not be closed;
- new streams must be accepted.
"""
client = self.get_client("deproxy")
client.parsing = False
self.start_all_services()
self.initiate_h2_connection(client)
# client opens stream with id 1 and does not close it
client.make_request(request=self.post_request, end_stream=False)
# client send invalid request and Tempesta returns RST_STREAM
stream_with_rst = 3
client.stream_id = stream_with_rst
client.send_request((self.get_request + [("host", "")], "asd"), "400")
# client open new stream
client.make_request(self.get_request, end_stream=True)
client.wait_for_response(3)
# client send DATA frame in stream 1 and it must be open.
client.stream_id = 1
client.make_request("body", end_stream=True)
client.wait_for_response(3)
self.assertRaises(
StreamClosedError, client.h2_connection._get_stream_by_id, stream_with_rst
)
self.assertFalse(
client.connection_is_closed(), "Tempesta closed connection after sending RST_STREAM."
)
def test_rst_stream_with_id_0(self):
"""
RST_STREAM frames MUST be associated with a stream. If a RST_STREAM frame
is received with a stream identifier of 0x00, the recipient MUST treat this
as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
RFC 9113 6.4
"""
client = self.get_client("deproxy")
self.start_all_services()
self.initiate_h2_connection(client)
# send RST_STREAM with id 0
client.send_bytes(b"\x00\x00\x04\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00")
self.assertTrue(
client.wait_for_connection_close(1),
"Tempesta did not close connection after receiving RST_STREAM with id 0.",
)
self.assertIn(ErrorCodes.PROTOCOL_ERROR, client.error_codes)
def test_goaway_frame_in_response(self):
"""
Tempesta must:
- close all streams for connection error (GOAWAY);
- return last_stream_id.
There is an inherent race condition between an endpoint starting new streams
and the remote peer sending a GOAWAY frame. To deal with this case, the GOAWAY
contains the stream identifier of the last peer-initiated stream that was or
might be processed on the sending endpoint in this connection. For instance,
if the server sends a GOAWAY frame, the identified stream is the highest-numbered
stream initiated by the client.
RFC 9113 6.8
"""
client = self.get_client("deproxy")
self.start_all_services()
self.initiate_h2_connection(client)
# Client opens many streams and does not close them
for stream_id in range(1, 6, 2):
client.stream_id = stream_id
client.make_request(request=self.post_request, end_stream=False)
# Client send DATA frame with stream id 0.
# Tempesta MUST return GOAWAY frame with PROTOCOL_ERROR
client.send_bytes(b"\x00\x00\x03\x00\x01\x00\x00\x00\x00asd")
self.assertTrue(client.wait_for_connection_close(3), "Tempesta did not send GOAWAY frame.")
self.assertIn(ErrorCodes.PROTOCOL_ERROR, client.error_codes)
self.assertEqual(
client.last_stream_id,
stream_id,
"Tempesta returned invalid last_stream_id in GOAWAY frame.",
)
def test_goaway_frame_in_request(self):
"""
Tempesta must not close connection after receiving GOAWAY frame.
GOAWAY allows an endpoint to gracefully stop accepting new streams while still
finishing processing of previously established streams.
RFC 9113 6.8
"""
client = self.get_client("deproxy")
self.start_all_services()
self.initiate_h2_connection(client)
# Client opens many streams and does not close them
for stream_id in range(1, 6, 2):
client.stream_id = stream_id
client.make_request(request=self.post_request, end_stream=False)
# Client send GOAWAY frame with PROTOCOL_ERROR as bytes
# because `_terminate_connection` method changes state machine to closed
client.send_bytes(b"\x00\x00\x08\x07\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x01")
# Client sends frames in already open streams.
# Tempesta must handle these frames and must not close streams,
# because sender closes connection, but not receiver.
for stream_id in range(1, 6, 2):
client.stream_id = stream_id
client.make_request(request="asd", end_stream=True)
self.assertTrue(
client.wait_for_response(), "Tempesta closed connection after receiving GOAWAY frame."
)
def test_double_header_frame_in_single_stream(self):
client = self.get_client("deproxy")
self.start_all_services()
self.initiate_h2_connection(client)
client.make_request(self.post_request, end_stream=False)
client.make_request([("header1", "header value1")], end_stream=True)
self.assertTrue(client.wait_for_connection_close())
self.assertIn(ErrorCodes.PROTOCOL_ERROR, client.error_codes)
def __assert_test(self, client, request_body: str, request_number: int):
server = self.get_server("deproxy")
self.assertTrue(client.wait_for_response(timeout=5))
self.assertEqual(client.last_response.status, "200")
self.assertEqual(len(server.requests), request_number)
checks.check_tempesta_request_and_response_stats(
tempesta=self.get_tempesta(),
cl_msg_received=request_number,
cl_msg_forwarded=request_number,
srv_msg_received=request_number,
srv_msg_forwarded=request_number,
)
error_msg = "Malformed request from Tempesta."
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)