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

92 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-15 22:10 +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 ( 

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 

21 

22 

23def build_parser(): 

24 p = argparse.ArgumentParser(description=__doc__) 

25 

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

31 

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 ) 

54 

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 ) 

60 

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)) 

66 

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 ) 

95 

96 return p 

97 

98 

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 

109 

110 

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 

116 

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

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

119 

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) 

157 

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

159 

160 if options.register is not False: 

161 from aiocoap.resourcedirectory.client.register import Registerer 

162 

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 ) 

180 

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() 

186 

187 

188sync_main = Main.sync_main 

189 

190if __name__ == "__main__": 

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

192 sync_main()