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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

82 statements  

1# This file is part of the Python aiocoap library project. 

2# 

3# Copyright (c) 2012-2014 Maciej Wasilak <http://sixpinetrees.blogspot.com/>, 

4# 2013-2014 Christian Amsüss <c.amsuess@energyharvesting.at> 

5# 

6# aiocoap is free software, this file is published under the MIT license as 

7# described in the accompanying LICENSE file. 

8 

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

10DatagramProtocol. 

11 

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

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

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

15 

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

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

18 

19Shortcomings 

20------------ 

21 

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

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

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

25 

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

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

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

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

30 

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

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

33 

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

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

36 

37* This transport is experimental and likely to change. 

38""" 

39 

40import asyncio 

41from collections import namedtuple 

42 

43from .. import error 

44from ..numbers import COAP_PORT 

45from .. import interfaces 

46from .generic_udp import GenericMessageInterface 

47from ..util import hostportjoin 

48from .. import defaults 

49 

50class _Address(namedtuple('_Address', ['serversocket', 'address']), interfaces.EndpointAddress): 

51 # hashability and equality follow from being a namedtuple 

52 def __repr__(self): 

53 return '<%s.%s via %s to %s>'%(__name__, type(self).__name__, self.serversocket, self.address) 

54 

55 def send(self, data): 

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

57 

58 # EnpointAddress interface 

59 

60 is_multicast = False 

61 is_multicast_locally = False 

62 

63 @property 

64 def hostinfo(self): 

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

66 # scope and interface identifier 

67 host, port, *_ = self.address 

68 if port == COAP_PORT: 

69 port = None 

70 return hostportjoin(host, port) 

71 

72 @property 

73 def uri_base(self): 

74 return self.scheme + '://' + self.hostinfo 

75 

76 @property 

77 def hostinfo_local(self): 

78 return self.serversocket.hostinfo_local 

79 

80 @property 

81 def uri_base_local(self): 

82 return self.scheme + '://' + self.hostinfo_local 

83 

84 scheme = 'coap' 

85 

86class _DatagramServerSocketSimple(asyncio.DatagramProtocol): 

87 # To be overridden by tinydtls_server 

88 _Address = _Address 

89 

90 @classmethod 

91 async def create(cls, bind, log, loop, message_interface: "GenericMessageInterface"): 

92 if bind is None or bind[0] in ('::', '0.0.0.0', '', None): 

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

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

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

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

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

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

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

100 

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

102 

103 transport, protocol = await loop.create_datagram_endpoint( 

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

105 local_addr=bind, 

106 reuse_port=defaults.has_reuse_port(), 

107 ) 

108 

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

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

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

112 protocol.hostinfo_local = hostportjoin(bind[0], bind[1] if bind[1] != COAP_PORT else None) 

113 

114 self = await ready 

115 self._loop = loop 

116 return self 

117 

118 def __init__(self, ready_callback, message_interface: "GenericMessageInterface", log): 

119 self._ready_callback = ready_callback 

120 self._message_interface = message_interface 

121 self.log = log 

122 

123 async def shutdown(self): 

124 self._transport.abort() 

125 

126 # interface like _DatagramClientSocketpoolSimple6 

127 

128 async def connect(self, sockaddr): 

129 # FIXME this is not regularly tested either 

130 

131 self.log.warning("Sending initial messages via a server socket is not recommended") 

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

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

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

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

136 # per-connection state). 

137 

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

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

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

141 # (host, port) tuple. 

142 addresses = await self._loop.getaddrinfo(*sockaddr, family=self._transport.get_extra_info('socket').family) 

143 if not addresses: 

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

145 # FIXME could do happy eyebals 

146 address = addresses[0][4] 

147 address = self._Address(self, address) 

148 return address 

149 

150 # datagram protocol interface 

151 

152 def connection_made(self, transport): 

153 self._transport = transport 

154 self._ready_callback(self) 

155 del self._ready_callback 

156 

157 def datagram_received(self, data, sockaddr): 

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

159 

160 def error_received(self, exception): 

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

162 self.log.warning("Ignoring error because it can not be mapped to any connection: %s", exception) 

163 

164 def connection_lost(self, exception): 

165 if exception is None: 

166 pass # regular shutdown 

167 else: 

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

169 

170class MessageInterfaceSimpleServer(GenericMessageInterface): 

171 # for alteration by tinydtls_server 

172 _default_port = COAP_PORT 

173 _serversocket = _DatagramServerSocketSimple 

174 

175 @classmethod 

176 async def create_server(cls, bind, ctx: interfaces.MessageManager, log, loop): 

177 self = cls(ctx, log, loop) 

178 bind = bind or ('::', None) 

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

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

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

182 # instead as outlined there. 

183 bind = (bind[0], self._default_port if bind[1] is None else bind[1] + (self._default_port - COAP_PORT)) 

184 

185 # Cyclic reference broken during shutdown 

186 self._pool = await self._serversocket.create(bind, log, self._loop, self) 

187 

188 return self 

189 

190 async def recognize_remote(self, remote): 

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

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

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

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

195 # is sent out 

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