Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/source/reference-io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
I/O in Trio
===========

Knowing what hostname you are actually connected to
---------------------------------------------------

If you pass a string hostname to Trio, for example in
`trio.socket.getaddrinfo` or `trio.SSLStream`, Trio will need to get a
bytes version of it at some point. To do this conversion yourself, we
recommend `trio.lowlevel.encode_hostname`. Notably, this differs from
what the Python standard library typically encodes hosts as.

.. autofunction:: trio.lowlevel.encode_hostname

.. _abstract-stream-api:

The abstract Stream API
Expand Down
1 change: 1 addition & 0 deletions newsfragments/11.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ensure that we consistently encode all hostnames using IDNA 2008.
36 changes: 22 additions & 14 deletions src/trio/_socket.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,27 @@ async def __aexit__(
return False


def encode_hostname(hostname: str) -> bytes:
"""Encode a hostname using the same IDNA 2008 encoding Trio uses.

If you want to know which exact hostname Trio will connect to, use this
function.
"""
# Given a unicode hostname, we want to do our own encoding using the idna
# module, rather than letting Python do it. (Python will use the old IDNA
# 2003 standard, and possibly get the wrong answer - see bpo-17305).
# However, the idna module is picky, and will refuse to process some valid
# hostname strings, like "::1". So if it's already ascii, we pass it
# through; otherwise, we encode it via IDNA.
try:
return hostname.encode("ascii")
except UnicodeEncodeError:
# UTS-46 defines various normalizations; in particular, by default
# idna.encode will error out if the hostname has Capital Letters
# in it; with uts46=True it will lowercase them instead.
return _idna.encode(hostname, uts46=True)


################################################################
# Overrides
################################################################
Expand Down Expand Up @@ -252,21 +273,8 @@ def numeric_only_failure(exc: BaseException) -> bool:
flags | _NUMERIC_ONLY,
)
# That failed; it's a real hostname. We better use a thread.
#
# Also, it might be a unicode hostname, in which case we want to do our
# own encoding using the idna module, rather than letting Python do
# it. (Python will use the old IDNA 2003 standard, and possibly get the
# wrong answer - see bpo-17305). However, the idna module is picky, and
# will refuse to process some valid hostname strings, like "::1". So if
# it's already ascii, we pass it through; otherwise, we encode it to.
if isinstance(host, str):
try:
host = host.encode("ascii")
except UnicodeEncodeError:
# UTS-46 defines various normalizations; in particular, by default
# idna.encode will error out if the hostname has Capital Letters
# in it; with uts46=True it will lowercase them instead.
host = _idna.encode(host, uts46=True)
host = encode_hostname(host)
hr = _resolver.get(None)
if hr is not None:
return await hr.getaddrinfo(host, port, family, type, proto, flags)
Expand Down
7 changes: 7 additions & 0 deletions src/trio/_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from . import _sync
from ._highlevel_generic import aclose_forcefully
from ._socket import encode_hostname
from ._util import ConflictDetector, final
from .abc import Listener, Stream

Expand Down Expand Up @@ -362,6 +363,12 @@ def __init__(
server_side: bool = False,
https_compatible: bool = False,
) -> None:
server_hostname = (
encode_hostname(server_hostname)
if isinstance(server_hostname, str)
else server_hostname
)

self.transport_stream: T_Stream = transport_stream
self._state = _State.OK
self._https_compatible = https_compatible
Expand Down
10 changes: 10 additions & 0 deletions src/trio/_tests/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,3 +1378,13 @@ async def setup(
await aclose_forcefully(ssl_listener)
await aclose_forcefully(ssl_client)
await aclose_forcefully(ssl_server)


async def test_server_hostname_is_idna_2008(client_ctx: SSLContext) -> None:
async with ssl_echo_server_raw(expect_fail=True) as sock:
good_ctx = client_ctx
s = SSLStream(sock, good_ctx, server_hostname="faß.de")

assert s.server_hostname.encode("ascii") == trio.lowlevel.encode_hostname(
"faß.de"
)
1 change: 1 addition & 0 deletions src/trio/lowlevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
wait_task_rescheduled as wait_task_rescheduled,
wait_writable as wait_writable,
)
from ._socket import encode_hostname as encode_hostname
from ._subprocess import open_process as open_process

# This is the union of a subset of trio/_core/ and some things from trio/*.py.
Expand Down
Loading