Coverage for aiocoap/interfaces.py: 91%

141 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-15 22:10 +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 

24 

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. 

30 

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 """ 

37 

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.""" 

43 

44 @abc.abstractmethod 

45 def send(self, message): 

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

47 

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. 

52 

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

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

55 

56 

57class EndpointAddress(metaclass=abc.ABCMeta): 

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

59 remote endpoint. 

60 

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>". 

68 

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`. 

72 

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). 

80 

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 """ 

88 

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 

94 

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.""" 

98 

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. 

104 

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

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

107 

108 @property 

109 def uri(self): 

110 """Deprecated alias for uri_base""" 

111 return self.uri_base 

112 

113 @property 

114 @abc.abstractmethod 

115 def uri_base(self): 

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

117 

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

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

120 

121 @property 

122 @abc.abstractmethod 

123 def uri_base_local(self): 

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

125 

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

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

128 

129 @property 

130 @abc.abstractmethod 

131 def is_multicast(self): 

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

133 

134 @property 

135 @abc.abstractmethod 

136 def is_multicast_locally(self): 

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

138 

139 @property 

140 @abc.abstractmethod 

141 def scheme(Self): 

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

143 

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.)""" 

147 

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 

152 

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 

164 

165 def as_response_address(self): 

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

167 this message 

168 

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 

174 

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. 

180 

181 This is experimental and may be changed without notice. 

182 

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 () 

191 

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). 

199 

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?) 

213 

214 

215class MessageManager(metaclass=abc.ABCMeta): 

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

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

218 

219 @abc.abstractmethod 

220 def dispatch_message(self, message): 

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

222 

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.""" 

227 

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""" 

233 

234 

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. 

243 

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). 

252 

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

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

255 

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.""" 

262 

263 

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 

267 

268 

269class RequestInterface(metaclass=abc.ABCMeta): 

270 @abc.abstractmethod 

271 async def fill_or_recognize_remote(self, message): 

272 pass 

273 

274 @abc.abstractmethod 

275 def request(self, request: Pipe): 

276 pass 

277 

278 

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 """ 

285 

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. 

290 

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. 

294 

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. 

303 

304 :meta private: 

305 (not actually private, just hiding from automodule due to being 

306 grouped with the important functions) 

307 """ 

308 

309 

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.""" 

314 

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

316 fullfilled with the response message. 

317 

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 """ 

323 

324 

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.""" 

328 

329 def __init__(self): 

330 super().__init__() 

331 

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. 

337 

338 from .blockwise import Block1Spool, Block2Cache 

339 

340 self._block1 = Block1Spool() 

341 self._block2 = Block2Cache() 

342 

343 @abc.abstractmethod 

344 async def render(self, request): 

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

346 

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. 

349 

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.""" 

354 

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).""" 

361 

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 ) 

370 

371 from .blockwise import Block1Spool, Block2Cache 

372 

373 self._block1 = Block1Spool() 

374 self._block2 = Block2Cache() 

375 

376 req = pipe.request 

377 

378 if await self.needs_blockwise_assembly(req): 

379 req = self._block1.feed_and_take(req) 

380 

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)) 

385 

386 res.opt.block1 = req.opt.block1 

387 else: 

388 res = await self.render(req) 

389 

390 pipe.add_response(res, is_last=True) 

391 

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. 

395 

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) 

416 

417 

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. 

421 

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 """ 

426 

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.""" 

437 

438 async def _render_to_pipe(self, pipe: Pipe) -> None: 

439 from .protocol import ServerObservation 

440 

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) 

444 

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). 

448 

449 servobs = ServerObservation() 

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

451 

452 try: 

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

454 

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 

462 

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) 

473 

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() 

482 

483 if response is None: 

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

485 

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

487 # here, and pick out block2:0. 

488 

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 

493 

494 pipe.add_response(response, is_last=is_last) 

495 

496 if is_last: 

497 return 

498 finally: 

499 servobs._cancellation_callback() 

500 

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)