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

Commit d8d9148

Browse files
committed
Merge branch 'development' into add_proxy_support
2 parents 3f84dce + 2889eeb commit d8d9148

File tree

14 files changed

+414
-49
lines changed

14 files changed

+414
-49
lines changed

.travis/install.sh

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ if [[ "$NGHTTP2" = true ]]; then
2222
# Now, download and install nghttp2's latest version.
2323
git clone https://github.com/tatsuhiro-t/nghttp2.git
2424
cd nghttp2
25+
DIR=`pwd`
26+
export PYTHONPATH="$DIR/lib/python${TRAVIS_PYTHON_VERSION}/site-packages"
27+
mkdir -p $PYTHONPATH
2528
autoreconf -i
2629
automake
2730
autoconf
28-
./configure --disable-threads
31+
./configure --disable-threads --prefix=`pwd`
2932
make
30-
sudo make install
33+
make install
3134

3235
# The makefile doesn't install into the active virtualenv. Install again.
3336
cd python

CONTRIBUTORS.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ In chronological order:
2929
- Jerome De Cuyper (@jdecuyper)
3030

3131
- Updated documentation and tests.
32+
- Added support for user-provided SSLContext objects.
33+
- Improved support for HTTP/2 error codes.
34+
- Added support for graceful connection closure.
3235

3336
- Fred Thomsen (@fredthomsen)
3437

HISTORY.rst

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,53 @@ Release History
44
dev
55
---
66

7+
*Feature Enhancement*
8+
9+
- Pay attention to max frame length changes from remote peers. Thanks to
10+
@jdecuyper!
11+
12+
*Bugfixes*
13+
14+
- Prevent hyper from emitting oversized frames. Thanks to @jdecuyper!
15+
- Prevent hyper from emitting RST_STREAM frames whenever it finishes consuming
16+
a stream.
17+
- Prevent hyper from emitting lots of RST_STREAM frames.
18+
19+
*Software Updates*
20+
21+
- Updated hyperframe to version 1.1.1.
22+
23+
0.4.0 (2015-06-21)
24+
------------------
25+
726
*New Features*
827

9-
- Support for upgrading plaintext HTTP/1.1 to plaintext HTTP/2. (`Issue 28`_)
28+
- HTTP/1.1 and HTTP/2 abstraction layer. Don't specify what version you want to
29+
use, just automatically get the best version the server supports!
30+
- Support for upgrading plaintext HTTP/1.1 to plaintext HTTP/2, with thanks to
31+
@fredthomsen! (`Issue #28`_)
1032
- ``HTTP11Connection`` and ``HTTPConnection`` objects are now both context
1133
managers.
34+
- Added support for ALPN negotiation when using PyOpenSSL. (`Issue #31`_)
35+
- Added support for user-provided SSLContext objects, with thanks to
36+
@jdecuyper! (`Issue #8`_)
37+
- Better support for HTTP/2 error codes, with thanks to @jdecuyper!
38+
(`Issue #119`_)
39+
- More gracefully close connections, with thanks to @jdecuyper! (`Issue #15`_)
40+
41+
*Structural Changes*
42+
43+
- The framing and HPACK layers were stripped out into their own libraries.
44+
45+
*Bugfixes*
46+
47+
- Properly verify hostnames when using PyOpenSSL.
1248

49+
.. _Issue #8: https://github.com/Lukasa/hyper/issues/8
50+
.. _Issue #15: https://github.com/Lukasa/hyper/issues/15
1351
.. _Issue #28: https://github.com/Lukasa/hyper/issues/28
52+
.. _Issue #31: https://github.com/Lukasa/hyper/issues/31
53+
.. _Issue #119: https://github.com/Lukasa/hyper/issues/119
1454

1555
0.3.1 (2015-04-03)
1656
------------------

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.PHONY: certs publish test
22

33
certs:
4-
curl http://ci.kennethreitz.org/job/ca-bundle/lastSuccessfulBuild/artifact/cacerts.pem -o hyper/certs.pem
4+
curl https://mkcert.org/generate/ -o hyper/certs.pem
55

66
publish:
77
python setup.py sdist upload

docs/source/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@
5555
# built documents.
5656
#
5757
# The short X.Y version.
58-
version = '0.3.1'
58+
version = '0.4.0'
5959
# The full version, including alpha/beta/rc tags.
60-
release = '0.3.1'
60+
release = '0.4.0'
6161

6262
# The language for content autogenerated by Sphinx. Refer to documentation
6363
# for a list of supported languages.

hyper/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
A module for providing an abstraction layer over the differences between
77
HTTP/1.1 and HTTP/2.
88
"""
9-
__version__ = '0.3.1'
9+
__version__ = '0.4.0'
1010

1111
from .common.connection import HTTPConnection
1212
from .http20.connection import HTTP20Connection

hyper/http20/connection.py

Lines changed: 110 additions & 25 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
@@ -113,9 +113,13 @@ def __init_state(self):
113113
# Streams are stored in a dictionary keyed off their stream IDs. We
114114
# also save the most recent one for easy access without having to walk
115115
# the dictionary.
116+
# Finally, we add a set of all streams that we or the remote party
117+
# forcefully closed with RST_STREAM, to avoid encountering issues where
118+
# frames were already in flight before the RST was processed.
116119
self.streams = {}
117120
self.recent_stream = None
118121
self.next_stream_id = 1
122+
self.reset_streams = set()
119123

120124
# Header encoding/decoding is at the connection scope, so we embed a
121125
# header encoder and a decoder. These get passed to child stream
@@ -126,6 +130,7 @@ def __init_state(self):
126130
# Values for the settings used on an HTTP/2 connection.
127131
self._settings = {
128132
SettingsFrame.INITIAL_WINDOW_SIZE: 65535,
133+
SettingsFrame.SETTINGS_MAX_FRAME_SIZE: FRAME_MAX_LEN,
129134
}
130135

131136
# The socket used to send data.
@@ -250,16 +255,17 @@ def _send_preamble(self):
250255
# The server will also send an initial settings frame, so get it.
251256
self._recv_cb()
252257

253-
def close(self):
258+
def close(self, error_code=None):
254259
"""
255260
Close the connection to the server.
256261
262+
:param error_code: (optional) The error code to reset all streams with.
257263
:returns: Nothing.
258264
"""
259265
# Close all streams
260266
for stream in list(self.streams.values()):
261267
log.debug("Close stream %d" % stream.stream_id)
262-
stream.close()
268+
stream.close(error_code)
263269

264270
# Send GoAway frame to the server
265271
try:
@@ -391,10 +397,14 @@ def receive_frame(self, frame):
391397
if 'ACK' not in frame.flags:
392398
self._update_settings(frame)
393399

394-
# Need to return an ack.
395-
f = SettingsFrame(0)
396-
f.flags.add('ACK')
397-
self._send_cb(f)
400+
# When the setting containing the max frame size value is out
401+
# of range, the spec dictates to tear down the connection.
402+
# Therefore we make sure the socket is still alive before
403+
# returning the ack.
404+
if self._sock is not None:
405+
f = SettingsFrame(0)
406+
f.flags.add('ACK')
407+
self._send_cb(f)
398408
elif frame.type == GoAwayFrame.type:
399409
# If we get GoAway with error code zero, we are doing a graceful
400410
# shutdown and all is well. Otherwise, throw an exception.
@@ -404,13 +414,19 @@ def receive_frame(self, frame):
404414
# code registry otherwise use the frame's additional data.
405415
if frame.error_code != 0:
406416
try:
407-
name, number, description = errors.get_data(frame.error_code)
417+
name, number, description = errors.get_data(
418+
frame.error_code
419+
)
408420
except ValueError:
409-
error_string = ("Encountered error code %d, extra data %s" %
410-
(frame.error_code, frame.additional_data))
421+
error_string = (
422+
"Encountered error code %d, extra data %s" %
423+
(frame.error_code, frame.additional_data)
424+
)
411425
else:
412-
error_string = ("Encountered error %s %s: %s" %
413-
(name, number, description))
426+
error_string = (
427+
"Encountered error %s %s: %s" %
428+
(name, number, description)
429+
)
414430

415431
raise ConnectionError(error_string)
416432

@@ -452,6 +468,25 @@ def _update_settings(self, frame):
452468

453469
self._settings[SettingsFrame.INITIAL_WINDOW_SIZE] = newsize
454470

471+
if SettingsFrame.SETTINGS_MAX_FRAME_SIZE in frame.settings:
472+
new_size = frame.settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE]
473+
if FRAME_MAX_LEN <= new_size <= FRAME_MAX_ALLOWED_LEN:
474+
self._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE] = (
475+
new_size
476+
)
477+
else:
478+
log.warning(
479+
"Frame size %d is outside of allowed range",
480+
new_size
481+
)
482+
483+
# Tear the connection down with error code PROTOCOL_ERROR
484+
self.close(1)
485+
error_string = (
486+
"Advertised frame size %d is outside of range" % (new_size)
487+
)
488+
raise ConnectionError(error_string)
489+
455490
def _new_stream(self, stream_id=None, local_closed=False):
456491
"""
457492
Returns a new stream object for this connection.
@@ -472,12 +507,18 @@ def _close_stream(self, stream_id, error_code=None):
472507
"""
473508
Called by a stream when it would like to be 'closed'.
474509
"""
475-
if error_code is not None:
476-
f = RstStreamFrame(stream_id)
477-
f.error_code = error_code
478-
self._send_cb(f)
479-
480-
del self.streams[stream_id]
510+
# Graceful shutdown of streams involves not emitting an error code
511+
# at all.
512+
if error_code:
513+
self._send_rst_frame(stream_id, error_code)
514+
else:
515+
# Just delete the stream.
516+
try:
517+
del self.streams[stream_id]
518+
except KeyError as e: # pragma: no cover
519+
log.warn(
520+
"Stream with id %d does not exist: %s",
521+
stream_id, e)
481522

482523
def _send_cb(self, frame, tolerate_peer_gone=False):
483524
"""
@@ -498,6 +539,14 @@ def _send_cb(self, frame, tolerate_peer_gone=False):
498539

499540
data = frame.serialize()
500541

542+
max_frame_size = self._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE]
543+
if frame.body_len > max_frame_size:
544+
raise ValueError(
545+
"Frame size %d exceeds maximum frame size setting %d" %
546+
(frame.body_len,
547+
self._settings[SettingsFrame.SETTINGS_MAX_FRAME_SIZE])
548+
)
549+
501550
log.info(
502551
"Sending frame %s on stream %d",
503552
frame.__class__.__name__,
@@ -507,7 +556,8 @@ def _send_cb(self, frame, tolerate_peer_gone=False):
507556
try:
508557
self._sock.send(data)
509558
except socket.error as e:
510-
if not tolerate_peer_gone or e.errno not in (errno.EPIPE, errno.ECONNRESET):
559+
if (not tolerate_peer_gone or
560+
e.errno not in (errno.EPIPE, errno.ECONNRESET)):
511561
raise
512562

513563
def _adjust_receive_window(self, frame_len):
@@ -538,6 +588,15 @@ def _consume_single_frame(self):
538588
# Parse the header. We can use the returned memoryview directly here.
539589
frame, length = Frame.parse_frame_header(header)
540590

591+
if (length > FRAME_MAX_LEN):
592+
log.warning(
593+
"Frame size exceeded on stream %d (received: %d, max: %d)",
594+
frame.stream_id,
595+
length,
596+
FRAME_MAX_LEN
597+
)
598+
self._send_rst_frame(frame.stream_id, 6) # 6 = FRAME_SIZE_ERROR
599+
541600
# Read the remaining data from the socket.
542601
data = self._recv_payload(length)
543602
self._consume_frame_payload(frame, data)
@@ -598,9 +657,14 @@ def _consume_frame_payload(self, frame, data):
598657
# the ENABLE_PUSH setting is 0, but the spec leaves the client
599658
# action undefined when they do it anyway. So we just refuse
600659
# the stream and go about our business.
601-
f = RstStreamFrame(frame.promised_stream_id)
602-
f.error_code = 7 # REFUSED_STREAM
603-
self._send_cb(f)
660+
self._send_rst_frame(frame.promised_stream_id, 7)
661+
662+
# If this frame was received on a stream that has been reset, drop it.
663+
if frame.stream_id in self.reset_streams:
664+
log.info(
665+
"Stream %s has been reset, dropping frame.", frame.stream_id
666+
)
667+
return
604668

605669
# Work out to whom this frame should go.
606670
if frame.stream_id != 0:
@@ -609,12 +673,15 @@ def _consume_frame_payload(self, frame, data):
609673
except KeyError:
610674
# If we receive an unexpected stream identifier then we
611675
# cancel the stream with an error of type PROTOCOL_ERROR
612-
f = RstStreamFrame(frame.stream_id)
613-
f.error_code = 1 # PROTOCOL_ERROR
614-
self._send_cb(f)
676+
self._send_rst_frame(frame.stream_id, 1)
615677
log.warning(
616678
"Unexpected stream identifier %d" % (frame.stream_id)
617679
)
680+
681+
# If this is a RST_STREAM frame, we may get more than one (because
682+
# of frames in flight). Keep track.
683+
if frame.type == RstStreamFrame.type:
684+
self.reset_streams.add(frame.stream_id)
618685
else:
619686
self.receive_frame(frame)
620687

@@ -640,6 +707,24 @@ def _recv_cb(self):
640707
except ConnectionResetError:
641708
break
642709

710+
def _send_rst_frame(self, stream_id, error_code):
711+
"""
712+
Send reset stream frame with error code and remove stream from map.
713+
"""
714+
f = RstStreamFrame(stream_id)
715+
f.error_code = error_code
716+
self._send_cb(f)
717+
718+
try:
719+
del self.streams[stream_id]
720+
except KeyError as e: # pragma: no cover
721+
log.warn(
722+
"Stream with id %d does not exist: %s",
723+
stream_id, e)
724+
725+
# Keep track of the fact that we reset this stream in case there are
726+
# other frames in flight.
727+
self.reset_streams.add(stream_id)
643728

644729
# The following two methods are the implementation of the context manager
645730
# protocol.

hyper/http20/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,10 @@ class ProtocolError(HTTP20Error):
4040
The remote party violated the HTTP/2 protocol.
4141
"""
4242
pass
43+
44+
45+
class StreamResetError(HTTP20Error):
46+
"""
47+
A stream was forcefully reset by the remote party.
48+
"""
49+
pass

0 commit comments

Comments
 (0)