Coverage for aiocoap / interfaces.py: 92%
144 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""This module provides interface base classes to various aiocoap software
6components, especially with respect to request and response handling. It
7describes `abstract base classes`_ for messages, endpoints etc.
9It is *completely unrelated* to the concept of "network interfaces".
11.. _`abstract base classes`: https://docs.python.org/3/library/abc"""
13from __future__ import annotations
15import abc
16import asyncio
17import warnings
19from aiocoap.pipe import Pipe
20from aiocoap.numbers.constants import MAX_REGULAR_BLOCK_SIZE_EXP
21from .util import DeprecationWarning
23from typing import Optional, Callable
26class MessageInterface(metaclass=abc.ABCMeta):
27 """A MessageInterface is an object that can exchange addressed messages over
28 unreliable transports. Implementations send and receive messages with
29 message type and message ID, and are driven by a Context that deals with
30 retransmission.
32 Usually, an MessageInterface refers to something like a local socket, and
33 send messages to different remote endpoints depending on the message's
34 addresses. Just as well, a MessageInterface can be useful for one single
35 address only, or use various local addresses depending on the remote
36 address.
37 """
39 @abc.abstractmethod
40 async def shutdown(self):
41 """Deactivate the complete transport, usually irrevertably. When the
42 coroutine returns, the object must have made sure that it can be
43 destructed by means of ref-counting or a garbage collector run."""
45 @abc.abstractmethod
46 def send(self, message):
47 """Send a given :class:`Message` object"""
49 @abc.abstractmethod
50 async def recognize_remote(self, remote):
51 """Return True if the remote is one belonging to this transport"""
53 @abc.abstractmethod
54 async def determine_remote(self, message):
55 """Return a value suitable for the message's remote property based on
56 its .opt.uri_host or .unresolved_remote.
58 May return None, which indicates that the MessageInterface can not
59 transport the message (typically because it is of the wrong scheme)."""
62class EndpointAddress(metaclass=abc.ABCMeta):
63 """An address that is suitable for routing through the application to a
64 remote endpoint.
66 Depending on the MessageInterface implementation used, an EndpointAddress
67 property of a message can mean the message is exchanged "with
68 [2001:db8::2:1]:5683, while my local address was [2001:db8:1::1]:5683"
69 (typical of UDP6), "over the connected <Socket at
70 0x1234>, wherever that's connected to" (simple6 or TCP) or "with
71 participant 0x01 of the OSCAP key 0x..., routed over <another
72 EndpointAddress>".
74 EndpointAddresses are only constructed by MessageInterface objects,
75 either for incoming messages or when populating a message's .remote in
76 :meth:`MessageInterface.determine_remote`.
78 There is no requirement that those address are always identical for a given
79 address. However, incoming addresses must be hashable and hash-compare
80 identically to requests from the same context. The "same context", for the
81 purpose of EndpointAddresses, means that the message must be eligible for
82 request/response, blockwise (de)composition and observations. (For example,
83 in a DTLS context, the hash must change between epochs due to RFC7252
84 Section 9.1.2).
86 So far, it is required that hash-identical objects also compare the same.
87 That requirement might go away in future to allow equality to reflect finer
88 details that are not hashed. (The only property that is currently known not
89 to be hashed is the local address in UDP6, because that is *unknown* in
90 initially sent packages, and thus disregarded for comparison but needed to
91 round-trip through responses.)
92 """
94 @property
95 @abc.abstractmethod
96 def hostinfo(self):
97 """The authority component of URIs that this endpoint represents when
98 request are sent to it
100 Note that the presence of a hostinfo does not necessarily mean that
101 globally meaningful or even syntactically valid URI can be constructed
102 out of it; use the :attr:`.uri` property for this."""
104 @property
105 @abc.abstractmethod
106 def hostinfo_local(self):
107 """The authority component of URIs that this endpoint represents when
108 requests are sent from it.
110 As with :attr:`.hostinfo`, this does not necessarily produce sufficient
111 input for a URI; use :attr:`.uri_local` instead."""
113 @property
114 def uri(self):
115 """Deprecated alias for uri_base"""
116 return self.uri_base
118 @property
119 @abc.abstractmethod
120 def uri_base(self):
121 """The base URI for the peer (typically scheme plus .hostinfo).
123 This raises :class:`.error.AnonymousHost` when executed on an address
124 whose peer coordinates can not be expressed meaningfully in a URI."""
126 @property
127 @abc.abstractmethod
128 def uri_base_local(self):
129 """The base URI for the local side of this remote.
131 This raises :class:`.error.AnonymousHost` when executed on an address
132 whose local coordinates can not be expressed meaningfully in a URI."""
134 @property
135 @abc.abstractmethod
136 def is_multicast(self):
137 """True if the remote address is a multicast address, otherwise false."""
139 @property
140 @abc.abstractmethod
141 def is_multicast_locally(self):
142 """True if the local address is a multicast address, otherwise false."""
144 @property
145 @abc.abstractmethod
146 def scheme(Self):
147 """The that is used with addresses of this kind
149 This is usually a class property. It is applicable to both sides of the
150 communication. (Should there ever be a scheme that addresses the
151 participants differently, a scheme_local will be added.)"""
153 @property
154 def maximum_block_size_exp(self) -> int:
155 """The maximum negotiated block size that can be sent to this remote."""
156 return MAX_REGULAR_BLOCK_SIZE_EXP
158 # Giving some slack so that barely-larger messages (like OSCORE typically
159 # are) don't get fragmented -- but still for migration to maximum message
160 # size so we don't have to guess any more how much may be option and how
161 # much payload
162 @property
163 def maximum_payload_size(self) -> int:
164 """The maximum payload size that can be sent to this remote. Only relevant
165 if maximum_block_size_exp is 7. This will be removed in favor of a maximum
166 message size when the block handlers can get serialization length
167 predictions from the remote."""
168 return 1124
170 def as_response_address(self):
171 """Address to be assigned to a response to messages that arrived with
172 this message
174 This can (and does, by default) return self, but gives the protocol the
175 opportunity to react to create a modified copy to deal with variations
176 from multicast.
177 """
178 return self
180 @property
181 def authenticated_claims(self):
182 """Iterable of objects representing any claims (e.g. an identity, or
183 generally objects that can be used to authorize particular accesses)
184 that were authenticated for this remote.
186 This is experimental and may be changed without notice.
188 Its primary use is on the server side; there, a request handler (or
189 resource decorator) can use the claims to decide whether the client is
190 authorized for a particular request. Use on the client side is planned
191 as a requirement on a request, although (especially on side-effect free
192 non-confidential requests) it can also be used in response
193 processing."""
194 # "no claims" is a good default
195 return ()
197 @property
198 @abc.abstractmethod
199 def blockwise_key(self):
200 """A hashable (ideally, immutable) value that is only the same for
201 remotes from which blocks may be combined. (With all current transports
202 that means that the network addresses need to be in there, and the
203 identity of the security context).
205 It does *not* just hinge on the identity of the address object, as a
206 first block may come in an OSCORE group request and follow-ups may come
207 in pairwise requests. (And there might be allowed relaxations on the
208 transport under OSCORE, but that'd need further discussion)."""
209 # FIXME: should this behave like something that keeps the address
210 # alive? Conversely, if the address gets deleted, can this reach the
211 # block keys and make their stuff vanish from the caches?
212 #
213 # FIXME: what do security mechanisms best put here? Currently it's a
214 # wild mix of keys (OSCORE -- only thing guaranteed to never be reused;
215 # DTLS client because it's available) and claims (DTLS server, because
216 # it's available and if the claims set matches it can't be that wrong
217 # either can it?)
220class MessageManager(metaclass=abc.ABCMeta):
221 """The interface an entity that drives a MessageInterface provides towards
222 the MessageInterface for callbacks and object acquisition."""
224 @abc.abstractmethod
225 def dispatch_message(self, message):
226 """Callback to be invoked with an incoming message"""
228 @abc.abstractmethod
229 def dispatch_error(self, error: Exception, remote):
230 """Callback to be invoked when the operating system indicated an error
231 condition from a particular remote."""
233 @property
234 @abc.abstractmethod
235 def client_credentials(self):
236 """A CredentialsMap that transports should consult when trying to
237 establish a security context"""
240class TokenInterface(metaclass=abc.ABCMeta):
241 @abc.abstractmethod
242 def send_message(
243 self, message, messageerror_monitor
244 ) -> Optional[Callable[[], None]]:
245 """Send a message. If it returns a a callable, the caller is asked to
246 call in case it no longer needs the message sent, and to dispose of if
247 it doesn't intend to any more.
249 messageerror_monitor is a function that will be called at most once by
250 the token interface: When the underlying layer is indicating that this
251 concrete message could not be processed. This is typically the case for
252 RSTs on from the message layer, and used to cancel observations. Errors
253 that are not likely to be specific to a message (like retransmission
254 timeouts, or ICMP errors) are reported through dispatch_error instead.
255 (While the information which concrete message triggered that might be
256 available, it is not likely to be relevant).
258 Currently, it is up to the TokenInterface to unset the no_response
259 option in response messages, and to possibly not send them."""
261 @abc.abstractmethod
262 async def fill_or_recognize_remote(self, message):
263 """Return True if the message is recognized to already have a .remote
264 managedy by this TokenInterface, or return True and set a .remote on
265 message if it should (by its unresolved remote or Uri-* options) be
266 routed through this TokenInterface, or return False otherwise."""
269class TokenManager(metaclass=abc.ABCMeta):
270 # to be described in full; at least there is a dispatch_error in analogy to MessageManager's
271 pass
274class RequestInterface(metaclass=abc.ABCMeta):
275 @abc.abstractmethod
276 async def fill_or_recognize_remote(self, message):
277 pass
279 @abc.abstractmethod
280 def request(self, request: Pipe):
281 pass
284class RequestProvider(metaclass=abc.ABCMeta):
285 """
286 .. automethod:: request
287 .. (which we have to list here manually because the private override in the
288 method is needed for the repeated signature in Context)
289 """
291 @abc.abstractmethod
292 def request(self, request_message, handle_blockwise=True):
293 """Create and act on a :class:`Request` object that will be handled
294 according to the provider's implementation.
296 Note that the request is not necessarily sent on the wire immediately;
297 it may (but, depend on the transport does not necessarily) rely on the
298 response to be waited for.
300 If handle_blockwise is True (the default), the request provider will
301 split the request and/or collect the response parts automatically. The
302 block size indicated by the remote is used, and can be decreased by
303 setting the message's :attr:`.remote.maximum_block_size_exp
304 <aiocoap.interfaces.EndpointAddress.maximum_block_size_exp>` property.
305 Note that by being a property of the remote, this may affect other
306 block-wise operations on the same remote -- this should be desirable
307 behavior.
309 :meta private:
310 (not actually private, just hiding from automodule due to being
311 grouped with the important functions)
312 """
315class Request(metaclass=abc.ABCMeta):
316 """A CoAP request, initiated by sending a message. Typically, this is not
317 instantiated directly, but generated by a :meth:`RequestProvider.request`
318 method."""
320 response = """A future that is present from the creation of the object and \
321 fulfilled with the response message.
323 When legitimate errors occur, this becomes an aiocoap.Error. (Eg. on
324 any kind of network failure, encryption trouble, or protocol
325 violations). Any other kind of exception raised from this is a bug in
326 aiocoap, and should better stop the whole application.
327 """
330class Resource(metaclass=abc.ABCMeta):
331 """Interface that is expected by a :class:`.protocol.Context` to be present
332 on the serversite, which renders all requests to that context."""
334 def __init__(self):
335 super().__init__()
337 # FIXME: These keep addresses alive, and thus possibly transports.
338 # Going through the shutdown dance per resource seems extraneous.
339 # Options are to accept addresses staying around (making sure they
340 # don't keep their transports alive, if that's a good idea), to hash
341 # them, or to make them weak.
343 from .blockwise import Block1Spool, Block2Cache
345 self._block1 = Block1Spool()
346 self._block2 = Block2Cache()
348 @abc.abstractmethod
349 async def render(self, request):
350 """Return a message that can be sent back to the requester.
352 This does not need to set any low-level message options like remote,
353 token or message type; it does however need to set a response code.
355 A response returned may carry a no_response option (which is actually
356 specified to apply to requests only); the underlying transports will
357 decide based on that and its code whether to actually transmit the
358 response."""
360 @abc.abstractmethod
361 async def needs_blockwise_assembly(self, request):
362 """Indicator whether aiocoap should assemble request blocks to a single
363 request and extract the requested blocks from a complete-resource
364 answer (True), or whether the resource will do that by itself
365 (False)."""
367 async def _render_to_pipe(self, pipe: Pipe) -> None:
368 if not hasattr(self, "_block1"):
369 warnings.warn(
370 "No attribute _block1 found on instance of "
371 f"{type(self).__name__}, make sure its __init__ code "
372 "properly calls super()!",
373 DeprecationWarning,
374 )
376 from .blockwise import Block1Spool, Block2Cache
378 self._block1 = Block1Spool()
379 self._block2 = Block2Cache()
381 req = pipe.request
383 if await self.needs_blockwise_assembly(req):
384 req = self._block1.feed_and_take(req)
386 # Note that unless the lambda gets called, we're not fully
387 # accessing req any more -- we're just looking at its block2
388 # option, and the blockwise key extracted earlier.
389 res = await self._block2.extract_or_insert(req, lambda: self.render(req))
391 res.opt.block1 = req.opt.block1
392 else:
393 res = await self.render(req)
395 pipe.add_response(res, is_last=True)
397 async def render_to_pipe(self, pipe: Pipe) -> None:
398 """Create any number of responses (as indicated by the request) into
399 the request stream.
401 This method is provided by the base Resource classes; if it is
402 overridden, then :meth:`~interfaces.Resource.render`, :meth:`needs_blockwise_assembly` and
403 :meth:`~.interfaces.ObservableResource.add_observation` are not used any more.
404 (They still need to be implemented to comply with the interface
405 definition, which is yet to be updated)."""
406 warnings.warn(
407 "Request interface is changing: Resources should "
408 "implement render_to_pipe or inherit from "
409 "resource.Resource which implements that based on any "
410 "provided render methods",
411 DeprecationWarning,
412 )
413 if isinstance(self, ObservableResource):
414 # While the above deprecation is used, a resource previously
415 # inheriting from (X, ObservableResource) with X inheriting from
416 # Resource might find itself using this method. When migrating over
417 # to inheriting from resource.Resource, this error will become
418 # apparent and this can die with the rest of this workaround.
419 return await ObservableResource._render_to_pipe(self, pipe)
420 await self._render_to_pipe(pipe)
423class ObservableResource(Resource, metaclass=abc.ABCMeta):
424 """Interface the :class:`.protocol.ServerObservation` uses to negotiate
425 whether an observation can be established based on a request.
427 This adds only functionality for registering and unregistering observations;
428 the notification contents will be retrieved from the resource using the
429 regular :meth:`~.Resource.render` method from crafted (fake) requests.
430 """
432 @abc.abstractmethod
433 async def add_observation(self, request, serverobservation):
434 """Before the incoming request is sent to :meth:`~.Resource.render`, the
435 :meth:`.add_observation` method is called. If the resource chooses to
436 accept the observation, it has to call the
437 `serverobservation.accept(cb)` with a callback that will be called when
438 the observation ends. After accepting, the ObservableResource should
439 call `serverobservation.trigger()` whenever it changes its state; the
440 ServerObservation will then initiate notifications by having the
441 request rendered again."""
443 async def _render_to_pipe(self, pipe: Pipe) -> None:
444 from .protocol import ServerObservation
446 # If block2:>0 comes along, we'd just ignore the observe
447 if pipe.request.opt.observe != 0:
448 return await Resource._render_to_pipe(self, pipe)
450 # If block1 happens here, we can probably just not support it for the
451 # time being. (Given that block1 + observe is untested and thus does
452 # not work so far anyway).
454 servobs = ServerObservation()
455 await self.add_observation(pipe.request, servobs)
457 try:
458 first_response = await self.render(pipe.request)
460 if (
461 not servobs._accepted
462 or servobs._early_deregister
463 or not first_response.code.is_successful()
464 ):
465 pipe.add_response(first_response, is_last=True)
466 return
468 # FIXME: observation numbers should actually not be per
469 # asyncio.task, but per (remote, token). if a client renews an
470 # observation (possibly with a new ETag or whatever is deemed
471 # legal), the new observation events should still carry larger
472 # numbers. (if they did not, the client might be tempted to discard
473 # them).
474 first_response.opt.observe = next_observation_number = 0
475 # If block2 were to happen here, we'd store the full response
476 # here, and pick out block2:0.
477 pipe.add_response(first_response, is_last=False)
479 while True:
480 await servobs._trigger
481 # if you wonder why the lines around this are not just `response =
482 # await servobs._trigger`, have a look at the 'double' tests in
483 # test_observe.py: A later triggering could have replaced
484 # servobs._trigger in the meantime.
485 response = servobs._trigger.result()
486 servobs._trigger = asyncio.get_running_loop().create_future()
488 if response is None:
489 response = await self.render(pipe.request)
491 # If block2 were to happen here, we'd store the full response
492 # here, and pick out block2:0.
494 is_last = servobs._late_deregister or not response.code.is_successful()
495 if not is_last:
496 next_observation_number += 1
497 response.opt.observe = next_observation_number
499 pipe.add_response(response, is_last=is_last)
501 if is_last:
502 return
503 finally:
504 servobs._cancellation_callback()
506 async def render_to_pipe(self, request: Pipe) -> None:
507 warnings.warn(
508 "Request interface is changing: Resources should "
509 "implement render_to_pipe or inherit from "
510 "resource.Resource which implements that based on any "
511 "provided render methods",
512 DeprecationWarning,
513 )
514 await self._render_to_pipe(request)