Coverage for src/aiocoap/message.py: 0%

287 statements  

« prev     ^ index     » next       coverage.py v7.7.0, created at 2025-03-20 17:26 +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 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> 

209 {self.opt._repr_html_()}{self.payload_html()}""" 

210 

211 def payload_html(self): 

212 """An HTML representation of the payload 

213 

214 The precise format is not guaranteed, but generally it may involve 

215 pretty-printing, syntax highlighting, and visible notes that content 

216 was reflowed or absent. 

217 

218 The result may vary depending on the available modules (falling back go 

219 plain HTML-escaped ``repr()`` of the payload). 

220 """ 

221 import html 

222 

223 if not self.payload: 

224 return "<p>No payload</p>" 

225 else: 

226 from . import defaults 

227 

228 if defaults.prettyprint_missing_modules(): 

229 return f"<code>{html.escape(repr(self.payload))}</code>" 

230 else: 

231 from .util.prettyprint import pretty_print, lexer_for_mime 

232 

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

234 import pygments 

235 from pygments.formatters import HtmlFormatter 

236 

237 try: 

238 lexer = lexer_for_mime(mediatype) 

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

240 except pygments.util.ClassNotFound: 

241 text = html.escape(text) 

242 return ( 

243 "<div>" 

244 + "".join( 

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

246 for n in notes 

247 ) 

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

249 + "</div>" 

250 ) 

251 

252 def copy(self, **kwargs): 

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

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

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

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

257 

258 new = type(self)( 

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

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

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

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

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

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

265 # around in a class-like fashion 

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

267 ) 

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

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

270 

271 if "uri" in kwargs: 

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

273 

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

275 setattr(new.opt, k, v) 

276 

277 return new 

278 

279 @classmethod 

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

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

282 try: 

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

284 except struct.error: 

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

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

287 if version != 1: 

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

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

290 token_length = vttkl & 0x0F 

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

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

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

294 msg.remote = remote 

295 return msg 

296 

297 def encode(self): 

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

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

300 raise TypeError( 

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

302 ) 

303 rawdata = bytes( 

304 [ 

305 (self.version << 6) 

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

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

308 ] 

309 ) 

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

311 rawdata += self.token 

312 rawdata += self.opt.encode() 

313 if len(self.payload) > 0: 

314 rawdata += bytes([0xFF]) 

315 rawdata += self.payload 

316 return rawdata 

317 

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

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

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

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

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

323 application using this method). 

324 

325 >>> from aiocoap.numbers import GET 

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

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

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

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

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

331 >>> m2.opt.size1 = 20 

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

333 True 

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

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

336 False 

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

338 >>> ignore = [OptionNumber.ETAG] 

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

340 True 

341 """ 

342 

343 options = [] 

344 

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

346 if option.number in ignore_options or ( 

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

348 ): 

349 continue 

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

351 

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

353 

354 # 

355 # splitting and merging messages into and from message blocks 

356 # 

357 

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

359 """Extract block from current message.""" 

360 if size_exp == 7: 

361 start = number * 1024 

362 size = 1024 * (max_bert_size // 1024) 

363 else: 

364 size = 2 ** (size_exp + 4) 

365 start = number * size 

366 

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

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

369 

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

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

372 

373 payload = self.payload[start:end] 

374 blockopt = (number, more, size_exp) 

375 

376 if self.code.is_request(): 

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

378 else: 

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

380 

381 def _append_request_block(self, next_block): 

382 """Modify message by appending another block""" 

383 if not self.code.is_request(): 

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

385 

386 block1 = next_block.opt.block1 

387 if block1.more: 

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

389 pass 

390 elif ( 

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

392 ): 

393 pass 

394 else: 

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

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

397 self.payload += next_block.payload 

398 self.opt.block1 = block1 

399 self.token = next_block.token 

400 self.mid = next_block.mid 

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

402 self.opt.block2 = next_block.opt.block2 

403 else: 

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

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

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

407 # specification even condones such behavior. 

408 raise ValueError() 

409 

410 def _append_response_block(self, next_block): 

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

412 Used when assembling incoming blockwise responses.""" 

413 if not self.code.is_response(): 

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

415 

416 block2 = next_block.opt.block2 

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

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

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

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

421 # sequentially clocks out data 

422 raise error.NotImplemented() 

423 

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

425 raise error.ResourceChanged() 

426 

427 self.payload += next_block.payload 

428 self.opt.block2 = block2 

429 self.token = next_block.token 

430 self.mid = next_block.mid 

431 

432 def _generate_next_block2_request(self, response): 

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

434 

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

436 server with "more" flag set.""" 

437 

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

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

440 # the last received block. 

441 

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

443 blockopt = optiontypes.BlockOption.BlockwiseTuple( 

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

445 ) 

446 

447 # has been checked in assembly, just making sure 

448 assert blockopt.start == len(response.payload), ( 

449 "Unexpected state of preassembled message" 

450 ) 

451 

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

453 

454 return self.copy( 

455 payload=b"", 

456 mid=None, 

457 token=None, 

458 block2=blockopt, 

459 block1=None, 

460 observe=None, 

461 ) 

462 

463 def _generate_next_block1_response(self): 

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

465 

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

467 client with "more" flag set.""" 

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

469 response.remote = self.remote 

470 if ( 

471 self.opt.block1.block_number == 0 

472 and self.opt.block1.size_exponent 

473 > self.transport_tuning.DEFAULT_BLOCK_SIZE_EXP 

474 ): 

475 new_size_exponent = self.transport_tuning.DEFAULT_BLOCK_SIZE_EXP 

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

477 else: 

478 response.opt.block1 = ( 

479 self.opt.block1.block_number, 

480 True, 

481 self.opt.block1.size_exponent, 

482 ) 

483 return response 

484 

485 # 

486 # the message in the context of network and addresses 

487 # 

488 

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

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

491 

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

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

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

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

496 host replaced by the responder's endpoint details. 

497 

498 This implements Section 6.5 of RFC7252. 

499 

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

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

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

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

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

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

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

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

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

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

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

511 """ 

512 

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

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

515 

516 if hasattr(self, "_original_request_uri"): 

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

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

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

520 return self._original_request_uri 

521 

522 if self.code.is_response(): 

523 refmsg = self.request 

524 

525 if refmsg.remote.is_multicast: 

526 if local_is_server: 

527 multicast_netloc_override = self.remote.hostinfo_local 

528 else: 

529 multicast_netloc_override = self.remote.hostinfo 

530 else: 

531 multicast_netloc_override = None 

532 else: 

533 refmsg = self 

534 multicast_netloc_override = None 

535 

536 proxyuri = refmsg.opt.proxy_uri 

537 if proxyuri is not None: 

538 return proxyuri 

539 

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

541 query = refmsg.opt.uri_query or () 

542 path = refmsg.opt.uri_path 

543 

544 if multicast_netloc_override is not None: 

545 netloc = multicast_netloc_override 

546 else: 

547 if local_is_server: 

548 netloc = refmsg.remote.hostinfo_local 

549 else: 

550 netloc = refmsg.remote.hostinfo 

551 

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

553 host, port = hostportsplit(netloc) 

554 

555 host = refmsg.opt.uri_host or host 

556 port = refmsg.opt.uri_port or port 

557 

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

559 # hpostportjoin/-split 

560 escaped_host = quote_nonascii(host) 

561 

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

563 # fail" 

564 

565 netloc = hostportjoin(escaped_host, port) 

566 

567 # FIXME this should follow coap section 6.5 more closely 

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

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

570 

571 fragment = None 

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

573 

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

575 # which of them it was 

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

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

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

579 

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

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

582 

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

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

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

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

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

588 how to handle the information correctly. 

589 

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

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

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

593 breaks virtual hosts but makes message sizes smaller. 

594 

595 This implements Section 6.4 of RFC7252. 

596 

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

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

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

600 violated. 

601 """ 

602 

603 try: 

604 parsed = urllib.parse.urlparse(uri) 

605 except ValueError as e: 

606 raise error.MalformedUrlError from e 

607 

608 if parsed.fragment: 

609 raise error.MalformedUrlError( 

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

611 ) 

612 

613 if not parsed.scheme: 

614 raise error.IncompleteUrlError() 

615 

616 if parsed.scheme not in coap_schemes: 

617 self.opt.proxy_uri = uri 

618 return 

619 

620 if not parsed.hostname: 

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

622 

623 if parsed.username or parsed.password: 

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

625 

626 try: 

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

628 # FIXME: This tolerates incomplete % sequences. 

629 self.opt.uri_path = [ 

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

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

632 ] 

633 else: 

634 self.opt.uri_path = [] 

635 if parsed.query: 

636 # FIXME: This tolerates incomplete % sequences. 

637 self.opt.uri_query = [ 

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

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

640 ] 

641 else: 

642 self.opt.uri_query = [] 

643 except UnicodeError as e: 

644 raise error.MalformedUrlError( 

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

646 ) from e 

647 

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

649 

650 try: 

651 _ = parsed.port 

652 except ValueError as e: 

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

654 

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

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

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

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

659 ) 

660 

661 if set_uri_host and not is_ip_literal: 

662 try: 

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

664 parsed.hostname, errors="strict" 

665 ).translate(_ascii_lowercase) 

666 except UnicodeError as e: 

667 raise error.MalformedUrlError( 

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

669 ) from e 

670 

671 # Deprecated accessors to moved functionality 

672 

673 @property 

674 def unresolved_remote(self): 

675 return self.remote.hostinfo 

676 

677 @unresolved_remote.setter 

678 def unresolved_remote(self, value): 

679 # should get a big fat deprecation warning 

680 if value is None: 

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

682 else: 

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

684 

685 @property 

686 def requested_scheme(self): 

687 if self.code.is_request(): 

688 return self.remote.scheme 

689 else: 

690 return self.request.requested_scheme 

691 

692 @requested_scheme.setter 

693 def requested_scheme(self, value): 

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

695 

696 @property 

697 def requested_proxy_uri(self): 

698 return self.request.opt.proxy_uri 

699 

700 @property 

701 def requested_hostinfo(self): 

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

703 

704 @property 

705 def requested_path(self): 

706 return self.request.opt.uri_path 

707 

708 @property 

709 def requested_query(self): 

710 return self.request.opt.uri_query 

711 

712 

713class UndecidedRemote( 

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

715): 

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

717 transport. 

718 

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

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

721 

722 * :attr:`scheme`: The scheme string 

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

724 in the URI. 

725 

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

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

728 in the hostinfo are normalized: 

729 

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

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

732 """ 

733 

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

735 maximum_block_size_exp = MAX_REGULAR_BLOCK_SIZE_EXP 

736 

737 def __new__(cls, scheme, hostinfo): 

738 if "[" in hostinfo: 

739 (host, port) = hostportsplit(hostinfo) 

740 ip = ipaddress.ip_address(host) 

741 host = str(ip) 

742 hostinfo = hostportjoin(host, port) 

743 

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

745 

746 @classmethod 

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

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

749 fragment or other components not expressed in an UndecidedRemote 

750 

751 >>> from aiocoap.message import UndecidedRemote 

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

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

754 """ 

755 

756 parsed = urllib.parse.urlparse(uri) 

757 

758 if parsed.username or parsed.password: 

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

760 

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

762 raise ValueError( 

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

764 ) 

765 

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

767 

768 

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

770 

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

772_quote_for_query = quote_factory( 

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

774) 

775 

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

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

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

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

780#: request. 

781#: 

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

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

784NoResponse = Sentinel("NoResponse")