Coverage for aiocoap/cli/proxy.py: 68%

93 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-16 16:09 +0000

1# SPDX-FileCopyrightText: Christian Amsüss and the aiocoap contributors 

2# 

3# SPDX-License-Identifier: MIT 

4 

5"""a plain CoAP proxy that can work both as forward and as reverse proxy""" 

6 

7import sys 

8import argparse 

9 

10import aiocoap 

11from aiocoap.proxy.server import ForwardProxyWithPooledObservations, ProxyWithPooledObservations, NameBasedVirtualHost, SubdomainVirtualHost, SubresourceVirtualHost, UnconditionalRedirector 

12from aiocoap.util.cli import AsyncCLIDaemon 

13from aiocoap.cli.common import add_server_arguments, server_context_from_arguments 

14 

15def build_parser(): 

16 p = argparse.ArgumentParser(description=__doc__) 

17 

18 mode = p.add_argument_group("mode", "Required argument for setting the operation mode") 

19 mode.add_argument('--forward', help="Run as forward proxy", action='store_true') 

20 mode.add_argument('--reverse', help="Run as reverse proxy", action='store_true') 

21 

22 details = p.add_argument_group("details", "Options that govern how requests go in and out") 

23 add_server_arguments(details) 

24 details.add_argument("--register", help="Register with a Resource directory", metavar='RD-URI', nargs='?', default=False) 

25 details.add_argument("--register-as", help="Endpoint name (with possibly a domain after a dot) to register as", metavar='EP[.D]', default=None) 

26 details.add_argument("--register-proxy", help="Ask the RD to serve as a reverse proxy. Note that this is only practical for --unconditional or --pathbased reverse proxies.", action='store_true') 

27 

28 r = p.add_argument_group('Rules', description="Sequence of forwarding rules " 

29 "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.") 

30 class TypedAppend(argparse.Action): 

31 def __call__(self, parser, namespace, values, option_string=None): 

32 if getattr(namespace, self.dest) is None: 

33 setattr(namespace, self.dest, []) 

34 getattr(namespace, self.dest).append((option_string, values)) 

35 r.add_argument('--namebased', help="If Uri-Host matches NAME, route to DEST", metavar="NAME:DEST", action=TypedAppend, dest='r') 

36 r.add_argument('--subdomainbased', help="If Uri-Host is anything.NAME, route to DEST", metavar="NAME:DEST", action=TypedAppend, dest='r') 

37 r.add_argument('--pathbased', help="If a requested path starts with PATH, split that part off and route to DEST", metavar="PATH:DEST", action=TypedAppend, dest='r') 

38 r.add_argument('--unconditional', help="Route all requests not previously matched to DEST", metavar="DEST", action=TypedAppend, dest='r') 

39 

40 return p 

41 

42def destsplit(dest): 

43 use_as_proxy = False 

44 rewrite_uri_host = False 

45 if dest.startswith('!'): 

46 dest = dest[1:] 

47 rewrite_uri_host = True 

48 if dest.startswith('@'): 

49 dest = dest[1:] 

50 use_as_proxy = True 

51 return dest, rewrite_uri_host, use_as_proxy 

52 

53class Main(AsyncCLIDaemon): 

54 async def start(self, args=None): 

55 parser = build_parser() 

56 options = parser.parse_args(args if args is not None else sys.argv[1:]) 

57 self.options = options 

58 

59 if not options.forward and not options.reverse: 

60 raise parser.error("At least one of --forward and --reverse must be given.") 

61 

62 self.outgoing_context = await aiocoap.Context.create_client_context() 

63 if options.forward: 

64 proxy = ForwardProxyWithPooledObservations(self.outgoing_context) 

65 else: 

66 proxy = ProxyWithPooledObservations(self.outgoing_context) 

67 for kind, data in options.r or (): 

68 if kind in ('--namebased', '--subdomainbased'): 

69 try: 

70 name, dest = data.split(':', 1) 

71 except Exception: 

72 raise parser.error("%s needs NAME:DEST as arguments" % kind) 

73 dest, rewrite_uri_host, use_as_proxy = destsplit(dest) 

74 if rewrite_uri_host and kind == '--subdomainbased': 

75 parser.error("The flag '!' makes no sense for subdomain based redirection as the subdomain data would be lost") 

76 r = (NameBasedVirtualHost if kind == '--namebased' else SubdomainVirtualHost)(name, dest, rewrite_uri_host, use_as_proxy) 

77 elif kind == '--pathbased': 

78 try: 

79 path, dest = data.split(':', 1) 

80 except Exception: 

81 raise parser.error("--pathbased needs PATH:DEST as arguments") 

82 r = SubresourceVirtualHost(path.split('/'), dest) 

83 elif kind == '--unconditional': 

84 dest, rewrite_uri_host, use_as_proxy = destsplit(data) 

85 if rewrite_uri_host: 

86 parser.error("The flag '!' makes no sense for unconditional redirection as the host name data would be lost") 

87 r = UnconditionalRedirector(dest, use_as_proxy) 

88 else: 

89 raise AssertionError('Unknown redirectory kind') 

90 proxy.add_redirector(r) 

91 

92 self.proxy_context = await server_context_from_arguments(proxy, options) 

93 

94 if options.register is not False: 

95 from aiocoap.resourcedirectory.client.register import Registerer 

96 

97 params = {} 

98 if options.register_as: 

99 ep, _, d = options.register_as.partition('.') 

100 params['ep'] = ep 

101 if d: 

102 params['d'] = d 

103 if options.register_proxy: 

104 # FIXME: Check this in discovery 

105 params['proxy'] = 'on' 

106 # FIXME: Construct this from settings (path-based), and forward results 

107 proxy.get_resources_as_linkheader = lambda: "" 

108 self.registerer = Registerer(self.proxy_context, rd=options.register, lt=60, 

109 registration_parameters=params) 

110 

111 async def shutdown(self): 

112 if self.options.register is not False: 

113 await self.registerer.shutdown() 

114 await self.outgoing_context.shutdown() 

115 await self.proxy_context.shutdown() 

116 

117sync_main = Main.sync_main 

118 

119if __name__ == "__main__": 

120 # if you want to run this using `python3 -m`, see http://bugs.python.org/issue22480 

121 sync_main()