Coverage for aiocoap / cli / rd.py: 56%
422 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-29 12:32 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-29 12:32 +0000
1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
2#
3# SPDX-License-Identifier: MIT
5"""A plain CoAP resource directory according to RFC9176_
7Known Caveats:
9 * It is very permissive. Not only is no security implemented.
11 * This may and will make exotic choices about discoverable paths wherever
12 it can (see StandaloneResourceDirectory documentation)
14 * Split-horizon is not implemented correctly
16 * Unless enforced by security (ie. not so far), endpoint and sector names
17 (ep, d) are not checked for their lengths or other validity.
19 * Simple registrations don't cache .well-known/core contents
21.. _RFC9176: https://datatracker.ietf.org/doc/html/rfc9176
22"""
24import string
25import sys
26import logging
27import asyncio
28import argparse
29from urllib.parse import urljoin
30import itertools
32import aiocoap
33from aiocoap.resource import (
34 Site,
35 Resource,
36 ObservableResource,
37 PathCapable,
38 WKCResource,
39 link_format_to_message,
40)
41from aiocoap.proxy.server import Proxy
42from aiocoap.util.cli import AsyncCLIDaemon
43import aiocoap.util.uri
44from aiocoap import error
45from aiocoap.cli.common import add_server_arguments, server_context_from_arguments
46from aiocoap.numbers import codes, ContentFormat
47import aiocoap.proxy.server
49from aiocoap.util.linkformat import Link, LinkFormat, parse
51from ..util.linkformat import link_header
53IMMUTABLE_PARAMETERS = ("ep", "d", "proxy")
56class NoActiveRegistration(error.ConstructionRenderableError):
57 code = codes.PROXYING_NOT_SUPPORTED
58 message = "no registration with that name"
61def query_split(msg):
62 """Split a message's query up into (key, [*value]) pairs from a
63 ?key=value&key2=value2 style Uri-Query options.
65 Keys without an `=` sign will have a None value, and all values are
66 expressed as an (at least 1-element) list of repetitions.
68 >>> m = aiocoap.Message(uri="coap://example.com/foo?k1=v1.1&k1=v1.2&obs")
69 >>> query_split(m)
70 {'k1': ['v1.1', 'v1.2'], 'obs': [None]}
71 """
72 result = {}
73 for q in msg.opt.uri_query:
74 if "=" not in q:
75 k = q
76 # matching the representation in link_header
77 v = None
78 else:
79 k, v = q.split("=", 1)
80 result.setdefault(k, []).append(v)
81 return result
84def pop_single_arg(query, name):
85 """Out of query which is the output of query_split, pick the single value
86 at the key name, raise a suitable BadRequest on error, or return None if
87 nothing is there. The value is removed from the query dictionary."""
89 if name not in query:
90 return None
91 if len(query[name]) > 1:
92 raise error.BadRequest("Multiple values for %r" % name)
93 return query.pop(name)[0]
96class CommonRD:
97 # "Key" here always means an (ep, d) tuple.
99 entity_prefix = ("reg",)
101 def __init__(self, proxy_domain=None, log=None):
102 super().__init__()
104 self.log = log or logging.getLogger("resource-directory")
106 self._by_key = {} # key -> Registration
107 self._by_path = {} # path -> Registration
109 self._updated_state_cb = []
111 self.proxy_domain = proxy_domain
112 self.proxy_active = {} # uri_host -> Remote
114 class Registration:
115 # FIXME: split this into soft and hard grace period (where the former
116 # may be 0). the node stays discoverable for the soft grace period, but
117 # the registration stays alive for a (possibly much longer, at least
118 # +lt) hard grace period, in which any action on the reg resource
119 # reactivates it -- preventing premature reuse of the resource URI
120 grace_period = 15
122 @property
123 def href(self):
124 return "/" + "/".join(self.path)
126 def __init__(
127 self,
128 static_registration_parameters,
129 path,
130 network_remote,
131 delete_cb,
132 update_cb,
133 registration_parameters,
134 proxy_host,
135 setproxyremote_cb,
136 ):
137 # note that this can not modify d and ep any more, since they are
138 # already part of the key and possibly the path
139 self.path = path
140 self.links = LinkFormat([])
142 self._delete_cb = delete_cb
143 self._update_cb = update_cb
145 self.registration_parameters = static_registration_parameters
146 self.lt = 90000
147 self.base_is_explicit = False
149 self.proxy_host = proxy_host
150 self._setproxyremote_cb = setproxyremote_cb
152 self.update_params(network_remote, registration_parameters, is_initial=True)
154 def update_params(
155 self, network_remote, registration_parameters, is_initial=False
156 ):
157 """Set the registration_parameters from the parsed query arguments,
158 update any effects of them, and trigger any observation updates if
159 required (the typical ones don't because their
160 registration_parameters are {} and all it does is restart the
161 lifetime counter)"""
163 if any(k in ("ep", "d") for k in registration_parameters.keys()):
164 # The ep and d of initial registrations are already popped out
165 raise error.BadRequest("Parameters 'd' and 'ep' can not be updated")
167 # Not in use class "R" or otherwise conflict with common parameters
168 if any(
169 k in ("page", "count", "rt", "href", "anchor")
170 for k in registration_parameters.keys()
171 ):
172 raise error.BadRequest("Unsuitable parameter for registration")
174 if (
175 is_initial or not self.base_is_explicit
176 ) and "base" not in registration_parameters:
177 # check early for validity to avoid side effects of requests
178 # answered with 4.xx
179 if self.proxy_host is None:
180 try:
181 network_base = network_remote.uri
182 except error.AnonymousHost:
183 raise error.BadRequest("explicit base required")
184 else:
185 # FIXME: Advertise alternative transports (write alternative-transports)
186 network_base = "coap://" + self.proxy_host
188 if is_initial:
189 # technically might be a re-registration, but we can't catch that at this point
190 actual_change = True
191 else:
192 actual_change = False
194 # Don't act while still checking
195 set_lt = None
196 set_base = None
198 if "lt" in registration_parameters:
199 try:
200 set_lt = int(pop_single_arg(registration_parameters, "lt"))
201 except ValueError:
202 raise error.BadRequest("lt must be numeric")
204 if "base" in registration_parameters:
205 set_base = pop_single_arg(registration_parameters, "base")
207 if set_lt is not None and self.lt != set_lt:
208 actual_change = True
209 self.lt = set_lt
210 if set_base is not None and (is_initial or self.base != set_base):
211 actual_change = True
212 self.base = set_base
213 self.base_is_explicit = True
215 if not self.base_is_explicit and (is_initial or self.base != network_base):
216 self.base = network_base
217 actual_change = True
219 if any(
220 v != self.registration_parameters.get(k)
221 for (k, v) in registration_parameters.items()
222 ):
223 self.registration_parameters.update(registration_parameters)
224 actual_change = True
226 if is_initial:
227 self._set_timeout()
228 else:
229 self.refresh_timeout()
231 if actual_change:
232 self._update_cb()
234 if self.proxy_host:
235 self._setproxyremote_cb(network_remote)
237 def delete(self):
238 self.timeout.cancel()
239 self._update_cb()
240 self._delete_cb()
242 def _set_timeout(self):
243 delay = self.lt + self.grace_period
244 # workaround for python issue20493
246 async def longwait(delay, callback):
247 await asyncio.sleep(delay)
248 callback()
250 self.timeout = asyncio.create_task(
251 longwait(delay, self.delete),
252 name="RD Timeout for %r" % self,
253 )
255 def refresh_timeout(self):
256 self.timeout.cancel()
257 self._set_timeout()
259 def get_host_link(self):
260 attr_pairs = []
261 for k, values in self.registration_parameters.items():
262 for v in values:
263 attr_pairs.append([k, v])
264 return Link(
265 href=self.href, attr_pairs=attr_pairs, base=self.base, rt="core.rd-ep"
266 )
268 def get_based_links(self):
269 """Produce a LinkFormat object that represents all statements in
270 the registration, resolved to the registration's base (and thus
271 suitable for comparing anchors)."""
272 result = []
273 for link in self.links.links:
274 href = urljoin(self.base, link.href)
275 if "anchor" in link:
276 absanchor = urljoin(self.base, link.anchor)
277 data = [(k, v) for (k, v) in link.attr_pairs if k != "anchor"] + [
278 ["anchor", absanchor]
279 ]
280 else:
281 data = link.attr_pairs + [["anchor", urljoin(href, "/")]]
282 result.append(Link(href, data))
283 return LinkFormat(result)
285 async def shutdown(self):
286 pass
288 def register_change_callback(self, callback):
289 """Ask RD to invoke the callback whenever any of the RD state
290 changed"""
291 # This has no unregister equivalent as it's only called by the lookup
292 # resources that are expected to be live for the remainder of the
293 # program, like the Registry is.
294 self._updated_state_cb.append(callback)
296 def _updated_state(self):
297 for cb in self._updated_state_cb:
298 cb()
300 def _new_pathtail(self):
301 for i in itertools.count(1):
302 # In the spirit of making legal but unconventional choices (see
303 # StandaloneResourceDirectory documentation): Whoever strips or
304 # ignores trailing slashes shall have a hard time keeping
305 # registrations alive.
306 path = (str(i), "")
307 if path not in self._by_path:
308 return path
310 def initialize_endpoint(self, network_remote, registration_parameters):
311 # copying around for later use in static, but not checking again
312 # because reading them from the original will already have screamed by
313 # the time this is used
314 static_registration_parameters = {
315 k: v
316 for (k, v) in registration_parameters.items()
317 if k in IMMUTABLE_PARAMETERS
318 }
320 ep = pop_single_arg(registration_parameters, "ep")
321 if ep is None:
322 raise error.BadRequest("ep argument missing")
323 d = pop_single_arg(registration_parameters, "d")
325 proxy = pop_single_arg(registration_parameters, "proxy")
327 if proxy is not None and proxy not in ("on", "yes", "ondemand"):
328 raise error.BadRequest("Unsupported proxy value")
330 if proxy == "on":
331 # FIXME: Deprecate more visibly -- but this has been in the impl
332 # for quite some time, and is presumably used, given that the
333 # missing "yes" went uncontested for years.
334 self.log.warning("Client uses old proprietary value proxy=on")
336 key = (ep, d)
338 if static_registration_parameters.pop("proxy", None):
339 # FIXME: 'ondemand' is done unconditionally
341 if not self.proxy_domain:
342 raise error.BadRequest("Proxying not enabled")
344 def is_usable(s):
345 # Host names per RFC1123 (which is stricter than what RFC3986 would allow).
346 #
347 # Only supporting lowercase names as to avoid ambiguities due
348 # to hostname capitalizatio normalization (otherwise it'd need
349 # to be first-registered-first-served)
350 return s and all(
351 x in string.ascii_lowercase + string.digits + "-" for x in s
352 )
354 if not is_usable(ep) or (d is not None and not is_usable(d)):
355 raise error.BadRequest(
356 "Proxying only supported for limited ep and d set (lowercase, digits, dash)"
357 )
359 proxy_host = ep
360 if d is not None:
361 proxy_host += "." + d
362 proxy_host = proxy_host + "." + self.proxy_domain
363 else:
364 proxy_host = None
366 # No more errors should fly out from below here, as side effects start now
368 try:
369 oldreg = self._by_key[key]
370 except KeyError:
371 path = self._new_pathtail()
372 else:
373 path = oldreg.path[len(self.entity_prefix) :]
374 oldreg.delete()
376 # this was the brutal way towards idempotency (delete and re-create).
377 # if any actions based on that are implemented here, they have yet to
378 # decide whether they'll treat idempotent recreations like deletions or
379 # just ignore them unless something otherwise unchangeable (ep, d)
380 # changes.
382 def delete():
383 del self._by_path[path]
384 del self._by_key[key]
385 self.proxy_active.pop(proxy_host, None)
387 def setproxyremote(remote):
388 self.proxy_active[proxy_host] = remote
390 reg = self.Registration(
391 static_registration_parameters,
392 self.entity_prefix + path,
393 network_remote,
394 delete,
395 self._updated_state,
396 registration_parameters,
397 proxy_host,
398 setproxyremote,
399 )
401 self._by_key[key] = reg
402 self._by_path[path] = reg
404 return reg
406 def get_endpoints(self):
407 return self._by_key.values()
410def link_format_from_message(message):
411 """Convert a response message into a LinkFormat object
413 This expects an explicit media type set on the response (or was explicitly requested)
414 """
415 certain_format = message.opt.content_format
416 if certain_format is None and message.request is not None:
417 certain_format = message.request.opt.accept
418 try:
419 if certain_format == ContentFormat.LINKFORMAT:
420 return parse(message.payload.decode("utf8"))
421 else:
422 raise error.UnsupportedMediaType()
423 except (UnicodeDecodeError, link_header.ParseException):
424 raise error.BadRequest()
427class ThingWithCommonRD:
428 def __init__(self, common_rd):
429 super().__init__()
430 self.common_rd = common_rd
432 if isinstance(self, ObservableResource):
433 self.common_rd.register_change_callback(self.updated_state)
436class DirectoryResource(ThingWithCommonRD, Resource):
437 ct = link_format_to_message.supported_ct # type: ignore
438 rt = "core.rd"
440 #: Issue a custom warning when registrations come in via this interface
441 registration_warning = None
443 async def render_post(self, request):
444 links = link_format_from_message(request)
446 registration_parameters = query_split(request)
448 if self.registration_warning:
449 # Conveniently placed so it could be changed to something setting
450 # additional registration_parameters instead
451 self.common_rd.log.warning(
452 "Warning from registration: %s", self.registration_warning
453 )
455 regresource = self.common_rd.initialize_endpoint(
456 request.remote, registration_parameters
457 )
458 regresource.links = links
460 return aiocoap.Message(code=aiocoap.CREATED, location_path=regresource.path)
463class RegistrationResource(Resource):
464 """The resource object wrapping a registration is just a very thin and
465 ephemeral object; all those methods could just as well be added to
466 Registration with `s/self.reg/self/g`, making RegistrationResource(reg) =
467 reg (or handleded in a single RegistrationDispatchSite), but this is kept
468 here for better separation of model and interface."""
470 def __init__(self, registration):
471 super().__init__()
472 self.reg = registration
474 async def render_get(self, request):
475 return link_format_to_message(request, self.reg.links)
477 def _update_params(self, msg):
478 query = query_split(msg)
479 self.reg.update_params(msg.remote, query)
481 async def render_post(self, request):
482 self._update_params(request)
484 if request.opt.content_format is not None or request.payload:
485 raise error.BadRequest("Registration update with body not specified")
487 return aiocoap.Message(code=aiocoap.CHANGED)
489 async def render_put(self, request):
490 # this is not mentioned in the current spec, but seems to make sense
491 links = link_format_from_message(request)
493 self._update_params(request)
494 self.reg.links = links
496 return aiocoap.Message(code=aiocoap.CHANGED)
498 async def render_delete(self, request):
499 self.reg.delete()
501 return aiocoap.Message(code=aiocoap.DELETED)
504class RegistrationDispatchSite(ThingWithCommonRD, Resource, PathCapable):
505 async def render(self, request):
506 try:
507 entity = self.common_rd._by_path[request.opt.uri_path]
508 except KeyError:
509 raise error.NotFound
511 entity = RegistrationResource(entity)
513 return await entity.render(request.copy(uri_path=()))
516def _paginate(candidates, query):
517 page = pop_single_arg(query, "page")
518 count = pop_single_arg(query, "count")
520 try:
521 candidates = list(candidates)
522 if page is not None:
523 candidates = candidates[int(page) * int(count) :]
524 if count is not None:
525 candidates = candidates[: int(count)]
526 except (KeyError, ValueError):
527 raise error.BadRequest("page requires count, and both must be ints")
529 return candidates
532def _link_matches(link, key, condition):
533 return any(k == key and condition(v) for (k, v) in link.attr_pairs)
536class EndpointLookupInterface(ThingWithCommonRD, ObservableResource):
537 ct = link_format_to_message.supported_ct # type: ignore
538 rt = "core.rd-lookup-ep"
540 async def render_get(self, request):
541 query = query_split(request)
543 candidates = self.common_rd.get_endpoints()
545 for search_key, search_values in query.items():
546 if search_key in ("page", "count"):
547 continue # filtered last
549 for search_value in search_values:
550 if search_value is not None and search_value.endswith("*"):
552 def matches(x, start=search_value[:-1]):
553 return x.startswith(start)
554 else:
556 def matches(x, search_value=search_value):
557 return x == search_value
559 if search_key in ("if", "rt"):
561 def matches(x, original_matches=matches):
562 return any(original_matches(v) for v in x.split())
564 if search_key == "href":
565 candidates = (
566 c
567 for c in candidates
568 if matches(c.href)
569 or any(matches(r.href) for r in c.get_based_links().links)
570 )
571 continue
573 candidates = (
574 c
575 for c in candidates
576 if (
577 search_key in c.registration_parameters
578 and any(
579 matches(x) for x in c.registration_parameters[search_key]
580 )
581 )
582 or any(
583 _link_matches(r, search_key, matches)
584 for r in c.get_based_links().links
585 )
586 )
588 candidates = _paginate(candidates, query)
590 result = [c.get_host_link() for c in candidates]
592 return link_format_to_message(request, LinkFormat(result))
595class ResourceLookupInterface(ThingWithCommonRD, ObservableResource):
596 ct = link_format_to_message.supported_ct # type: ignore
597 rt = "core.rd-lookup-res"
599 async def render_get(self, request):
600 query = query_split(request)
602 eps = self.common_rd.get_endpoints()
603 candidates = ((e, c) for e in eps for c in e.get_based_links().links)
605 for search_key, search_values in query.items():
606 if search_key in ("page", "count"):
607 continue # filtered last
609 for search_value in search_values:
610 if search_value is not None and search_value.endswith("*"):
612 def matches(x, start=search_value[:-1]):
613 return x.startswith(start)
614 else:
616 def matches(x, search_value=search_value):
617 return x == search_value
619 if search_key in ("if", "rt"):
621 def matches(x, original_matches=matches):
622 return any(original_matches(v) for v in x.split())
624 if search_key == "href":
625 candidates = (
626 (e, c)
627 for (e, c) in candidates
628 if matches(c.href)
629 or matches(
630 e.href
631 ) # FIXME: They SHOULD give this as relative as we do, but don't have to
632 )
633 continue
635 candidates = (
636 (e, c)
637 for (e, c) in candidates
638 if _link_matches(c, search_key, matches)
639 or (
640 search_key in e.registration_parameters
641 and any(
642 matches(x) for x in e.registration_parameters[search_key]
643 )
644 )
645 )
647 # strip endpoint
648 candidates = (c for (e, c) in candidates)
650 candidates = _paginate(candidates, query)
652 # strip needless anchors
653 candidates = [
654 Link(link.href, [(k, v) for (k, v) in link.attr_pairs if k != "anchor"])
655 if dict(link.attr_pairs)["anchor"] == urljoin(link.href, "/")
656 else link
657 for link in candidates
658 ]
660 return link_format_to_message(request, LinkFormat(candidates))
663class SimpleRegistration(ThingWithCommonRD, Resource):
664 #: Issue a custom warning when registrations come in via this interface
665 registration_warning = None
667 def __init__(self, common_rd, context):
668 super().__init__(common_rd)
669 self.context = context
671 async def render_post(self, request):
672 query = query_split(request)
674 if "base" in query:
675 raise error.BadRequest("base is not allowed in simple registrations")
677 await self.process_request(
678 network_remote=request.remote,
679 registration_parameters=query,
680 )
682 return aiocoap.Message(code=aiocoap.CHANGED)
684 async def process_request(self, network_remote, registration_parameters):
685 if "proxy" not in registration_parameters:
686 try:
687 network_base = network_remote.uri
688 except error.AnonymousHost:
689 raise error.BadRequest("explicit base required")
691 fetch_address = network_base + "/.well-known/core"
692 get = aiocoap.Message(uri=fetch_address)
693 else:
694 # ignoring that there might be a based present, that will err later
695 get = aiocoap.Message(uri_path=[".well-known", "core"])
696 get.remote = network_remote
698 get.code = aiocoap.GET
699 get.opt.accept = ContentFormat.LINKFORMAT
701 try:
702 response = await self.context.request(get).response_raising
703 except error.ResponseWrappingError as e:
704 # Note that ResponseWrappingError is *not* itself renderable
705 raise error.BadRequest(f"got error {e.coapmessage.code.dotted}")
706 # No handling needed here: This raises renderable error
707 links = link_format_from_message(response)
709 if self.registration_warning:
710 # Conveniently placed so it could be changed to something setting
711 # additional registration_parameters instead
712 self.common_rd.log.warning(
713 "Warning from registration: %s", self.registration_warning
714 )
715 registration = self.common_rd.initialize_endpoint(
716 network_remote, registration_parameters
717 )
718 registration.links = links
721class SimpleRegistrationWKC(WKCResource, SimpleRegistration):
722 def __init__(self, listgenerator, common_rd, context):
723 super().__init__(
724 listgenerator=listgenerator, common_rd=common_rd, context=context
725 )
726 self.registration_warning = "via .well-known/core"
729class StandaloneResourceDirectory(Proxy, Site):
730 """A site that contains all function sets of the CoAP Resource Directoru
732 To prevent or show ossification of example paths in the specification, all
733 function set paths are configurable and default to values that are
734 different from the specification (but still recognizable)."""
736 rd_path = ("resourcedirectory", "")
737 ep_lookup_path = ("endpoint-lookup", "")
738 res_lookup_path = ("resource-lookup", "")
740 def __init__(self, context, lwm2m_compat=None, **kwargs):
741 if lwm2m_compat is True:
742 self.rd_path = ("rd",)
744 # Double inheritance: works as everything up of Proxy has the same interface
745 super().__init__(outgoing_context=context)
747 common_rd = CommonRD(**kwargs)
749 self.add_resource(
750 [".well-known", "core"],
751 SimpleRegistrationWKC(
752 self.get_resources_as_linkheader, common_rd=common_rd, context=context
753 ),
754 )
755 self.add_resource(
756 [".well-known", "rd"],
757 SimpleRegistration(common_rd=common_rd, context=context),
758 )
760 self.add_resource(self.rd_path, DirectoryResource(common_rd=common_rd))
761 if list(self.rd_path) != ["rd"] and lwm2m_compat is None:
762 second_dir_resource = DirectoryResource(common_rd=common_rd)
763 second_dir_resource.registration_warning = "via unannounced /rd"
764 # Hide from listing
765 second_dir_resource.get_link_description = lambda *args: None
766 self.add_resource(["rd"], second_dir_resource)
767 self.add_resource(
768 self.ep_lookup_path, EndpointLookupInterface(common_rd=common_rd)
769 )
770 self.add_resource(
771 self.res_lookup_path, ResourceLookupInterface(common_rd=common_rd)
772 )
774 self.add_resource(
775 common_rd.entity_prefix, RegistrationDispatchSite(common_rd=common_rd)
776 )
778 self.common_rd = common_rd
780 def apply_redirection(self, request):
781 # Fully overriding so we don't need to set an add_redirector
783 try:
784 actual_remote = self.common_rd.proxy_active[request.opt.uri_host]
785 except KeyError:
786 self.common_rd.log.info(
787 "Request to proxied host %r rejected: No such registration",
788 request.opt.uri_host,
789 )
790 raise NoActiveRegistration
791 self.common_rd.log.debug(
792 "Forwarding request to %r to remote %s", request.opt.uri_host, actual_remote
793 )
794 request.remote = actual_remote
795 request.opt.uri_host = None
796 return request
798 async def shutdown(self):
799 await self.common_rd.shutdown()
801 async def render(self, request):
802 # Full override switching which of the parents' behavior to choose
804 if (
805 self.common_rd.proxy_domain is not None
806 and request.opt.uri_host is not None
807 and request.opt.uri_host.endswith("." + self.common_rd.proxy_domain)
808 ): # in self.common_rd.proxy_active:
809 return await Proxy.render(self, request)
810 else:
811 return await Site.render(self, request)
813 # See render; necessary on all functions thanks to https://github.com/chrysn/aiocoap/issues/251
815 async def needs_blockwise_assembly(self, request):
816 if request.opt.uri_host in self.common_rd.proxy_active:
817 return await Proxy.needs_blockwise_assembly(self, request)
818 else:
819 return await Site.needs_blockwise_assembly(self, request)
821 async def add_observation(self, request, serverobservation):
822 if request.opt.uri_host in self.common_rd.proxy_active:
823 return await Proxy.add_observation(self, request, serverobservation)
824 else:
825 return await Site.add_observation(self, request, serverobservation)
828def build_parser():
829 p = argparse.ArgumentParser(description=__doc__)
831 add_server_arguments(p)
833 p.add_argument(
834 "--version", action="version", version="%(prog)s " + aiocoap.meta.version
835 )
836 p.add_argument(
837 "--proxy-domain",
838 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.",
839 type=str,
840 )
841 p.add_argument(
842 "--lwm2m-compat",
843 help="Compatibility mode for LwM2M clients that can not perform some discovery steps (moving the registration resource to `/rd`)",
844 action="store_true",
845 default=None,
846 )
847 p.add_argument(
848 "--no-lwm2m-compat",
849 help="Disable all compatibility with LwM2M clients that can not perform some discovery steps (not even accepting registrations at `/rd` with warnings)",
850 action="store_false",
851 dest="lwm2m_compat",
852 )
853 p.add_argument(
854 "--verbose",
855 help="Increase debug log output (repeat for increased verbosity)",
856 action="count",
857 default=0,
858 )
860 return p
863class Main(AsyncCLIDaemon):
864 async def start(self, args=None):
865 parser = build_parser()
866 options = parser.parse_args(args if args is not None else sys.argv[1:])
868 # Putting in an empty site to construct the site with a context
869 self.context = await server_context_from_arguments(None, options)
871 self.log = logging.getLogger("resource-directory")
872 if options.verbose >= 2:
873 self.log.setLevel(logging.DEBUG)
874 elif options.verbose == 1:
875 self.log.setLevel(logging.INFO)
877 self.site = StandaloneResourceDirectory(
878 context=self.context,
879 proxy_domain=options.proxy_domain,
880 lwm2m_compat=options.lwm2m_compat,
881 log=self.log,
882 )
883 self.context.serversite = self.site
885 async def shutdown(self):
886 await self.site.shutdown()
887 await self.context.shutdown()
890sync_main = Main.sync_main
892if __name__ == "__main__":
893 sync_main()