diff --git a/hyper/common/util.py b/hyper/common/util.py index 6d199a0c..a2278b54 100644 --- a/hyper/common/util.py +++ b/hyper/common/util.py @@ -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 @@ -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" diff --git a/hyper/http11/connection.py b/hyper/http11/connection.py index 61361c35..48bde2f0 100644 --- a/hyper/http11/connection.py +++ b/hyper/http11/connection.py @@ -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 @@ -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: diff --git a/hyper/http11/response.py b/hyper/http11/response.py index ee23be08..318ab659 100644 --- a/hyper/http11/response.py +++ b/hyper/http11/response.py @@ -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__) @@ -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 diff --git a/hyper/http20/connection.py b/hyper/http20/connection.py index 31dc7a47..b8a2901e 100644 --- a/hyper/http20/connection.py +++ b/hyper/http20/connection.py @@ -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 @@ -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): diff --git a/hyper/http20/response.py b/hyper/http20/response.py index bb339b2f..94b20f9a 100644 --- a/hyper/http20/response.py +++ b/hyper/http20/response.py @@ -11,6 +11,7 @@ from ..common.decoder import DeflateDecoder from ..common.headers import HTTPHeaderMap +from ..common.util import HTTPVersion log = logging.getLogger(__name__) @@ -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. diff --git a/setup.py b/setup.py index cedea1e4..861c883a 100644 --- a/setup.py +++ b/setup.py @@ -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'] } ) diff --git a/test/test_http11.py b/test/test_http11.py index d4039588..5f0fcf7a 100644 --- a/test/test_http11.py +++ b/test/test_http11.py @@ -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 @@ -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): diff --git a/test/test_hyper.py b/test/test_hyper.py index da0aad57..59d354c7 100644 --- a/test/test_hyper.py +++ b/test/test_hyper.py @@ -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 @@ -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) @@ -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 + class TestHTTP20Adapter(object): def test_adapter_reuses_connections(self): diff --git a/test/test_integration.py b/test/test_integration.py index 6a6c2600..c51f061f 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -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 @@ -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.