Coverage for aiocoap/cli/common.py: 66%
59 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#!/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 ..util import hostportsplit
35from ..protocol import Context
36from ..credentials import CredentialsMap
39class _HelpBind(argparse.Action):
40 def __init__(self, *args, **kwargs):
41 kwargs["nargs"] = 0
42 super().__init__(*args, **kwargs)
44 def __call__(self, parser, namespace, values, option_string=None):
45 print(
46 "The --bind option can take either of the following formats:"
47 "\n :port -- bind to a given port on all available interfaces"
48 "\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)"
49 "\n host:port -- bind only to a specific port on a given host"
50 "\n\nBy default, the server will bind to all available addressess and protocols on the respective default ports."
51 "\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)."
52 "\n",
53 file=sys.stderr,
54 )
55 parser.exit()
58def add_server_arguments(parser):
59 """Add the --bind option to an argparse parser"""
61 def hostportsplit_helper(arg):
62 """Wrapper around hostportsplit that gives better error messages than
63 'invalid hostportsplit value'"""
65 if arg.isnumeric():
66 raise parser.error(
67 f"Invalid argument to --bind. Did you mean --bind :{arg}?"
68 )
70 try:
71 return hostportsplit(arg)
72 except ValueError:
73 raise parser.error(
74 f"Invalid argument to --bind. Did you mean --bind '[{arg}]'?"
75 if arg.count(":") >= 2 and "[" not in arg
76 else " See --help-bind for details."
77 )
79 parser.add_argument(
80 "--bind",
81 help="Host and/or port to bind to (see --help-bind for details)",
82 type=hostportsplit_helper,
83 default=None,
84 )
86 parser.add_argument(
87 "--credentials",
88 help="JSON file pointing to credentials for the server's identity/ies.",
89 type=Path,
90 )
92 # These are to be eventually migrated into credentials
93 parser.add_argument(
94 "--tls-server-certificate",
95 help="TLS certificate (chain) to present to connecting clients (in PEM format)",
96 metavar="CRT",
97 )
98 parser.add_argument(
99 "--tls-server-key",
100 help="TLS key to load that supports the server certificate",
101 metavar="KEY",
102 )
104 parser.add_argument("--help-bind", help=argparse.SUPPRESS, action=_HelpBind)
107def extract_server_arguments(namespace):
108 """Given the output of .parse() on a ArgumentParser that had
109 add_server_arguments called with it, remove the resulting option in-place
110 from namespace and return them in a separate namespace."""
112 server_arguments = type(namespace)()
113 server_arguments.bind = namespace.bind
114 server_arguments.tls_server_certificate = namespace.tls_server_certificate
115 server_arguments.tls_server_key = namespace.tls_server_key
116 server_arguments.credentials = namespace.credentials
118 del namespace.bind
119 del namespace.tls_server_certificate
120 del namespace.tls_server_key
121 del namespace.credentials
122 del namespace.help_bind
124 return server_arguments
127async def server_context_from_arguments(site, namespace, **kwargs):
128 """Create a bound context like
129 :meth:`.aiocoap.Context.create_server_context`, but take the bind and TLS
130 settings from a namespace returned from an argparse parser that has had
131 :func:`add_server_arguments` run on it.
132 """
134 if namespace.tls_server_certificate:
135 import ssl
137 ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
138 ssl_context.load_cert_chain(
139 certfile=namespace.tls_server_certificate, keyfile=namespace.tls_server_key
140 )
141 ssl_context.set_alpn_protocols(["coap"])
142 ssl_context.sni_callback = lambda obj, name, context: setattr(
143 obj, "indicated_server_name", name
144 )
145 else:
146 ssl_context = None
148 if namespace.credentials:
149 server_credentials = CredentialsMap()
150 try:
151 import cbor2
152 import cbor_diag
154 server_credentials.load_from_dict(
155 cbor2.loads(cbor_diag.diag2cbor(namespace.credentials.open().read()))
156 )
157 except ImportError:
158 import json
160 server_credentials.load_from_dict(
161 json.load(namespace.credentials.open("rb"))
162 )
164 # FIXME: could be non-OSCORE as well -- can we build oscore_sitewrapper
165 # in such a way it only depends on the OSCORE dependencies if there are
166 # actual identities present?
167 from aiocoap.oscore_sitewrapper import OscoreSiteWrapper
169 site = OscoreSiteWrapper(site, server_credentials)
170 else:
171 server_credentials = None
173 return await Context.create_server_context(
174 site,
175 namespace.bind,
176 _ssl_context=ssl_context,
177 server_credentials=server_credentials,
178 **kwargs,
179 )