Skip to content

Commit 40aa2d1

Browse files
authored
H2 settings (#648)
1 parent c7e0940 commit 40aa2d1

File tree

10 files changed

+436
-162
lines changed

10 files changed

+436
-162
lines changed

awscrt/http.py

Lines changed: 236 additions & 116 deletions
Large diffs are not rendered by default.

crt/aws-lc

crt/s2n

Submodule s2n updated from b8a9aa4 to 1c5798b

pyproject.toml

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
[build-system]
22
requires = [
33
"setuptools>=75.3.1",
4-
"wheel>=0.45.1", # used by our setup.py
4+
"wheel>=0.45.1", # used by our setup.py
55
]
66
build-backend = "setuptools.build_meta"
77

88
[project]
99
name = "awscrt"
10-
license = {text = "Apache-2.0", files = ["LICENSE"]}
11-
authors = [{name = "Amazon Web Services, Inc", email = "[email protected]"}]
10+
license = { text = "Apache-2.0", files = ["LICENSE"] }
11+
authors = [
12+
{ name = "Amazon Web Services, Inc", email = "[email protected]" },
13+
]
1214
description = "A common runtime for AWS Python projects"
1315
readme = "README.md"
1416
requires-python = ">=3.8"
@@ -19,9 +21,7 @@ classifiers = [
1921
"Operating System :: Unix",
2022
"Operating System :: MacOS",
2123
]
22-
dynamic = [
23-
"version",
24-
]
24+
dynamic = ["version"]
2525

2626
[project.urls]
2727
github = "https://github.com/awslabs/aws-crt-python"
@@ -31,9 +31,10 @@ releasenotes = "https://github.com/awslabs/aws-crt-python/releases"
3131

3232
[project.optional-dependencies]
3333
dev = [
34-
"autopep8>=2.3.1", # for code formatting
35-
"build>=1.2.2", # for building wheels
34+
"autopep8>=2.3.1", # for code formatting
35+
"build>=1.2.2", # for building wheels
3636
"sphinx>=7.2.6,<7.3; python_version >= '3.9'", # for building docs
37-
"websockets>=13.1", # for tests
38-
"h2", # for tests
37+
"websockets>=13.1", # for tests
38+
# for tests, restrict to exact version for test_http_client/TestClientMockServer/_on_remote_settings_changed that relies on the implementation details. Also, this is no needs to update this package.
39+
"h2==4.1.0",
3940
]

source/http_connection.c

Lines changed: 114 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,85 @@ static void s_on_client_connection_setup(
138138
PyGILState_Release(state);
139139
}
140140

141+
/* Function to convert Python list of Http2Setting to C array of aws_http2_setting */
142+
static int s_convert_http2_settings(
143+
PyObject *initial_settings_py,
144+
struct aws_allocator *allocator,
145+
struct aws_http2_setting *out_settings[],
146+
size_t *out_size) {
147+
Py_ssize_t py_list_size = PyList_Size(initial_settings_py);
148+
if (py_list_size == 0) {
149+
*out_size = 0;
150+
*out_settings = NULL;
151+
return AWS_OP_SUCCESS;
152+
}
153+
154+
*out_settings = aws_mem_calloc(allocator, py_list_size, sizeof(struct aws_http2_setting));
155+
156+
for (Py_ssize_t i = 0; i < py_list_size; i++) {
157+
PyObject *setting_py = PyList_GetItem(initial_settings_py, i);
158+
159+
/* Get id attribute */
160+
enum aws_http2_settings_id id = PyObject_GetAttrAsIntEnum(setting_py, "Http2Setting", "id");
161+
if (PyErr_Occurred()) {
162+
goto error;
163+
}
164+
165+
/* Get value attribute */
166+
uint32_t value = PyObject_GetAttrAsUint32(setting_py, "Http2Setting", "value");
167+
if (PyErr_Occurred()) {
168+
goto error;
169+
}
170+
(*out_settings)[i].id = id;
171+
(*out_settings)[i].value = value;
172+
}
173+
174+
*out_size = (size_t)py_list_size;
175+
return AWS_OP_SUCCESS;
176+
error:
177+
*out_size = 0;
178+
aws_mem_release(allocator, *out_settings);
179+
*out_settings = NULL;
180+
return AWS_OP_ERR;
181+
}
182+
183+
static void s_http2_on_remote_settings_change(
184+
struct aws_http_connection *http2_connection,
185+
const struct aws_http2_setting *settings_array,
186+
size_t num_settings,
187+
void *user_data) {
188+
(void)http2_connection;
189+
struct http_connection_binding *connection = user_data;
190+
191+
PyGILState_STATE state;
192+
if (aws_py_gilstate_ensure(&state)) {
193+
return; /* Python has shut down. Nothing matters anymore, but don't crash */
194+
}
195+
// Create a new list to hold tuples
196+
PyObject *py_settings_list = PyList_New(num_settings);
197+
if (!py_settings_list) {
198+
PyErr_WriteUnraisable(PyErr_Occurred());
199+
return;
200+
}
201+
for (size_t i = 0; i < num_settings; i++) {
202+
PyObject *tuple = Py_BuildValue("(iI)", settings_array[i].id, settings_array[i].value);
203+
if (!tuple) {
204+
PyErr_WriteUnraisable(PyErr_Occurred());
205+
goto done;
206+
}
207+
PyList_SetItem(py_settings_list, i, tuple); /* steals reference to tuple */
208+
}
209+
PyObject *result = PyObject_CallMethod(connection->py_core, "_on_remote_settings_changed", "(O)", py_settings_list);
210+
if (!result) {
211+
PyErr_WriteUnraisable(PyErr_Occurred());
212+
goto done;
213+
}
214+
Py_DECREF(result);
215+
done:
216+
Py_XDECREF(py_settings_list);
217+
PyGILState_Release(state);
218+
}
219+
141220
PyObject *aws_py_http_client_connection_new(PyObject *self, PyObject *args) {
142221
(void)self;
143222

@@ -150,18 +229,23 @@ PyObject *aws_py_http_client_connection_new(PyObject *self, PyObject *args) {
150229
PyObject *socket_options_py;
151230
PyObject *tls_options_py;
152231
PyObject *proxy_options_py;
232+
PyObject *initial_settings_py;
233+
PyObject *on_remote_settings_changed_py;
153234
PyObject *py_core;
235+
bool success = false;
154236

155237
if (!PyArg_ParseTuple(
156238
args,
157-
"Os#IOOOO",
239+
"Os#IOOOOOO",
158240
&bootstrap_py,
159241
&host_name,
160242
&host_name_len,
161243
&port_number,
162244
&socket_options_py,
163245
&tls_options_py,
164246
&proxy_options_py,
247+
&initial_settings_py,
248+
&on_remote_settings_changed_py,
165249
&py_core)) {
166250
return NULL;
167251
}
@@ -172,23 +256,34 @@ PyObject *aws_py_http_client_connection_new(PyObject *self, PyObject *args) {
172256
}
173257

174258
struct http_connection_binding *connection = aws_mem_calloc(allocator, 1, sizeof(struct http_connection_binding));
175-
if (!connection) {
176-
return PyErr_AwsLastError();
177-
}
178-
179259
/* From hereon, we need to clean up if errors occur */
260+
struct aws_http2_setting *http2_settings = NULL;
261+
size_t http2_settings_count = 0;
262+
struct aws_http2_connection_options http2_options = {0};
180263

181264
struct aws_tls_connection_options *tls_options = NULL;
182265
if (tls_options_py != Py_None) {
183266
tls_options = aws_py_get_tls_connection_options(tls_options_py);
184267
if (!tls_options) {
185-
goto error;
268+
goto done;
186269
}
187270
}
188271

189272
struct aws_socket_options socket_options;
190273
if (!aws_py_socket_options_init(&socket_options, socket_options_py)) {
191-
goto error;
274+
goto done;
275+
}
276+
277+
if (initial_settings_py != Py_None) {
278+
/* Get the array from the pylist */
279+
if (s_convert_http2_settings(initial_settings_py, allocator, &http2_settings, &http2_settings_count)) {
280+
goto done;
281+
}
282+
http2_options.initial_settings_array = http2_settings;
283+
http2_options.num_initial_settings = http2_settings_count;
284+
}
285+
if (on_remote_settings_changed_py != Py_None) {
286+
http2_options.on_remote_settings_change = s_http2_on_remote_settings_change;
192287
}
193288

194289
/* proxy options are optional */
@@ -197,7 +292,7 @@ PyObject *aws_py_http_client_connection_new(PyObject *self, PyObject *args) {
197292
if (proxy_options_py != Py_None) {
198293
proxy_options = &proxy_options_storage;
199294
if (!aws_py_http_proxy_options_init(proxy_options, proxy_options_py)) {
200-
goto error;
295+
goto done;
201296
}
202297
}
203298

@@ -214,21 +309,27 @@ PyObject *aws_py_http_client_connection_new(PyObject *self, PyObject *args) {
214309
.socket_options = &socket_options,
215310
.on_setup = s_on_client_connection_setup,
216311
.on_shutdown = s_on_connection_shutdown,
312+
.http2_options = &http2_options,
217313
};
218314

219315
connection->py_core = py_core;
220316
Py_INCREF(connection->py_core);
221317

222318
if (aws_http_client_connect(&http_options)) {
223319
PyErr_SetAwsLastError();
224-
goto error;
320+
goto done;
225321
}
322+
success = true;
226323

324+
done:
325+
if (http2_settings) {
326+
aws_mem_release(allocator, http2_settings);
327+
}
328+
if (!success) {
329+
s_connection_destroy(connection);
330+
return NULL;
331+
}
227332
Py_RETURN_NONE;
228-
229-
error:
230-
s_connection_destroy(connection);
231-
return NULL;
232333
}
233334

234335
PyObject *aws_py_http_connection_close(PyObject *self, PyObject *args) {

test/test_http_client.py

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
# SPDX-License-Identifier: Apache-2.0.
33

4-
import awscrt.exceptions
5-
from awscrt.http import HttpClientConnection, HttpClientStream, HttpHeaders, HttpProxyOptions, HttpRequest, HttpVersion, Http2ClientConnection
6-
from awscrt.io import ClientBootstrap, ClientTlsContext, DefaultHostResolver, EventLoopGroup, TlsConnectionOptions, TlsContextOptions, TlsCipherPref
7-
from concurrent.futures import Future, thread
8-
from http.server import HTTPServer, SimpleHTTPRequestHandler
9-
from io import BytesIO
10-
import os
11-
import ssl
12-
from test import NativeResourceTest
13-
import threading
14-
import unittest
15-
from urllib.parse import urlparse
16-
import subprocess
17-
import sys
18-
import socket
194
import time
5+
import socket
6+
import sys
7+
import subprocess
8+
from urllib.parse import urlparse
9+
import unittest
10+
import threading
11+
from test import NativeResourceTest
12+
import ssl
13+
import os
14+
from io import BytesIO
15+
from http.server import HTTPServer, SimpleHTTPRequestHandler
16+
from concurrent.futures import Future, thread
17+
from awscrt.io import ClientBootstrap, ClientTlsContext, DefaultHostResolver, EventLoopGroup, TlsConnectionOptions, TlsContextOptions, TlsCipherPref
18+
from awscrt.http import HttpClientConnection, HttpClientStream, HttpHeaders, HttpProxyOptions, HttpRequest, HttpVersion, Http2ClientConnection, Http2Setting, Http2SettingID
19+
import awscrt.exceptions
2020

2121

2222
class Response:
@@ -449,7 +449,24 @@ def tearDown(self):
449449
self.p_server.kill()
450450
super().tearDown()
451451

452-
def _new_mock_connection(self):
452+
def _on_remote_settings_changed(self, settings):
453+
# The mock server has the default settings with
454+
# ENABLE_PUSH = 0
455+
# MAX_CONCURRENT_STREAMS = 100
456+
# MAX_HEADER_LIST_SIZE = 2**16
457+
# using [email protected], code can be found in
458+
# https://github.com/python-hyper/h2/blob/191ac06e0949fcfe3367b06eeb101a5a1a335964/src/h2/connection.py#L340-L359
459+
# Check the settings here
460+
self.assertEqual(len(settings), 3)
461+
for i in settings:
462+
if i.id == Http2SettingID.ENABLE_PUSH:
463+
self.assertEqual(i.value, 0)
464+
if i.id == Http2SettingID.MAX_CONCURRENT_STREAMS:
465+
self.assertEqual(i.value, 100)
466+
if i.id == Http2SettingID.MAX_HEADER_LIST_SIZE:
467+
self.assertEqual(i.value, 2**16)
468+
469+
def _new_mock_connection(self, initial_settings=None):
453470

454471
event_loop_group = EventLoopGroup()
455472
host_resolver = DefaultHostResolver(event_loop_group)
@@ -465,11 +482,15 @@ def _new_mock_connection(self):
465482
tls_conn_opt = tls_ctx.new_connection_options()
466483
tls_conn_opt.set_server_name(self.mock_server_url.hostname)
467484
tls_conn_opt.set_alpn_list(["h2"])
485+
if initial_settings is None:
486+
initial_settings = [Http2Setting(Http2SettingID.ENABLE_PUSH, 0)]
468487

469488
connection_future = Http2ClientConnection.new(host_name=self.mock_server_url.hostname,
470489
port=port,
471490
bootstrap=bootstrap,
472-
tls_connection_options=tls_conn_opt)
491+
tls_connection_options=tls_conn_opt,
492+
initial_settings=initial_settings,
493+
on_remote_settings_changed=self._on_remote_settings_changed)
473494
return connection_future.result(self.timeout)
474495

475496
def test_h2_mock_server_manual_write(self):
@@ -571,6 +592,37 @@ def test_h2_mock_server_manual_write_lifetime(self):
571592
stream.completion_future.result()
572593
self.assertEqual(None, connection.close().exception(self.timeout))
573594

595+
def test_h2_mock_server_settings(self):
596+
exception = None
597+
try:
598+
# invalid settings, should throw an exception
599+
initial_settings = [100]
600+
connection = self._new_mock_connection(initial_settings)
601+
except Exception as e:
602+
exception = e
603+
self.assertIsNotNone(exception)
604+
605+
connection = self._new_mock_connection()
606+
# check we set an h2 connection
607+
self.assertEqual(connection.version, HttpVersion.Http2)
608+
609+
request = HttpRequest('POST', self.mock_server_url.path)
610+
request.headers.add('host', self.mock_server_url.hostname)
611+
response = Response()
612+
stream = connection.request(request, response.on_response, response.on_body, manual_write=True)
613+
stream.activate()
614+
exception = None
615+
stream.write_data(BytesIO(b'hello'), True)
616+
617+
self.assertIsNone(exception)
618+
stream_completion_result = stream.completion_future.result(80)
619+
# check result
620+
self.assertEqual(200, response.status_code)
621+
self.assertEqual(200, stream_completion_result)
622+
print(response.body)
623+
624+
self.assertEqual(None, connection.close().exception(self.timeout))
625+
574626

575627
if __name__ == '__main__':
576628
unittest.main()

0 commit comments

Comments
 (0)