# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors # # SPDX-License-Identifier: MIT """This module implements a MessageInterface for UDP based on the asyncio DatagramProtocol. This is a simple version that works only for servers bound to a single unicast address. It provides a server backend in situations when :mod:`.udp6` is unavailable and :mod:`.simple6` needs to be used for clients. While it is in theory capable of sending requests too, it should not be used like that, because it won't receive ICMP errors (see below). Shortcomings ------------ * This implementation does not receive ICMP errors. This violates the CoAP standard and can lead to unnecessary network traffic, bad user experience (when used for client requests) or even network attack amplification. * The server can not be used with the "any-address" (``::``, ``0.0.0.0``). If it were allowed to bind there, it would not receive any indication from the operating system as to which of its own addresses a request was sent, and could not send the response with the appropriate sender address. (The :mod:`udp6` transport does not suffer that shortcoming, simplesocketserver is typically only used when that is unavailable). With simplesocketserver, you need to explicitly give the IP address of your server in the ``bind`` argument of :meth:`aiocoap.protocol.Context.create_server_context`. * This transport is experimental and likely to change. """ import asyncio from collections import namedtuple from .. import error from ..numbers import COAP_PORT, constants from .. import interfaces from .generic_udp import GenericMessageInterface from ..util import hostportjoin from .. import defaults class _Address( namedtuple("_Address", ["serversocket", "address"]), interfaces.EndpointAddress ): # hashability and equality follow from being a namedtuple def __repr__(self): return "<%s.%s via %s to %s>" % ( __name__, type(self).__name__, self.serversocket, self.address, ) def send(self, data): self.serversocket._transport.sendto(data, self.address) # EnpointAddress interface is_multicast = False is_multicast_locally = False # Unlike for other remotes, this is settable per instance. maximum_block_size_exp = constants.MAX_REGULAR_BLOCK_SIZE_EXP @property def hostinfo(self): # `host` already contains the interface identifier, so throwing away # scope and interface identifier host, port, *_ = self.address if port == COAP_PORT: port = None return hostportjoin(host, port) @property def uri_base(self): return self.scheme + "://" + self.hostinfo @property def hostinfo_local(self): return self.serversocket.hostinfo_local @property def uri_base_local(self): return self.scheme + "://" + self.hostinfo_local scheme = "coap" @property def blockwise_key(self): return self.address class _DatagramServerSocketSimple(asyncio.DatagramProtocol): # To be overridden by tinydtls_server _Address = _Address @classmethod async def create( cls, bind, log, loop, message_interface: "GenericMessageInterface" ): if bind is None or bind[0] in ("::", "0.0.0.0", "", None): # If you feel tempted to remove this check, think about what # happens if two configured addresses can both route to a # requesting endpoint, how that endpoint is supposed to react to a # response from the other address, and if that case is not likely # to ever happen in your field of application, think about what you # tell the first user where it does happen anyway. raise ValueError("The transport can not be bound to any-address.") ready = asyncio.get_running_loop().create_future() transport, protocol = await loop.create_datagram_endpoint( lambda: cls(ready.set_result, message_interface, log), local_addr=bind, reuse_port=defaults.has_reuse_port(), ) # Conveniently, we only bind to a single port (because we need to know # the return address, not because we insist we know the local # hostinfo), and can thus store the local hostinfo without distinction protocol.hostinfo_local = hostportjoin( bind[0], bind[1] if bind[1] != COAP_PORT else None ) self = await ready self._loop = loop return self def __init__( self, ready_callback, message_interface: "GenericMessageInterface", log ): self._ready_callback = ready_callback self._message_interface = message_interface self.log = log async def shutdown(self): self._transport.abort() # interface like _DatagramClientSocketpoolSimple6 async def connect(self, sockaddr): # FIXME this is not regularly tested either self.log.warning( "Sending initial messages via a server socket is not recommended" ) # A legitimate case is when something stores return addresses as # URI(part)s and not as remotes. (In similar transports this would also be # the case if the address's connection is dropped from the pool, but # that doesn't happen here since there is no pooling as there is no # per-connection state). # getaddrinfo is not only to needed to resolve any host names (which # would not be recognized otherwise), but also to get a complete (host, # port, zoneinfo, whatwasthefourth) tuple from what is passed in as a # (host, port) tuple. addresses = await self._loop.getaddrinfo( *sockaddr, family=self._transport.get_extra_info("socket").family ) if not addresses: raise error.NetworkError("No addresses found for %s" % sockaddr[0]) # FIXME could do happy eyebals address = addresses[0][4] address = self._Address(self, address) return address # datagram protocol interface def connection_made(self, transport): self._transport = transport self._ready_callback(self) del self._ready_callback def datagram_received(self, data, sockaddr): self._message_interface._received_datagram(self._Address(self, sockaddr), data) def error_received(self, exception): # This is why this whole implementation is a bad idea (but still the best we got on some platforms) self.log.warning( "Ignoring error because it can not be mapped to any connection: %s", exception, ) def connection_lost(self, exception): if exception is None: pass # regular shutdown else: self.log.error("Received unexpected connection loss: %s", exception) class MessageInterfaceSimpleServer(GenericMessageInterface): # for alteration by tinydtls_server _default_port = COAP_PORT _serversocket = _DatagramServerSocketSimple @classmethod async def create_server( cls, bind, ctx: interfaces.MessageManager, log, loop, *args, **kwargs ): self = cls(ctx, log, loop) bind = bind or ("::", None) # Interpret None as 'default port', but still allow to bind to 0 for # servers that want a random port (eg. when the service URLs are # advertised out-of-band anyway). LwM2M clients should use simple6 # instead as outlined there. bind = ( bind[0], self._default_port if bind[1] is None else bind[1] + (self._default_port - COAP_PORT), ) # Cyclic reference broken during shutdown self._pool = await self._serversocket.create(bind, log, self._loop, self) # type: ignore return self async def recognize_remote(self, remote): # FIXME: This is never tested (as is the connect method) because all # tests create client contexts client-side (which don't build a # simplesocketserver), and because even when a server context is # created, there's a simple6 that grabs such addresses before a request # is sent out return isinstance(remote, _Address) and remote.serversocket is self._pool