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

add HTTP version to connection and responses #296

Merged
merged 2 commits into from Dec 7, 2016
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions hyper/common/util.py
Expand Up @@ -5,6 +5,8 @@

General utility functions for use with hyper.
"""
from enum import Enum

from hyper.compat import unicode, bytes, imap
from ..packages.rfc3986.uri import URIReference
from ..compat import is_py3
Expand Down Expand Up @@ -57,3 +59,11 @@ def to_native_string(string, encoding='utf-8'):
return string

return string.decode(encoding) if is_py3 else string.encode(encoding)


class HTTPVersion(Enum):
"""
Collection of all HTTP versions used in hyper.
"""
http11 = "HTTP/1.1"
http20 = "HTTP/2"
5 changes: 4 additions & 1 deletion hyper/http11/connection.py
Expand Up @@ -20,7 +20,7 @@
from ..common.bufsocket import BufferedSocket
from ..common.exceptions import TLSUpgrade, HTTPUpgrade
from ..common.headers import HTTPHeaderMap
from ..common.util import to_bytestring, to_host_port_tuple
from ..common.util import to_bytestring, to_host_port_tuple, HTTPVersion
from ..compat import bytes

# We prefer pycohttpparser to the pure-Python interpretation
Expand Down Expand Up @@ -56,6 +56,9 @@ class HTTP11Connection(object):
and one also isn't provided in the ``proxy`` parameter,
defaults to 8080.
"""

version = HTTPVersion.http11

def __init__(self, host, port=None, secure=None, ssl_context=None,
proxy_host=None, proxy_port=None, **kwargs):
if port is None:
Expand Down
4 changes: 4 additions & 0 deletions hyper/http11/response.py
Expand Up @@ -13,6 +13,7 @@
from ..common.decoder import DeflateDecoder
from ..common.exceptions import ChunkedDecodeError, InvalidResponseError
from ..common.exceptions import ConnectionResetError
from ..common.util import HTTPVersion

log = logging.getLogger(__name__)

Expand All @@ -23,6 +24,9 @@ class HTTP11Response(object):
provides access to the response headers and the entity body. The response
is an iterable object and can be used in a with statement.
"""

version = HTTPVersion.http11

def __init__(self, code, reason, headers, sock, connection=None):
#: The reason phrase returned by the server.
self.reason = reason
Expand Down
7 changes: 6 additions & 1 deletion hyper/http20/connection.py
Expand Up @@ -14,7 +14,9 @@
from ..common.exceptions import ConnectionResetError
from ..common.bufsocket import BufferedSocket
from ..common.headers import HTTPHeaderMap
from ..common.util import to_host_port_tuple, to_native_string, to_bytestring
from ..common.util import (
to_host_port_tuple, to_native_string, to_bytestring, HTTPVersion
)
from ..compat import unicode, bytes
from .stream import Stream
from .response import HTTP20Response, HTTP20Push
Expand Down Expand Up @@ -91,6 +93,9 @@ class HTTP20Connection(object):
and one also isn't provided in the ``proxy`` parameter, defaults to
8080.
"""

version = HTTPVersion.http20

def __init__(self, host, port=None, secure=None, window_manager=None,
enable_push=False, ssl_context=None, proxy_host=None,
proxy_port=None, force_proto=None, **kwargs):
Expand Down
4 changes: 4 additions & 0 deletions hyper/http20/response.py
Expand Up @@ -11,6 +11,7 @@

from ..common.decoder import DeflateDecoder
from ..common.headers import HTTPHeaderMap
from ..common.util import HTTPVersion

log = logging.getLogger(__name__)

Expand All @@ -36,6 +37,9 @@ class HTTP20Response(object):
the persistent connections used in HTTP/2 this has no effect, and is done
soley for compatibility).
"""

version = HTTPVersion.http20

def __init__(self, headers, stream):
#: The reason phrase returned by the server. This is not used in
#: HTTP/2, and so is always the empty string.
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Expand Up @@ -96,6 +96,7 @@ def run_tests(self):
# module at lower than 1.0, because it doesn't support CFFI v1.0 yet.
':platform_python_implementation == "PyPy" and python_full_version < "2.7.9"': [
'cryptography<1.0'
]
],
':python_version == "2.7" or python_version == "3.3"': ['enum34>=1.0.4, <2']
}
)
13 changes: 13 additions & 0 deletions test/test_http11.py
Expand Up @@ -19,6 +19,7 @@
from hyper.http11.response import HTTP11Response
from hyper.common.headers import HTTPHeaderMap
from hyper.common.exceptions import ChunkedDecodeError, ConnectionResetError
from hyper.common.util import HTTPVersion
from hyper.compat import bytes, zlib_compressobj


Expand Down Expand Up @@ -838,6 +839,18 @@ def test_closing_chunked_reads_dont_call_close_callback(self):
assert r._sock is None
assert connection.close.call_count == 1

def test_connection_version(self):
c = HTTP11Connection('httpbin.org')
assert c.version is HTTPVersion.http11

def test_response_version(self):
d = DummySocket()
headers = {
b'transfer-encoding': [b'chunked'], b'connection': [b'close']
}
r = HTTP11Response(200, 'OK', headers, d)
assert r.version is HTTPVersion.http11


class DummySocket(object):
def __init__(self):
Expand Down
10 changes: 9 additions & 1 deletion test/test_hyper.py
Expand Up @@ -16,7 +16,7 @@
combine_repeated_headers, split_repeated_headers, h2_safe_headers
)
from hyper.common.headers import HTTPHeaderMap
from hyper.common.util import to_bytestring
from hyper.common.util import to_bytestring, HTTPVersion
from hyper.compat import zlib_compressobj, is_py2
from hyper.contrib import HTTP20Adapter
import hyper.http20.errors as errors
Expand Down Expand Up @@ -82,6 +82,10 @@ def test_connections_can_parse_ipv6_hosts_and_ports(self):
assert c.proxy_host == 'ffff:aaaa::1'
assert c.proxy_port == 8443

def test_connection_version(self):
c = HTTP20Connection('www.google.com')
assert c.version is HTTPVersion.http20

def test_ping(self, frame_buffer):
def data_callback(chunk, **kwargs):
frame_buffer.add_data(chunk)
Expand Down Expand Up @@ -1097,6 +1101,10 @@ def test_read_compressed_frames(self):

assert received == b'this is test data'

def test_response_version(self):
r = HTTP20Response(HTTPHeaderMap([(':status', '200')]), None)
assert r.version is HTTPVersion.http20

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also get a few tests for the abstract HTTPConnection object passing this through?


class TestHTTP20Adapter(object):
def test_adapter_reuses_connections(self):
Expand Down
95 changes: 95 additions & 0 deletions test/test_integration.py
Expand Up @@ -15,6 +15,7 @@
from h2.frame_buffer import FrameBuffer
from hyper.compat import ssl
from hyper.contrib import HTTP20Adapter
from hyper.common.util import HTTPVersion
from hyperframe.frame import (
Frame, SettingsFrame, WindowUpdateFrame, DataFrame, HeadersFrame,
GoAwayFrame, RstStreamFrame
Expand Down Expand Up @@ -935,6 +936,100 @@ def socket_handler(listener):

self.tear_down()

def test_version_after_tls_upgrade(self, monkeypatch):
self.set_up()

# We need to patch the ssl_wrap_socket method to ensure that we
# forcefully upgrade.
old_wrap_socket = hyper.http11.connection.wrap_socket

def wrap(*args):
sock, _ = old_wrap_socket(*args)
return sock, 'h2'

monkeypatch.setattr(hyper.http11.connection, 'wrap_socket', wrap)

send_event = threading.Event()

def socket_handler(listener):
sock = listener.accept()[0]

receive_preamble(sock)

# Send the headers for the response. This response has no body.
f = build_headers_frame(
[(':status', '200'), ('content-length', '0')]
)
f.flags.add('END_STREAM')
f.stream_id = 1
sock.sendall(f.serialize())

# Wait for the message from the main thread.
send_event.wait()
sock.close()

self._start_server(socket_handler)
c = hyper.HTTPConnection(self.host, self.port, secure=True)

assert c.version is HTTPVersion.http11
assert c.version is not HTTPVersion.http20
c.request('GET', '/')
send_event.set()
assert c.version is HTTPVersion.http20

self.tear_down()

def test_version_after_http_upgrade(self):
self.set_up()
self.secure = False

send_event = threading.Event()

def socket_handler(listener):
sock = listener.accept()[0]

# We should get the initial request.
data = b''
while not data.endswith(b'\r\n\r\n'):
data += sock.recv(65535)
assert b'upgrade: h2c\r\n' in data

send_event.wait()

# We need to send back a response.
resp = (
b'HTTP/1.1 101 Upgrade\r\n'
b'Server: socket-level-server\r\n'
b'Content-Length: 0\r\n'
b'Connection: upgrade\r\n'
b'Upgrade: h2c\r\n'
b'\r\n'
)
sock.sendall(resp)

# We get a message for connection open, specifically the preamble.
receive_preamble(sock)

# Send the headers for the response. This response has a body.
f = build_headers_frame(
[(':status', '200'), ('content-length', '0')]
)
f.stream_id = 1
f.flags.add('END_STREAM')
sock.sendall(f.serialize())

self._start_server(socket_handler)

c = hyper.HTTPConnection(self.host, self.port)
assert c.version is HTTPVersion.http11
c.request('GET', '/')
send_event.set()
resp = c.get_response()
assert c.version is HTTPVersion.http20
assert resp.version is HTTPVersion.http20

self.tear_down()


class TestRequestsAdapter(SocketLevelTest):
# This uses HTTP/2.
Expand Down