Coverage for aiocoap / cli / common.py: 64%
70 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-17 12:28 +0000
1#!/usr/bin/env python3
3# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors
4#
5# SPDX-License-Identifier: MIT
7"""Common options of aiocoap command line utilities
9Unlike those in :mod:`aiocoap.util.cli`, these are particular to aiocoap
10functionality.
12Typical use is like this::
14>>> p = argparse.ArgumentParser()
15>>> p.add_argument('--foo') # doctest: +ELLIPSIS
16_...
17>>> add_server_arguments(p)
18>>> opts = p.parse_args(['--bind', '[::1]:56830', '--foo=bar'])
20You can then either pass opts directly to
21:func:`server_context_from_arguments`, or split up the arguments::
23>>> server_opts = extract_server_arguments(opts)
24>>> opts
25Namespace(foo='bar')
27Then, server_opts can be passed to `server_context_from_arguments`.
28"""
30import sys
31import argparse
32from pathlib import Path
34from ..config import Config
35from ..util import hostportsplit
36from ..protocol import Context
37from ..credentials import CredentialsMap
40class _HelpBind(argparse.Action):
41 def __init__(self, *args, **kwargs):
42 kwargs["nargs"] = 0
43 super().__init__(*args, **kwargs)
45 def __call__(self, parser, namespace, values, option_string=None):
46 print(
47 "The --bind option can take either of the following formats:"
48 "\n :port -- bind to a given port on all available interfaces"
49 "\n host -- bind to default ports on a given host name (can also be an IP address; IPv6 addresses need to be in square brackets)"
50 "\n host:port -- bind only to a specific port on a given host"
51 "\n\nBy default, the server will bind to all available addresses and protocols on the respective default ports."
52 "\nIf a port is specified, and (D)TLS support is available, those protocols will be bound to one port higher (as are the default ports, 5683 for CoAP and 5684 for CoAP over (D)TLS)."
53 "\n",
54 file=sys.stderr,
55 )
56 parser.exit()
59def add_server_arguments(parser):
60 """Add the --bind and similar options to an argparse parser"""
62 def hostportsplit_helper(arg):
63 """Wrapper around hostportsplit that gives better error messages than
64 'invalid hostportsplit value'"""
66 if arg.isnumeric():
67 raise parser.error(
68 f"Invalid argument to --bind. Did you mean --bind :{arg}?"
69 )
71 try:
72 return hostportsplit(arg)
73 except ValueError:
74 raise parser.error(
75 f"Invalid argument to --bind. Did you mean --bind '[{arg}]'?"
76 if arg.count(":") >= 2 and "[" not in arg
77 else " See --help-bind for details."
78 )
80 parser.add_argument(
81 "--server-config",
82 help="Configuration file to load",
83 type=Path,
84 )
86 parser.add_argument(
87 "--bind",
88 help="Host and/or port to bind to (see --help-bind for details)",
89 type=hostportsplit_helper,
90 default=None,
91 )
93 parser.add_argument(
94 "--credentials",
95 help="JSON file pointing to credentials for the server's identity/ies.",
96 type=Path,
97 )
99 # These are to be eventually migrated into credentials
100 parser.add_argument(
101 "--tls-server-certificate",
102 help="TLS certificate (chain) to present to connecting clients (in PEM format)",
103 metavar="CRT",
104 )
105 parser.add_argument(
106 "--tls-server-key",
107 help="TLS key to load that supports the server certificate",
108 metavar="KEY",
109 )
111 parser.add_argument("--help-bind", help=argparse.SUPPRESS, action=_HelpBind)
114def extract_server_arguments(namespace):
115 """Given the output of .parse() on a ArgumentParser that had
116 add_server_arguments called with it, remove the resulting option in-place
117 from namespace and return them in a separate namespace."""
119 server_arguments = type(namespace)()
120 server_arguments.server_config = namespace.server_config
121 server_arguments.bind = namespace.bind
122 server_arguments.tls_server_certificate = namespace.tls_server_certificate
123 server_arguments.tls_server_key = namespace.tls_server_key
124 server_arguments.credentials = namespace.credentials
126 del namespace.server_config
127 del namespace.bind
128 del namespace.tls_server_certificate
129 del namespace.tls_server_key
130 del namespace.credentials
131 del namespace.help_bind
133 return server_arguments
136async def server_context_from_arguments(site, namespace, **kwargs):
137 """Create a bound context like
138 :meth:`.aiocoap.Context.create_server_context`, but take the bind and TLS
139 settings from a namespace returned from an argparse parser that has had
140 :func:`add_server_arguments` run on it.
141 """
143 if namespace.server_config:
144 try:
145 config = Config.load_from_file(namespace.server_config)
146 except ValueError as e:
147 # FIXME: We should pass the argument parser in so that we can err out through that.
148 print(
149 f"Error loading file {namespace.server_config!r}: {e}", file=sys.stderr
150 )
151 sys.exit(1)
152 else:
153 config = Config()
155 if namespace.tls_server_certificate:
156 import ssl
158 ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
159 ssl_context.load_cert_chain(
160 certfile=namespace.tls_server_certificate, keyfile=namespace.tls_server_key
161 )
162 ssl_context.set_alpn_protocols(["coap"])
163 ssl_context.sni_callback = lambda obj, name, context: setattr(
164 obj, "indicated_server_name", name
165 )
166 else:
167 ssl_context = None
169 if namespace.credentials:
170 server_credentials = CredentialsMap()
171 try:
172 import cbor2
173 import cbor_diag
175 server_credentials.load_from_dict(
176 cbor2.loads(cbor_diag.diag2cbor(namespace.credentials.open().read()))
177 )
178 except ImportError:
179 import json
181 server_credentials.load_from_dict(
182 json.load(namespace.credentials.open("rb"))
183 )
185 # FIXME: could be non-OSCORE as well -- can we build oscore_sitewrapper
186 # in such a way it only depends on the OSCORE dependencies if there are
187 # actual identities present?
188 from aiocoap.oscore_sitewrapper import OscoreSiteWrapper
190 site = OscoreSiteWrapper(site, server_credentials)
191 else:
192 server_credentials = None
194 return await Context.create_server_context(
195 site,
196 namespace.bind,
197 _ssl_context=ssl_context,
198 server_credentials=server_credentials,
199 transports=config.transport,
200 **kwargs,
201 )