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

Plaintext http20 via http11 upgrade #136

Merged
merged 15 commits into from
Jun 8, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 31 additions & 6 deletions hyper/common/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@

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 ..tls import H2_NPN_PROTOCOLS

from ..tls import H2_NPN_PROTOCOLS, H2C_PROTOCOL

class HTTPConnection(object):
"""
Expand All @@ -27,8 +26,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
Expand Down Expand Up @@ -56,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.
Expand Down Expand Up @@ -107,6 +106,32 @@ 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
)

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 self._conn.get_response(1)

# The following two methods are the implementation of the context manager
# protocol.
def __enter__(self): # pragma: no cover
Expand Down
9 changes: 9 additions & 0 deletions hyper/common/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 26 additions & 3 deletions hyper/http11/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@
import logging
import os
import socket
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
from ..common.exceptions import TLSUpgrade, HTTPUpgrade
from ..common.headers import HTTPHeaderMap
from ..common.util import to_bytestring
from ..compat import bytes

from ..packages.hyperframe.frame import SettingsFrame

# We prefer pycohttpparser to the pure-Python interpretation
try: # pragma: no cover
Expand Down Expand Up @@ -67,6 +69,9 @@ 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
self._sock = None

Expand Down Expand Up @@ -94,7 +99,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):
Expand Down Expand Up @@ -130,6 +135,10 @@ 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

# We may need extra headers.
if body:
body_type = self._add_body_headers(headers, body)
Expand Down Expand Up @@ -166,6 +175,10 @@ def get_response(self):

self._sock.advance_buffer(response.consumed)

if (response.status == 101 and
b'upgrade' in headers['connection'] and H2C_PROTOCOL.encode('utf-8') in headers['upgrade']):
raise HTTPUpgrade(H2C_PROTOCOL, self._sock)

return HTTP11Response(
response.status,
response.msg.tobytes(),
Expand Down Expand Up @@ -217,6 +230,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
Expand Down
25 changes: 19 additions & 6 deletions hyper/http20/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,7 +27,6 @@

log = logging.getLogger(__name__)


class HTTP20Connection(object):
"""
An object representing a single HTTP/2 connection to a server.
Expand All @@ -43,6 +42,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 <hyper.http20.window.BaseFlowControlManager>`.
Expand All @@ -55,7 +57,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.
Expand All @@ -69,6 +71,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

Expand Down Expand Up @@ -210,9 +219,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 = H2C_PROTOCOL

log.debug("Selected NPN protocol: %s", proto)
assert proto in H2_NPN_PROTOCOLS
assert proto in H2_NPN_PROTOCOLS or proto == H2C_PROTOCOL

self._sock = BufferedSocket(sock, self.network_buffer_size)

Expand Down Expand Up @@ -272,7 +285,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)

Expand Down
1 change: 1 addition & 0 deletions hyper/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
66 changes: 57 additions & 9 deletions test/test_abstraction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -26,11 +26,12 @@ def test_h2_kwargs(self):
assert c._h2_kwargs == {
'window_manager': True,
'enable_push': True,
'secure': False,
'ssl_context': True,
'other_kwarg': True,
}

def test_upgrade(self, monkeypatch):
def test_tls_upgrade(self, monkeypatch):
monkeypatch.setattr(
hyper.common.connection, 'HTTP11Connection', DummyH1Connection
)
Expand All @@ -45,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'
Loading