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

1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors 

2# 

3# SPDX-License-Identifier: MIT 

4 

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. 

8 

9The emulation is enabled depending on 

10:func:`aiocoap.defaults.use_ai_v4mapped_emulation`. 

11 

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. 

15 

16Future development 

17------------------ 

18 

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""" 

24 

25import ipaddress 

26import socket 

27import warnings 

28 

29from ...defaults import use_ai_v4mapped_emulation 

30 

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.""" 

35 

36 emulation_applies = family == socket.AF_INET6 and flags & socket.AI_V4MAPPED 

37 

38 use_emulation = use_ai_v4mapped_emulation() 

39 

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) 

42 

43 emulated = await _emulate(loop, host, port, family=family, type=type, proto=proto, flags=flags) 

44 

45 if use_emulation: 

46 if isinstance(emulated, Exception): 

47 raise emulated 

48 return emulated 

49 

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)) 

53 

54 if isinstance(actual, Exception): 

55 raise actual 

56 return actual 

57 

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 

62 

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 

71 

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 

76 

77 if isinstance(v6_result, list): 

78 return v6_result 

79 

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) 

83 

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 ] 

90 

91 assert v6_result is not None, "IPv4 address was not accketed in v4 getaddrinfo" 

92 raise v6_result 

93 

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