Coverage for aiocoap/transports/simplesocketserver.py: 85%
85 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""This module implements a MessageInterface for UDP based on the asyncio
6DatagramProtocol.
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.
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).
15Shortcomings
16------------
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.
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.
27 (The :mod:`udp6<aiocoap.transports.udp6>` transport does not suffer that shortcoming,
28 simplesocketserver is typically only used when that is unavailable).
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`.
33* This transport is experimental and likely to change.
34"""
36import asyncio
37from collections import namedtuple
39from .. import error
40from ..numbers import COAP_PORT, constants
41from .. import interfaces
42from .generic_udp import GenericMessageInterface
43from ..util import hostportjoin
44from .. import defaults
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 )
59 def send(self, data):
60 self.serversocket._transport.sendto(data, self.address)
62 # EnpointAddress interface
64 is_multicast = False
65 is_multicast_locally = False
67 # Unlike for other remotes, this is settable per instance.
68 maximum_block_size_exp = constants.MAX_REGULAR_BLOCK_SIZE_EXP
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)
79 @property
80 def uri_base(self):
81 return self.scheme + "://" + self.hostinfo
83 @property
84 def hostinfo_local(self):
85 return self.serversocket.hostinfo_local
87 @property
88 def uri_base_local(self):
89 return self.scheme + "://" + self.hostinfo_local
91 scheme = "coap"
93 @property
94 def blockwise_key(self):
95 return self.address
98class _DatagramServerSocketSimple(asyncio.DatagramProtocol):
99 # To be overridden by tinydtls_server
100 _Address = _Address
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.")
115 ready = asyncio.get_running_loop().create_future()
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 )
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 )
130 self = await ready
131 self._loop = loop
132 return self
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
141 async def shutdown(self):
142 self._transport.abort()
144 # interface like _DatagramClientSocketpoolSimple6
146 async def connect(self, sockaddr):
147 # FIXME this is not regularly tested either
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).
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
172 # datagram protocol interface
174 def connection_made(self, transport):
175 self._transport = transport
176 self._ready_callback(self)
177 del self._ready_callback
179 def datagram_received(self, data, sockaddr):
180 self._message_interface._received_datagram(self._Address(self, sockaddr), data)
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 )
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)
196class MessageInterfaceSimpleServer(GenericMessageInterface):
197 # for alteration by tinydtls_server
198 _default_port = COAP_PORT
199 _serversocket = _DatagramServerSocketSimple
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 )
218 # Cyclic reference broken during shutdown
219 self._pool = await self._serversocket.create(bind, log, self._loop, self) # type: ignore
221 return self
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