From 8416072ad78b846547dde4e34c8554c3e4f395fb Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sun, 31 May 2015 16:25:19 -0400 Subject: [PATCH 01/14] Added support for plaintext http20 --- hyper/common/connection.py | 4 ++-- hyper/http20/connection.py | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index bde9747e..11d8aad2 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -27,8 +27,8 @@ class HTTPConnection(object): ``'http2bin.org'``, ``'http2bin.org:443'`` or ``'127.0.0.1'``. :param port: (optional) The port to connect to. If not provided and one also isn't provided in the ``host`` parameter, defaults to 443. - :param secure: (optional, HTTP/1.1 only) Whether the request should use - TLS. Defaults to ``False`` for most requests, but to ``True`` for any + :param secure: (optional) Whether the request should use TLS. + Defaults to ``False`` for most requests, but to ``True`` for any request issued to port 443. :param window_manager: (optional) The class to use to manage flow control windows. This needs to be a subclass of the diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index 665e659c..905b636f 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -27,6 +27,7 @@ log = logging.getLogger(__name__) +H2_CLEARTEXT_PROTOCOL = 'h2c' class HTTP20Connection(object): """ @@ -43,6 +44,9 @@ class HTTP20Connection(object): ``'http2bin.org'``, ``'http2bin.org:443'`` or ``'127.0.0.1'``. :param port: (optional) The port to connect to. If not provided and one also isn't provided in the ``host`` parameter, defaults to 443. + :param secure: (optional) Whether the request should use TLS. Defaults to + ``False`` for most requests, but to ``True`` for any request issued to + port 443. :param window_manager: (optional) The class to use to manage flow control windows. This needs to be a subclass of the :class:`BaseFlowControlManager `. @@ -55,7 +59,7 @@ class HTTP20Connection(object): :param ssl_context: (optional) A class with custom certificate settings. If not provided then hyper's default ``SSLContext`` is used instead. """ - def __init__(self, host, port=None, window_manager=None, enable_push=False, + def __init__(self, host, port=None, secure=None, window_manager=None, enable_push=False, ssl_context=None, **kwargs): """ Creates an HTTP/2 connection to a specific server. @@ -69,6 +73,13 @@ def __init__(self, host, port=None, window_manager=None, enable_push=False, else: self.host, self.port = host, port + if secure is not None: + self.secure = secure + elif self.port == 443: + self.secure = True + else: + self.secure = False + self._enable_push = enable_push self.ssl_context = ssl_context @@ -210,9 +221,13 @@ def connect(self): if self._sock is None: sock = socket.create_connection((self.host, self.port), 5) - sock, proto = wrap_socket(sock, self.host, self.ssl_context) + if self.secure: + sock, proto = wrap_socket(sock, self.host, self.ssl_context) + else: + proto = H2_CLEARTEXT_PROTOCOL + log.debug("Selected NPN protocol: %s", proto) - assert proto in H2_NPN_PROTOCOLS + assert proto in (H2_NPN_PROTOCOLS, H2_CLEARTEXT_PROTOCOL) self._sock = BufferedSocket(sock, self.network_buffer_size) From c860485b24ae3b3aa82f82f73a7438bb3444caef Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sun, 31 May 2015 18:53:58 -0400 Subject: [PATCH 02/14] Add HTTP upgrade functionality --- hyper/common/connection.py | 20 +++++++++++--------- hyper/common/exceptions.py | 9 +++++++++ hyper/http11/connection.py | 21 +++++++++++++++++++-- hyper/http20/connection.py | 8 ++++---- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index 11d8aad2..cc2cc643 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -5,12 +5,11 @@ Hyper's HTTP/1.1 and HTTP/2 abstraction layer. """ -from .exceptions import TLSUpgrade +from .exceptions import TLSUpgrade, HTTPUpgrade from ..http11.connection import HTTP11Connection -from ..http20.connection import HTTP20Connection +from ..http20.connection import HTTP20Connection, H2C_PROTOCOL from ..tls import H2_NPN_PROTOCOLS - class HTTPConnection(object): """ An object representing a single HTTP connection to a server. @@ -88,12 +87,15 @@ def request(self, method, url, body=None, headers={}): return self._conn.request( method=method, url=url, body=body, headers=headers ) - except TLSUpgrade as e: - # We upgraded in the NPN/ALPN handshake. We can just go straight to - # the world of HTTP/2. Replace the backing object and insert the - # socket into it. - assert e.negotiated in H2_NPN_PROTOCOLS - + except (TLSUpgrade, HTTPUpgrade) as e: + # We upgraded in the NPN/ALPN handshake or via the HTTP Upgrade + # mechanism. We can just go straight to the world of HTTP/2. + #Replace the backing object and insert the socket into it. + if(type(e) is TLSUpgrade): + assert e.negotiated in H2_NPN_PROTOCOLS + else: + assert e.negotiated == H2C_PROTOCOL + self._conn = HTTP20Connection( self._host, self._port, **self._h2_kwargs ) diff --git a/hyper/common/exceptions.py b/hyper/common/exceptions.py index dae3f185..1f0f9459 100644 --- a/hyper/common/exceptions.py +++ b/hyper/common/exceptions.py @@ -51,3 +51,12 @@ def __init__(self, negotiated, sock): super(TLSUpgrade, self).__init__() self.negotiated = negotiated self.sock = sock + +class HTTPUpgrade(Exception): + """ + We upgraded to a new protocol via the HTTP Upgrade response. + """ + def __init__(self, negotiated, sock): + super(HTTPUpgrade, self).__init__() + self.negotiated = negotiated + self.sock = sock diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 210d9599..c46186ac 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -8,15 +8,18 @@ import logging import os import socket +import base64 from .response import HTTP11Response from ..tls import wrap_socket from ..common.bufsocket import BufferedSocket -from ..common.exceptions import TLSUpgrade +from ..common.exceptions import TLSUpgrade, HTTPUpgrade from ..common.headers import HTTPHeaderMap from ..common.util import to_bytestring from ..compat import bytes +from ..http20.connection import H2C_PROTOCOL +from ..packages.hyperframe.frame import SettingsFrame # We prefer pycohttpparser to the pure-Python interpretation try: # pragma: no cover @@ -94,7 +97,7 @@ def connect(self): if self.secure: sock, proto = wrap_socket(sock, self.host, self.ssl_context) - log.debug("Selected NPN protocol: %s", proto) + log.debug("Selected protocol: %s", proto) sock = BufferedSocket(sock, self.network_buffer_size) if proto not in ('http/1.1', None): @@ -130,6 +133,15 @@ def request(self, method, url, body=None, headers={}): if self._sock is None: self.connect() + #add HTTP Upgrade headers + headers[b'connection'] = b'Upgrade, HTTP2-Settings' + headers[b'upgrade'] = H2C_PROTOCOL + + #need to encode SETTINGS frame payload in Base64 and put into the HTTP-2 Settings header + http2Settings = SettingsFrame(0) + http2Settings.settings[SettingsFrame.INITIAL_WINDOW_SIZE] = 65535 + headers[b'HTTP2-Settings'] = base64.b64encode(http2Settings.serialize_body()) + # We may need extra headers. if body: body_type = self._add_body_headers(headers, body) @@ -166,6 +178,11 @@ def get_response(self): self._sock.advance_buffer(response.consumed) + if(response.status == 101 and + response.getheader('Connection') == 'Upgrade' and + response.getheader('Upgrade') == H2C_PROTOCOL): + raise HTTPUpgrade(proto, sock) + return HTTP11Response( response.status, response.msg.tobytes(), diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index 905b636f..ecd5a078 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -25,9 +25,9 @@ import logging import socket -log = logging.getLogger(__name__) +H2C_PROTOCOL = 'h2c' -H2_CLEARTEXT_PROTOCOL = 'h2c' +log = logging.getLogger(__name__) class HTTP20Connection(object): """ @@ -224,10 +224,10 @@ def connect(self): if self.secure: sock, proto = wrap_socket(sock, self.host, self.ssl_context) else: - proto = H2_CLEARTEXT_PROTOCOL + proto = H2C_PROTOCOL log.debug("Selected NPN protocol: %s", proto) - assert proto in (H2_NPN_PROTOCOLS, H2_CLEARTEXT_PROTOCOL) + assert proto in (H2_NPN_PROTOCOLS, H2C_PROTOCOL) self._sock = BufferedSocket(sock, self.network_buffer_size) From 97e529e06a833f84e8901beeee5ad83478c9a302 Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Mon, 1 Jun 2015 00:04:18 -0400 Subject: [PATCH 03/14] Fixing HTTP upgrade functionality --- hyper/common/connection.py | 40 +++++++++++++++++++++++++++++--------- hyper/common/exceptions.py | 3 ++- hyper/http11/connection.py | 14 +++++++------ hyper/http20/connection.py | 2 +- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index cc2cc643..6277ddf2 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -55,7 +55,7 @@ def __init__(self, self._h1_kwargs = {'secure': secure, 'ssl_context': ssl_context} self._h2_kwargs = { 'window_manager': window_manager, 'enable_push': enable_push, - 'ssl_context': ssl_context + 'secure': secure, 'ssl_context': ssl_context } # Add any unexpected kwargs to both dictionaries. @@ -87,14 +87,11 @@ def request(self, method, url, body=None, headers={}): return self._conn.request( method=method, url=url, body=body, headers=headers ) - except (TLSUpgrade, HTTPUpgrade) as e: - # We upgraded in the NPN/ALPN handshake or via the HTTP Upgrade - # mechanism. We can just go straight to the world of HTTP/2. - #Replace the backing object and insert the socket into it. - if(type(e) is TLSUpgrade): - assert e.negotiated in H2_NPN_PROTOCOLS - else: - assert e.negotiated == H2C_PROTOCOL + except TLSUpgrade as e: + # We upgraded in the NPN/ALPN handshake. We can just go straight to + # the world of HTTP/2. Replace the backing object and insert the + # socket into it. + assert e.negotiated in H2_NPN_PROTOCOLS self._conn = HTTP20Connection( self._host, self._port, **self._h2_kwargs @@ -109,6 +106,31 @@ def request(self, method, url, body=None, headers={}): method=method, url=url, body=body, headers=headers ) + def get_response(self): + """ + Returns a response object. + """ + try: + return self._conn.get_response() + except HTTPUpgrade as e: + # We upgraded via the HTTP Upgrade mechanism. We can just + #go straight to the world of HTTP/2. Replace the backing object + #and insert the socket into it. + assert e.negotiated == H2C_PROTOCOL + + self._conn = HTTP20Connection( + self._host, self._port, **self._h2_kwargs + ) + + #stream id 1 is used by the upgrade request and response + self.next_stream_id += 2 + self._conn._sock = e.sock + + # HTTP/2 preamble must be sent after receipt of a HTTP/1.1 101 + self._conn._send_preamble() + + return e.resp + # Can anyone say 'proxy object pattern'? def __getattr__(self, name): return getattr(self._conn, name) diff --git a/hyper/common/exceptions.py b/hyper/common/exceptions.py index 1f0f9459..1d2b6a73 100644 --- a/hyper/common/exceptions.py +++ b/hyper/common/exceptions.py @@ -56,7 +56,8 @@ class HTTPUpgrade(Exception): """ We upgraded to a new protocol via the HTTP Upgrade response. """ - def __init__(self, negotiated, sock): + def __init__(self, negotiated, sock, resp): super(HTTPUpgrade, self).__init__() self.negotiated = negotiated self.sock = sock + self.resp = resp diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index c46186ac..5edf174b 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -19,6 +19,7 @@ from ..compat import bytes from ..http20.connection import H2C_PROTOCOL +from ..http20.response import HTTP20Response from ..packages.hyperframe.frame import SettingsFrame # We prefer pycohttpparser to the pure-Python interpretation @@ -138,9 +139,9 @@ def request(self, method, url, body=None, headers={}): headers[b'upgrade'] = H2C_PROTOCOL #need to encode SETTINGS frame payload in Base64 and put into the HTTP-2 Settings header - http2Settings = SettingsFrame(0) - http2Settings.settings[SettingsFrame.INITIAL_WINDOW_SIZE] = 65535 - headers[b'HTTP2-Settings'] = base64.b64encode(http2Settings.serialize_body()) + http2_settings = SettingsFrame(0) + http2_settings.settings[SettingsFrame.INITIAL_WINDOW_SIZE] = 65535 + headers[b'HTTP2-Settings'] = base64.b64encode(http2_settings.serialize_body()) # We may need extra headers. if body: @@ -179,9 +180,10 @@ def get_response(self): self._sock.advance_buffer(response.consumed) if(response.status == 101 and - response.getheader('Connection') == 'Upgrade' and - response.getheader('Upgrade') == H2C_PROTOCOL): - raise HTTPUpgrade(proto, sock) + b'upgrade' in headers['connection'] and bytes(H2C_PROTOCOL, 'utf-8') in headers['upgrade']): + # HTTP/1.1 requests that are upgrade to HTTP/2.0 are responded to with steam id of 1 + headers[b':status'] = str(response.status) + raise HTTPUpgrade(H2C_PROTOCOL, self._sock, HTTP20Response(headers, )) return HTTP11Response( response.status, diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index ecd5a078..80083b5c 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -287,7 +287,7 @@ def putrequest(self, method, selector, **kwargs): # HTTP/2 specific. These are: ":method", ":scheme", ":authority" and # ":path". We can set all of these now. s.add_header(":method", method) - s.add_header(":scheme", "https") # We only support HTTPS at this time. + s.add_header(":scheme", "https" if self.secure else "http") s.add_header(":authority", self.host) s.add_header(":path", selector) From 7c00e5a47b464eaa97e7cd8b5cac76f4cd5b6a96 Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Mon, 1 Jun 2015 01:05:38 -0400 Subject: [PATCH 04/14] Cleaned up http11 upgrade to plaintext http20 --- hyper/common/connection.py | 15 ++++++++------- hyper/common/exceptions.py | 3 +-- hyper/http11/connection.py | 5 +---- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index 6277ddf2..0dcc67bd 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -114,22 +114,23 @@ def get_response(self): return self._conn.get_response() except HTTPUpgrade as e: # We upgraded via the HTTP Upgrade mechanism. We can just - #go straight to the world of HTTP/2. Replace the backing object - #and insert the socket into it. + # go straight to the world of HTTP/2. Replace the backing object + # and insert the socket into it. assert e.negotiated == H2C_PROTOCOL self._conn = HTTP20Connection( self._host, self._port, **self._h2_kwargs ) - #stream id 1 is used by the upgrade request and response - self.next_stream_id += 2 self._conn._sock = e.sock - + # stream id 1 is used by the upgrade request and response + # and is half-closed by the client + self._conn._new_stream(stream_id=1, local_closed=True) + # HTTP/2 preamble must be sent after receipt of a HTTP/1.1 101 self._conn._send_preamble() - - return e.resp + + return self._conn.get_response(1) # Can anyone say 'proxy object pattern'? def __getattr__(self, name): diff --git a/hyper/common/exceptions.py b/hyper/common/exceptions.py index 1d2b6a73..1f0f9459 100644 --- a/hyper/common/exceptions.py +++ b/hyper/common/exceptions.py @@ -56,8 +56,7 @@ class HTTPUpgrade(Exception): """ We upgraded to a new protocol via the HTTP Upgrade response. """ - def __init__(self, negotiated, sock, resp): + def __init__(self, negotiated, sock): super(HTTPUpgrade, self).__init__() self.negotiated = negotiated self.sock = sock - self.resp = resp diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 5edf174b..8eb2efed 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -19,7 +19,6 @@ from ..compat import bytes from ..http20.connection import H2C_PROTOCOL -from ..http20.response import HTTP20Response from ..packages.hyperframe.frame import SettingsFrame # We prefer pycohttpparser to the pure-Python interpretation @@ -181,9 +180,7 @@ def get_response(self): if(response.status == 101 and b'upgrade' in headers['connection'] and bytes(H2C_PROTOCOL, 'utf-8') in headers['upgrade']): - # HTTP/1.1 requests that are upgrade to HTTP/2.0 are responded to with steam id of 1 - headers[b':status'] = str(response.status) - raise HTTPUpgrade(H2C_PROTOCOL, self._sock, HTTP20Response(headers, )) + raise HTTPUpgrade(H2C_PROTOCOL, self._sock) return HTTP11Response( response.status, From d76543dc72627435d398593092515786d575858b Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Wed, 3 Jun 2015 10:02:09 -0400 Subject: [PATCH 05/14] Made some review changes --- hyper/common/connection.py | 2 +- hyper/http11/connection.py | 24 ++++++++++++++---------- hyper/http20/connection.py | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index 0dcc67bd..12099648 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -92,7 +92,7 @@ def request(self, method, url, body=None, headers={}): # the world of HTTP/2. Replace the backing object and insert the # socket into it. assert e.negotiated in H2_NPN_PROTOCOLS - + self._conn = HTTP20Connection( self._host, self._port, **self._h2_kwargs ) diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 8eb2efed..68eb3558 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -132,15 +132,9 @@ def request(self, method, url, body=None, headers={}): if self._sock is None: self.connect() - - #add HTTP Upgrade headers - headers[b'connection'] = b'Upgrade, HTTP2-Settings' - headers[b'upgrade'] = H2C_PROTOCOL - #need to encode SETTINGS frame payload in Base64 and put into the HTTP-2 Settings header - http2_settings = SettingsFrame(0) - http2_settings.settings[SettingsFrame.INITIAL_WINDOW_SIZE] = 65535 - headers[b'HTTP2-Settings'] = base64.b64encode(http2_settings.serialize_body()) + # TODO: Only send upgrade headers on first request + self._add_upgrade_headers(headers) # We may need extra headers. if body: @@ -178,8 +172,8 @@ def get_response(self): self._sock.advance_buffer(response.consumed) - if(response.status == 101 and - b'upgrade' in headers['connection'] and bytes(H2C_PROTOCOL, 'utf-8') in headers['upgrade']): + if (response.status == 101 and + b'upgrade' in headers['connection'] and H2C_PROTOCOL.decode('utf-8') in headers['upgrade']): raise HTTPUpgrade(H2C_PROTOCOL, self._sock) return HTTP11Response( @@ -233,6 +227,16 @@ def _add_body_headers(self, headers, body): headers[b'transfer-encoding'] = b'chunked' return BODY_CHUNKED + def _add_upgrade_headers(self, headers): + # Add HTTP Upgrade headers. + headers[b'connection'] = b'Upgrade, HTTP2-Settings' + headers[b'upgrade'] = H2C_PROTOCOL + + # Encode SETTINGS frame payload in Base64 and put into the HTTP-2 Settings header. + http2_settings = SettingsFrame(0) + http2_settings.settings[SettingsFrame.INITIAL_WINDOW_SIZE] = 65535 + headers[b'HTTP2-Settings'] = base64.b64encode(http2_settings.serialize_body()) + def _send_body(self, body, body_type): """ Handles the HTTP/1.1 logic for sending HTTP bodies. This does magical diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index 80083b5c..ee09bc22 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -227,7 +227,7 @@ def connect(self): proto = H2C_PROTOCOL log.debug("Selected NPN protocol: %s", proto) - assert proto in (H2_NPN_PROTOCOLS, H2C_PROTOCOL) + assert proto in H2_NPN_PROTOCOLS or proto == H2C_PROTOCOL self._sock = BufferedSocket(sock, self.network_buffer_size) From 9098b850bc47b87950d03241e0bd48805a709c9a Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sat, 6 Jun 2015 10:07:14 -0400 Subject: [PATCH 06/14] Move H2C_PROTOCOL declaration --- hyper/http11/connection.py | 3 +-- hyper/http20/connection.py | 4 +--- hyper/tls.py | 1 + 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 68eb3558..6e44ea29 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -11,14 +11,13 @@ import base64 from .response import HTTP11Response -from ..tls import wrap_socket +from ..tls import wrap_socket, H2C_PROTOCOL from ..common.bufsocket import BufferedSocket from ..common.exceptions import TLSUpgrade, HTTPUpgrade from ..common.headers import HTTPHeaderMap from ..common.util import to_bytestring from ..compat import bytes -from ..http20.connection import H2C_PROTOCOL from ..packages.hyperframe.frame import SettingsFrame # We prefer pycohttpparser to the pure-Python interpretation diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index ee09bc22..2d2d00cd 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -5,7 +5,7 @@ Objects that build hyper's connection-level HTTP/2 abstraction. """ -from ..tls import wrap_socket, H2_NPN_PROTOCOLS +from ..tls import wrap_socket, H2_NPN_PROTOCOLS, H2C_PROTOCOL from ..common.exceptions import ConnectionResetError from ..common.bufsocket import BufferedSocket from ..common.headers import HTTPHeaderMap @@ -25,8 +25,6 @@ import logging import socket -H2C_PROTOCOL = 'h2c' - log = logging.getLogger(__name__) class HTTP20Connection(object): diff --git a/hyper/tls.py b/hyper/tls.py index cdbc1483..d3f603ff 100644 --- a/hyper/tls.py +++ b/hyper/tls.py @@ -14,6 +14,7 @@ H2_NPN_PROTOCOLS = [NPN_PROTOCOL, 'h2-16', 'h2-15', 'h2-14'] SUPPORTED_NPN_PROTOCOLS = H2_NPN_PROTOCOLS + ['http/1.1'] +H2C_PROTOCOL = 'h2c' # We have a singleton SSLContext object. There's no reason to be creating one # per connection. From 4bdeb54ee8bccbfe68c6094b3341fc332de552d2 Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sat, 6 Jun 2015 10:27:36 -0400 Subject: [PATCH 07/14] Change H2C_PROTOCOL import that was missed on previous commit --- hyper/common/connection.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hyper/common/connection.py b/hyper/common/connection.py index 12099648..ba347419 100644 --- a/hyper/common/connection.py +++ b/hyper/common/connection.py @@ -7,8 +7,8 @@ """ from .exceptions import TLSUpgrade, HTTPUpgrade from ..http11.connection import HTTP11Connection -from ..http20.connection import HTTP20Connection, H2C_PROTOCOL -from ..tls import H2_NPN_PROTOCOLS +from ..http20.connection import HTTP20Connection +from ..tls import H2_NPN_PROTOCOLS, H2C_PROTOCOL class HTTPConnection(object): """ From 329372f0423b567ff10b9097d0c30cfc709648b0 Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sat, 6 Jun 2015 10:44:15 -0400 Subject: [PATCH 08/14] Only send HTTP upgrade headers on first request --- hyper/http11/connection.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 6e44ea29..98109559 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -69,6 +69,8 @@ def __init__(self, host, port=None, secure=None, ssl_context=None, else: self.secure = False + self._send_http_upgrade = not self.secure + self.ssl_context = ssl_context self._sock = None @@ -131,9 +133,10 @@ def request(self, method, url, body=None, headers={}): if self._sock is None: self.connect() - - # TODO: Only send upgrade headers on first request - self._add_upgrade_headers(headers) + + if(self._send_http_upgrade): + self._add_upgrade_headers(headers) + self._send_http_upgrade = False # We may need extra headers. if body: From 78d44bc2fa3507c0765f06dd3f9bdf564418b27a Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sat, 6 Jun 2015 17:33:35 -0400 Subject: [PATCH 09/14] Fix checking of header in response to HTTP upgrade request --- hyper/http11/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 98109559..18f9250b 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -175,7 +175,7 @@ def get_response(self): self._sock.advance_buffer(response.consumed) if (response.status == 101 and - b'upgrade' in headers['connection'] and H2C_PROTOCOL.decode('utf-8') in headers['upgrade']): + b'upgrade' in headers['connection'] and H2C_PROTOCOL.encode('utf-8') in headers['upgrade']): raise HTTPUpgrade(H2C_PROTOCOL, self._sock) return HTTP11Response( From 2320c1535bd02374f552d2fbc50a02b3845e2164 Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sat, 6 Jun 2015 22:02:15 -0400 Subject: [PATCH 10/14] Fix test_abstraction failure --- test/test_abstraction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_abstraction.py b/test/test_abstraction.py index 17385d8b..cd24e5b9 100644 --- a/test/test_abstraction.py +++ b/test/test_abstraction.py @@ -26,6 +26,7 @@ def test_h2_kwargs(self): assert c._h2_kwargs == { 'window_manager': True, 'enable_push': True, + 'secure': False, 'ssl_context': True, 'other_kwarg': True, } From e965aa398ab1ef70bbb5b1fbc137c1cd7481eff5 Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sun, 7 Jun 2015 17:02:43 -0400 Subject: [PATCH 11/14] Fix existing HTTP11Connection test cases --- test/test_http11.py | 62 +++++++++++++++++++++++++++++---------------- test_release.py | 2 +- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/test/test_http11.py b/test/test_http11.py index a30b6567..4774f680 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -38,16 +38,16 @@ def test_pycohttpparser_installs_correctly(self): assert True def test_initialization_no_port(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') - assert c.host == 'http2bin.org' + assert c.host == 'httpbin.org' assert c.port == 80 assert not c.secure def test_initialization_inline_port(self): - c = HTTP11Connection('http2bin.org:443') + c = HTTP11Connection('httpbin.org:443') - assert c.host == 'http2bin.org' + assert c.host == 'httpbin.org' assert c.port == 443 assert c.secure @@ -66,7 +66,7 @@ def test_can_override_security(self): assert not c.secure def test_basic_request(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() c.request('GET', '/get', headers={'User-Agent': 'hyper'}) @@ -74,7 +74,10 @@ def test_basic_request(self): expected = ( b"GET /get HTTP/1.1\r\n" b"User-Agent: hyper\r\n" - b"host: http2bin.org\r\n" + b"connection: Upgrade, HTTP2-Settings\r\n" + b"upgrade: h2c\r\n" + b"HTTP2-Settings: AAQAAP//\r\n" + b"host: httpbin.org\r\n" b"\r\n" ) received = b''.join(sock.queue) @@ -82,7 +85,7 @@ def test_basic_request(self): assert received == expected def test_request_with_bytestring_body(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() c.request( @@ -95,8 +98,11 @@ def test_request_with_bytestring_body(self): expected = ( b"POST /post HTTP/1.1\r\n" b"User-Agent: hyper\r\n" + b"connection: Upgrade, HTTP2-Settings\r\n" + b"upgrade: h2c\r\n" + b"HTTP2-Settings: AAQAAP//\r\n" b"content-length: 2\r\n" - b"host: http2bin.org\r\n" + b"host: httpbin.org\r\n" b"\r\n" b"hi" ) @@ -116,7 +122,7 @@ def fake_fstat(*args): try: hyper.http11.connection.os.fstat = fake_fstat - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() f = DummyFile(b'some binary data') @@ -124,8 +130,11 @@ def fake_fstat(*args): expected = ( b"POST /post HTTP/1.1\r\n" + b"connection: Upgrade, HTTP2-Settings\r\n" + b"upgrade: h2c\r\n" + b"HTTP2-Settings: AAQAAP//\r\n" b"content-length: 16\r\n" - b"host: http2bin.org\r\n" + b"host: httpbin.org\r\n" b"\r\n" b"some binary data" ) @@ -138,7 +147,7 @@ def fake_fstat(*args): hyper.http11.connection.os.fstat = old_fstat def test_request_with_generator_body(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() def body(): yield b'hi' @@ -149,8 +158,11 @@ def body(): expected = ( b"POST /post HTTP/1.1\r\n" + b"connection: Upgrade, HTTP2-Settings\r\n" + b"upgrade: h2c\r\n" + b"HTTP2-Settings: AAQAAP//\r\n" b"transfer-encoding: chunked\r\n" - b"host: http2bin.org\r\n" + b"host: httpbin.org\r\n" b"\r\n" b"2\r\nhi\r\n" b"5\r\nthere\r\n" @@ -162,7 +174,7 @@ def body(): assert received == expected def test_content_length_overrides_generator(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() def body(): yield b'hi' @@ -176,7 +188,10 @@ def body(): expected = ( b"POST /post HTTP/1.1\r\n" b"content-length: 10\r\n" - b"host: http2bin.org\r\n" + b"connection: Upgrade, HTTP2-Settings\r\n" + b"upgrade: h2c\r\n" + b"HTTP2-Settings: AAQAAP//\r\n" + b"host: httpbin.org\r\n" b"\r\n" b"hitheresir" ) @@ -185,7 +200,7 @@ def body(): assert received == expected def test_chunked_overrides_body(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() f = DummyFile(b'oneline\nanotherline') @@ -200,7 +215,10 @@ def test_chunked_overrides_body(self): expected = ( b"POST /post HTTP/1.1\r\n" b"transfer-encoding: chunked\r\n" - b"host: http2bin.org\r\n" + b"connection: Upgrade, HTTP2-Settings\r\n" + b"upgrade: h2c\r\n" + b"HTTP2-Settings: AAQAAP//\r\n" + b"host: httpbin.org\r\n" b"\r\n" b"8\r\noneline\n\r\n" b"b\r\nanotherline\r\n" @@ -211,7 +229,7 @@ def test_chunked_overrides_body(self): assert received == expected def test_get_response(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() sock._buffer= BytesIO( @@ -234,7 +252,7 @@ def test_get_response(self): assert r.read() == b'' def test_response_short_reads(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = sock = DummySocket() sock._buffer= BytesIO( @@ -254,7 +272,7 @@ def test_response_short_reads(self): assert r.read(5) == b'' def test_request_with_unicodestring_body(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = DummySocket() with pytest.raises(ValueError): @@ -277,7 +295,7 @@ def fake_fstat(*args): try: hyper.http11.connection.os.fstat = fake_fstat - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = DummySocket() f = DummyFile(b'') @@ -290,7 +308,7 @@ def fake_fstat(*args): hyper.http11.connection.os.fstat = old_fstat def test_request_with_unicode_generator_body(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = DummySocket() def body(): yield u'hi' @@ -301,7 +319,7 @@ def body(): c.request('POST', '/post', body=body()) def test_content_length_overrides_generator_unicode(self): - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') c._sock = DummySocket() def body(): yield u'hi' diff --git a/test_release.py b/test_release.py index c444b0bd..429812ab 100644 --- a/test_release.py +++ b/test_release.py @@ -90,7 +90,7 @@ def test_hitting_http2bin_org_http11(self): """ This test function uses hyper's HTTP/1.1 support to talk to http2bin """ - c = HTTP11Connection('http2bin.org') + c = HTTP11Connection('httpbin.org') # Here are some nice URLs. urls = [ From ed0067c71f986865a6da1de00b88f5195af8bf59 Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sun, 7 Jun 2015 18:20:46 -0400 Subject: [PATCH 12/14] Add abstraction test case for http upgrade --- test/test_abstraction.py | 65 ++++++++++++++++++++++++++++++++++------ test_release.py | 4 +-- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/test/test_abstraction.py b/test/test_abstraction.py index cd24e5b9..76e6499b 100644 --- a/test/test_abstraction.py +++ b/test/test_abstraction.py @@ -2,7 +2,7 @@ import hyper.common.connection from hyper.common.connection import HTTPConnection -from hyper.common.exceptions import TLSUpgrade +from hyper.common.exceptions import TLSUpgrade, HTTPUpgrade class TestHTTPConnection(object): def test_h1_kwargs(self): @@ -31,7 +31,7 @@ def test_h2_kwargs(self): 'other_kwarg': True, } - def test_upgrade(self, monkeypatch): + def test_tls_upgrade(self, monkeypatch): monkeypatch.setattr( hyper.common.connection, 'HTTP11Connection', DummyH1Connection ) @@ -46,23 +46,70 @@ def test_upgrade(self, monkeypatch): assert r == 'h2' assert isinstance(c._conn, DummyH2Connection) - assert c._conn._sock == 'totally a socket' + assert c._conn._sock == 'totally a secure socket' + + def test_http_upgrade(self, monkeypatch): + monkeypatch.setattr( + hyper.common.connection, 'HTTP11Connection', DummyH1Connection + ) + monkeypatch.setattr( + hyper.common.connection, 'HTTP20Connection', DummyH2Connection + ) + c = HTTPConnection('test', 80) + + assert isinstance(c._conn, DummyH1Connection) + + c.request('GET', '/') + resp = c.get_response() + + assert resp == 'h2c' + assert isinstance(c._conn, DummyH2Connection) + assert c._conn._sock == 'totally a non-secure socket' class DummyH1Connection(object): - def __init__(self, *args, **kwargs): - pass + def __init__(self, host, port=None, secure=None, **kwargs): + self.host = host + self.port = port + + if secure is not None: + self.secure = secure + elif self.port == 443: + self.secure = True + else: + self.secure = False def request(self, *args, **kwargs): - raise TLSUpgrade('h2', 'totally a socket') + if(self.secure): + raise TLSUpgrade('h2', 'totally a secure socket') + + def get_response(self): + if(not self.secure): + raise HTTPUpgrade('h2c', 'totally a non-secure socket') class DummyH2Connection(object): - def __init__(self, *args, **kwargs): - pass + def __init__(self, host, port=None, secure=None, **kwargs): + self.host = host + self.port = port + + if secure is not None: + self.secure = secure + elif self.port == 443: + self.secure = True + else: + self.secure = False def _send_preamble(self): pass + def _new_stream(self, *args, **kwargs): + pass + def request(self, *args, **kwargs): - return 'h2' + if(self.secure): + return 'h2' + + def get_response(self, *args, **kwargs): + if(not self.secure): + return 'h2c' diff --git a/test_release.py b/test_release.py index 429812ab..e1e0aa9e 100644 --- a/test_release.py +++ b/test_release.py @@ -86,9 +86,9 @@ def test_hitting_http2bin_org(self): assert all(map(lambda r: r.status_code == 200, responses)) assert all(map(lambda r: r.text, responses)) - def test_hitting_http2bin_org_http11(self): + def test_hitting_httpbin_org_http11(self): """ - This test function uses hyper's HTTP/1.1 support to talk to http2bin + This test function uses hyper's HTTP/1.1 support to talk to httpbin """ c = HTTP11Connection('httpbin.org') From 17c23056dd9391dbb7410ef94a22694df45373c9 Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sun, 7 Jun 2015 22:46:14 -0400 Subject: [PATCH 13/14] Add http upgrade headers only sent once test --- hyper/http11/connection.py | 3 ++- test/test_http11.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 18f9250b..717ee5a7 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -69,6 +69,7 @@ def __init__(self, host, port=None, secure=None, ssl_context=None, else: self.secure = False + # only send http upgrade headers for non-secure connection self._send_http_upgrade = not self.secure self.ssl_context = ssl_context @@ -133,7 +134,7 @@ def request(self, method, url, body=None, headers={}): if self._sock is None: self.connect() - + if(self._send_http_upgrade): self._add_upgrade_headers(headers) self._send_http_upgrade = False diff --git a/test/test_http11.py b/test/test_http11.py index 4774f680..c5479415 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -334,6 +334,24 @@ def body(): body=body() ) + def test_http_upgrade_headers_only_sent_once(self): + c = HTTP11Connection('httpbin.org') + c._sock = sock = DummySocket() + + c.request('GET', '/get', headers={'User-Agent': 'hyper'}) + + sock.queue = [] + c.request('GET', '/get', headers={'User-Agent': 'hyper'}) + received = b''.join(sock.queue) + + expected = ( + b"GET /get HTTP/1.1\r\n" + b"User-Agent: hyper\r\n" + b"host: httpbin.org\r\n" + b"\r\n" + ) + + assert received == expected class TestHTTP11Response(object): def test_short_circuit_read(self): From 016d73cd10c83186c73cc4a8582b172d6549f70f Mon Sep 17 00:00:00 2001 From: Fred Thomsen Date: Sun, 7 Jun 2015 22:49:27 -0400 Subject: [PATCH 14/14] Fix if syntax --- hyper/http11/connection.py | 2 +- test/test_abstraction.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 717ee5a7..e6d21e4d 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -135,7 +135,7 @@ def request(self, method, url, body=None, headers={}): if self._sock is None: self.connect() - if(self._send_http_upgrade): + if self._send_http_upgrade: self._add_upgrade_headers(headers) self._send_http_upgrade = False diff --git a/test/test_abstraction.py b/test/test_abstraction.py index 76e6499b..f231e509 100644 --- a/test/test_abstraction.py +++ b/test/test_abstraction.py @@ -80,11 +80,11 @@ def __init__(self, host, port=None, secure=None, **kwargs): self.secure = False def request(self, *args, **kwargs): - if(self.secure): + if self.secure: raise TLSUpgrade('h2', 'totally a secure socket') def get_response(self): - if(not self.secure): + if not self.secure: raise HTTPUpgrade('h2c', 'totally a non-secure socket') @@ -107,9 +107,9 @@ def _new_stream(self, *args, **kwargs): pass def request(self, *args, **kwargs): - if(self.secure): + if self.secure: return 'h2' def get_response(self, *args, **kwargs): - if(not self.secure): + if not self.secure: return 'h2c'