Skip to content

Commit 7d7aef1

Browse files
multanipre-commit-ci[bot]Dreamsorcerer
committed
Support passing a custom server name parameter on HTTPS connection (#7541)
This adds the missing support to set the `server_hostname` setting when creating TCP connection, when the underlying connection is authenticated using TLS. See the documentation for the 2 stdlib functions: * https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.create_connection * https://docs.python.org/3/library/asyncio-eventloop.html#opening-network-connections This would be needed to support features in clients using aiohttp, such as tomplus/kubernetes_asyncio#267 The default behavior should not change, but this would allow on a per-connection basis to specify a custom server name to check the certificate name against. Closes: #7114 (for reference, similar implementation in urllib3: urllib3/urllib3#1397) - [x] I think the code is well written - [x] Unit tests for the changes exist - [x] Documentation reflects the changes - [x] If you provide code modification, please add yourself to `CONTRIBUTORS.txt` * The format is &lt;Name&gt; &lt;Surname&gt;. * Please keep alphabetical order, the file is sorted by names. - [x] Add a new news fragment into the `CHANGES` folder * name it `<issue_id>.<type>` for example (588.bugfix) * if you don't have an `issue_id` change it to the pr id after creating the pr * ensure type is one of the following: * `.feature`: Signifying a new feature. * `.bugfix`: Signifying a bug fix. * `.doc`: Signifying a documentation improvement. * `.removal`: Signifying a deprecation or removal of public API. * `.misc`: A ticket has been closed, but it is not of interest to users. * Make sure to use full sentences with correct case and punctuation, for example: "Fix issue with non-ascii contents in doctest text files." --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Sam Bull <[email protected]> (cherry picked from commit ac29dea)
1 parent bdeca03 commit 7d7aef1

File tree

8 files changed

+175
-3
lines changed

8 files changed

+175
-3
lines changed

CHANGES/7114.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Support passing a custom server name parameter to HTTPS connection

CONTRIBUTORS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ Joel Watts
172172
Jon Nabozny
173173
Jonas Krüger Svensson
174174
Jonas Obrist
175+
Jonathan Ballet
175176
Jonathan Wright
176177
Jonny Tan
177178
Joongi Kim

aiohttp/client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,7 @@ async def _request(
398398
fingerprint: Optional[bytes] = None,
399399
ssl_context: Optional[SSLContext] = None,
400400
ssl: Optional[Union[SSLContext, Literal[False], Fingerprint]] = None,
401+
server_hostname: Optional[str] = None,
401402
proxy_headers: Optional[LooseHeaders] = None,
402403
trace_request_ctx: Optional[SimpleNamespace] = None,
403404
read_bufsize: Optional[int] = None,
@@ -551,6 +552,7 @@ async def _request(
551552
timer=timer,
552553
session=self,
553554
ssl=ssl,
555+
server_hostname=server_hostname,
554556
proxy_headers=proxy_headers,
555557
traces=traces,
556558
trust_env=self.trust_env,

aiohttp/client_reqrep.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ def __init__(
277277
proxy_headers: Optional[LooseHeaders] = None,
278278
traces: Optional[List["Trace"]] = None,
279279
trust_env: bool = False,
280+
server_hostname: Optional[str] = None,
280281
):
281282

282283
if loop is None:
@@ -306,6 +307,7 @@ def __init__(
306307
self.response_class: Type[ClientResponse] = real_response_class
307308
self._timer = timer if timer is not None else TimerNoop()
308309
self._ssl = ssl
310+
self.server_hostname = server_hostname
309311

310312
if loop.get_debug():
311313
self._source_traceback = traceback.extract_stack(sys._getframe(1))

aiohttp/connector.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,7 +1101,7 @@ async def _start_tls_connection(
11011101
underlying_transport,
11021102
tls_proto,
11031103
sslcontext,
1104-
server_hostname=req.host,
1104+
server_hostname=req.server_hostname or req.host,
11051105
ssl_handshake_timeout=timeout.total,
11061106
)
11071107
except BaseException:
@@ -1183,6 +1183,10 @@ def drop_exception(fut: "asyncio.Future[List[Dict[str, Any]]]") -> None:
11831183
host = hinfo["host"]
11841184
port = hinfo["port"]
11851185

1186+
server_hostname = (
1187+
(req.server_hostname or hinfo["hostname"]) if sslcontext else None
1188+
)
1189+
11861190
try:
11871191
transp, proto = await self._wrap_create_connection(
11881192
self._factory,
@@ -1193,7 +1197,7 @@ def drop_exception(fut: "asyncio.Future[List[Dict[str, Any]]]") -> None:
11931197
family=hinfo["family"],
11941198
proto=hinfo["proto"],
11951199
flags=hinfo["flags"],
1196-
server_hostname=hinfo["hostname"] if sslcontext else None,
1200+
server_hostname=server_hostname,
11971201
local_addr=self._local_addr,
11981202
req=req,
11991203
client_error=client_error,

docs/client_reference.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ The client session supports the context manager protocol for self closing.
365365
timeout=sentinel, ssl=None, \
366366
verify_ssl=None, fingerprint=None, \
367367
ssl_context=None, proxy_headers=None, \
368-
auto_decompress=None)
368+
server_hostname=None, auto_decompress=None)
369369
:async:
370370
:noindexentry:
371371

@@ -515,6 +515,13 @@ The client session supports the context manager protocol for self closing.
515515

516516
Use ``ssl=aiohttp.Fingerprint(digest)``
517517

518+
:param str server_hostname: Sets or overrides the host name that the
519+
target server’s certificate will be matched against.
520+
521+
See :py:meth:`asyncio.loop.create_connection` for more information.
522+
523+
.. versionadded:: 3.9
524+
518525
:param ssl.SSLContext ssl_context: ssl context used for processing
519526
*HTTPS* requests (optional).
520527

tests/test_connector.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import uuid
1111
from collections import deque
12+
from contextlib import closing
1213
from unittest import mock
1314

1415
import pytest
@@ -555,6 +556,36 @@ async def certificate_error(*args, **kwargs):
555556
assert isinstance(ctx.value, aiohttp.ClientSSLError)
556557

557558

559+
async def test_tcp_connector_server_hostname_default(loop) -> None:
560+
conn = aiohttp.TCPConnector(loop=loop)
561+
562+
with mock.patch.object(
563+
conn._loop, "create_connection", autospec=True, spec_set=True
564+
) as create_connection:
565+
create_connection.return_value = mock.Mock(), mock.Mock()
566+
567+
req = ClientRequest("GET", URL("https://127.0.0.1:443"), loop=loop)
568+
569+
with closing(await conn.connect(req, [], ClientTimeout())):
570+
assert create_connection.call_args.kwargs["server_hostname"] == "127.0.0.1"
571+
572+
573+
async def test_tcp_connector_server_hostname_override(loop) -> None:
574+
conn = aiohttp.TCPConnector(loop=loop)
575+
576+
with mock.patch.object(
577+
conn._loop, "create_connection", autospec=True, spec_set=True
578+
) as create_connection:
579+
create_connection.return_value = mock.Mock(), mock.Mock()
580+
581+
req = ClientRequest(
582+
"GET", URL("https://127.0.0.1:443"), loop=loop, server_hostname="localhost"
583+
)
584+
585+
with closing(await conn.connect(req, [], ClientTimeout())):
586+
assert create_connection.call_args.kwargs["server_hostname"] == "localhost"
587+
588+
558589
async def test_tcp_connector_multiple_hosts_errors(loop) -> None:
559590
conn = aiohttp.TCPConnector(loop=loop)
560591

tests/test_proxy.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,130 @@ async def make_conn():
191191
connector.connect(req, None, aiohttp.ClientTimeout())
192192
)
193193

194+
@mock.patch("aiohttp.connector.ClientRequest")
195+
def test_proxy_server_hostname_default(self, ClientRequestMock) -> None:
196+
proxy_req = ClientRequest(
197+
"GET", URL("http://proxy.example.com"), loop=self.loop
198+
)
199+
ClientRequestMock.return_value = proxy_req
200+
201+
proxy_resp = ClientResponse(
202+
"get",
203+
URL("http://proxy.example.com"),
204+
request_info=mock.Mock(),
205+
writer=mock.Mock(),
206+
continue100=None,
207+
timer=TimerNoop(),
208+
traces=[],
209+
loop=self.loop,
210+
session=mock.Mock(),
211+
)
212+
proxy_req.send = make_mocked_coro(proxy_resp)
213+
proxy_resp.start = make_mocked_coro(mock.Mock(status=200))
214+
215+
async def make_conn():
216+
return aiohttp.TCPConnector()
217+
218+
connector = self.loop.run_until_complete(make_conn())
219+
connector._resolve_host = make_mocked_coro(
220+
[
221+
{
222+
"hostname": "hostname",
223+
"host": "127.0.0.1",
224+
"port": 80,
225+
"family": socket.AF_INET,
226+
"proto": 0,
227+
"flags": 0,
228+
}
229+
]
230+
)
231+
232+
tr, proto = mock.Mock(), mock.Mock()
233+
self.loop.create_connection = make_mocked_coro((tr, proto))
234+
self.loop.start_tls = make_mocked_coro(mock.Mock())
235+
236+
req = ClientRequest(
237+
"GET",
238+
URL("https://www.python.org"),
239+
proxy=URL("http://proxy.example.com"),
240+
loop=self.loop,
241+
)
242+
self.loop.run_until_complete(
243+
connector._create_connection(req, None, aiohttp.ClientTimeout())
244+
)
245+
246+
self.assertEqual(
247+
self.loop.start_tls.call_args.kwargs["server_hostname"], "www.python.org"
248+
)
249+
250+
self.loop.run_until_complete(proxy_req.close())
251+
proxy_resp.close()
252+
self.loop.run_until_complete(req.close())
253+
254+
@mock.patch("aiohttp.connector.ClientRequest")
255+
def test_proxy_server_hostname_override(self, ClientRequestMock) -> None:
256+
proxy_req = ClientRequest(
257+
"GET",
258+
URL("http://proxy.example.com"),
259+
loop=self.loop,
260+
)
261+
ClientRequestMock.return_value = proxy_req
262+
263+
proxy_resp = ClientResponse(
264+
"get",
265+
URL("http://proxy.example.com"),
266+
request_info=mock.Mock(),
267+
writer=mock.Mock(),
268+
continue100=None,
269+
timer=TimerNoop(),
270+
traces=[],
271+
loop=self.loop,
272+
session=mock.Mock(),
273+
)
274+
proxy_req.send = make_mocked_coro(proxy_resp)
275+
proxy_resp.start = make_mocked_coro(mock.Mock(status=200))
276+
277+
async def make_conn():
278+
return aiohttp.TCPConnector()
279+
280+
connector = self.loop.run_until_complete(make_conn())
281+
connector._resolve_host = make_mocked_coro(
282+
[
283+
{
284+
"hostname": "hostname",
285+
"host": "127.0.0.1",
286+
"port": 80,
287+
"family": socket.AF_INET,
288+
"proto": 0,
289+
"flags": 0,
290+
}
291+
]
292+
)
293+
294+
tr, proto = mock.Mock(), mock.Mock()
295+
self.loop.create_connection = make_mocked_coro((tr, proto))
296+
self.loop.start_tls = make_mocked_coro(mock.Mock())
297+
298+
req = ClientRequest(
299+
"GET",
300+
URL("https://www.python.org"),
301+
proxy=URL("http://proxy.example.com"),
302+
server_hostname="server-hostname.example.com",
303+
loop=self.loop,
304+
)
305+
self.loop.run_until_complete(
306+
connector._create_connection(req, None, aiohttp.ClientTimeout())
307+
)
308+
309+
self.assertEqual(
310+
self.loop.start_tls.call_args.kwargs["server_hostname"],
311+
"server-hostname.example.com",
312+
)
313+
314+
self.loop.run_until_complete(proxy_req.close())
315+
proxy_resp.close()
316+
self.loop.run_until_complete(req.close())
317+
194318
@mock.patch("aiohttp.connector.ClientRequest")
195319
def test_https_connect(self, ClientRequestMock) -> None:
196320
proxy_req = ClientRequest(

0 commit comments

Comments
 (0)