Coverage for aiocoap/cli/proxy.py: 67%
92 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 proxy that can work both as forward and as reverse proxy"""
7import sys
8import argparse
10import aiocoap
11from aiocoap.proxy.server import (
12 ForwardProxyWithPooledObservations,
13 ProxyWithPooledObservations,
14 NameBasedVirtualHost,
15 SubdomainVirtualHost,
16 SubresourceVirtualHost,
17 UnconditionalRedirector,
18)
19from aiocoap.util.cli import AsyncCLIDaemon
20from aiocoap.cli.common import add_server_arguments, server_context_from_arguments
23def build_parser():
24 p = argparse.ArgumentParser(description=__doc__)
26 mode = p.add_argument_group(
27 "mode", "Required argument for setting the operation mode"
28 )
29 mode.add_argument("--forward", help="Run as forward proxy", action="store_true")
30 mode.add_argument("--reverse", help="Run as reverse proxy", action="store_true")
32 details = p.add_argument_group(
33 "details", "Options that govern how requests go in and out"
34 )
35 add_server_arguments(details)
36 details.add_argument(
37 "--register",
38 help="Register with a Resource directory",
39 metavar="RD-URI",
40 nargs="?",
41 default=False,
42 )
43 details.add_argument(
44 "--register-as",
45 help="Endpoint name (with possibly a domain after a dot) to register as",
46 metavar="EP[.D]",
47 default=None,
48 )
49 details.add_argument(
50 "--register-proxy",
51 help="Ask the RD to serve as a reverse proxy. Note that this is only practical for --unconditional or --pathbased reverse proxies.",
52 action="store_true",
53 )
55 r = p.add_argument_group(
56 "Rules",
57 description="Sequence of forwarding rules "
58 "that, if matched by a request, specify a forwarding destination. Destinations can be prefixed to change their behavior: With an '@' sign, they are treated as forward proxies. With a '!' sign, the destination is set as Uri-Host.",
59 )
61 class TypedAppend(argparse.Action):
62 def __call__(self, parser, namespace, values, option_string=None):
63 if getattr(namespace, self.dest) is None:
64 setattr(namespace, self.dest, [])
65 getattr(namespace, self.dest).append((option_string, values))
67 r.add_argument(
68 "--namebased",
69 help="If Uri-Host matches NAME, route to DEST",
70 metavar="NAME:DEST",
71 action=TypedAppend,
72 dest="r",
73 )
74 r.add_argument(
75 "--subdomainbased",
76 help="If Uri-Host is anything.NAME, route to DEST",
77 metavar="NAME:DEST",
78 action=TypedAppend,
79 dest="r",
80 )
81 r.add_argument(
82 "--pathbased",
83 help="If a requested path starts with PATH, split that part off and route to DEST",
84 metavar="PATH:DEST",
85 action=TypedAppend,
86 dest="r",
87 )
88 r.add_argument(
89 "--unconditional",
90 help="Route all requests not previously matched to DEST",
91 metavar="DEST",
92 action=TypedAppend,
93 dest="r",
94 )
96 return p
99def destsplit(dest):
100 use_as_proxy = False
101 rewrite_uri_host = False
102 if dest.startswith("!"):
103 dest = dest[1:]
104 rewrite_uri_host = True
105 if dest.startswith("@"):
106 dest = dest[1:]
107 use_as_proxy = True
108 return dest, rewrite_uri_host, use_as_proxy
111class Main(AsyncCLIDaemon):
112 async def start(self, args=None):
113 parser = build_parser()
114 options = parser.parse_args(args if args is not None else sys.argv[1:])
115 self.options = options
117 if not options.forward and not options.reverse:
118 raise parser.error("At least one of --forward and --reverse must be given.")
120 self.outgoing_context = await aiocoap.Context.create_client_context()
121 if options.forward:
122 proxy = ForwardProxyWithPooledObservations(self.outgoing_context)
123 else:
124 proxy = ProxyWithPooledObservations(self.outgoing_context)
125 for kind, data in options.r or ():
126 if kind in ("--namebased", "--subdomainbased"):
127 try:
128 name, dest = data.split(":", 1)
129 except Exception:
130 raise parser.error("%s needs NAME:DEST as arguments" % kind)
131 dest, rewrite_uri_host, use_as_proxy = destsplit(dest)
132 if rewrite_uri_host and kind == "--subdomainbased":
133 parser.error(
134 "The flag '!' makes no sense for subdomain based redirection as the subdomain data would be lost"
135 )
136 r = (
137 NameBasedVirtualHost
138 if kind == "--namebased"
139 else SubdomainVirtualHost
140 )(name, dest, rewrite_uri_host, use_as_proxy)
141 elif kind == "--pathbased":
142 try:
143 path, dest = data.split(":", 1)
144 except Exception:
145 raise parser.error("--pathbased needs PATH:DEST as arguments")
146 r = SubresourceVirtualHost(path.split("/"), dest)
147 elif kind == "--unconditional":
148 dest, rewrite_uri_host, use_as_proxy = destsplit(data)
149 if rewrite_uri_host:
150 parser.error(
151 "The flag '!' makes no sense for unconditional redirection as the host name data would be lost"
152 )
153 r = UnconditionalRedirector(dest, use_as_proxy)
154 else:
155 raise AssertionError("Unknown redirectory kind")
156 proxy.add_redirector(r)
158 self.proxy_context = await server_context_from_arguments(proxy, options)
160 if options.register is not False:
161 from aiocoap.resourcedirectory.client.register import Registerer
163 params = {}
164 if options.register_as:
165 ep, _, d = options.register_as.partition(".")
166 params["ep"] = ep
167 if d:
168 params["d"] = d
169 if options.register_proxy:
170 # FIXME: Check this in discovery
171 params["proxy"] = "on"
172 # FIXME: Construct this from settings (path-based), and forward results
173 proxy.get_resources_as_linkheader = lambda: ""
174 self.registerer = Registerer(
175 self.proxy_context,
176 rd=options.register,
177 lt=60,
178 registration_parameters=params,
179 )
181 async def shutdown(self):
182 if self.options.register is not False:
183 await self.registerer.shutdown()
184 await self.outgoing_context.shutdown()
185 await self.proxy_context.shutdown()
188sync_main = Main.sync_main
190if __name__ == "__main__":
191 # if you want to run this using `python3 -m`, see http://bugs.python.org/issue22480
192 sync_main()