Coverage for aiocoap/util/asyncio/getaddrinfo_v4mapped.py: 80%
45 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-16 16:09 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-16 16:09 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""Helper module to work around platform limitations where V6ONLY=0 can be set,
6but getaddrinfo can not deal with AI_V4MAPPED; this is currently known to
7affect only Android.
9The emulation is enabled depending on
10:func:`aiocoap.defaults.use_ai_v4mapped_emulation`.
12Unless Python is used with optimization enabled, emulation will be run on all
13getaddrinfo calls in addition to the regular call, and a warning is shown if
14they disagree.
16Future development
17------------------
19It may be that for proper happy-eyeballs support, AI_V4MAPPED is not quite the
20right way to go: It will show a mapped address only if there are no V6 results
21-- whereas on links without V6 connectivity, one would like to receive both
22results and try them in order.
23"""
25import ipaddress
26import socket
27import warnings
29from ...defaults import use_ai_v4mapped_emulation
31async def getaddrinfo(loop, host, port, *, family=0, type=0, proto=0, flags=0):
32 """A wrapper around getaddrinfo that soft-implements AI_V4MAPPED on
33 platforms that do not have that option, but do actually support dual
34 stack."""
36 emulation_applies = family == socket.AF_INET6 and flags & socket.AI_V4MAPPED
38 use_emulation = use_ai_v4mapped_emulation()
40 if not emulation_applies or (not use_emulation and not __debug__):
41 return await loop.getaddrinfo(host, port, family=family, type=type, proto=proto, flags=flags)
43 emulated = await _emulate(loop, host, port, family=family, type=type, proto=proto, flags=flags)
45 if use_emulation:
46 if isinstance(emulated, Exception):
47 raise emulated
48 return emulated
50 actual = await _getaddrinfo_nonraising(loop, host, port, family=family, type=type, proto=proto, flags=flags)
51 if actual != emulated:
52 warnings.warn("Emulation of V4MAPPED addresses is erroneous: System returned %s, emulation returned %s" % (actual, emulated))
54 if isinstance(actual, Exception):
55 raise actual
56 return actual
58async def _emulate(loop, host, port, *, family=0, type=0, proto=0, flags=0):
59 assert family == socket.AF_INET6
60 assert flags & socket.AI_V4MAPPED
61 new_flags = flags & ~socket.AI_V4MAPPED
63 # Control-flow-by-exception is not a good thing to have, but there's no
64 # standard library function for "is this an IPv6 address".
65 try:
66 ipaddress.IPv4Address(host)
67 except ipaddress.AddressValueError:
68 is_v4address = False
69 else:
70 is_v4address = True
72 if not is_v4address:
73 v6_result = await _getaddrinfo_nonraising(loop, host, port, family=socket.AF_INET6, type=type, proto=proto, flags=new_flags)
74 else:
75 v6_result = None
77 if isinstance(v6_result, list):
78 return v6_result
80 # This should never hit the "this is a V6 address" case, because those
81 # already give a good v6_result
82 v4_result = await _getaddrinfo_nonraising(loop, host, port, family=socket.AF_INET, type=type, proto=proto, flags=new_flags)
84 if isinstance(v4_result, list):
85 return [
86 (socket.AF_INET6, type, proto, canonname, ('::ffff:' + sa_ip, sa_port, 0, 0))
87 for (_, type, proto, canonname, (sa_ip, sa_port))
88 in v4_result
89 ]
91 assert v6_result is not None, "IPv4 address was not accketed in v4 getaddrinfo"
92 raise v6_result
94async def _getaddrinfo_nonraising(loop, *args, **kwargs):
95 """A version of loop.getaddinfo() that just returns its exception rather than raising it"""
96 try:
97 return await loop.getaddrinfo(*args, **kwargs)
98 except socket.gaierror as e:
99 return e