Coverage for aiocoap/interfaces.py: 91%

140 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-16 16:09 +0000

1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors 

2# 

3# SPDX-License-Identifier: MIT 

4 

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. 

8 

9It is *completely unrelated* to the concept of "network interfaces". 

10 

11.. _`abstract base classes`: https://docs.python.org/3/library/abc""" 

12 

13from __future__ import annotations 

14 

15import abc 

16import asyncio 

17import warnings 

18 

19from aiocoap.pipe import Pipe 

20from aiocoap.numbers.constants import MAX_REGULAR_BLOCK_SIZE_EXP 

21 

22from typing import Optional, Callable 

23 

24class MessageInterface(metaclass=abc.ABCMeta): 

25 """A MessageInterface is an object that can exchange addressed messages over 

26 unreliable transports. Implementations send and receive messages with 

27 message type and message ID, and are driven by a Context that deals with 

28 retransmission. 

29 

30 Usually, an MessageInterface refers to something like a local socket, and 

31 send messages to different remote endpoints depending on the message's 

32 addresses. Just as well, a MessageInterface can be useful for one single 

33 address only, or use various local addresses depending on the remote 

34 address. 

35 """ 

36 

37 @abc.abstractmethod 

38 async def shutdown(self): 

39 """Deactivate the complete transport, usually irrevertably. When the 

40 coroutine returns, the object must have made sure that it can be 

41 destructed by means of ref-counting or a garbage collector run.""" 

42 

43 @abc.abstractmethod 

44 def send(self, message): 

45 """Send a given :class:`Message` object""" 

46 

47 @abc.abstractmethod 

48 async def determine_remote(self, message): 

49 """Return a value suitable for the message's remote property based on 

50 its .opt.uri_host or .unresolved_remote. 

51 

52 May return None, which indicates that the MessageInterface can not 

53 transport the message (typically because it is of the wrong scheme).""" 

54 

55class EndpointAddress(metaclass=abc.ABCMeta): 

56 """An address that is suitable for routing through the application to a 

57 remote endpoint. 

58 

59 Depending on the MessageInterface implementation used, an EndpointAddress 

60 property of a message can mean the message is exchanged "with 

61 [2001:db8::2:1]:5683, while my local address was [2001:db8:1::1]:5683" 

62 (typical of UDP6), "over the connected <Socket at 

63 0x1234>, whereever that's connected to" (simple6 or TCP) or "with 

64 participant 0x01 of the OSCAP key 0x..., routed over <another 

65 EndpointAddress>". 

66 

67 EndpointAddresses are only concstructed by MessageInterface objects, 

68 either for incoming messages or when populating a message's .remote in 

69 :meth:`MessageInterface.determine_remote`. 

70 

71 There is no requirement that those address are always identical for a given 

72 address. However, incoming addresses must be hashable and hash-compare 

73 identically to requests from the same context. The "same context", for the 

74 purpose of EndpointAddresses, means that the message must be eligible for 

75 request/response, blockwise (de)composition and observations. (For example, 

76 in a DTLS context, the hash must change between epochs due to RFC7252 

77 Section 9.1.2). 

78 

79 So far, it is required that hash-identical objects also compare the same. 

80 That requirement might go away in future to allow equality to reflect finer 

81 details that are not hashed. (The only property that is currently known not 

82 to be hashed is the local address in UDP6, because that is *unknown* in 

83 initially sent packages, and thus disregarded for comparison but needed to 

84 round-trip through responses.) 

85 """ 

86 

87 @property 

88 @abc.abstractmethod 

89 def hostinfo(self): 

90 """The authority component of URIs that this endpoint represents when 

91 request are sent to it 

92 

93 Note that the presence of a hostinfo does not necessarily mean that 

94 globally meaningful or even syntactically valid URI can be constructed 

95 out of it; use the :attr:`.uri` property for this.""" 

96 

97 @property 

98 @abc.abstractmethod 

99 def hostinfo_local(self): 

100 """The authority component of URIs that this endpoint represents when 

101 requests are sent from it. 

102 

103 As with :attr:`.hostinfo`, this does not necessarily produce sufficient 

104 input for a URI; use :attr:`.uri_local` instead.""" 

105 

106 @property 

107 def uri(self): 

108 """Deprecated alias for uri_base""" 

109 return self.uri_base 

110 

111 @property 

112 @abc.abstractmethod 

113 def uri_base(self): 

114 """The base URI for the peer (typically scheme plus .hostinfo). 

115 

116 This raises :class:`.error.AnonymousHost` when executed on an address 

117 whose peer coordinates can not be expressed meaningfully in a URI.""" 

118 

119 @property 

120 @abc.abstractmethod 

121 def uri_base_local(self): 

122 """The base URI for the local side of this remote. 

123 

124 This raises :class:`.error.AnonymousHost` when executed on an address 

125 whose local coordinates can not be expressed meaningfully in a URI.""" 

126 

127 @property 

128 @abc.abstractmethod 

129 def is_multicast(self): 

130 """True if the remote address is a multicast address, otherwise false.""" 

131 

132 @property 

133 @abc.abstractmethod 

134 def is_multicast_locally(self): 

135 """True if the local address is a multicast address, otherwise false.""" 

136 

137 @property 

138 @abc.abstractmethod 

139 def scheme(Self): 

140 """The that is used with addresses of this kind 

141 

142 This is usually a class property. It is applicable to both sides of the 

143 communication. (Should there ever be a scheme that addresses the 

144 participants differently, a scheme_local will be added.)""" 

145 

146 maximum_block_size_exp = MAX_REGULAR_BLOCK_SIZE_EXP 

147 """The maximum negotiated block size that can be sent to this remote.""" 

148 

149 # Giving some slack so that barely-larger messages (like OSCORE typically 

150 # are) don't get fragmented -- but still for migration to maximum message 

151 # size so we don't have to guess any more how much may be option and how 

152 # much payload 

153 maximum_payload_size = 1124 

154 """The maximum payload size that can be sent to this remote. Only relevant 

155 if maximum_block_size_exp is 7. This will be removed in favor of a maximum 

156 message size when the block handlers can get serialization length 

157 predictions from the remote.""" 

158 

159 def as_response_address(self): 

160 """Address to be assigned to a response to messages that arrived with 

161 this message 

162 

163 This can (and does, by default) return self, but gives the protocol the 

164 opportunity to react to create a modified copy to deal with variations 

165 from multicast. 

166 """ 

167 return self 

168 

169 @property 

170 def authenticated_claims(self): 

171 """Iterable of objects representing any claims (e.g. an identity, or 

172 generally objects that can be used to authorize particular accesses) 

173 that were authenticated for this remote. 

174 

175 This is experimental and may be changed without notice. 

176 

177 Its primary use is on the server side; there, a request handler (or 

178 resource decorator) can use the claims to decide whether the client is 

179 authorized for a particular request. Use on the client side is planned 

180 as a requirement on a request, although (especially on side-effect free 

181 non-confidential requests) it can also be used in response 

182 processing.""" 

183 # "no claims" is a good default 

184 return () 

185 

186 @property 

187 @abc.abstractmethod 

188 def blockwise_key(self): 

189 """A hashable (ideally, immutable) value that is only the same for 

190 remotes from which blocks may be combined. (With all current transports 

191 that means that the network addresses need to be in there, and the 

192 identity of the security context). 

193 

194 It does *not* just hinge on the identity of the address object, as a 

195 first block may come in an OSCORE group request and follow-ups may come 

196 in pairwise requests. (And there might be allowed relaxations on the 

197 transport under OSCORE, but that'd need further discussion).""" 

198 # FIXME: should this behave like something that keeps the address 

199 # alive? Conversely, if the address gets deleted, can this reach the 

200 # block keys and make their stuff vanish from the caches? 

201 # 

202 # FIXME: what do security mechanisms best put here? Currently it's a 

203 # wild mix of keys (OSCORE -- only thing guaranteed to never be reused; 

204 # DTLS client because it's available) and claims (DTLS server, because 

205 # it's available and if the claims set matches it can't be that wrong 

206 # either can it?) 

207 

208class MessageManager(metaclass=abc.ABCMeta): 

209 """The interface an entity that drives a MessageInterface provides towards 

210 the MessageInterface for callbacks and object acquisition.""" 

211 

212 @abc.abstractmethod 

213 def dispatch_message(self, message): 

214 """Callback to be invoked with an incoming message""" 

215 

216 @abc.abstractmethod 

217 def dispatch_error(self, error: Exception, remote): 

218 """Callback to be invoked when the operating system indicated an error 

219 condition from a particular remote.""" 

220 

221 @property 

222 @abc.abstractmethod 

223 def client_credentials(self): 

224 """A CredentialsMap that transports should consult when trying to 

225 establish a security context""" 

226 

227class TokenInterface(metaclass=abc.ABCMeta): 

228 @abc.abstractmethod 

229 def send_message(self, message, messageerror_monitor) -> Optional[Callable[[], None]]: 

230 """Send a message. If it returns a a callable, the caller is asked to 

231 call in case it no longer needs the message sent, and to dispose of if 

232 it doesn't intend to any more. 

233 

234 messageerror_monitor is a function that will be called at most once by 

235 the token interface: When the underlying layer is indicating that this 

236 concrete message could not be processed. This is typically the case for 

237 RSTs on from the message layer, and used to cancel observations. Errors 

238 that are not likely to be specific to a message (like retransmission 

239 timeouts, or ICMP errors) are reported through dispatch_error instead. 

240 (While the information which concrete message triggered that might be 

241 available, it is not likely to be relevant). 

242 

243 Currently, it is up to the TokenInterface to unset the no_response 

244 option in response messages, and to possibly not send them.""" 

245 

246 @abc.abstractmethod 

247 async def fill_or_recognize_remote(self, message): 

248 """Return True if the message is recognized to already have a .remote 

249 managedy by this TokenInterface, or return True and set a .remote on 

250 message if it should (by its unresolved remote or Uri-* options) be 

251 routed through this TokenInterface, or return False otherwise.""" 

252 

253class TokenManager(metaclass=abc.ABCMeta): 

254 # to be described in full; at least there is a dispatch_error in analogy to MessageManager's 

255 pass 

256 

257class RequestInterface(metaclass=abc.ABCMeta): 

258 @abc.abstractmethod 

259 async def fill_or_recognize_remote(self, message): 

260 pass 

261 

262 @abc.abstractmethod 

263 def request(self, request: Pipe): 

264 pass 

265 

266class RequestProvider(metaclass=abc.ABCMeta): 

267 @abc.abstractmethod 

268 def request(self, request_message): 

269 """Create and act on a a :class:`Request` object that will be handled 

270 according to the provider's implementation. 

271 

272 Note that the request is not necessarily sent on the wire immediately; 

273 it may (but, depend on the transport does not necessarily) rely on the 

274 response to be waited for.""" 

275 

276class Request(metaclass=abc.ABCMeta): 

277 """A CoAP request, initiated by sending a message. Typically, this is not 

278 instanciated directly, but generated by a :meth:`RequestProvider.request` 

279 method.""" 

280 

281 response = """A future that is present from the creation of the object and \ 

282 fullfilled with the response message. 

283 

284 When legitimate errors occur, this becomes an aiocoap.Error. (Eg. on 

285 any kind of network failure, encryption trouble, or protocol 

286 violations). Any other kind of exception raised from this is a bug in 

287 aiocoap, and should better stop the whole application. 

288 """ 

289 

290class Resource(metaclass=abc.ABCMeta): 

291 """Interface that is expected by a :class:`.protocol.Context` to be present 

292 on the serversite, which renders all requests to that context.""" 

293 

294 def __init__(self): 

295 super().__init__() 

296 

297 # FIXME: These keep addresses alive, and thus possibly transports. 

298 # Going through the shutdown dance per resource seems extraneous. 

299 # Options are to accept addresses staying around (making sure they 

300 # don't keep their transports alive, if that's a good idea), to hash 

301 # them, or to make them weak. 

302 

303 from .blockwise import Block1Spool, Block2Cache 

304 self._block1 = Block1Spool() 

305 self._block2 = Block2Cache() 

306 

307 @abc.abstractmethod 

308 async def render(self, request): 

309 """Return a message that can be sent back to the requester. 

310 

311 This does not need to set any low-level message options like remote, 

312 token or message type; it does however need to set a response code. 

313 

314 A response returned may carry a no_response option (which is actually 

315 specified to apply to requests only); the underlying transports will 

316 decide based on that and its code whether to actually transmit the 

317 response.""" 

318 

319 @abc.abstractmethod 

320 async def needs_blockwise_assembly(self, request): 

321 """Indicator to the :class:`.protocol.Responder` about whether it 

322 should assemble request blocks to a single request and extract the 

323 requested blocks from a complete-resource answer (True), or whether 

324 the resource will do that by itself (False).""" 

325 

326 async def _render_to_pipe(self, request: Pipe): 

327 if not hasattr(self, "_block1"): 

328 warnings.warn("No attribute _block1 found on instance of " 

329 f"{type(self).__name__}, make sure its __init__ code " 

330 "properly calls super()!", DeprecationWarning) 

331 

332 from .blockwise import Block1Spool, Block2Cache 

333 self._block1 = Block1Spool() 

334 self._block2 = Block2Cache() 

335 

336 req = request.request 

337 

338 if await self.needs_blockwise_assembly(req): 

339 req = self._block1.feed_and_take(req) 

340 

341 # Note that unless the lambda get's called, we're not fully 

342 # accessing req any more -- we're just looking at its block2 

343 # option, and the blockwise key extracted earlier. 

344 res = await self._block2.extract_or_insert(req, lambda: self.render(req)) 

345 

346 res.opt.block1 = req.opt.block1 

347 else: 

348 res = await self.render(req) 

349 

350 request.add_response(res, is_last=True) 

351 

352 async def render_to_pipe(self, request: Pipe): 

353 """Create any number of responses (as indicated by the request) into 

354 the request stream. 

355 

356 This method is provided by the base Resource classes; if it is 

357 overridden, then :meth:`~.Resource.render`, :meth:`needs_blockwise_assembly` and 

358 :meth:`~.ObservableResource.add_observation` are not used any more. 

359 (They still need to be implemented to comply with the interface 

360 definition, which is yet to be updated).""" 

361 warnings.warn("Request interface is changing: Resources should " 

362 "implement render_to_pipe or inherit from " 

363 "resource.Resource which implements that based on any " 

364 "provided render methods", DeprecationWarning) 

365 if isinstance(self, ObservableResource): 

366 # While the above deprecation is used, a resource previously 

367 # inheriting from (X, ObservableResource) with X inheriting from 

368 # Resource might find itself using this method. When migrating over 

369 # to inheriting from resource.Resource, this error will become 

370 # apparent and this can die with the rest of this workaround. 

371 return await ObservableResource._render_to_pipe(self, request) 

372 return await self._render_to_pipe(request) 

373 

374class ObservableResource(Resource, metaclass=abc.ABCMeta): 

375 """Interface the :class:`.protocol.ServerObservation` uses to negotiate 

376 whether an observation can be established based on a request. 

377 

378 This adds only functionality for registering and unregistering observations; 

379 the notification contents will be retrieved from the resource using the 

380 regular :meth:`~.Resource.render` method from crafted (fake) requests. 

381 """ 

382 @abc.abstractmethod 

383 async def add_observation(self, request, serverobservation): 

384 """Before the incoming request is sent to :meth:`~.Resource.render`, the 

385 :meth:`.add_observation` method is called. If the resource chooses to 

386 accept the observation, it has to call the 

387 `serverobservation.accept(cb)` with a callback that will be called when 

388 the observation ends. After accepting, the ObservableResource should 

389 call `serverobservation.trigger()` whenever it changes its state; the 

390 ServerObservation will then initiate notifications by having the 

391 request rendered again.""" 

392 

393 

394 async def _render_to_pipe(self, pipe): 

395 from .protocol import ServerObservation 

396 

397 # If block2:>0 comes along, we'd just ignore the observe 

398 if pipe.request.opt.observe != 0: 

399 return await Resource._render_to_pipe(self, pipe) 

400 

401 # If block1 happens here, we can probably just not support it for the 

402 # time being. (Given that block1 + observe is untested and thus does 

403 # not work so far anyway). 

404 

405 servobs = ServerObservation() 

406 await self.add_observation(pipe.request, servobs) 

407 

408 try: 

409 first_response = await self.render(pipe.request) 

410 

411 if not servobs._accepted or servobs._early_deregister or \ 

412 not first_response.code.is_successful(): 

413 pipe.add_response(first_response, is_last=True) 

414 return 

415 

416 # FIXME: observation numbers should actually not be per 

417 # asyncio.task, but per (remote, token). if a client renews an 

418 # observation (possibly with a new ETag or whatever is deemed 

419 # legal), the new observation events should still carry larger 

420 # numbers. (if they did not, the client might be tempted to discard 

421 # them). 

422 first_response.opt.observe = next_observation_number = 0 

423 # If block2 were to happen here, we'd store the full response 

424 # here, and pick out block2:0. 

425 pipe.add_response(first_response, is_last=False) 

426 

427 while True: 

428 await servobs._trigger 

429 # if you wonder why the lines around this are not just `response = 

430 # await servobs._trigger`, have a look at the 'double' tests in 

431 # test_observe.py: A later triggering could have replaced 

432 # servobs._trigger in the meantime. 

433 response = servobs._trigger.result() 

434 servobs._trigger = asyncio.get_running_loop().create_future() 

435 

436 if response is None: 

437 response = await self.render(pipe.request) 

438 

439 # If block2 were to happen here, we'd store the full response 

440 # here, and pick out block2:0. 

441 

442 is_last = servobs._late_deregister or not response.code.is_successful() 

443 if not is_last: 

444 next_observation_number += 1 

445 response.opt.observe = next_observation_number 

446 

447 pipe.add_response(response, is_last=is_last) 

448 

449 if is_last: 

450 return 

451 finally: 

452 servobs._cancellation_callback() 

453 

454 async def render_to_pipe(self, request: Pipe): 

455 warnings.warn("Request interface is changing: Resources should " 

456 "implement render_to_pipe or inherit from " 

457 "resource.Resource which implements that based on any " 

458 "provided render methods", DeprecationWarning) 

459 return await self._render_to_pipe(request)