Skip to content
This repository was archived by the owner on Jan 13, 2021. It is now read-only.

Commit bbd1830

Browse files
committed
Merge commit 'a031605' into pr135
2 parents 5d8b321 + a031605 commit bbd1830

File tree

3 files changed

+190
-23
lines changed

3 files changed

+190
-23
lines changed

hyper/http20/connection.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ..packages.hyperframe.frame import (
1313
FRAMES, DataFrame, HeadersFrame, PushPromiseFrame, RstStreamFrame,
1414
SettingsFrame, Frame, WindowUpdateFrame, GoAwayFrame, PingFrame,
15-
BlockedFrame
15+
BlockedFrame, FRAME_MAX_LEN, FRAME_MAX_ALLOWED_LEN
1616
)
1717
from ..packages.hpack.hpack_compat import Encoder, Decoder
1818
from .stream import Stream
@@ -123,6 +123,7 @@ def __init_state(self):
123123
# Values for the settings used on an HTTP/2 connection.
124124
self._settings = {
125125
SettingsFrame.INITIAL_WINDOW_SIZE: 65535,
126+
SettingsFrame.SETTINGS_MAX_FRAME_SIZE: FRAME_MAX_LEN,
126127
}
127128

128129
# The socket used to send data.
@@ -247,16 +248,17 @@ def _send_preamble(self):
247248
# The server will also send an initial settings frame, so get it.
248249
self._recv_cb()
249250

250-
def close(self):
251+
def close(self, error_code=None):
251252
"""
252253
Close the connection to the server.
253254
255+
:param error_code: (optional) The error code to reset all streams with.
254256
:returns: Nothing.
255257
"""
256258
# Close all streams
257259
for stream in list(self.streams.values()):
258260
log.debug("Close stream %d" % stream.stream_id)
259-
stream.close()
261+
stream.close(error_code)
260262

261263
# Send GoAway frame to the server
262264
try:
@@ -388,10 +390,14 @@ def receive_frame(self, frame):
388390
if 'ACK' not in frame.flags:
389391
self._update_settings(frame)
390392

391-
# Need to return an ack.
392-
f = SettingsFrame(0)
393-
f.flags.add('ACK')
394-
self._send_cb(f)
393+
# When the setting containing the max frame size value is out
394+
# of range, the spec dictates to tear down the connection.
395+
# Therefore we make sure the socket is still alive before
396+
# returning the ack.
397+
if self._sock is not None:
398+
f = SettingsFrame(0)
399+
f.flags.add('ACK')
400+
self._send_cb(f)
395401
elif frame.type == GoAwayFrame.type:
396402
# If we get GoAway with error code zero, we are doing a graceful
397403
# shutdown and all is well. Otherwise, throw an exception.
@@ -449,6 +455,20 @@ def _update_settings(self, frame):
449455

450456
self._settings[SettingsFrame.INITIAL_WINDOW_SIZE] = newsize
451457

458+
if SettingsFrame.SETTINGS_MAX_FRAME_SIZE in frame.settings:
459+
new_size = frame.settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE]
460+
if FRAME_MAX_LEN <= new_size <= FRAME_MAX_ALLOWED_LEN:
461+
self._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] = new_size
462+
else:
463+
log.warning(
464+
"Frame size %d is outside of allowed range",
465+
new_size)
466+
# Tear the connection down with error code PROTOCOL_ERROR
467+
self.close(1)
468+
error_string = ("Advertised frame size %d is outside of range" %
469+
(new_size))
470+
raise ConnectionError(error_string)
471+
452472
def _new_stream(self, stream_id=None, local_closed=False):
453473
"""
454474
Returns a new stream object for this connection.
@@ -469,12 +489,7 @@ def _close_stream(self, stream_id, error_code=None):
469489
"""
470490
Called by a stream when it would like to be 'closed'.
471491
"""
472-
if error_code is not None:
473-
f = RstStreamFrame(stream_id)
474-
f.error_code = error_code
475-
self._send_cb(f)
476-
477-
del self.streams[stream_id]
492+
self._send_rst_frame(stream_id, error_code)
478493

479494
def _send_cb(self, frame, tolerate_peer_gone=False):
480495
"""
@@ -495,6 +510,13 @@ def _send_cb(self, frame, tolerate_peer_gone=False):
495510

496511
data = frame.serialize()
497512

513+
if frame.body_len > self._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE]:
514+
raise ValueError(
515+
"Frame size %d exceeds maximum frame size setting %d" %
516+
(frame.body_len,
517+
self._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE])
518+
)
519+
498520
log.info(
499521
"Sending frame %s on stream %d",
500522
frame.__class__.__name__,
@@ -535,6 +557,15 @@ def _consume_single_frame(self):
535557
# Parse the header. We can use the returned memoryview directly here.
536558
frame, length = Frame.parse_frame_header(header)
537559

560+
if (length > FRAME_MAX_LEN):
561+
log.warning(
562+
"Frame size exceeded on stream %d (received: %d, max: %d)",
563+
frame.stream_id,
564+
length,
565+
FRAME_MAX_LEN
566+
)
567+
self._send_rst_frame(frame.stream_id, 6) # 6 = FRAME_SIZE_ERROR
568+
538569
# Read the remaining data from the socket.
539570
data = self._recv_payload(length)
540571
self._consume_frame_payload(frame, data)
@@ -595,9 +626,7 @@ def _consume_frame_payload(self, frame, data):
595626
# the ENABLE_PUSH setting is 0, but the spec leaves the client
596627
# action undefined when they do it anyway. So we just refuse
597628
# the stream and go about our business.
598-
f = RstStreamFrame(frame.promised_stream_id)
599-
f.error_code = 7 # REFUSED_STREAM
600-
self._send_cb(f)
629+
self._send_rst_frame(frame.promised_stream_id, 7)
601630

602631
# Work out to whom this frame should go.
603632
if frame.stream_id != 0:
@@ -606,9 +635,7 @@ def _consume_frame_payload(self, frame, data):
606635
except KeyError:
607636
# If we receive an unexpected stream identifier then we
608637
# cancel the stream with an error of type PROTOCOL_ERROR
609-
f = RstStreamFrame(frame.stream_id)
610-
f.error_code = 1 # PROTOCOL_ERROR
611-
self._send_cb(f)
638+
self._send_rst_frame(frame.stream_id, 1)
612639
log.warning(
613640
"Unexpected stream identifier %d" % (frame.stream_id)
614641
)
@@ -637,6 +664,20 @@ def _recv_cb(self):
637664
except ConnectionResetError:
638665
break
639666

667+
def _send_rst_frame(self, stream_id, error_code):
668+
"""
669+
Send reset stream frame with error code and remove stream from map.
670+
"""
671+
f = RstStreamFrame(stream_id)
672+
f.error_code = error_code
673+
self._send_cb(f)
674+
675+
try:
676+
del self.streams[stream_id]
677+
except KeyError as e: # pragma: no cover
678+
log.warn(
679+
"Stream with id %d does not exist: %s",
680+
stream_id, e)
640681

641682
# The following two methods are the implementation of the context manager
642683
# protocol.

hyper/http20/stream.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,8 +368,9 @@ def close(self, error_code=None):
368368
:returns: Nothing.
369369
"""
370370
# Right now let's not bother with grace, let's just call close on the
371-
# connection.
372-
self._close_cb(self.stream_id, error_code)
371+
# connection. If not error code is provided then assume it is a
372+
# gracefull shutdown.
373+
self._close_cb(self.stream_id, error_code or 0)
373374

374375
def _handle_header_block(self, headers):
375376
"""

test/test_hyper.py

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from hyper.packages.hyperframe.frame import (
33
Frame, DataFrame, RstStreamFrame, SettingsFrame,
44
PushPromiseFrame, PingFrame, WindowUpdateFrame, HeadersFrame,
5-
ContinuationFrame, BlockedFrame, GoAwayFrame,
5+
ContinuationFrame, BlockedFrame, GoAwayFrame, FRAME_MAX_LEN, FRAME_MAX_ALLOWED_LEN
66
)
77
from hyper.packages.hpack.hpack_compat import Encoder, Decoder
88
from hyper.http20.connection import HTTP20Connection
@@ -27,7 +27,7 @@
2727
import socket
2828
import zlib
2929
from io import BytesIO
30-
30+
import hyper
3131

3232
def decode_frame(frame_data):
3333
f, length = Frame.parse_frame_header(frame_data[:9])
@@ -195,6 +195,7 @@ def test_closed_connections_are_reset(self):
195195
assert c.decoder is not decoder
196196
assert c._settings == {
197197
SettingsFrame.INITIAL_WINDOW_SIZE: 65535,
198+
SettingsFrame.SETTINGS_MAX_FRAME_SIZE: FRAME_MAX_LEN,
198199
}
199200
assert c._out_flow_control_window == 65535
200201
assert c.window_manager is not wm
@@ -245,6 +246,71 @@ def test_connections_increment_send_window_properly(self):
245246

246247
assert c._out_flow_control_window == 65535 + 1000
247248

249+
def test_connections_handle_resizing_max_frame_size_properly(self):
250+
sock = DummySocket()
251+
f = SettingsFrame(0)
252+
f.settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] = 65536 # 2^16
253+
c = HTTP20Connection('www.google.com')
254+
c._sock = sock
255+
256+
# 'Receive' the SETTINGS frame.
257+
c.receive_frame(f)
258+
259+
# Confirm that the setting is stored and the max frame size increased.
260+
assert c._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] == 65536
261+
262+
# Confirm we got a SETTINGS ACK.
263+
f2 = decode_frame(sock.queue[0])
264+
assert isinstance(f2, SettingsFrame)
265+
assert f2.stream_id == 0
266+
assert f2.flags == set(['ACK'])
267+
268+
def test_connections_handle_too_small_max_frame_size_properly(self):
269+
sock = DummySocket()
270+
f = SettingsFrame(0)
271+
f.settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] = 1024
272+
c = HTTP20Connection('www.google.com')
273+
c._sock = sock
274+
275+
# 'Receive' the SETTINGS frame.
276+
with pytest.raises(ConnectionError):
277+
c.receive_frame(f)
278+
279+
# The value advertised by an endpoint MUST be between 2^14 and
280+
# 2^24-1 octets. Confirm that the max frame size did not increase.
281+
assert c._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] == FRAME_MAX_LEN
282+
283+
# When the setting containing the max frame size value is out of range,
284+
# the spec dictates to tear down the connection.
285+
assert c._sock == None
286+
287+
# Check if GoAway frame was correctly sent to the endpoint
288+
f = decode_frame(sock.queue[0])
289+
assert isinstance(f, GoAwayFrame)
290+
291+
def test_connections_handle_too_big_max_frame_size_properly(self):
292+
sock = DummySocket()
293+
f = SettingsFrame(0)
294+
f.settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] = 67108864 # 2^26
295+
c = HTTP20Connection('www.google.com')
296+
c._sock = sock
297+
298+
# 'Receive' the SETTINGS frame.
299+
with pytest.raises(ConnectionError):
300+
c.receive_frame(f)
301+
302+
# The value advertised by an endpoint MUST be between 2^14 and
303+
# 2^24-1 octets. Confirm that the max frame size did not increase.
304+
assert c._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] == FRAME_MAX_LEN
305+
306+
# When the setting containing the max frame size value is out of range,
307+
# the spec dictates to tear down the connection.
308+
assert c._sock == None
309+
310+
# Check if GoAway frame was correctly sent to the endpoint
311+
f = decode_frame(sock.queue[0])
312+
assert isinstance(f, GoAwayFrame)
313+
248314
def test_connections_handle_resizing_header_tables_properly(self):
249315
sock = DummySocket()
250316
f = SettingsFrame(0)
@@ -1282,6 +1348,65 @@ def data_callback(frame):
12821348
assert isinstance(f, RstStreamFrame)
12831349
assert f.error_code == 1 # PROTOCOL_ERROR
12841350

1351+
def test_connection_sends_rst_frame_if_frame_size_too_large(self):
1352+
sock = DummySocket()
1353+
d = DataFrame(1)
1354+
# Receive oversized frame on the client side.
1355+
# Create huge data frame that exceeds the FRAME_MAX_LEN value in order
1356+
# to trigger the reset frame with error code 6 (FRAME_SIZE_ERROR).
1357+
# FRAME_MAX_LEN is a constant value for the hyper client and cannot
1358+
# be updated as of now.
1359+
d.data = b''.join([b"hi there client" for x in range(40)])
1360+
sock.buffer = BytesIO(d.serialize())
1361+
1362+
frames = []
1363+
1364+
def send_rst_frame(stream_id, error_code):
1365+
f = RstStreamFrame(stream_id)
1366+
f.error_code = error_code
1367+
frames.append(f)
1368+
1369+
c = HTTP20Connection('www.google.com')
1370+
c._sock = sock
1371+
c._send_rst_frame = send_rst_frame
1372+
c.request('GET', '/')
1373+
c._recv_cb()
1374+
1375+
assert len(frames) == 1
1376+
f = frames[0]
1377+
assert isinstance(f, RstStreamFrame)
1378+
assert f.stream_id == 1
1379+
assert f.error_code == 6 #FRAME_SIZE_ERROR
1380+
1381+
def test_connection_stream_is_removed_when_receiving_out_of_range_frame(self):
1382+
sock = DummySocket()
1383+
d = DataFrame(1)
1384+
d.data = b''.join([b"hi there sir" for x in range(40)])
1385+
sock.buffer = BytesIO(d.serialize())
1386+
1387+
c = HTTP20Connection('www.google.com')
1388+
c._sock = sock
1389+
c.request('GET', '/')
1390+
1391+
# Make sure the stream gets removed from the map
1392+
# after receiving an out of size data frame
1393+
assert len(c.streams) == 1
1394+
c._recv_cb()
1395+
assert len(c.streams) == 0
1396+
1397+
def test_connection_error_when_send_out_of_range_frame(self):
1398+
# Send oversized frame to the server side.
1399+
# Create huge data frame that exceeds the intitial FRAME_MAX_LEN setting
1400+
# in order to trigger a value error when sending it.
1401+
# Note that the value of the FRAME_MAX_LEN setting can be updated
1402+
# by the server through a settings frame.
1403+
d = DataFrame(1)
1404+
d.data = b''.join([b"hi there server" for x in range(1500)])
1405+
1406+
c = HTTP20Connection('www.google.com')
1407+
c._sock = DummySocket()
1408+
with pytest.raises(ValueError):
1409+
c._send_cb(d)
12851410

12861411
# Some utility classes for the tests.
12871412
class NullEncoder(object):

0 commit comments

Comments
 (0)