Coverage for aiocoap/cli/rd.py: 57%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

403 statements  

1# This file is part of the Python aiocoap library project. 

2# 

3# Copyright (c) 2012-2014 Maciej Wasilak <http://sixpinetrees.blogspot.com/>, 

4# 2013-2014 Christian Amsüss <c.amsuess@energyharvesting.at> 

5# 

6# aiocoap is free software, this file is published under the MIT license as 

7# described in the accompanying LICENSE file. 

8 

9"""A plain CoAP resource directory according to 

10draft-ietf-core-resource-directory-25 

11 

12Known Caveats: 

13 

14 * It is very permissive. Not only is no security implemented. 

15 

16 * This may and will make exotic choices about discoverable paths whereever 

17 it can (see StandaloneResourceDirectory documentation) 

18 

19 * Split-horizon is not implemented correctly 

20 

21 * Unless enforced by security (ie. not so far), endpoint and sector names 

22 (ep, d) are not checked for their lengths or other validity. 

23 

24 * Simple registrations don't cache .well-known/core contents 

25""" 

26 

27import string 

28import sys 

29import logging 

30import asyncio 

31import argparse 

32from urllib.parse import urljoin 

33import itertools 

34 

35import aiocoap 

36from aiocoap.resource import Site, Resource, ObservableResource, PathCapable, WKCResource, link_format_to_message 

37from aiocoap.proxy.server import Proxy 

38from aiocoap.util.cli import AsyncCLIDaemon 

39import aiocoap.util.uri 

40from aiocoap import error 

41from aiocoap.cli.common import add_server_arguments, server_context_from_arguments 

42from aiocoap.numbers import ContentFormat 

43import aiocoap.proxy.server 

44 

45from aiocoap.util.linkformat import Link, LinkFormat, parse 

46from ..util.asyncio import py38args 

47 

48import link_header 

49 

50IMMUTABLE_PARAMETERS = ('ep', 'd', 'proxy') 

51 

52def query_split(msg): 

53 """Split a message's query up into (key, [*value]) pairs from a 

54 ?key=value&key2=value2 style Uri-Query options. 

55 

56 Keys without an `=` sign will have a None value, and all values are 

57 expressed as an (at least 1-element) list of repetitions. 

58 

59 >>> m = aiocoap.Message(uri="coap://example.com/foo?k1=v1.1&k1=v1.2&obs") 

60 >>> query_split(m) 

61 {'k1': ['v1.1', 'v1.2'], 'obs': [None]} 

62 """ 

63 result = {} 

64 for q in msg.opt.uri_query: 

65 if '=' not in q: 

66 k = q 

67 # matching the representation in link_header 

68 v = None 

69 else: 

70 k, v = q.split('=', 1) 

71 result.setdefault(k, []).append(v) 

72 return result 

73 

74def pop_single_arg(query, name): 

75 """Out of query which is the output of query_split, pick the single value 

76 at the key name, raise a suitable BadRequest on error, or return None if 

77 nothing is there. The value is removed from the query dictionary.""" 

78 

79 if name not in query: 

80 return None 

81 if len(query[name]) > 1: 

82 raise error.BadRequest("Multiple values for %r" % name) 

83 return query.pop(name)[0] 

84 

85class CommonRD: 

86 # "Key" here always means an (ep, d) tuple. 

87 

88 entity_prefix = ("reg",) 

89 

90 def __init__(self, proxy_domain=None): 

91 super().__init__() 

92 

93 self._by_key = {} # key -> Registration 

94 self._by_path = {} # path -> Registration 

95 

96 self._updated_state_cb = [] 

97 

98 self.proxy_domain = proxy_domain 

99 self.proxy_active = {} # uri_host -> Remote 

100 

101 class Registration: 

102 # FIXME: split this into soft and hard grace period (where the former 

103 # may be 0). the node stays discoverable for the soft grace period, but 

104 # the registration stays alive for a (possibly much longer, at least 

105 # +lt) hard grace period, in which any action on the reg resource 

106 # reactivates it -- preventing premature reuse of the resource URI 

107 grace_period = 15 

108 

109 @property 

110 def href(self): 

111 return '/' + '/'.join(self.path) 

112 

113 def __init__(self, static_registration_parameters, path, network_remote, delete_cb, update_cb, registration_parameters, proxy_host, setproxyremote_cb): 

114 # note that this can not modify d and ep any more, since they are 

115 # already part of the key and possibly the path 

116 self.path = path 

117 self.links = LinkFormat([]) 

118 

119 self._delete_cb = delete_cb 

120 self._update_cb = update_cb 

121 

122 self.registration_parameters = static_registration_parameters 

123 self.lt = 90000 

124 self.base_is_explicit = False 

125 

126 self.proxy_host = proxy_host 

127 self._setproxyremote_cb = setproxyremote_cb 

128 

129 self.update_params(network_remote, registration_parameters, is_initial=True) 

130 

131 def update_params(self, network_remote, registration_parameters, is_initial=False): 

132 """Set the registration_parameters from the parsed query arguments, 

133 update any effects of them, and and trigger any observation 

134 observation updates if requried (the typical ones don't because 

135 their registration_parameters are {} and all it does is restart the 

136 lifetime counter)""" 

137 

138 if any(k in ('ep', 'd') for k in registration_parameters.keys()): 

139 # The ep and d of initial registrations are already popped out 

140 raise error.BadRequest("Parameters 'd' and 'ep' can not be updated") 

141 

142 # Not in use class "R" or otherwise conflict with common parameters 

143 if any(k in ('page', 'count', 'rt', 'href', 'anchor') for k in 

144 registration_parameters.keys()): 

145 raise error.BadRequest("Unsuitable parameter for registration") 

146 

147 if (is_initial or not self.base_is_explicit) and 'base' not in \ 

148 registration_parameters: 

149 # check early for validity to avoid side effects of requests 

150 # answered with 4.xx 

151 if self.proxy_host is None: 

152 try: 

153 network_base = network_remote.uri 

154 except error.AnonymousHost: 

155 raise error.BadRequest("explicit base required") 

156 else: 

157 # FIXME: Advertise alternative transports (write alternative-transports) 

158 network_base = 'coap://' + self.proxy_host 

159 

160 if is_initial: 

161 # technically might be a re-registration, but we can't catch that at this point 

162 actual_change = True 

163 else: 

164 actual_change = False 

165 

166 # Don't act while still checking 

167 set_lt = None 

168 set_base = None 

169 

170 if 'lt' in registration_parameters: 

171 try: 

172 set_lt = int(pop_single_arg(registration_parameters, 'lt')) 

173 except ValueError: 

174 raise error.BadRequest("lt must be numeric") 

175 

176 if 'base' in registration_parameters: 

177 set_base = pop_single_arg(registration_parameters, 'base') 

178 

179 if set_lt is not None and self.lt != set_lt: 

180 actual_change = True 

181 self.lt = set_lt 

182 if set_base is not None and (is_initial or self.base != set_base): 

183 actual_change = True 

184 self.base = set_base 

185 self.base_is_explicit = True 

186 

187 if not self.base_is_explicit and (is_initial or self.base != network_base): 

188 self.base = network_base 

189 actual_change = True 

190 

191 if any(v != self.registration_parameters.get(k) for (k, v) in registration_parameters.items()): 

192 self.registration_parameters.update(registration_parameters) 

193 actual_change = True 

194 

195 if is_initial: 

196 self._set_timeout() 

197 else: 

198 self.refresh_timeout() 

199 

200 if actual_change: 

201 self._update_cb() 

202 

203 if self.proxy_host: 

204 self._setproxyremote_cb(network_remote) 

205 

206 def delete(self): 

207 self.timeout.cancel() 

208 self._update_cb() 

209 self._delete_cb() 

210 

211 def _set_timeout(self): 

212 delay = self.lt + self.grace_period 

213 # workaround for python issue20493 

214 

215 async def longwait(delay, callback): 

216 await asyncio.sleep(delay) 

217 callback() 

218 self.timeout = asyncio.create_task( 

219 longwait(delay, self.delete), 

220 **py38args(name="RD Timeout for %r" % self) 

221 ) 

222 

223 def refresh_timeout(self): 

224 self.timeout.cancel() 

225 self._set_timeout() 

226 

227 def get_host_link(self): 

228 attr_pairs = [] 

229 for (k, values) in self.registration_parameters.items(): 

230 for v in values: 

231 attr_pairs.append([k, v]) 

232 return Link(href=self.href, attr_pairs=attr_pairs, base=self.base, rt="core.rd-ep") 

233 

234 def get_based_links(self): 

235 """Produce a LinkFormat object that represents all statements in 

236 the registration, resolved to the registration's base (and thus 

237 suitable for comparing anchors).""" 

238 result = [] 

239 for l in self.links.links: 

240 href = urljoin(self.base, l.href) 

241 if 'anchor' in l: 

242 absanchor = urljoin(self.base, l.anchor) 

243 data = [(k, v) for (k, v) in l.attr_pairs if k != 'anchor'] + [['anchor', absanchor]] 

244 else: 

245 data = l.attr_pairs + [['anchor', urljoin(href, '/')]] 

246 result.append(Link(href, data)) 

247 return LinkFormat(result) 

248 

249 async def shutdown(self): 

250 pass 

251 

252 def register_change_callback(self, callback): 

253 """Ask RD to invoke the callback whenever any of the RD state 

254 changed""" 

255 # This has no unregister equivalent as it's only called by the lookup 

256 # resources that are expected to be live for the remainder of the 

257 # program, like the Registry is. 

258 self._updated_state_cb.append(callback) 

259 

260 def _updated_state(self): 

261 for cb in self._updated_state_cb: 

262 cb() 

263 

264 def _new_pathtail(self): 

265 for i in itertools.count(1): 

266 # In the spirit of making legal but unconvential choices (see 

267 # StandaloneResourceDirectory documentation): Whoever strips or 

268 # ignores trailing slashes shall have a hard time keeping 

269 # registrations alive. 

270 path = (str(i), '') 

271 if path not in self._by_path: 

272 return path 

273 

274 def initialize_endpoint(self, network_remote, registration_parameters): 

275 # copying around for later use in static, but not checking again 

276 # because reading them from the original will already have screamed by 

277 # the time this is used 

278 static_registration_parameters = {k: v for (k, v) in registration_parameters.items() if k in IMMUTABLE_PARAMETERS} 

279 

280 ep = pop_single_arg(registration_parameters, 'ep') 

281 if ep is None: 

282 raise error.BadRequest("ep argument missing") 

283 d = pop_single_arg(registration_parameters, 'd') 

284 

285 proxy = pop_single_arg(registration_parameters, 'proxy') 

286 

287 if proxy is not None and proxy != 'on': 

288 raise error.BadRequest("Unsupported proxy value") 

289 

290 key = (ep, d) 

291 

292 if static_registration_parameters.pop('proxy', None): 

293 # FIXME: 'ondemand' is done unconditionally 

294 

295 if not self.proxy_domain: 

296 raise error.BadRequest("Proxying not enabled") 

297 

298 def is_usable(s): 

299 # Host names per RFC1123 (which is stricter than what RFC3986 would allow). 

300 # 

301 # Only supporting lowercase names as to avoid ambiguities due 

302 # to hostname capitalizatio normalization (otherwise it'd need 

303 # to be first-registered-first-served) 

304 return s and all(x in string.ascii_lowercase + string.digits + '-' for x in s) 

305 if not is_usable(ep) or (d is not None and not is_usable(d)): 

306 raise error.BadRequest("Proxying only supported for limited ep and d set (lowercase, digits, dash)") 

307 

308 proxy_host = ep 

309 if d is not None: 

310 proxy_host += '.' + d 

311 proxy_host = proxy_host + '.' + self.proxy_domain 

312 else: 

313 proxy_host = None 

314 

315 # No more errors should fly out from below here, as side effects start now 

316 

317 try: 

318 oldreg = self._by_key[key] 

319 except KeyError: 

320 path = self._new_pathtail() 

321 else: 

322 path = oldreg.path[len(self.entity_prefix):] 

323 oldreg.delete() 

324 

325 # this was the brutal way towards idempotency (delete and re-create). 

326 # if any actions based on that are implemented here, they have yet to 

327 # decide wheter they'll treat idempotent recreations like deletions or 

328 # just ignore them unless something otherwise unchangeable (ep, d) 

329 # changes. 

330 

331 def delete(): 

332 del self._by_path[path] 

333 del self._by_key[key] 

334 self.proxy_active.pop(proxy_host, None) 

335 

336 def setproxyremote(remote): 

337 self.proxy_active[proxy_host] = remote 

338 

339 reg = self.Registration(static_registration_parameters, self.entity_prefix + path, network_remote, delete, 

340 self._updated_state, registration_parameters, proxy_host, setproxyremote) 

341 

342 self._by_key[key] = reg 

343 self._by_path[path] = reg 

344 

345 return reg 

346 

347 def get_endpoints(self): 

348 return self._by_key.values() 

349 

350def link_format_from_message(message): 

351 """Convert a response message into a LinkFormat object 

352 

353 This expects an explicit media type set on the response (or was explicitly requested) 

354 """ 

355 certain_format = message.opt.content_format 

356 if certain_format is None: 

357 certain_format = message.request.opt.accept 

358 try: 

359 if certain_format == ContentFormat.LINKFORMAT: 

360 return parse(message.payload.decode('utf8')) 

361 else: 

362 raise error.UnsupportedMediaType() 

363 except (UnicodeDecodeError, link_header.ParseException): 

364 raise error.BadRequest() 

365 

366class ThingWithCommonRD: 

367 def __init__(self, common_rd): 

368 super().__init__() 

369 self.common_rd = common_rd 

370 

371 if isinstance(self, ObservableResource): 

372 self.common_rd.register_change_callback(self.updated_state) 

373 

374class DirectoryResource(ThingWithCommonRD, Resource): 

375 ct = link_format_to_message.supported_ct 

376 rt = "core.rd" 

377 

378 #: Issue a custom warning when registrations come in via this interface 

379 registration_warning = None 

380 

381 async def render_post(self, request): 

382 links = link_format_from_message(request) 

383 

384 registration_parameters = query_split(request) 

385 

386 if self.registration_warning: 

387 # Conveniently placed so it could be changed to something setting 

388 # additional registration_parameters instead 

389 logging.warning("Warning from registration: %s", self.registration_warning) 

390 

391 regresource = self.common_rd.initialize_endpoint(request.remote, registration_parameters) 

392 regresource.links = links 

393 

394 return aiocoap.Message(code=aiocoap.CREATED, location_path=regresource.path) 

395 

396class RegistrationResource(Resource): 

397 """The resource object wrapping a registration is just a very thin and 

398 ephemeral object; all those methods could just as well be added to 

399 Registration with `s/self.reg/self/g`, making RegistrationResource(reg) = 

400 reg (or handleded in a single RegistrationDispatchSite), but this is kept 

401 here for better separation of model and interface.""" 

402 

403 def __init__(self, registration): 

404 self.reg = registration 

405 

406 async def render_get(self, request): 

407 return link_format_from_message(request, self.reg.links) 

408 

409 def _update_params(self, msg): 

410 query = query_split(msg) 

411 self.reg.update_params(msg.remote, query) 

412 

413 async def render_post(self, request): 

414 self._update_params(request) 

415 

416 if request.opt.content_format is not None or request.payload: 

417 raise error.BadRequest("Registration update with body not specified") 

418 

419 return aiocoap.Message(code=aiocoap.CHANGED) 

420 

421 async def render_put(self, request): 

422 # this is not mentioned in the current spec, but seems to make sense 

423 links = link_format_from_message(request) 

424 

425 self._update_params(request) 

426 self.reg.links = links 

427 

428 return aiocoap.Message(code=aiocoap.CHANGED) 

429 

430 async def render_delete(self, request): 

431 self.reg.delete() 

432 

433 return aiocoap.Message(code=aiocoap.DELETED) 

434 

435class RegistrationDispatchSite(ThingWithCommonRD, Resource, PathCapable): 

436 async def render(self, request): 

437 try: 

438 entity = self.common_rd._by_path[request.opt.uri_path] 

439 except KeyError: 

440 raise error.NotFound 

441 

442 entity = RegistrationResource(entity) 

443 

444 return await entity.render(request.copy(uri_path=())) 

445 

446def _paginate(candidates, query): 

447 page = pop_single_arg(query, 'page') 

448 count = pop_single_arg(query, 'count') 

449 

450 try: 

451 candidates = list(candidates) 

452 if page is not None: 

453 candidates = candidates[int(page) * int(count):] 

454 if count is not None: 

455 candidates = candidates[:int(count)] 

456 except (KeyError, ValueError): 

457 raise error.BadRequest("page requires count, and both must be ints") 

458 

459 return candidates 

460 

461def _link_matches(link, key, condition): 

462 return any(k == key and condition(v) for (k, v) in link.attr_pairs) 

463 

464class EndpointLookupInterface(ThingWithCommonRD, ObservableResource): 

465 ct = link_format_to_message.supported_ct 

466 rt = "core.rd-lookup-ep" 

467 

468 async def render_get(self, request): 

469 query = query_split(request) 

470 

471 candidates = self.common_rd.get_endpoints() 

472 

473 for search_key, search_values in query.items(): 

474 if search_key in ('page', 'count'): 

475 continue # filtered last 

476 

477 for search_value in search_values: 

478 if search_value is not None and search_value.endswith('*'): 

479 def matches(x, start=search_value[:-1]): 

480 return x.startswith(start) 

481 else: 

482 def matches(x, search_value=search_value): 

483 return x == search_value 

484 

485 if search_key in ('if', 'rt'): 

486 def matches(x, original_matches=matches): 

487 return any(original_matches(v) for v in x.split()) 

488 

489 if search_key == 'href': 

490 candidates = (c for c in candidates if 

491 matches(c.href) or 

492 any(matches(r.href) for r in c.get_based_links().links) 

493 ) 

494 continue 

495 

496 candidates = (c for c in candidates if 

497 (search_key in c.registration_parameters and any(matches(x) for x in c.registration_parameters[search_key])) or 

498 any(_link_matches(r, search_key, matches) for r in c.get_based_links().links) 

499 ) 

500 

501 candidates = _paginate(candidates, query) 

502 

503 result = [c.get_host_link() for c in candidates] 

504 

505 return link_format_to_message(request, LinkFormat(result)) 

506 

507class ResourceLookupInterface(ThingWithCommonRD, ObservableResource): 

508 ct = link_format_to_message.supported_ct 

509 rt = "core.rd-lookup-res" 

510 

511 async def render_get(self, request): 

512 query = query_split(request) 

513 

514 eps = self.common_rd.get_endpoints() 

515 candidates = ((e, c) for e in eps for c in e.get_based_links().links) 

516 

517 for search_key, search_values in query.items(): 

518 if search_key in ('page', 'count'): 

519 continue # filtered last 

520 

521 for search_value in search_values: 

522 if search_value is not None and search_value.endswith('*'): 

523 def matches(x, start=search_value[:-1]): 

524 return x.startswith(start) 

525 else: 

526 def matches(x, search_value=search_value): 

527 return x == search_value 

528 

529 if search_key in ('if', 'rt'): 

530 def matches(x, original_matches=matches): 

531 return any(original_matches(v) for v in x.split()) 

532 

533 if search_key == 'href': 

534 candidates = ((e, c) for (e, c) in candidates if 

535 matches(c.href) or 

536 matches(e.href) # FIXME: They SHOULD give this as relative as we do, but don't have to 

537 ) 

538 continue 

539 

540 candidates = ((e, c) for (e, c) in candidates if 

541 _link_matches(c, search_key, matches) or 

542 (search_key in e.registration_parameters and any(matches(x) for x in e.registration_parameters[search_key])) 

543 ) 

544 

545 # strip endpoint 

546 candidates = (c for (e, c) in candidates) 

547 

548 candidates = _paginate(candidates, query) 

549 

550 # strip needless anchors 

551 candidates = [ 

552 Link(l.href, [(k, v) for (k, v) in l.attr_pairs if k != 'anchor']) 

553 if dict(l.attr_pairs)['anchor'] == urljoin(l.href, '/') 

554 else l 

555 for l in candidates] 

556 

557 return link_format_to_message(request, LinkFormat(candidates)) 

558 

559class SimpleRegistration(ThingWithCommonRD, Resource): 

560 #: Issue a custom warning when registrations come in via this interface 

561 registration_warning = None 

562 

563 def __init__(self, common_rd, context): 

564 super().__init__(common_rd) 

565 self.context = context 

566 

567 async def render_post(self, request): 

568 query = query_split(request) 

569 

570 if 'base' in query: 

571 raise error.BadRequest("base is not allowed in simple registrations") 

572 

573 await self.process_request( 

574 network_remote=request.remote, 

575 registration_parameters=query, 

576 ) 

577 

578 return aiocoap.Message(code=aiocoap.CHANGED) 

579 

580 async def process_request(self, network_remote, registration_parameters): 

581 if 'proxy' not in registration_parameters: 

582 try: 

583 network_base = network_remote.uri 

584 except error.AnonymousHost: 

585 raise error.BadRequest("explicit base required") 

586 

587 fetch_address = (network_base + '/.well-known/core') 

588 get = aiocoap.Message(uri=fetch_address) 

589 else: 

590 # ignoring that there might be a based present, that will err later 

591 get = aiocoap.Message(uri_path=['.well-known', 'core']) 

592 get.remote = network_remote 

593 

594 get.code = aiocoap.GET 

595 get.opt.accept = ContentFormat.LINKFORMAT 

596 

597 # not trying to catch anything here -- the errors are most likely well renderable into the final response 

598 response = await self.context.request(get).response_raising 

599 links = link_format_from_message(response) 

600 

601 if self.registration_warning: 

602 # Conveniently placed so it could be changed to something setting 

603 # additional registration_parameters instead 

604 logging.warning("Warning from registration: %s", self.registration_warning) 

605 registration = self.common_rd.initialize_endpoint(network_remote, registration_parameters) 

606 registration.links = links 

607 

608class SimpleRegistrationWKC(WKCResource, SimpleRegistration): 

609 def __init__(self, listgenerator, common_rd, context): 

610 WKCResource.__init__(self, listgenerator) 

611 SimpleRegistration.__init__(self, common_rd, context) 

612 self.registration_warning = "via .well-known/core" 

613 

614class StandaloneResourceDirectory(Proxy, Site): 

615 """A site that contains all function sets of the CoAP Resource Directoru 

616 

617 To prevent or show ossification of example paths in the specification, all 

618 function set paths are configurable and default to values that are 

619 different from the specification (but still recognizable).""" 

620 

621 rd_path = ("resourcedirectory", "") 

622 ep_lookup_path = ("endpoint-lookup", "") 

623 res_lookup_path = ("resource-lookup", "") 

624 

625 def __init__(self, context, lwm2m_compat=None, **kwargs): 

626 if lwm2m_compat is True: 

627 self.rd_path = ("rd",) 

628 

629 # Double inheritance: works as everything up of Proxy has the same interface 

630 super().__init__(outgoing_context=context) 

631 

632 common_rd = CommonRD(**kwargs) 

633 

634 self.add_resource([".well-known", "core"], SimpleRegistrationWKC(self.get_resources_as_linkheader, common_rd=common_rd, context=context)) 

635 self.add_resource([".well-known", "rd"], SimpleRegistration(common_rd=common_rd, context=context)) 

636 

637 self.add_resource(self.rd_path, DirectoryResource(common_rd=common_rd)) 

638 if list(self.rd_path) != ["rd"] and lwm2m_compat is None: 

639 second_dir_resource = DirectoryResource(common_rd=common_rd) 

640 second_dir_resource.registration_warning = "via unannounced /rd" 

641 # Hide from listing 

642 second_dir_resource.get_link_description = lambda *args: None 

643 self.add_resource(["rd"], second_dir_resource) 

644 self.add_resource(self.ep_lookup_path, EndpointLookupInterface(common_rd=common_rd)) 

645 self.add_resource(self.res_lookup_path, ResourceLookupInterface(common_rd=common_rd)) 

646 

647 self.add_resource(common_rd.entity_prefix, RegistrationDispatchSite(common_rd=common_rd)) 

648 

649 self.common_rd = common_rd 

650 

651 def apply_redirection(self, request): 

652 # Fully overriding so we don't need to set an add_redirector 

653 

654 # infallible as the request only gets here if the proxy path is chosen 

655 actual_remote = self.common_rd.proxy_active[request.opt.uri_host] 

656 request.remote = actual_remote 

657 request.opt.uri_host = None 

658 return request 

659 

660 async def shutdown(self): 

661 await self.common_rd.shutdown() 

662 

663 async def render(self, request): 

664 # Full override switching which of the parents' behavior to choose 

665 

666 if request.opt.uri_host in self.common_rd.proxy_active: 

667 # This is never the case if proxying is disabled. 

668 return await Proxy.render(self, request) 

669 else: 

670 return await Site.render(self, request) 

671 

672 # See render; necessary on all functions thanks to https://github.com/chrysn/aiocoap/issues/251 

673 

674 async def needs_blockwise_assembly(self, request): 

675 if request.opt.uri_host in self.common_rd.proxy_active: 

676 return await Proxy.needs_blockwise_assembly(self, request) 

677 else: 

678 return await Site.needs_blockwise_assembly(self, request) 

679 

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

681 if request.opt.uri_host in self.common_rd.proxy_active: 

682 return await Proxy.add_observation(self, request, serverobservation) 

683 else: 

684 return await Site.add_observation(self, request, serverobservation) 

685 

686def build_parser(): 

687 p = argparse.ArgumentParser(description=__doc__) 

688 

689 add_server_arguments(p) 

690 

691 return p 

692 

693class Main(AsyncCLIDaemon): 

694 async def start(self, args=None): 

695 parser = build_parser() 

696 parser.add_argument("--proxy-domain", help="Enable the RD proxy extension. Example: `.proxy.example.net` will produce base URIs like `coap://node1.proxy.example.net/`. The names must all resolve to an address the RD is bound to.", type=str) 

697 parser.add_argument("--lwm2m-compat", help="Compatibility mode for LwM2M clients that can not perform some discovery steps (moving the registration resource to `/rd`)", action='store_true', default=None) 

698 parser.add_argument("--no-lwm2m-compat", help="Disable all compativility with LwM2M clients that can not perform some discovery steps (not even accepting registrations at `/rd` with warnings)", action='store_false', dest='lwm2m_compat') 

699 options = parser.parse_args(args if args is not None else sys.argv[1:]) 

700 

701 # Putting in an empty site to construct the site with a context 

702 self.context = await server_context_from_arguments(None, options) 

703 

704 self.site = StandaloneResourceDirectory(context=self.context, proxy_domain=options.proxy_domain, lwm2m_compat=options.lwm2m_compat) 

705 self.context.serversite = self.site 

706 

707 async def shutdown(self): 

708 await self.site.shutdown() 

709 await self.context.shutdown() 

710 

711sync_main = Main.sync_main 

712 

713if __name__ == "__main__": 

714 sync_main()