]> git.ipfire.org Git - location/libloc.git/blame - src/python/location-query.in
location-exporter: Warn, but do not fail on invalid input
[location/libloc.git] / src / python / location-query.in
CommitLineData
5118a4b8
MT
1#!/usr/bin/python3
2###############################################################################
3# #
4# libloc - A library to determine the location of someone on the Internet #
5# #
6# Copyright (C) 2017 IPFire Development Team <info@ipfire.org> #
7# #
8# This library is free software; you can redistribute it and/or #
9# modify it under the terms of the GNU Lesser General Public #
10# License as published by the Free Software Foundation; either #
11# version 2.1 of the License, or (at your option) any later version. #
12# #
13# This library is distributed in the hope that it will be useful, #
14# but WITHOUT ANY WARRANTY; without even the implied warranty of #
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
16# Lesser General Public License for more details. #
17# #
18###############################################################################
19
20import argparse
4439e317
MT
21import ipaddress
22import os
23import socket
5118a4b8 24import sys
a68a46f5 25import time
5118a4b8
MT
26
27# Load our location module
28import location
7dccb767 29from location.i18n import _
5118a4b8 30
4439e317
MT
31# Output formatters
32
33class OutputFormatter(object):
71e0ad0b
MT
34 def __init__(self, ns):
35 self.ns = ns
36
4439e317
MT
37 def __enter__(self):
38 # Open the output
39 self.open()
40
41 return self
42
43 def __exit__(self, type, value, tb):
44 if tb is None:
45 self.close()
46
71e0ad0b
MT
47 @property
48 def name(self):
49 if "country_code" in self.ns:
50 return "networks_country_%s" % self.ns.country_code[0]
51
52 elif "asn" in self.ns:
53 return "networks_AS%s" % self.ns.asn[0]
54
4439e317
MT
55 def open(self):
56 pass
57
58 def close(self):
59 pass
60
61 def network(self, network):
62 print(network)
63
64
6da14cc1
MT
65class IpsetOutputFormatter(OutputFormatter):
66 """
67 For nftables
68 """
69 def open(self):
70 print("create %s hash:net family inet hashsize 1024 maxelem 65536" % self.name)
71
72 def network(self, network):
73 print("add %s %s" % (self.name, network))
74
75
71e0ad0b
MT
76class NftablesOutputFormatter(OutputFormatter):
77 """
78 For nftables
79 """
80 def open(self):
81 print("define %s = {" % self.name)
82
83 def close(self):
84 print("}")
85
86 def network(self, network):
87 print(" %s," % network)
88
89
4439e317
MT
90class XTGeoIPOutputFormatter(OutputFormatter):
91 """
92 Formats the output in that way, that it can be loaded by
93 the xt_geoip kernel module from xtables-addons.
94 """
95 def network(self, network):
96 n = ipaddress.ip_network("%s" % network)
97
98 for address in (n.network_address, n.broadcast_address):
99 bytes = socket.inet_pton(
100 socket.AF_INET6 if address.version == 6 else socket.AF_INET,
101 "%s" % address,
102 )
103
104 os.write(1, bytes)
105
106
5118a4b8 107class CLI(object):
4439e317 108 output_formats = {
6da14cc1 109 "ipset" : IpsetOutputFormatter,
4439e317 110 "list" : OutputFormatter,
71e0ad0b 111 "nftables" : NftablesOutputFormatter,
4439e317
MT
112 "xt_geoip" : XTGeoIPOutputFormatter,
113 }
114
5118a4b8
MT
115 def parse_cli(self):
116 parser = argparse.ArgumentParser(
117 description=_("Location Database Command Line Interface"),
118 )
119 subparsers = parser.add_subparsers()
120
121 # Global configuration flags
122 parser.add_argument("--debug", action="store_true",
123 help=_("Enable debug output"))
bc1f5f53
MT
124 parser.add_argument("--quiet", action="store_true",
125 help=_("Enable quiet mode"))
5118a4b8 126
ddb184be
MT
127 # version
128 parser.add_argument("--version", action="version",
d2714e4a 129 version="%(prog)s @VERSION@")
ddb184be 130
2538ed9a
MT
131 # database
132 parser.add_argument("--database", "-d",
133 default="@databasedir@/database.db", help=_("Path to database"),
134 )
135
726f9984
MT
136 # public key
137 parser.add_argument("--public-key", "-k",
138 default="@databasedir@/signing-key.pem", help=_("Public Signing Key"),
139 )
140
9f64f1eb
MT
141 # Show the database version
142 version = subparsers.add_parser("version",
143 help=_("Show database version"))
144 version.set_defaults(func=self.handle_version)
145
5118a4b8
MT
146 # lookup an IP address
147 lookup = subparsers.add_parser("lookup",
148 help=_("Lookup one or multiple IP addresses"),
149 )
150 lookup.add_argument("address", nargs="+")
151 lookup.set_defaults(func=self.handle_lookup)
152
a68a46f5
MT
153 # Dump the whole database
154 dump = subparsers.add_parser("dump",
155 help=_("Dump the entire database"),
156 )
157 dump.add_argument("output", nargs="?", type=argparse.FileType("w"))
158 dump.set_defaults(func=self.handle_dump)
159
fadc1af0
MT
160 # Get AS
161 get_as = subparsers.add_parser("get-as",
162 help=_("Get information about one or multiple Autonomous Systems"),
163 )
164 get_as.add_argument("asn", nargs="+")
165 get_as.set_defaults(func=self.handle_get_as)
166
da3e360e
MT
167 # Search for AS
168 search_as = subparsers.add_parser("search-as",
169 help=_("Search for Autonomous Systems that match the string"),
170 )
171 search_as.add_argument("query", nargs=1)
172 search_as.set_defaults(func=self.handle_search_as)
173
43154ed7
MT
174 # List all networks in an AS
175 list_networks_by_as = subparsers.add_parser("list-networks-by-as",
176 help=_("Lists all networks in an AS"),
177 )
178 list_networks_by_as.add_argument("asn", nargs=1, type=int)
44e5ef71 179 list_networks_by_as.add_argument("--family", choices=("ipv6", "ipv4"))
4439e317
MT
180 list_networks_by_as.add_argument("--output-format",
181 choices=self.output_formats.keys(), default="list")
43154ed7
MT
182 list_networks_by_as.set_defaults(func=self.handle_list_networks_by_as)
183
ccc7ab4e 184 # List all networks in a country
b5cdfad7 185 list_networks_by_cc = subparsers.add_parser("list-networks-by-cc",
ccc7ab4e
MT
186 help=_("Lists all networks in a country"),
187 )
b5cdfad7 188 list_networks_by_cc.add_argument("country_code", nargs=1)
44e5ef71 189 list_networks_by_cc.add_argument("--family", choices=("ipv6", "ipv4"))
4439e317
MT
190 list_networks_by_cc.add_argument("--output-format",
191 choices=self.output_formats.keys(), default="list")
b5cdfad7 192 list_networks_by_cc.set_defaults(func=self.handle_list_networks_by_cc)
ccc7ab4e 193
bbdb2e0a
MT
194 # List all networks with flags
195 list_networks_by_flags = subparsers.add_parser("list-networks-by-flags",
196 help=_("Lists all networks with flags"),
197 )
198 list_networks_by_flags.add_argument("--anonymous-proxy",
199 action="store_true", help=_("Anonymous Proxies"),
200 )
201 list_networks_by_flags.add_argument("--satellite-provider",
202 action="store_true", help=_("Satellite Providers"),
203 )
204 list_networks_by_flags.add_argument("--anycast",
205 action="store_true", help=_("Anycasts"),
206 )
44e5ef71 207 list_networks_by_flags.add_argument("--family", choices=("ipv6", "ipv4"))
bbdb2e0a
MT
208 list_networks_by_flags.add_argument("--output-format",
209 choices=self.output_formats.keys(), default="list")
210 list_networks_by_flags.set_defaults(func=self.handle_list_networks_by_flags)
211
78f37815
MT
212 args = parser.parse_args()
213
bc1f5f53 214 # Configure logging
f9de5e61
MT
215 if args.debug:
216 location.logger.set_level(logging.DEBUG)
bc1f5f53
MT
217 elif args.quiet:
218 location.logger.set_level(logging.WARNING)
f9de5e61 219
78f37815
MT
220 # Print usage if no action was given
221 if not "func" in args:
222 parser.print_usage()
223 sys.exit(2)
224
225 return args
5118a4b8
MT
226
227 def run(self):
228 # Parse command line arguments
229 args = self.parse_cli()
230
2538ed9a
MT
231 # Open database
232 try:
233 db = location.Database(args.database)
234 except FileNotFoundError as e:
235 sys.stderr.write("location-query: Could not open database %s: %s\n" \
236 % (args.database, e))
237 sys.exit(1)
238
6961aaf3
MT
239 # Translate family (if present)
240 if "family" in args:
241 if args.family == "ipv6":
242 args.family = socket.AF_INET6
243 elif args.family == "ipv4":
244 args.family = socket.AF_INET
245 else:
246 args.family = 0
44e5ef71 247
5118a4b8 248 # Call function
228d0e74
MT
249 try:
250 ret = args.func(db, args)
251
252 # Catch invalid inputs
253 except ValueError as e:
254 sys.stderr.write("%s\n" % e)
255 ret = 2
5118a4b8
MT
256
257 # Return with exit code
258 if ret:
259 sys.exit(ret)
260
261 # Otherwise just exit
262 sys.exit(0)
263
9f64f1eb
MT
264 def handle_version(self, db, ns):
265 """
266 Print the version of the database
267 """
268 t = time.strftime(
269 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
270 )
271
272 print(t)
273
2538ed9a 274 def handle_lookup(self, db, ns):
5118a4b8
MT
275 ret = 0
276
fbf925c8
MT
277 format = " %-24s: %s"
278
5118a4b8
MT
279 for address in ns.address:
280 try:
fbf925c8 281 network = db.lookup(address)
5118a4b8 282 except ValueError:
9f2f5d13 283 print(_("Invalid IP address: %s") % address, file=sys.stderr)
5118a4b8
MT
284
285 args = {
286 "address" : address,
fbf925c8 287 "network" : network,
5118a4b8
MT
288 }
289
290 # Nothing found?
fbf925c8 291 if not network:
9f2f5d13 292 print(_("Nothing found for %(address)s") % args, file=sys.stderr)
5118a4b8
MT
293 ret = 1
294 continue
295
fbf925c8
MT
296 print("%s:" % address)
297 print(format % (_("Network"), network))
5118a4b8 298
fbf925c8
MT
299 # Print country
300 if network.country_code:
072ca100
MT
301 country = db.get_country(network.country_code)
302
303 print(format % (
304 _("Country"),
305 country.name if country else network.country_code),
306 )
5118a4b8 307
fbf925c8
MT
308 # Print AS information
309 if network.asn:
310 autonomous_system = db.get_as(network.asn)
5118a4b8 311
fbf925c8
MT
312 print(format % (
313 _("Autonomous System"),
314 autonomous_system or "AS%s" % network.asn),
315 )
5118a4b8 316
ee83fe2e
MT
317 # Anonymous Proxy
318 if network.has_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY):
319 print(format % (
320 _("Anonymous Proxy"), _("yes"),
321 ))
322
323 # Satellite Provider
324 if network.has_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER):
325 print(format % (
326 _("Satellite Provider"), _("yes"),
327 ))
328
329 # Anycast
330 if network.has_flag(location.NETWORK_FLAG_ANYCAST):
331 print(format % (
332 _("Anycast"), _("yes"),
333 ))
334
5118a4b8
MT
335 return ret
336
a68a46f5
MT
337 def handle_dump(self, db, ns):
338 # Use output file or write to stdout
339 f = ns.output or sys.stdout
340
d6d06375
MT
341 # Format everything like this
342 format = "%-24s %s\n"
343
a68a46f5
MT
344 # Write metadata
345 f.write("#\n# Location Database Export\n#\n")
346
347 f.write("# Generated: %s\n" % time.strftime(
348 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
349 ))
350
351 if db.vendor:
352 f.write("# Vendor: %s\n" % db.vendor)
353
354 if db.license:
355 f.write("# License: %s\n" % db.license)
356
357 f.write("#\n")
358
359 if db.description:
360 for line in db.description.splitlines():
361 f.write("# %s\n" % line)
362
363 f.write("#\n")
364
365 # Iterate over all ASes
366 for a in db.ases:
367 f.write("\n")
d6d06375
MT
368 f.write(format % ("aut-num:", "AS%s" % a.number))
369 f.write(format % ("name:", a.name))
370
371 flags = {
372 location.NETWORK_FLAG_ANONYMOUS_PROXY : "is-anonymous-proxy:",
373 location.NETWORK_FLAG_SATELLITE_PROVIDER : "is-satellite-provider:",
374 location.NETWORK_FLAG_ANYCAST : "is-anycast:",
375 }
a68a46f5
MT
376
377 # Iterate over all networks
378 for n in db.networks:
379 f.write("\n")
d6d06375 380 f.write(format % ("net:", n))
a68a46f5
MT
381
382 if n.country_code:
d6d06375 383 f.write(format % ("country:", n.country_code))
a68a46f5
MT
384
385 if n.asn:
03cd8096 386 f.write(format % ("aut-num:", n.asn))
d6d06375
MT
387
388 # Print all flags
389 for flag in flags:
390 if n.has_flag(flag):
391 f.write(format % (flags[flag], "yes"))
a68a46f5 392
2538ed9a 393 def handle_get_as(self, db, ns):
fadc1af0
MT
394 """
395 Gets information about Autonomous Systems
396 """
397 ret = 0
398
399 for asn in ns.asn:
400 try:
401 asn = int(asn)
402 except ValueError:
9f2f5d13 403 print(_("Invalid ASN: %s") % asn, file=sys.stderr)
fadc1af0
MT
404 ret = 1
405 continue
406
407 # Fetch AS from database
2538ed9a 408 a = db.get_as(asn)
fadc1af0
MT
409
410 # Nothing found
411 if not a:
9f2f5d13 412 print(_("Could not find AS%s") % asn, file=sys.stderr)
fadc1af0
MT
413 ret = 1
414 continue
415
416 print(_("AS%(asn)s belongs to %(name)s") % { "asn" : a.number, "name" : a.name })
417
418 return ret
5118a4b8 419
2538ed9a 420 def handle_search_as(self, db, ns):
da3e360e
MT
421 for query in ns.query:
422 # Print all matches ASes
2538ed9a 423 for a in db.search_as(query):
da3e360e
MT
424 print(a)
425
4439e317
MT
426 def __get_output_formatter(self, ns):
427 try:
428 cls = self.output_formats[ns.output_format]
429 except KeyError:
430 cls = OutputFormatter
431
71e0ad0b 432 return cls(ns)
4439e317 433
43154ed7 434 def handle_list_networks_by_as(self, db, ns):
4439e317
MT
435 with self.__get_output_formatter(ns) as f:
436 for asn in ns.asn:
437 # Print all matching networks
44e5ef71 438 for n in db.search_networks(asn=asn, family=ns.family):
4439e317 439 f.network(n)
43154ed7 440
ccc7ab4e 441 def handle_list_networks_by_cc(self, db, ns):
4439e317
MT
442 with self.__get_output_formatter(ns) as f:
443 for country_code in ns.country_code:
444 # Print all matching networks
44e5ef71 445 for n in db.search_networks(country_code=country_code, family=ns.family):
4439e317
MT
446 f.network(n)
447
bbdb2e0a
MT
448 def handle_list_networks_by_flags(self, db, ns):
449 flags = 0
450
451 if ns.anonymous_proxy:
452 flags |= location.NETWORK_FLAG_ANONYMOUS_PROXY
453
454 if ns.satellite_provider:
455 flags |= location.NETWORK_FLAG_SATELLITE_PROVIDER
456
457 if ns.anycast:
458 flags |= location.NETWORK_FLAG_ANYCAST
459
228d0e74
MT
460 if not flags:
461 raise ValueError(_("You must at least pass one flag"))
462
bbdb2e0a 463 with self.__get_output_formatter(ns) as f:
44e5ef71 464 for n in db.search_networks(flags=flags, family=ns.family):
bbdb2e0a
MT
465 f.network(n)
466
ccc7ab4e 467
5118a4b8
MT
468def main():
469 # Run the command line interface
470 c = CLI()
471 c.run()
472
473main()