diff --git a/aws-common-runtime/aws-c-cal b/aws-common-runtime/aws-c-cal index b30e81b34..3215b8f64 160000 --- a/aws-common-runtime/aws-c-cal +++ b/aws-common-runtime/aws-c-cal @@ -1 +1 @@ -Subproject commit b30e81b343609484fe2d6892698ecd4419777f7f +Subproject commit 3215b8f640056487205bea4306ee5e6794748c01 diff --git a/aws-common-runtime/aws-c-common b/aws-common-runtime/aws-c-common index 6f0787f0c..f71fc5008 160000 --- a/aws-common-runtime/aws-c-common +++ b/aws-common-runtime/aws-c-common @@ -1 +1 @@ -Subproject commit 6f0787f0c567326a5313188af7cb3b4dbfef1a24 +Subproject commit f71fc5008f400d76a5f65722aefe5fa48cde94f8 diff --git a/aws-common-runtime/aws-c-compression b/aws-common-runtime/aws-c-compression index 4b32a9b8b..f2be13afe 160000 --- a/aws-common-runtime/aws-c-compression +++ b/aws-common-runtime/aws-c-compression @@ -1 +1 @@ -Subproject commit 4b32a9b8bdf07698fd080bade472a5e135abb70a +Subproject commit f2be13afe410611fcac07b6519b96ce1ad4e4831 diff --git a/aws-common-runtime/aws-c-http b/aws-common-runtime/aws-c-http index f5096cab2..4fff6a8bb 160000 --- a/aws-common-runtime/aws-c-http +++ b/aws-common-runtime/aws-c-http @@ -1 +1 @@ -Subproject commit f5096cab294d201bc4a217ed5679f912baa14684 +Subproject commit 4fff6a8bbf87fb71b78add511bf17eb735e1456b diff --git a/aws-common-runtime/aws-c-io b/aws-common-runtime/aws-c-io index 60a451a6a..31ec9372c 160000 --- a/aws-common-runtime/aws-c-io +++ b/aws-common-runtime/aws-c-io @@ -1 +1 @@ -Subproject commit 60a451a6a7269f1f99718950cd34befcf7e8a096 +Subproject commit 31ec9372cbe4462cc083d018b6ac507d4afc93f6 diff --git a/awscrt/http.py b/awscrt/http.py index 5d290d3a9..42265d56b 100644 --- a/awscrt/http.py +++ b/awscrt/http.py @@ -28,14 +28,19 @@ class HttpConnectionBase(NativeResource): shutdown_future (concurrent.futures.Future): Completes when the connection has finished shutting down. Future will contain a result of None, or an exception indicating why shutdown occurred. Note that the connection may have been garbage-collected before this future completes. + _version (HttpVersion): Protocol version the connection used. """ - __slots__ = ('shutdown_future') + __slots__ = ('shutdown_future', '_version') def __init__(self): super(HttpConnectionBase, self).__init__() self.shutdown_future = Future() + @property + def version(self): + return self._version + def close(self): """ Close the connection. @@ -94,9 +99,10 @@ def new(cls, connection._host_name = host_name connection._port = port - def on_connection_setup(binding, error_code): + def on_connection_setup(binding, error_code, http_version): if error_code == 0: connection._binding = binding + connection._version = HttpVersion(http_version) future.set_result(connection) else: future.set_exception(awscrt.exceptions.from_code(error_code)) @@ -386,6 +392,13 @@ class HttpProxyAuthenticationType(IntEnum): Basic = 1 +class HttpVersion(IntEnum): + Unknown = 0 + Http1_0 = 1 + Http1_1 = 2 + Http2 = 3 + + class HttpProxyOptions(object): """ Proxy options for HTTP clients. diff --git a/elasticurl.py b/elasticurl.py old mode 100644 new mode 100755 index 5e49b7773..7703dc42d --- a/elasticurl.py +++ b/elasticurl.py @@ -77,9 +77,20 @@ def print_header_list(headers): parser.add_argument( '-p', '--alpn', + default=["h2", "http/1.1"], required=False, help='STRING: protocol for ALPN. May be specified multiple times.', action='append') +parser.add_argument( + '--http2', + required=False, + help='HTTP/2 connection required', + action="store_true") +parser.add_argument( + '--http1_1', + required=False, + help='HTTP/1.1 connection required', + action="store_true") parser.add_argument( '-v', '--verbose', @@ -117,6 +128,16 @@ def print_header_list(headers): io.init_logging(log_level, log_output) + +required_version = http.HttpVersion.Unknown +if args.http1_1: + required_version = http.HttpVersion.Http1_1 + args.alpn = ["http/1.1"] +if args.http2: + required_version = http.HttpVersion.Http2 + args.alpn = ["h2"] + + # an event loop group is needed for IO operations. Unless you're a server or a client doing hundreds of connections # you only want one of these. event_loop_group = io.EventLoopGroup(1) @@ -139,6 +160,8 @@ def print_header_list(headers): else: if scheme == 'http': port = 80 + if args.http2: + sys.exit("Error, we don't support h2c, please use TLS for HTTP/2 connection") tls_connection_options = None @@ -201,6 +224,12 @@ def on_incoming_body(http_stream, chunk, **kwargs): connection = connect_future.result(10) connection.shutdown_future.add_done_callback(on_connection_shutdown) +if required_version: + if connection.version != required_version: + error_msg = "Error. The requested HTTP version " + args.alpn[0] + " is not supported by the peer." + sys.exit(error_msg) + + request = http.HttpRequest(args.method, body_stream=data_stream) if args.get: diff --git a/source/http_connection.c b/source/http_connection.c index 2bf8aa880..b7812f396 100644 --- a/source/http_connection.c +++ b/source/http_connection.c @@ -124,7 +124,7 @@ static void s_on_client_connection_setup( if (aws_py_gilstate_ensure(&state)) { return; /* Python has shut down. Nothing matters anymore, but don't crash */ } - + enum aws_http_version http_version = AWS_HTTP_VERSION_UNKNOWN; /* If setup was successful, encapsulate binding so we can pass it to python */ PyObject *capsule = NULL; if (!error_code) { @@ -132,10 +132,13 @@ static void s_on_client_connection_setup( if (!capsule) { error_code = AWS_ERROR_UNKNOWN; } + http_version = aws_http_connection_get_version(native_connection); } /* Invoke on_setup, then clear our reference to it */ - PyObject *result = PyObject_CallFunction(connection->on_setup, "(Oi)", capsule ? capsule : Py_None, error_code); + PyObject *result = + PyObject_CallFunction(connection->on_setup, "(Oii)", capsule ? capsule : Py_None, error_code, http_version); + if (result) { Py_DECREF(result); } else { diff --git a/test/test_http_client.py b/test/test_http_client.py index 15619235d..df917bbc5 100644 --- a/test/test_http_client.py +++ b/test/test_http_client.py @@ -13,7 +13,7 @@ from __future__ import absolute_import import awscrt.exceptions -from awscrt.http import HttpClientConnection, HttpClientStream, HttpHeaders, HttpProxyOptions, HttpRequest +from awscrt.http import HttpClientConnection, HttpClientStream, HttpHeaders, HttpProxyOptions, HttpRequest, HttpVersion from awscrt.io import ClientBootstrap, ClientTlsContext, DefaultHostResolver, EventLoopGroup, TlsConnectionOptions, TlsContextOptions from concurrent.futures import Future from io import open # Python2's built-in open() doesn't return a stream @@ -21,6 +21,10 @@ import ssl from test import NativeResourceTest import threading +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse import unittest # Use a built-in Python HTTP server to test the awscrt's HTTP client @@ -309,6 +313,47 @@ def test_proxy_http(self): proxy_options = HttpProxyOptions(host_name=PROXY_HOST, port=PROXY_PORT) self._test_get(secure=False, proxy_options=proxy_options) + def _new_h2_client_connection(self, url): + event_loop_group = EventLoopGroup() + host_resolver = DefaultHostResolver(event_loop_group) + bootstrap = ClientBootstrap(event_loop_group, host_resolver) + + port = 443 + scheme = 'https' + tls_ctx_options = TlsContextOptions() + tls_ctx = ClientTlsContext(tls_ctx_options) + tls_conn_opt = tls_ctx.new_connection_options() + tls_conn_opt.set_server_name(url.hostname) + tls_conn_opt.set_alpn_list(["h2"]) + + connection_future = HttpClientConnection.new(host_name=url.hostname, + port=port, + bootstrap=bootstrap, + tls_connection_options=tls_conn_opt) + return connection_future.result(self.timeout) + + def test_h2_client(self): + url = urlparse("https://d1cz66xoahf9cl.cloudfront.net/http_test_doc.txt") + connection = self._new_h2_client_connection(url) + # check we set an h2 connection + self.assertEqual(connection.version, HttpVersion.Http2) + + request = HttpRequest('GET', url.path) + request.headers.add('host', url.hostname) + response = Response() + stream = connection.request(request, response.on_response, response.on_body) + stream.activate() + + # wait for stream to complete + stream_completion_result = stream.completion_future.result(self.timeout) + + # check result + self.assertEqual(200, response.status_code) + self.assertEqual(200, stream_completion_result) + self.assertEqual(14428801, len(response.body)) + + self.assertEqual(None, connection.close().exception(self.timeout)) + if __name__ == '__main__': unittest.main()