Coverage for aiocoap/transports/rfc8323common.py: 82%
105 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"""Common code for the tcp and the ws modules, both of which are based on
6RFC8323 mechanisms, but differ in their underlying protocol implementations
7(asyncio stream vs. websockets module) far enough that they only share small
8portions of their code"""
10import asyncio
11from typing import Optional
12from aiocoap import Message
13from aiocoap import optiontypes, util
14from aiocoap.numbers.codes import CSM, PING, PONG, RELEASE, ABORT
15from aiocoap import error
18class CloseConnection(Exception):
19 """Raised in RFC8323 common processing to trigger a connection shutdown on
20 the TCP / WebSocket side.
22 The TCP / WebSocket side should send the exception's argument on to the
23 token manager, close the connection, and does not need to perform further
24 logging."""
27class RFC8323Remote:
28 """Mixin for Remotes for all the common RFC8323 processing
30 Implementations still need the per-transport parts, especially a
31 _send_message and an _abort_with implementation.
32 """
34 # CSM received from the peer. The receive hook should abort suitably when
35 # receiving a non-CSM message and this is not set yet.
36 _remote_settings: Optional[Message]
38 # Parameter usually set statically per implementation
39 _my_max_message_size = 1024 * 1024
41 def __init__(self):
42 self._remote_settings = None
44 is_multicast = False
45 is_multicast_locally = False
47 # implementing interfaces.EndpointAddress
49 def __repr__(self):
50 return "<%s at %#x, hostinfo %s, local %s>" % (
51 type(self).__name__,
52 id(self),
53 self.hostinfo,
54 self.hostinfo_local,
55 )
57 @property
58 def hostinfo(self):
59 # keeping _remote_hostinfo and _local_hostinfo around structurally rather than in
60 # hostinfo / hostinfo_local form looks odd now, but on the long run the
61 # remote should be able to tell the message what its default Uri-Host
62 # value is
63 return util.hostportjoin(*self._remote_hostinfo)
65 @property
66 def hostinfo_local(self):
67 return util.hostportjoin(*self._local_hostinfo)
69 @property
70 def uri_base(self):
71 if self._local_is_server:
72 raise error.AnonymousHost(
73 "Client side of %s can not be expressed as a URI" % self._ctx._scheme
74 )
75 else:
76 return self._ctx._scheme + "://" + self.hostinfo
78 @property
79 def uri_base_local(self):
80 if self._local_is_server:
81 return self._ctx._scheme + "://" + self.hostinfo_local
82 else:
83 raise error.AnonymousHost(
84 "Client side of %s can not be expressed as a URI" % self._ctx._scheme
85 )
87 @property
88 def maximum_block_size_exp(self):
89 if self._remote_settings is None:
90 # This is assuming that we can do BERT, so a first Block1 would be
91 # exponent 7 but still only 1k -- because by the time we send this,
92 # we typically haven't seen a CSM yet, so we'd be stuck with 6
93 # because 7959 says we can't increase the exponent...
94 #
95 # FIXME: test whether we're properly using lower block sizes if
96 # server says that szx=7 is not OK.
97 return 7
99 max_message_size = (self._remote_settings or {}).get("max-message-size", 1152)
100 has_blockwise = (self._remote_settings or {}).get("block-wise-transfer", False)
101 if max_message_size > 1152 and has_blockwise:
102 return 7
103 return 6 # FIXME: deal with smaller max-message-size
105 @property
106 def maximum_payload_size(self):
107 # see maximum_payload_size of interfaces comment
108 slack = 100
110 max_message_size = (self._remote_settings or {}).get("max-message-size", 1152)
111 has_blockwise = (self._remote_settings or {}).get("block-wise-transfer", False)
112 if max_message_size > 1152 and has_blockwise:
113 return ((max_message_size - 128) // 1024) * 1024 + slack
114 return 1024 + slack # FIXME: deal with smaller max-message-size
116 @property
117 def blockwise_key(self):
118 return (self._remote_hostinfo, self._local_hostinfo)
120 # Utility methods for implementing an RFC8323 transport
122 def _send_initial_csm(self):
123 my_csm = Message(code=CSM)
124 # this is a tad awkward in construction because the options objects
125 # were designed under the assumption that the option space is constant
126 # for all message codes.
127 block_length = optiontypes.UintOption(2, self._my_max_message_size)
128 my_csm.opt.add_option(block_length)
129 supports_block = optiontypes.UintOption(4, 0)
130 my_csm.opt.add_option(supports_block)
131 self._send_message(my_csm)
133 def _process_signaling(self, msg):
134 if msg.code == CSM:
135 if self._remote_settings is None:
136 self._remote_settings = {}
137 for opt in msg.opt.option_list():
138 # FIXME: this relies on the relevant option numbers to be
139 # opaque; message parsing should already use the appropriate
140 # option types, or re-think the way options are parsed
141 if opt.number == 2:
142 self._remote_settings["max-message-size"] = int.from_bytes(
143 opt.value, "big"
144 )
145 elif opt.number == 4:
146 self._remote_settings["block-wise-transfer"] = True
147 elif opt.number.is_critical():
148 self.abort("Option not supported", bad_csm_option=opt.number)
149 else:
150 pass # ignoring elective CSM options
151 elif msg.code in (PING, PONG, RELEASE, ABORT):
152 # not expecting data in any of them as long as Custody is not implemented
153 for opt in msg.opt.option_list():
154 if opt.number.is_critical():
155 self.abort("Unknown critical option")
156 else:
157 pass
159 if msg.code == PING:
160 pong = Message(code=PONG, token=msg.token)
161 self._send_message(pong)
162 elif msg.code == PONG:
163 pass
164 elif msg.code == RELEASE:
165 # The behavior SHOULD be enhanced to answer outstanding
166 # requests, but it is unclear to which extent this side may
167 # still use the connection.
168 self.log.info(
169 "Received Release, closing on this end (options: %s)", msg.opt
170 )
171 raise CloseConnection(
172 error.RemoteServerShutdown("Peer released connection")
173 )
174 elif msg.code == ABORT:
175 self.log.warning("Received Abort (options: %s)", msg.opt)
176 raise CloseConnection(
177 error.RemoteServerShutdown("Peer aborted connection")
178 )
179 else:
180 self.abort("Unknown signalling code")
182 def abort(self, errormessage=None, bad_csm_option=None):
183 self.log.warning("Aborting connection: %s", errormessage)
184 abort_msg = Message(code=ABORT)
185 if errormessage is not None:
186 abort_msg.payload = errormessage.encode("utf8")
187 if bad_csm_option is not None:
188 bad_csm_option_option = optiontypes.UintOption(2, bad_csm_option)
189 abort_msg.opt.add_option(bad_csm_option_option)
190 self._abort_with(abort_msg)
192 async def release(self):
193 """Send Release message, (not implemented:) wait for connection to be
194 actually closed by the peer.
196 Subclasses should extend this to await closing of the connection,
197 especially if they'd get into lock-up states otherwise (was would
198 WebSockets).
199 """
200 self.log.info("Releasing connection %s", self)
201 release_msg = Message(code=RELEASE)
202 self._send_message(release_msg)
204 try:
205 # FIXME: we could wait for the peer to close the connection, but a)
206 # that'll need some work on the interface between this module and
207 # ws/tcp, and b) we have no peers to test this with that would
208 # produce any sensible data (as aiocoap on release just closes).
209 pass
210 except asyncio.CancelledError:
211 self.log.warning(
212 "Connection %s was not closed by peer in time after release", self
213 )