Coverage for aiocoap/cli/rd.py: 56%
416 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-11-28 12:34 +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 whereever
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 unconvential 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 != "on":
328 raise error.BadRequest("Unsupported proxy value")
330 key = (ep, d)
332 if static_registration_parameters.pop("proxy", None):
333 # FIXME: 'ondemand' is done unconditionally
335 if not self.proxy_domain:
336 raise error.BadRequest("Proxying not enabled")
338 def is_usable(s):
339 # Host names per RFC1123 (which is stricter than what RFC3986 would allow).
340 #
341 # Only supporting lowercase names as to avoid ambiguities due
342 # to hostname capitalizatio normalization (otherwise it'd need
343 # to be first-registered-first-served)
344 return s and all(
345 x in string.ascii_lowercase + string.digits + "-" for x in s
346 )
348 if not is_usable(ep) or (d is not None and not is_usable(d)):
349 raise error.BadRequest(
350 "Proxying only supported for limited ep and d set (lowercase, digits, dash)"
351 )
353 proxy_host = ep
354 if d is not None:
355 proxy_host += "." + d
356 proxy_host = proxy_host + "." + self.proxy_domain
357 else:
358 proxy_host = None
360 # No more errors should fly out from below here, as side effects start now
362 try:
363 oldreg = self._by_key[key]
364 except KeyError:
365 path = self._new_pathtail()
366 else:
367 path = oldreg.path[len(self.entity_prefix) :]
368 oldreg.delete()
370 # this was the brutal way towards idempotency (delete and re-create).
371 # if any actions based on that are implemented here, they have yet to
372 # decide wheter they'll treat idempotent recreations like deletions or
373 # just ignore them unless something otherwise unchangeable (ep, d)
374 # changes.
376 def delete():
377 del self._by_path[path]
378 del self._by_key[key]
379 self.proxy_active.pop(proxy_host, None)
381 def setproxyremote(remote):
382 self.proxy_active[proxy_host] = remote
384 reg = self.Registration(
385 static_registration_parameters,
386 self.entity_prefix + path,
387 network_remote,
388 delete,
389 self._updated_state,
390 registration_parameters,
391 proxy_host,
392 setproxyremote,
393 )
395 self._by_key[key] = reg
396 self._by_path[path] = reg
398 return reg
400 def get_endpoints(self):
401 return self._by_key.values()
404def link_format_from_message(message):
405 """Convert a response message into a LinkFormat object
407 This expects an explicit media type set on the response (or was explicitly requested)
408 """
409 certain_format = message.opt.content_format
410 if certain_format is None and hasattr(message, "request"):
411 certain_format = message.request.opt.accept
412 try:
413 if certain_format == ContentFormat.LINKFORMAT:
414 return parse(message.payload.decode("utf8"))
415 else:
416 raise error.UnsupportedMediaType()
417 except (UnicodeDecodeError, link_header.ParseException):
418 raise error.BadRequest()
421class ThingWithCommonRD:
422 def __init__(self, common_rd):
423 super().__init__()
424 self.common_rd = common_rd
426 if isinstance(self, ObservableResource):
427 self.common_rd.register_change_callback(self.updated_state)
430class DirectoryResource(ThingWithCommonRD, Resource):
431 ct = link_format_to_message.supported_ct # type: ignore
432 rt = "core.rd"
434 #: Issue a custom warning when registrations come in via this interface
435 registration_warning = None
437 async def render_post(self, request):
438 links = link_format_from_message(request)
440 registration_parameters = query_split(request)
442 if self.registration_warning:
443 # Conveniently placed so it could be changed to something setting
444 # additional registration_parameters instead
445 self.common_rd.log.warning(
446 "Warning from registration: %s", self.registration_warning
447 )
449 regresource = self.common_rd.initialize_endpoint(
450 request.remote, registration_parameters
451 )
452 regresource.links = links
454 return aiocoap.Message(code=aiocoap.CREATED, location_path=regresource.path)
457class RegistrationResource(Resource):
458 """The resource object wrapping a registration is just a very thin and
459 ephemeral object; all those methods could just as well be added to
460 Registration with `s/self.reg/self/g`, making RegistrationResource(reg) =
461 reg (or handleded in a single RegistrationDispatchSite), but this is kept
462 here for better separation of model and interface."""
464 def __init__(self, registration):
465 super().__init__()
466 self.reg = registration
468 async def render_get(self, request):
469 return link_format_to_message(request, self.reg.links)
471 def _update_params(self, msg):
472 query = query_split(msg)
473 self.reg.update_params(msg.remote, query)
475 async def render_post(self, request):
476 self._update_params(request)
478 if request.opt.content_format is not None or request.payload:
479 raise error.BadRequest("Registration update with body not specified")
481 return aiocoap.Message(code=aiocoap.CHANGED)
483 async def render_put(self, request):
484 # this is not mentioned in the current spec, but seems to make sense
485 links = link_format_from_message(request)
487 self._update_params(request)
488 self.reg.links = links
490 return aiocoap.Message(code=aiocoap.CHANGED)
492 async def render_delete(self, request):
493 self.reg.delete()
495 return aiocoap.Message(code=aiocoap.DELETED)
498class RegistrationDispatchSite(ThingWithCommonRD, Resource, PathCapable):
499 async def render(self, request):
500 try:
501 entity = self.common_rd._by_path[request.opt.uri_path]
502 except KeyError:
503 raise error.NotFound
505 entity = RegistrationResource(entity)
507 return await entity.render(request.copy(uri_path=()))
510def _paginate(candidates, query):
511 page = pop_single_arg(query, "page")
512 count = pop_single_arg(query, "count")
514 try:
515 candidates = list(candidates)
516 if page is not None:
517 candidates = candidates[int(page) * int(count) :]
518 if count is not None:
519 candidates = candidates[: int(count)]
520 except (KeyError, ValueError):
521 raise error.BadRequest("page requires count, and both must be ints")
523 return candidates
526def _link_matches(link, key, condition):
527 return any(k == key and condition(v) for (k, v) in link.attr_pairs)
530class EndpointLookupInterface(ThingWithCommonRD, ObservableResource):
531 ct = link_format_to_message.supported_ct # type: ignore
532 rt = "core.rd-lookup-ep"
534 async def render_get(self, request):
535 query = query_split(request)
537 candidates = self.common_rd.get_endpoints()
539 for search_key, search_values in query.items():
540 if search_key in ("page", "count"):
541 continue # filtered last
543 for search_value in search_values:
544 if search_value is not None and search_value.endswith("*"):
546 def matches(x, start=search_value[:-1]):
547 return x.startswith(start)
548 else:
550 def matches(x, search_value=search_value):
551 return x == search_value
553 if search_key in ("if", "rt"):
555 def matches(x, original_matches=matches):
556 return any(original_matches(v) for v in x.split())
558 if search_key == "href":
559 candidates = (
560 c
561 for c in candidates
562 if matches(c.href)
563 or any(matches(r.href) for r in c.get_based_links().links)
564 )
565 continue
567 candidates = (
568 c
569 for c in candidates
570 if (
571 search_key in c.registration_parameters
572 and any(
573 matches(x) for x in c.registration_parameters[search_key]
574 )
575 )
576 or any(
577 _link_matches(r, search_key, matches)
578 for r in c.get_based_links().links
579 )
580 )
582 candidates = _paginate(candidates, query)
584 result = [c.get_host_link() for c in candidates]
586 return link_format_to_message(request, LinkFormat(result))
589class ResourceLookupInterface(ThingWithCommonRD, ObservableResource):
590 ct = link_format_to_message.supported_ct # type: ignore
591 rt = "core.rd-lookup-res"
593 async def render_get(self, request):
594 query = query_split(request)
596 eps = self.common_rd.get_endpoints()
597 candidates = ((e, c) for e in eps for c in e.get_based_links().links)
599 for search_key, search_values in query.items():
600 if search_key in ("page", "count"):
601 continue # filtered last
603 for search_value in search_values:
604 if search_value is not None and search_value.endswith("*"):
606 def matches(x, start=search_value[:-1]):
607 return x.startswith(start)
608 else:
610 def matches(x, search_value=search_value):
611 return x == search_value
613 if search_key in ("if", "rt"):
615 def matches(x, original_matches=matches):
616 return any(original_matches(v) for v in x.split())
618 if search_key == "href":
619 candidates = (
620 (e, c)
621 for (e, c) in candidates
622 if matches(c.href)
623 or matches(
624 e.href
625 ) # FIXME: They SHOULD give this as relative as we do, but don't have to
626 )
627 continue
629 candidates = (
630 (e, c)
631 for (e, c) in candidates
632 if _link_matches(c, search_key, matches)
633 or (
634 search_key in e.registration_parameters
635 and any(
636 matches(x) for x in e.registration_parameters[search_key]
637 )
638 )
639 )
641 # strip endpoint
642 candidates = (c for (e, c) in candidates)
644 candidates = _paginate(candidates, query)
646 # strip needless anchors
647 candidates = [
648 Link(link.href, [(k, v) for (k, v) in link.attr_pairs if k != "anchor"])
649 if dict(link.attr_pairs)["anchor"] == urljoin(link.href, "/")
650 else link
651 for link in candidates
652 ]
654 return link_format_to_message(request, LinkFormat(candidates))
657class SimpleRegistration(ThingWithCommonRD, Resource):
658 #: Issue a custom warning when registrations come in via this interface
659 registration_warning = None
661 def __init__(self, common_rd, context):
662 super().__init__(common_rd)
663 self.context = context
665 async def render_post(self, request):
666 query = query_split(request)
668 if "base" in query:
669 raise error.BadRequest("base is not allowed in simple registrations")
671 await self.process_request(
672 network_remote=request.remote,
673 registration_parameters=query,
674 )
676 return aiocoap.Message(code=aiocoap.CHANGED)
678 async def process_request(self, network_remote, registration_parameters):
679 if "proxy" not in registration_parameters:
680 try:
681 network_base = network_remote.uri
682 except error.AnonymousHost:
683 raise error.BadRequest("explicit base required")
685 fetch_address = network_base + "/.well-known/core"
686 get = aiocoap.Message(uri=fetch_address)
687 else:
688 # ignoring that there might be a based present, that will err later
689 get = aiocoap.Message(uri_path=[".well-known", "core"])
690 get.remote = network_remote
692 get.code = aiocoap.GET
693 get.opt.accept = ContentFormat.LINKFORMAT
695 # not trying to catch anything here -- the errors are most likely well renderable into the final response
696 response = await self.context.request(get).response_raising
697 links = link_format_from_message(response)
699 if self.registration_warning:
700 # Conveniently placed so it could be changed to something setting
701 # additional registration_parameters instead
702 self.common_rd.log.warning(
703 "Warning from registration: %s", self.registration_warning
704 )
705 registration = self.common_rd.initialize_endpoint(
706 network_remote, registration_parameters
707 )
708 registration.links = links
711class SimpleRegistrationWKC(WKCResource, SimpleRegistration):
712 def __init__(self, listgenerator, common_rd, context):
713 super().__init__(
714 listgenerator=listgenerator, common_rd=common_rd, context=context
715 )
716 self.registration_warning = "via .well-known/core"
719class StandaloneResourceDirectory(Proxy, Site):
720 """A site that contains all function sets of the CoAP Resource Directoru
722 To prevent or show ossification of example paths in the specification, all
723 function set paths are configurable and default to values that are
724 different from the specification (but still recognizable)."""
726 rd_path = ("resourcedirectory", "")
727 ep_lookup_path = ("endpoint-lookup", "")
728 res_lookup_path = ("resource-lookup", "")
730 def __init__(self, context, lwm2m_compat=None, **kwargs):
731 if lwm2m_compat is True:
732 self.rd_path = ("rd",)
734 # Double inheritance: works as everything up of Proxy has the same interface
735 super().__init__(outgoing_context=context)
737 common_rd = CommonRD(**kwargs)
739 self.add_resource(
740 [".well-known", "core"],
741 SimpleRegistrationWKC(
742 self.get_resources_as_linkheader, common_rd=common_rd, context=context
743 ),
744 )
745 self.add_resource(
746 [".well-known", "rd"],
747 SimpleRegistration(common_rd=common_rd, context=context),
748 )
750 self.add_resource(self.rd_path, DirectoryResource(common_rd=common_rd))
751 if list(self.rd_path) != ["rd"] and lwm2m_compat is None:
752 second_dir_resource = DirectoryResource(common_rd=common_rd)
753 second_dir_resource.registration_warning = "via unannounced /rd"
754 # Hide from listing
755 second_dir_resource.get_link_description = lambda *args: None
756 self.add_resource(["rd"], second_dir_resource)
757 self.add_resource(
758 self.ep_lookup_path, EndpointLookupInterface(common_rd=common_rd)
759 )
760 self.add_resource(
761 self.res_lookup_path, ResourceLookupInterface(common_rd=common_rd)
762 )
764 self.add_resource(
765 common_rd.entity_prefix, RegistrationDispatchSite(common_rd=common_rd)
766 )
768 self.common_rd = common_rd
770 def apply_redirection(self, request):
771 # Fully overriding so we don't need to set an add_redirector
773 try:
774 actual_remote = self.common_rd.proxy_active[request.opt.uri_host]
775 except KeyError:
776 self.common_rd.log.info(
777 "Request to proxied host %r rejected: No such registration",
778 request.opt.uri_host,
779 )
780 raise NoActiveRegistration
781 self.common_rd.log.debug(
782 "Forwarding request to %r to remote %s", request.opt.uri_host, actual_remote
783 )
784 request.remote = actual_remote
785 request.opt.uri_host = None
786 return request
788 async def shutdown(self):
789 await self.common_rd.shutdown()
791 async def render(self, request):
792 # Full override switching which of the parents' behavior to choose
794 if (
795 self.common_rd.proxy_domain is not None
796 and request.opt.uri_host is not None
797 and request.opt.uri_host.endswith("." + self.common_rd.proxy_domain)
798 ): # in self.common_rd.proxy_active:
799 return await Proxy.render(self, request)
800 else:
801 return await Site.render(self, request)
803 # See render; necessary on all functions thanks to https://github.com/chrysn/aiocoap/issues/251
805 async def needs_blockwise_assembly(self, request):
806 if request.opt.uri_host in self.common_rd.proxy_active:
807 return await Proxy.needs_blockwise_assembly(self, request)
808 else:
809 return await Site.needs_blockwise_assembly(self, request)
811 async def add_observation(self, request, serverobservation):
812 if request.opt.uri_host in self.common_rd.proxy_active:
813 return await Proxy.add_observation(self, request, serverobservation)
814 else:
815 return await Site.add_observation(self, request, serverobservation)
818def build_parser():
819 p = argparse.ArgumentParser(description=__doc__)
821 add_server_arguments(p)
823 return p
826class Main(AsyncCLIDaemon):
827 async def start(self, args=None):
828 parser = build_parser()
829 parser.add_argument(
830 "--proxy-domain",
831 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.",
832 type=str,
833 )
834 parser.add_argument(
835 "--lwm2m-compat",
836 help="Compatibility mode for LwM2M clients that can not perform some discovery steps (moving the registration resource to `/rd`)",
837 action="store_true",
838 default=None,
839 )
840 parser.add_argument(
841 "--no-lwm2m-compat",
842 help="Disable all compativility with LwM2M clients that can not perform some discovery steps (not even accepting registrations at `/rd` with warnings)",
843 action="store_false",
844 dest="lwm2m_compat",
845 )
846 parser.add_argument(
847 "--verbose",
848 help="Increase debug log output (repeat for increased verbosity)",
849 action="count",
850 default=0,
851 )
852 options = parser.parse_args(args if args is not None else sys.argv[1:])
854 # Putting in an empty site to construct the site with a context
855 self.context = await server_context_from_arguments(None, options)
857 self.log = logging.getLogger("resource-directory")
858 if options.verbose >= 2:
859 self.log.setLevel(logging.DEBUG)
860 elif options.verbose == 1:
861 self.log.setLevel(logging.INFO)
863 self.site = StandaloneResourceDirectory(
864 context=self.context,
865 proxy_domain=options.proxy_domain,
866 lwm2m_compat=options.lwm2m_compat,
867 log=self.log,
868 )
869 self.context.serversite = self.site
871 async def shutdown(self):
872 await self.site.shutdown()
873 await self.context.shutdown()
876sync_main = Main.sync_main
878if __name__ == "__main__":
879 sync_main()