Coverage for aiocoap/transports/simplesocketserver.py: 85%

85 statements  

« prev     ^ index     » next       coverage.py v7.5.4, created at 2024-07-10 11:47 +0000

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

2# 

3# SPDX-License-Identifier: MIT 

4 

5"""This module implements a MessageInterface for UDP based on the asyncio 

6DatagramProtocol. 

7 

8This is a simple version that works only for servers bound to a single unicast 

9address. It provides a server backend in situations when :mod:`.udp6` is 

10unavailable and :mod:`.simple6` needs to be used for clients. 

11 

12While it is in theory capable of sending requests too, it should not be used 

13like that, because it won't receive ICMP errors (see below). 

14 

15Shortcomings 

16------------ 

17 

18* This implementation does not receive ICMP errors. This violates the CoAP 

19 standard and can lead to unnecessary network traffic, bad user experience 

20 (when used for client requests) or even network attack amplification. 

21 

22* The server can not be used with the "any-address" (``::``, ``0.0.0.0``). 

23 If it were allowed to bind there, it would not receive any indication from the operating system 

24 as to which of its own addresses a request was sent, 

25 and could not send the response with the appropriate sender address. 

26 

27 (The :mod:`udp6<aiocoap.transports.udp6>` transport does not suffer that shortcoming, 

28 simplesocketserver is typically only used when that is unavailable). 

29 

30 With simplesocketserver, you need to explicitly give the IP address of your server 

31 in the ``bind`` argument of :meth:`aiocoap.protocol.Context.create_server_context`. 

32 

33* This transport is experimental and likely to change. 

34""" 

35 

36import asyncio 

37from collections import namedtuple 

38 

39from .. import error 

40from ..numbers import COAP_PORT, constants 

41from .. import interfaces 

42from .generic_udp import GenericMessageInterface 

43from ..util import hostportjoin 

44from .. import defaults 

45 

46 

47class _Address( 

48 namedtuple("_Address", ["serversocket", "address"]), interfaces.EndpointAddress 

49): 

50 # hashability and equality follow from being a namedtuple 

51 def __repr__(self): 

52 return "<%s.%s via %s to %s>" % ( 

53 __name__, 

54 type(self).__name__, 

55 self.serversocket, 

56 self.address, 

57 ) 

58 

59 def send(self, data): 

60 self.serversocket._transport.sendto(data, self.address) 

61 

62 # EnpointAddress interface 

63 

64 is_multicast = False 

65 is_multicast_locally = False 

66 

67 # Unlike for other remotes, this is settable per instance. 

68 maximum_block_size_exp = constants.MAX_REGULAR_BLOCK_SIZE_EXP 

69 

70 @property 

71 def hostinfo(self): 

72 # `host` already contains the interface identifier, so throwing away 

73 # scope and interface identifier 

74 host, port, *_ = self.address 

75 if port == COAP_PORT: 

76 port = None 

77 return hostportjoin(host, port) 

78 

79 @property 

80 def uri_base(self): 

81 return self.scheme + "://" + self.hostinfo 

82 

83 @property 

84 def hostinfo_local(self): 

85 return self.serversocket.hostinfo_local 

86 

87 @property 

88 def uri_base_local(self): 

89 return self.scheme + "://" + self.hostinfo_local 

90 

91 scheme = "coap" 

92 

93 @property 

94 def blockwise_key(self): 

95 return self.address 

96 

97 

98class _DatagramServerSocketSimple(asyncio.DatagramProtocol): 

99 # To be overridden by tinydtls_server 

100 _Address = _Address 

101 

102 @classmethod 

103 async def create( 

104 cls, bind, log, loop, message_interface: "GenericMessageInterface" 

105 ): 

106 if bind is None or bind[0] in ("::", "0.0.0.0", "", None): 

107 # If you feel tempted to remove this check, think about what 

108 # happens if two configured addresses can both route to a 

109 # requesting endpoint, how that endpoint is supposed to react to a 

110 # response from the other address, and if that case is not likely 

111 # to ever happen in your field of application, think about what you 

112 # tell the first user where it does happen anyway. 

113 raise ValueError("The transport can not be bound to any-address.") 

114 

115 ready = asyncio.get_running_loop().create_future() 

116 

117 transport, protocol = await loop.create_datagram_endpoint( 

118 lambda: cls(ready.set_result, message_interface, log), 

119 local_addr=bind, 

120 reuse_port=defaults.has_reuse_port(), 

121 ) 

122 

123 # Conveniently, we only bind to a single port (because we need to know 

124 # the return address, not because we insist we know the local 

125 # hostinfo), and can thus store the local hostinfo without distinction 

126 protocol.hostinfo_local = hostportjoin( 

127 bind[0], bind[1] if bind[1] != COAP_PORT else None 

128 ) 

129 

130 self = await ready 

131 self._loop = loop 

132 return self 

133 

134 def __init__( 

135 self, ready_callback, message_interface: "GenericMessageInterface", log 

136 ): 

137 self._ready_callback = ready_callback 

138 self._message_interface = message_interface 

139 self.log = log 

140 

141 async def shutdown(self): 

142 self._transport.abort() 

143 

144 # interface like _DatagramClientSocketpoolSimple6 

145 

146 async def connect(self, sockaddr): 

147 # FIXME this is not regularly tested either 

148 

149 self.log.warning( 

150 "Sending initial messages via a server socket is not recommended" 

151 ) 

152 # A legitimate case is when something stores return addresses as 

153 # URI(part)s and not as remotes. (In similar transports this'd also be 

154 # the case if the address's connection is dropped from the pool, but 

155 # that doesn't happen here since there is no pooling as there is no 

156 # per-connection state). 

157 

158 # getaddrinfo is not only to needed to resolve any host names (which 

159 # would not be recognized otherwise), but also to get a complete (host, 

160 # port, zoneinfo, whatwasthefourth) tuple from what is passed in as a 

161 # (host, port) tuple. 

162 addresses = await self._loop.getaddrinfo( 

163 *sockaddr, family=self._transport.get_extra_info("socket").family 

164 ) 

165 if not addresses: 

166 raise error.NetworkError("No addresses found for %s" % sockaddr[0]) 

167 # FIXME could do happy eyebals 

168 address = addresses[0][4] 

169 address = self._Address(self, address) 

170 return address 

171 

172 # datagram protocol interface 

173 

174 def connection_made(self, transport): 

175 self._transport = transport 

176 self._ready_callback(self) 

177 del self._ready_callback 

178 

179 def datagram_received(self, data, sockaddr): 

180 self._message_interface._received_datagram(self._Address(self, sockaddr), data) 

181 

182 def error_received(self, exception): 

183 # This is why this whole implementation is a bad idea (but still the best we got on some platforms) 

184 self.log.warning( 

185 "Ignoring error because it can not be mapped to any connection: %s", 

186 exception, 

187 ) 

188 

189 def connection_lost(self, exception): 

190 if exception is None: 

191 pass # regular shutdown 

192 else: 

193 self.log.error("Received unexpected connection loss: %s", exception) 

194 

195 

196class MessageInterfaceSimpleServer(GenericMessageInterface): 

197 # for alteration by tinydtls_server 

198 _default_port = COAP_PORT 

199 _serversocket = _DatagramServerSocketSimple 

200 

201 @classmethod 

202 async def create_server( 

203 cls, bind, ctx: interfaces.MessageManager, log, loop, *args, **kwargs 

204 ): 

205 self = cls(ctx, log, loop) 

206 bind = bind or ("::", None) 

207 # Interpret None as 'default port', but still allow to bind to 0 for 

208 # servers that want a random port (eg. when the service URLs are 

209 # advertised out-of-band anyway). LwM2M clients should use simple6 

210 # instead as outlined there. 

211 bind = ( 

212 bind[0], 

213 self._default_port 

214 if bind[1] is None 

215 else bind[1] + (self._default_port - COAP_PORT), 

216 ) 

217 

218 # Cyclic reference broken during shutdown 

219 self._pool = await self._serversocket.create(bind, log, self._loop, self) # type: ignore 

220 

221 return self 

222 

223 async def recognize_remote(self, remote): 

224 # FIXME: This is never tested (as is the connect method) because all 

225 # tests create client contexts client-side (which don't build a 

226 # simplesocketserver), and because even when a server context is 

227 # created, there's a simple6 that grabs such addresses before a request 

228 # is sent out 

229 return isinstance(remote, _Address) and remote.serversocket is self._pool