Coverage for aiocoap/message.py: 84%

285 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 

5from __future__ import annotations 

6 

7import ipaddress 

8import urllib.parse 

9import struct 

10import copy 

11import string 

12from collections import namedtuple 

13 

14from . import error, optiontypes 

15from .numbers.codes import Code, CHANGED 

16from .numbers.types import Type 

17from .numbers.constants import TransportTuning, MAX_REGULAR_BLOCK_SIZE_EXP 

18from .options import Options 

19from .util import hostportjoin, hostportsplit, Sentinel, quote_nonascii 

20from .util.uri import quote_factory, unreserved, sub_delims 

21from . import interfaces 

22 

23__all__ = ["Message", "NoResponse"] 

24 

25# FIXME there should be a proper inteface for this that does all the urllib 

26# patching possibly required and works with pluggable transports. urls qualify 

27# if they can be parsed into the Proxy-Scheme / Uri-* structure. 

28coap_schemes = ["coap", "coaps", "coap+tcp", "coaps+tcp", "coap+ws", "coaps+ws"] 

29 

30# Monkey patch urllib to make URL joining available in CoAP 

31# This is a workaround for <http://bugs.python.org/issue23759>. 

32urllib.parse.uses_relative.extend(coap_schemes) 

33urllib.parse.uses_netloc.extend(coap_schemes) 

34 

35 

36class Message(object): 

37 """CoAP Message with some handling metadata 

38 

39 This object's attributes provide access to the fields in a CoAP message and 

40 can be directly manipulated. 

41 

42 * Some attributes are additional data that do not round-trip through 

43 serialization and deserialization. They are marked as "non-roundtrippable". 

44 * Some attributes that need to be filled for submission of the message can 

45 be left empty by most applications, and will be taken care of by the 

46 library. Those are marked as "managed". 

47 

48 The attributes are: 

49 

50 * :attr:`payload`: The payload (body) of the message as bytes. 

51 * :attr:`mtype`: Message type (CON, ACK etc, see :mod:`.numbers.types`). 

52 Managed unless set by the application. 

53 * :attr:`code`: The code (either request or response code), see 

54 :mod:`.numbers.codes`. 

55 * :attr:`opt`: A container for the options, see :class:`.options.Options`. 

56 

57 * :attr:`mid`: The message ID. Managed by the :class:`.Context`. 

58 * :attr:`token`: The message's token as bytes. Managed by the :class:`.Context`. 

59 * :attr:`remote`: The socket address of the other side, managed by the 

60 :class:`.protocol.Request` by resolving the ``.opt.uri_host`` or 

61 ``unresolved_remote``, or by the stack by echoing the incoming 

62 request's. Follows the :class:`.interfaces.EndpointAddress` interface. 

63 Non-roundtrippable. 

64 

65 While a message has not been transmitted, the property is managed by the 

66 :class:`.Message` itself using the :meth:`.set_request_uri()` or the 

67 constructor `uri` argument. 

68 * :attr:`transport_tuning`: Parameters used by one or more transports to 

69 guide transmission. These are purely advisory hints; unknown properties 

70 of that object are ignored, and transports consider them over built-in 

71 constants on a best-effort basis. 

72 

73 Note that many attributes are mandatory if this is not None; it is 

74 recommended that any objects passed in here are based on the 

75 :class:`aiocoap.numbers.constants.TransportTuning` class. 

76 

77 * :attr:`request`: The request to which an incoming response message 

78 belongs; only available at the client. Managed by the 

79 :class:`.interfaces.RequestProvider` (typically a :class:`.Context`). 

80 

81 These properties are still available but deprecated: 

82 

83 * requested_*: Managed by the :class:`.protocol.Request` a response results 

84 from, and filled with the request's URL data. Non-roundtrippable. 

85 

86 * unresolved_remote: ``host[:port]`` (strictly speaking; hostinfo as in a 

87 URI) formatted string. If this attribute is set, it overrides 

88 ``.RequestManageropt.uri_host`` (and ``-_port``) when it comes to filling the 

89 ``remote`` in an outgoing request. 

90 

91 Use this when you want to send a request with a host name that would not 

92 normally resolve to the destination address. (Typically, this is used for 

93 proxying.) 

94 

95 Options can be given as further keyword arguments at message construction 

96 time. This feature is experimental, as future message parameters could 

97 collide with options. 

98 

99 

100 The four messages involved in an exchange 

101 ----------------------------------------- 

102 

103 :: 

104 

105 Requester Responder 

106 

107 +-------------+ +-------------+ 

108 | request msg | ---- send request ---> | request msg | 

109 +-------------+ +-------------+ 

110 | 

111 processed into 

112 | 

113 v 

114 +-------------+ +-------------+ 

115 | response m. | <--- send response --- | response m. | 

116 +-------------+ +-------------+ 

117 

118 

119 The above shows the four message instances involved in communication 

120 between an aiocoap client and server process. Boxes represent instances of 

121 Message, and the messages on the same line represent a single CoAP as 

122 passed around on the network. Still, they differ in some aspects: 

123 

124 * The requested URI will look different between requester and responder 

125 if the requester uses a host name and does not send it in the message. 

126 * If the request was sent via multicast, the response's requested URI 

127 differs from the request URI because it has the responder's address 

128 filled in. That address is not known at the responder's side yet, as 

129 it is typically filled out by the network stack. 

130 * It is yet unclear whether the response's URI should contain an IP 

131 literal or a host name in the unicast case if the Uri-Host option was 

132 not sent. 

133 * Properties like Message ID and token will differ if a proxy was 

134 involved. 

135 * Some options or even the payload may differ if a proxy was involved. 

136 """ 

137 

138 def __init__( 

139 self, 

140 *, 

141 mtype=None, 

142 mid=None, 

143 code=None, 

144 payload=b"", 

145 token=b"", 

146 uri=None, 

147 transport_tuning=None, 

148 **kwargs, 

149 ): 

150 self.version = 1 

151 if mtype is None: 

152 # leave it unspecified for convenience, sending functions will know what to do 

153 self.mtype = None 

154 else: 

155 self.mtype = Type(mtype) 

156 self.mid = mid 

157 if code is None: 

158 # as above with mtype 

159 self.code = None 

160 else: 

161 self.code = Code(code) 

162 self.token = token 

163 self.payload = payload 

164 self.opt = Options() 

165 

166 self.remote = None 

167 

168 self.transport_tuning = transport_tuning or TransportTuning() 

169 

170 # deprecation error, should go away roughly after 0.2 release 

171 if self.payload is None: 

172 raise TypeError("Payload must not be None. Use empty string instead.") 

173 

174 if uri: 

175 self.set_request_uri(uri) 

176 

177 for k, v in kwargs.items(): 

178 setattr(self.opt, k, v) 

179 

180 def __repr__(self): 

181 return "<aiocoap.Message at %#x: %s %s (%s, %s) remote %s%s%s>" % ( 

182 id(self), 

183 self.mtype if self.mtype is not None else "no mtype,", 

184 self.code, 

185 "MID %s" % self.mid if self.mid is not None else "no MID", 

186 "token %s" % self.token.hex() if self.token else "empty token", 

187 self.remote, 

188 ", %s option(s)" % len(self.opt._options) if self.opt._options else "", 

189 ", %s byte(s) payload" % len(self.payload) if self.payload else "", 

190 ) 

191 

192 def _repr_html_(self): 

193 """An HTML representation for Jupyter and similar environments 

194 

195 While the precise format is not guaranteed, it will be usable through 

196 tooltips and possibly fold-outs: 

197 

198 >>> from aiocoap import * 

199 >>> msg = Message(code=GET, uri="coap://localhost/other/separate") 

200 >>> html = msg._repr_html_() 

201 >>> 'Message with code <abbr title="Request Code 0.01">GET</abbr>' in html 

202 True 

203 >>> '3 options</summary>' in html 

204 True 

205 """ 

206 import html 

207 

208 if not self.payload: 

209 payload_rendered = "<p>No payload</p>" 

210 else: 

211 from . import defaults 

212 

213 if defaults.prettyprint_missing_modules(): 

214 payload_rendered = f"<code>{html.escape(repr(self.payload))}</code>" 

215 else: 

216 from .util.prettyprint import pretty_print, lexer_for_mime 

217 

218 (notes, mediatype, text) = pretty_print(self) 

219 import pygments 

220 from pygments.formatters import HtmlFormatter 

221 

222 try: 

223 lexer = lexer_for_mime(mediatype) 

224 text = pygments.highlight(text, lexer, HtmlFormatter()) 

225 except pygments.util.ClassNotFound: 

226 text = html.escape(text) 

227 payload_rendered = ( 

228 "<div>" 

229 + "".join( 

230 f'<p style="color:gray;font-size:small;">{html.escape(n)}</p>' 

231 for n in notes 

232 ) 

233 + f"<pre>{text}</pre>" 

234 + "</div>" 

235 ) 

236 return f"""<details style="padding-left:1em"><summary style="margin-left:-1em;display:list-item;">Message with code {self.code._repr_html_() if self.code is not None else 'None'}, remote {html.escape(str(self.remote))}</summary> 

237 {self.opt._repr_html_()}{payload_rendered}""" 

238 

239 def copy(self, **kwargs): 

240 """Create a copy of the Message. kwargs are treated like the named 

241 arguments in the constructor, and update the copy.""" 

242 # This is part of moving messages in an "immutable" direction; not 

243 # necessarily hard immutable. Let's see where this goes. 

244 

245 new = type(self)( 

246 mtype=kwargs.pop("mtype", self.mtype), 

247 mid=kwargs.pop("mid", self.mid), 

248 code=kwargs.pop("code", self.code), 

249 payload=kwargs.pop("payload", self.payload), 

250 token=kwargs.pop("token", self.token), 

251 # Assuming these are not readily mutated, but rather passed 

252 # around in a class-like fashion 

253 transport_tuning=kwargs.pop("transport_tuning", self.transport_tuning), 

254 ) 

255 new.remote = kwargs.pop("remote", self.remote) 

256 new.opt = copy.deepcopy(self.opt) 

257 

258 if "uri" in kwargs: 

259 new.set_request_uri(kwargs.pop("uri")) 

260 

261 for k, v in kwargs.items(): 

262 setattr(new.opt, k, v) 

263 

264 return new 

265 

266 @classmethod 

267 def decode(cls, rawdata, remote=None): 

268 """Create Message object from binary representation of message.""" 

269 try: 

270 (vttkl, code, mid) = struct.unpack("!BBH", rawdata[:4]) 

271 except struct.error: 

272 raise error.UnparsableMessage("Incoming message too short for CoAP") 

273 version = (vttkl & 0xC0) >> 6 

274 if version != 1: 

275 raise error.UnparsableMessage("Fatal Error: Protocol Version must be 1") 

276 mtype = (vttkl & 0x30) >> 4 

277 token_length = vttkl & 0x0F 

278 msg = Message(mtype=mtype, mid=mid, code=code) 

279 msg.token = rawdata[4 : 4 + token_length] 

280 msg.payload = msg.opt.decode(rawdata[4 + token_length :]) 

281 msg.remote = remote 

282 return msg 

283 

284 def encode(self): 

285 """Create binary representation of message from Message object.""" 

286 if self.code is None or self.mtype is None or self.mid is None: 

287 raise TypeError( 

288 "Fatal Error: Code, Message Type and Message ID must not be None." 

289 ) 

290 rawdata = bytes( 

291 [ 

292 (self.version << 6) 

293 + ((self.mtype & 0x03) << 4) 

294 + (len(self.token) & 0x0F) 

295 ] 

296 ) 

297 rawdata += struct.pack("!BH", self.code, self.mid) 

298 rawdata += self.token 

299 rawdata += self.opt.encode() 

300 if len(self.payload) > 0: 

301 rawdata += bytes([0xFF]) 

302 rawdata += self.payload 

303 return rawdata 

304 

305 def get_cache_key(self, ignore_options=()): 

306 """Generate a hashable and comparable object (currently a tuple) from 

307 the message's code and all option values that are part of the cache key 

308 and not in the optional list of ignore_options (which is the list of 

309 option numbers that are not technically NoCacheKey but handled by the 

310 application using this method). 

311 

312 >>> from aiocoap.numbers import GET 

313 >>> m1 = Message(code=GET) 

314 >>> m2 = Message(code=GET) 

315 >>> m1.opt.uri_path = ('s', '1') 

316 >>> m2.opt.uri_path = ('s', '1') 

317 >>> m1.opt.size1 = 10 # the only no-cache-key option in the base spec 

318 >>> m2.opt.size1 = 20 

319 >>> m1.get_cache_key() == m2.get_cache_key() 

320 True 

321 >>> m2.opt.etag = b'000' 

322 >>> m1.get_cache_key() == m2.get_cache_key() 

323 False 

324 >>> from aiocoap.numbers.optionnumbers import OptionNumber 

325 >>> ignore = [OptionNumber.ETAG] 

326 >>> m1.get_cache_key(ignore) == m2.get_cache_key(ignore) 

327 True 

328 """ 

329 

330 options = [] 

331 

332 for option in self.opt.option_list(): 

333 if option.number in ignore_options or ( 

334 option.number.is_safetoforward() and option.number.is_nocachekey() 

335 ): 

336 continue 

337 options.append((option.number, option.value)) 

338 

339 return (self.code, tuple(options)) 

340 

341 # 

342 # splitting and merging messages into and from message blocks 

343 # 

344 

345 def _extract_block(self, number, size_exp, max_bert_size): 

346 """Extract block from current message.""" 

347 if size_exp == 7: 

348 start = number * 1024 

349 size = 1024 * (max_bert_size // 1024) 

350 else: 

351 size = 2 ** (size_exp + 4) 

352 start = number * size 

353 

354 if start >= len(self.payload): 

355 raise error.BadRequest("Block request out of bounds") 

356 

357 end = start + size if start + size < len(self.payload) else len(self.payload) 

358 more = True if end < len(self.payload) else False 

359 

360 payload = self.payload[start:end] 

361 blockopt = (number, more, size_exp) 

362 

363 if self.code.is_request(): 

364 return self.copy(payload=payload, mid=None, block1=blockopt) 

365 else: 

366 return self.copy(payload=payload, mid=None, block2=blockopt) 

367 

368 def _append_request_block(self, next_block): 

369 """Modify message by appending another block""" 

370 if not self.code.is_request(): 

371 raise ValueError("_append_request_block only works on requests.") 

372 

373 block1 = next_block.opt.block1 

374 if block1.more: 

375 if len(next_block.payload) == block1.size: 

376 pass 

377 elif ( 

378 block1.size_exponent == 7 and len(next_block.payload) % block1.size == 0 

379 ): 

380 pass 

381 else: 

382 raise error.BadRequest("Payload size does not match Block1") 

383 if block1.start == len(self.payload): 

384 self.payload += next_block.payload 

385 self.opt.block1 = block1 

386 self.token = next_block.token 

387 self.mid = next_block.mid 

388 if not block1.more and next_block.opt.block2 is not None: 

389 self.opt.block2 = next_block.opt.block2 

390 else: 

391 # possible extension point: allow messages with "gaps"; then 

392 # ValueError would only be raised when trying to overwrite an 

393 # existing part; it is doubtful though that the blockwise 

394 # specification even condones such behavior. 

395 raise ValueError() 

396 

397 def _append_response_block(self, next_block): 

398 """Append next block to current response message. 

399 Used when assembling incoming blockwise responses.""" 

400 if not self.code.is_response(): 

401 raise ValueError("_append_response_block only works on responses.") 

402 

403 block2 = next_block.opt.block2 

404 if not block2.is_valid_for_payload_size(len(next_block.payload)): 

405 raise error.UnexpectedBlock2("Payload size does not match Block2") 

406 if block2.start != len(self.payload): 

407 # Does not need to be implemented as long as the requesting code 

408 # sequentially clocks out data 

409 raise error.NotImplemented() 

410 

411 if next_block.opt.etag != self.opt.etag: 

412 raise error.ResourceChanged() 

413 

414 self.payload += next_block.payload 

415 self.opt.block2 = block2 

416 self.token = next_block.token 

417 self.mid = next_block.mid 

418 

419 def _generate_next_block2_request(self, response): 

420 """Generate a sub-request for next response block. 

421 

422 This method is used by client after receiving blockwise response from 

423 server with "more" flag set.""" 

424 

425 # Note: response here is the assembled response, but (due to 

426 # _append_response_block's workings) it carries the Block2 option of 

427 # the last received block. 

428 

429 next_after_received = len(response.payload) // response.opt.block2.size 

430 blockopt = optiontypes.BlockOption.BlockwiseTuple( 

431 next_after_received, False, response.opt.block2.size_exponent 

432 ) 

433 

434 # has been checked in assembly, just making sure 

435 assert blockopt.start == len( 

436 response.payload 

437 ), "Unexpected state of preassembled message" 

438 

439 blockopt = blockopt.reduced_to(response.remote.maximum_block_size_exp) 

440 

441 return self.copy( 

442 payload=b"", 

443 mid=None, 

444 token=None, 

445 block2=blockopt, 

446 block1=None, 

447 observe=None, 

448 ) 

449 

450 def _generate_next_block1_response(self): 

451 """Generate a response to acknowledge incoming request block. 

452 

453 This method is used by server after receiving blockwise request from 

454 client with "more" flag set.""" 

455 response = Message(code=CHANGED, token=self.token) 

456 response.remote = self.remote 

457 if ( 

458 self.opt.block1.block_number == 0 

459 and self.opt.block1.size_exponent 

460 > self.transport_tuning.DEFAULT_BLOCK_SIZE_EXP 

461 ): 

462 new_size_exponent = self.transport_tuning.DEFAULT_BLOCK_SIZE_EXP 

463 response.opt.block1 = (0, True, new_size_exponent) 

464 else: 

465 response.opt.block1 = ( 

466 self.opt.block1.block_number, 

467 True, 

468 self.opt.block1.size_exponent, 

469 ) 

470 return response 

471 

472 # 

473 # the message in the context of network and addresses 

474 # 

475 

476 def get_request_uri(self, *, local_is_server=False): 

477 """The absolute URI this message belongs to. 

478 

479 For requests, this is composed from the options (falling back to the 

480 remote). For responses, this is largely taken from the original request 

481 message (so far, that could have been trackecd by the requesting 

482 application as well), but -- in case of a multicast request -- with the 

483 host replaced by the responder's endpoint details. 

484 

485 This implements Section 6.5 of RFC7252. 

486 

487 By default, these values are only valid on the client. To determine a 

488 message's request URI on the server, set the local_is_server argument 

489 to True. Note that determining the request URI on the server is brittle 

490 when behind a reverse proxy, may not be possible on all platforms, and 

491 can only be applied to a request message in a renderer (for the 

492 response message created by the renderer will only be populated when it 

493 gets transmitted; simple manual copying of the request's remote to the 

494 response will not magically make this work, for in the very case where 

495 the request and response's URIs differ, that would not catch the 

496 difference and still report the multicast address, while the actual 

497 sending address will only be populated by the operating system later). 

498 """ 

499 

500 # maybe this function does not belong exactly *here*, but it belongs to 

501 # the results of .request(message), which is currently a message itself. 

502 

503 if hasattr(self, "_original_request_uri"): 

504 # During server-side processing, a message's options may be altered 

505 # to the point where its options don't accurately reflect its URI 

506 # any more. In that case, this is stored. 

507 return self._original_request_uri 

508 

509 if self.code.is_response(): 

510 refmsg = self.request 

511 

512 if refmsg.remote.is_multicast: 

513 if local_is_server: 

514 multicast_netloc_override = self.remote.hostinfo_local 

515 else: 

516 multicast_netloc_override = self.remote.hostinfo 

517 else: 

518 multicast_netloc_override = None 

519 else: 

520 refmsg = self 

521 multicast_netloc_override = None 

522 

523 proxyuri = refmsg.opt.proxy_uri 

524 if proxyuri is not None: 

525 return proxyuri 

526 

527 scheme = refmsg.opt.proxy_scheme or refmsg.remote.scheme 

528 query = refmsg.opt.uri_query or () 

529 path = refmsg.opt.uri_path 

530 

531 if multicast_netloc_override is not None: 

532 netloc = multicast_netloc_override 

533 else: 

534 if local_is_server: 

535 netloc = refmsg.remote.hostinfo_local 

536 else: 

537 netloc = refmsg.remote.hostinfo 

538 

539 if refmsg.opt.uri_host is not None or refmsg.opt.uri_port is not None: 

540 host, port = hostportsplit(netloc) 

541 

542 host = refmsg.opt.uri_host or host 

543 port = refmsg.opt.uri_port or port 

544 

545 # FIXME: This sounds like it should be part of 

546 # hpostportjoin/-split 

547 escaped_host = quote_nonascii(host) 

548 

549 # FIXME: "If host is not valid reg-name / IP-literal / IPv4address, 

550 # fail" 

551 

552 netloc = hostportjoin(escaped_host, port) 

553 

554 # FIXME this should follow coap section 6.5 more closely 

555 query = "&".join(_quote_for_query(q) for q in query) 

556 path = "".join("/" + _quote_for_path(p) for p in path) or "/" 

557 

558 fragment = None 

559 params = "" # are they not there at all? 

560 

561 # Eases debugging, for when they raise from urunparse you won't know 

562 # which of them it was 

563 assert scheme is not None, "Remote has no scheme set" 

564 assert netloc is not None, "Remote has no netloc set" 

565 return urllib.parse.urlunparse((scheme, netloc, path, params, query, fragment)) 

566 

567 def set_request_uri(self, uri, *, set_uri_host=True): 

568 """Parse a given URI into the uri_* fields of the options. 

569 

570 The remote does not get set automatically; instead, the remote data is 

571 stored in the uri_host and uri_port options. That is because name resolution 

572 is coupled with network specifics the protocol will know better by the 

573 time the message is sent. Whatever sends the message, be it the 

574 protocol itself, a proxy wrapper or an alternative transport, will know 

575 how to handle the information correctly. 

576 

577 When ``set_uri_host=False`` is passed, the host/port is stored in the 

578 ``unresolved_remote`` message property instead of the uri_host option; 

579 as a result, the unresolved host name is not sent on the wire, which 

580 breaks virtual hosts but makes message sizes smaller. 

581 

582 This implements Section 6.4 of RFC7252. 

583 

584 This raises IncompleteUrlError if URI references are passed in (instead 

585 of a full URI), and MalformedUrlError if the URI specification or the 

586 library's expectations of URI shapes (eg. 'coap+tcp:no-slashes') are 

587 violated. 

588 """ 

589 

590 try: 

591 parsed = urllib.parse.urlparse(uri) 

592 except ValueError as e: 

593 raise error.MalformedUrlError from e 

594 

595 if parsed.fragment: 

596 raise error.MalformedUrlError( 

597 "Fragment identifiers can not be set on a request URI" 

598 ) 

599 

600 if not parsed.scheme: 

601 raise error.IncompleteUrlError() 

602 

603 if parsed.scheme not in coap_schemes: 

604 self.opt.proxy_uri = uri 

605 return 

606 

607 if not parsed.hostname: 

608 raise error.MalformedUrlError("CoAP URIs need a hostname") 

609 

610 if parsed.username or parsed.password: 

611 raise error.MalformedUrlError("User name and password not supported.") 

612 

613 try: 

614 if parsed.path not in ("", "/"): 

615 # FIXME: This tolerates incomplete % sequences. 

616 self.opt.uri_path = [ 

617 urllib.parse.unquote(x, errors="strict") 

618 for x in parsed.path.split("/")[1:] 

619 ] 

620 else: 

621 self.opt.uri_path = [] 

622 if parsed.query: 

623 # FIXME: This tolerates incomplete % sequences. 

624 self.opt.uri_query = [ 

625 urllib.parse.unquote(x, errors="strict") 

626 for x in parsed.query.split("&") 

627 ] 

628 else: 

629 self.opt.uri_query = [] 

630 except UnicodeError as e: 

631 raise error.MalformedUrlError( 

632 "Percent encoded strings in CoAP URIs need to be UTF-8 encoded" 

633 ) from e 

634 

635 self.remote = UndecidedRemote(parsed.scheme, parsed.netloc) 

636 

637 try: 

638 _ = parsed.port 

639 except ValueError as e: 

640 raise error.MalformedUrlError("Port must be numeric") from e 

641 

642 is_ip_literal = parsed.netloc.startswith("[") or ( 

643 parsed.hostname.count(".") == 3 

644 and all(c in "0123456789." for c in parsed.hostname) 

645 and all(int(x) <= 255 for x in parsed.hostname.split(".")) 

646 ) 

647 

648 if set_uri_host and not is_ip_literal: 

649 try: 

650 self.opt.uri_host = urllib.parse.unquote( 

651 parsed.hostname, errors="strict" 

652 ).translate(_ascii_lowercase) 

653 except UnicodeError as e: 

654 raise error.MalformedUrlError( 

655 "Percent encoded strings in CoAP URI hosts need to be UTF-8 encoded" 

656 ) from e 

657 

658 # Deprecated accessors to moved functionality 

659 

660 @property 

661 def unresolved_remote(self): 

662 return self.remote.hostinfo 

663 

664 @unresolved_remote.setter 

665 def unresolved_remote(self, value): 

666 # should get a big fat deprecation warning 

667 if value is None: 

668 self.remote = UndecidedRemote("coap", None) 

669 else: 

670 self.remote = UndecidedRemote("coap", value) 

671 

672 @property 

673 def requested_scheme(self): 

674 if self.code.is_request(): 

675 return self.remote.scheme 

676 else: 

677 return self.request.requested_scheme 

678 

679 @requested_scheme.setter 

680 def requested_scheme(self, value): 

681 self.remote = UndecidedRemote(value, self.remote.hostinfo) 

682 

683 @property 

684 def requested_proxy_uri(self): 

685 return self.request.opt.proxy_uri 

686 

687 @property 

688 def requested_hostinfo(self): 

689 return self.request.opt.uri_host or self.request.unresolved_remote 

690 

691 @property 

692 def requested_path(self): 

693 return self.request.opt.uri_path 

694 

695 @property 

696 def requested_query(self): 

697 return self.request.opt.uri_query 

698 

699 

700class UndecidedRemote( 

701 namedtuple("_UndecidedRemote", ("scheme", "hostinfo")), interfaces.EndpointAddress 

702): 

703 """Remote that is set on messages that have not been sent through any any 

704 transport. 

705 

706 It describes scheme, hostname and port that were set in 

707 :meth:`.set_request_uri()` or when setting a URI per Message constructor. 

708 

709 * :attr:`scheme`: The scheme string 

710 * :attr:`hostinfo`: The authority component of the URI, as it would occur 

711 in the URI. 

712 

713 In order to produce URIs identical to those received in responses, and 

714 because the underlying types should really be binary anyway, IP addresses 

715 in the hostinfo are normalized: 

716 

717 >>> UndecidedRemote("coap+tcp", "[::0001]:1234") 

718 UndecidedRemote(scheme='coap+tcp', hostinfo='[::1]:1234') 

719 """ 

720 

721 # This is settable per instance, for other transports to pick it up. 

722 maximum_block_size_exp = MAX_REGULAR_BLOCK_SIZE_EXP 

723 

724 def __new__(cls, scheme, hostinfo): 

725 if "[" in hostinfo: 

726 (host, port) = hostportsplit(hostinfo) 

727 ip = ipaddress.ip_address(host) 

728 host = str(ip) 

729 hostinfo = hostportjoin(host, port) 

730 

731 return super().__new__(cls, scheme, hostinfo) 

732 

733 @classmethod 

734 def from_pathless_uri(cls, uri: str) -> UndecidedRemote: 

735 """Create an UndecidedRemote for a given URI that has no query, path, 

736 fragment or other components not expressed in an UndecidedRemote 

737 

738 >>> from aiocoap.message import UndecidedRemote 

739 >>> UndecidedRemote.from_pathless_uri("coap://localhost") 

740 UndecidedRemote(scheme='coap', hostinfo='localhost') 

741 """ 

742 

743 parsed = urllib.parse.urlparse(uri) 

744 

745 if parsed.username or parsed.password: 

746 raise ValueError("User name and password not supported.") 

747 

748 if parsed.path not in ("", "/") or parsed.query or parsed.fragment: 

749 raise ValueError( 

750 "Paths and query and fragment can not be set on an UndecidedRemote" 

751 ) 

752 

753 return cls(parsed.scheme, parsed.netloc) 

754 

755 

756_ascii_lowercase = str.maketrans(string.ascii_uppercase, string.ascii_lowercase) 

757 

758_quote_for_path = quote_factory(unreserved + sub_delims + ":@") 

759_quote_for_query = quote_factory( 

760 unreserved + "".join(c for c in sub_delims if c != "&") + ":@/?" 

761) 

762 

763#: Result that can be returned from a render method instead of a Message when 

764#: due to defaults (eg. multicast link-format queries) or explicit 

765#: configuration (eg. the No-Response option), no response should be sent at 

766#: all. Note that per RFC7967 section 2, an ACK is still sent to a CON 

767#: request. 

768#: 

769#: Depercated; set the no_response option on a regular response instead (see 

770#: :meth:`.interfaces.Resource.render` for details). 

771NoResponse = Sentinel("NoResponse")