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