]> git.ipfire.org Git - location/libloc.git/blob - src/python/location.in
Merge location-exporter(8) into location(8)
[location/libloc.git] / src / python / location.in
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
20 import argparse
21 import datetime
22 import ipaddress
23 import logging
24 import os
25 import re
26 import shutil
27 import socket
28 import sys
29 import time
30
31 # Load our location module
32 import location
33 import location.downloader
34 import location.export
35
36 from location.i18n import _
37
38 # Setup logging
39 log = logging.getLogger("location")
40
41 # Output formatters
42
43 class CLI(object):
44 def parse_cli(self):
45 parser = argparse.ArgumentParser(
46 description=_("Location Database Command Line Interface"),
47 )
48 subparsers = parser.add_subparsers()
49
50 # Global configuration flags
51 parser.add_argument("--debug", action="store_true",
52 help=_("Enable debug output"))
53 parser.add_argument("--quiet", action="store_true",
54 help=_("Enable quiet mode"))
55
56 # version
57 parser.add_argument("--version", action="version",
58 version="%(prog)s @VERSION@")
59
60 # database
61 parser.add_argument("--database", "-d",
62 default="@databasedir@/database.db", help=_("Path to database"),
63 )
64
65 # public key
66 parser.add_argument("--public-key", "-k",
67 default="@databasedir@/signing-key.pem", help=_("Public Signing Key"),
68 )
69
70 # Show the database version
71 version = subparsers.add_parser("version",
72 help=_("Show database version"))
73 version.set_defaults(func=self.handle_version)
74
75 # lookup an IP address
76 lookup = subparsers.add_parser("lookup",
77 help=_("Lookup one or multiple IP addresses"),
78 )
79 lookup.add_argument("address", nargs="+")
80 lookup.set_defaults(func=self.handle_lookup)
81
82 # Dump the whole database
83 dump = subparsers.add_parser("dump",
84 help=_("Dump the entire database"),
85 )
86 dump.add_argument("output", nargs="?", type=argparse.FileType("w"))
87 dump.set_defaults(func=self.handle_dump)
88
89 # Update
90 update = subparsers.add_parser("update", help=_("Update database"))
91 update.set_defaults(func=self.handle_update)
92
93 # Verify
94 verify = subparsers.add_parser("verify",
95 help=_("Verify the downloaded database"))
96 verify.set_defaults(func=self.handle_verify)
97
98 # Get AS
99 get_as = subparsers.add_parser("get-as",
100 help=_("Get information about one or multiple Autonomous Systems"),
101 )
102 get_as.add_argument("asn", nargs="+")
103 get_as.set_defaults(func=self.handle_get_as)
104
105 # Search for AS
106 search_as = subparsers.add_parser("search-as",
107 help=_("Search for Autonomous Systems that match the string"),
108 )
109 search_as.add_argument("query", nargs=1)
110 search_as.set_defaults(func=self.handle_search_as)
111
112 # List all networks in an AS
113 list_networks_by_as = subparsers.add_parser("list-networks-by-as",
114 help=_("Lists all networks in an AS"),
115 )
116 list_networks_by_as.add_argument("asn", nargs=1, type=int)
117 list_networks_by_as.add_argument("--family", choices=("ipv6", "ipv4"))
118 list_networks_by_as.add_argument("--format",
119 choices=location.export.formats.keys(), default="list")
120 list_networks_by_as.set_defaults(func=self.handle_list_networks_by_as)
121
122 # List all networks in a country
123 list_networks_by_cc = subparsers.add_parser("list-networks-by-cc",
124 help=_("Lists all networks in a country"),
125 )
126 list_networks_by_cc.add_argument("country_code", nargs=1)
127 list_networks_by_cc.add_argument("--family", choices=("ipv6", "ipv4"))
128 list_networks_by_cc.add_argument("--format",
129 choices=location.export.formats.keys(), default="list")
130 list_networks_by_cc.set_defaults(func=self.handle_list_networks_by_cc)
131
132 # List all networks with flags
133 list_networks_by_flags = subparsers.add_parser("list-networks-by-flags",
134 help=_("Lists all networks with flags"),
135 )
136 list_networks_by_flags.add_argument("--anonymous-proxy",
137 action="store_true", help=_("Anonymous Proxies"),
138 )
139 list_networks_by_flags.add_argument("--satellite-provider",
140 action="store_true", help=_("Satellite Providers"),
141 )
142 list_networks_by_flags.add_argument("--anycast",
143 action="store_true", help=_("Anycasts"),
144 )
145 list_networks_by_flags.add_argument("--family", choices=("ipv6", "ipv4"))
146 list_networks_by_flags.add_argument("--format",
147 choices=location.export.formats.keys(), default="list")
148 list_networks_by_flags.set_defaults(func=self.handle_list_networks_by_flags)
149
150 # Export
151 export = subparsers.add_parser("export",
152 help=_("Exports data in many formats to load it into packet filters"),
153 )
154 export.add_argument("--format", help=_("Output format"),
155 choices=location.export.formats.keys(), default="list")
156 export.add_argument("--directory", help=_("Output directory"), required=True)
157 export.add_argument("--family",
158 help=_("Specify address family"), choices=("ipv6", "ipv4"),
159 )
160 export.add_argument("objects", nargs="+", help=_("List country codes or ASNs to export"))
161 export.set_defaults(func=self.handle_export)
162
163 args = parser.parse_args()
164
165 # Configure logging
166 if args.debug:
167 location.logger.set_level(logging.DEBUG)
168 elif args.quiet:
169 location.logger.set_level(logging.WARNING)
170
171 # Print usage if no action was given
172 if not "func" in args:
173 parser.print_usage()
174 sys.exit(2)
175
176 return args
177
178 def run(self):
179 # Parse command line arguments
180 args = self.parse_cli()
181
182 # Open database
183 try:
184 db = location.Database(args.database)
185 except FileNotFoundError as e:
186 sys.stderr.write("location: Could not open database %s: %s\n" \
187 % (args.database, e))
188 sys.exit(1)
189
190 # Translate family (if present)
191 if "family" in args:
192 if args.family == "ipv6":
193 args.family = socket.AF_INET6
194 elif args.family == "ipv4":
195 args.family = socket.AF_INET
196 else:
197 args.family = 0
198
199 # Call function
200 try:
201 ret = args.func(db, args)
202
203 # Catch invalid inputs
204 except ValueError as e:
205 sys.stderr.write("%s\n" % e)
206 ret = 2
207
208 # Return with exit code
209 if ret:
210 sys.exit(ret)
211
212 # Otherwise just exit
213 sys.exit(0)
214
215 def handle_version(self, db, ns):
216 """
217 Print the version of the database
218 """
219 t = time.strftime(
220 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
221 )
222
223 print(t)
224
225 def handle_lookup(self, db, ns):
226 ret = 0
227
228 format = " %-24s: %s"
229
230 for address in ns.address:
231 try:
232 network = db.lookup(address)
233 except ValueError:
234 print(_("Invalid IP address: %s") % address, file=sys.stderr)
235
236 args = {
237 "address" : address,
238 "network" : network,
239 }
240
241 # Nothing found?
242 if not network:
243 print(_("Nothing found for %(address)s") % args, file=sys.stderr)
244 ret = 1
245 continue
246
247 print("%s:" % address)
248 print(format % (_("Network"), network))
249
250 # Print country
251 if network.country_code:
252 country = db.get_country(network.country_code)
253
254 print(format % (
255 _("Country"),
256 country.name if country else network.country_code),
257 )
258
259 # Print AS information
260 if network.asn:
261 autonomous_system = db.get_as(network.asn)
262
263 print(format % (
264 _("Autonomous System"),
265 autonomous_system or "AS%s" % network.asn),
266 )
267
268 # Anonymous Proxy
269 if network.has_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY):
270 print(format % (
271 _("Anonymous Proxy"), _("yes"),
272 ))
273
274 # Satellite Provider
275 if network.has_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER):
276 print(format % (
277 _("Satellite Provider"), _("yes"),
278 ))
279
280 # Anycast
281 if network.has_flag(location.NETWORK_FLAG_ANYCAST):
282 print(format % (
283 _("Anycast"), _("yes"),
284 ))
285
286 return ret
287
288 def handle_dump(self, db, ns):
289 # Use output file or write to stdout
290 f = ns.output or sys.stdout
291
292 # Format everything like this
293 format = "%-24s %s\n"
294
295 # Write metadata
296 f.write("#\n# Location Database Export\n#\n")
297
298 f.write("# Generated: %s\n" % time.strftime(
299 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
300 ))
301
302 if db.vendor:
303 f.write("# Vendor: %s\n" % db.vendor)
304
305 if db.license:
306 f.write("# License: %s\n" % db.license)
307
308 f.write("#\n")
309
310 if db.description:
311 for line in db.description.splitlines():
312 f.write("# %s\n" % line)
313
314 f.write("#\n")
315
316 # Iterate over all ASes
317 for a in db.ases:
318 f.write("\n")
319 f.write(format % ("aut-num:", "AS%s" % a.number))
320 f.write(format % ("name:", a.name))
321
322 flags = {
323 location.NETWORK_FLAG_ANONYMOUS_PROXY : "is-anonymous-proxy:",
324 location.NETWORK_FLAG_SATELLITE_PROVIDER : "is-satellite-provider:",
325 location.NETWORK_FLAG_ANYCAST : "is-anycast:",
326 }
327
328 # Iterate over all networks
329 for n in db.networks:
330 f.write("\n")
331 f.write(format % ("net:", n))
332
333 if n.country_code:
334 f.write(format % ("country:", n.country_code))
335
336 if n.asn:
337 f.write(format % ("aut-num:", n.asn))
338
339 # Print all flags
340 for flag in flags:
341 if n.has_flag(flag):
342 f.write(format % (flags[flag], "yes"))
343
344 def handle_get_as(self, db, ns):
345 """
346 Gets information about Autonomous Systems
347 """
348 ret = 0
349
350 for asn in ns.asn:
351 try:
352 asn = int(asn)
353 except ValueError:
354 print(_("Invalid ASN: %s") % asn, file=sys.stderr)
355 ret = 1
356 continue
357
358 # Fetch AS from database
359 a = db.get_as(asn)
360
361 # Nothing found
362 if not a:
363 print(_("Could not find AS%s") % asn, file=sys.stderr)
364 ret = 1
365 continue
366
367 print(_("AS%(asn)s belongs to %(name)s") % { "asn" : a.number, "name" : a.name })
368
369 return ret
370
371 def handle_search_as(self, db, ns):
372 for query in ns.query:
373 # Print all matches ASes
374 for a in db.search_as(query):
375 print(a)
376
377 def handle_update(self, db, ns):
378 # Fetch the timestamp we need from DNS
379 t = location.discover_latest_version()
380
381 # Parse timestamp into datetime format
382 timestamp = datetime.datetime.fromtimestamp(t) if t else None
383
384 # Check the version of the local database
385 if db and timestamp and db.created_at >= timestamp.timestamp():
386 log.info("Already on the latest version")
387 return
388
389 # Download the database into the correct directory
390 tmpdir = os.path.dirname(ns.database)
391
392 # Create a downloader
393 d = location.downloader.Downloader()
394
395 # Try downloading a new database
396 try:
397 t = d.download(public_key=ns.public_key, timestamp=timestamp, tmpdir=tmpdir)
398
399 # If no file could be downloaded, log a message
400 except FileNotFoundError as e:
401 log.error("Could not download a new database")
402 return 1
403
404 # If we have not received a new file, there is nothing to do
405 if not t:
406 return 3
407
408 # Move temporary file to destination
409 shutil.move(t.name, ns.database)
410
411 return 0
412
413 def handle_verify(self, ns):
414 try:
415 db = location.Database(ns.database)
416 except FileNotFoundError as e:
417 log.error("%s: %s" % (ns.database, e))
418 return 127
419
420 # Verify the database
421 with open(ns.public_key, "r") as f:
422 if not db.verify(f):
423 log.error("Could not verify database")
424 return 1
425
426 # Success
427 log.debug("Database successfully verified")
428 return 0
429
430 def __get_output_formatter(self, ns):
431 try:
432 cls = location.export.formats[ns.format]
433 except KeyError:
434 cls = location.export.OutputFormatter
435
436 return cls
437
438 def handle_list_networks_by_as(self, db, ns):
439 writer = self.__get_output_formatter(ns)
440
441 for asn in ns.asn:
442 f = writer(sys.stdout, prefix="AS%s" % asn)
443
444 # Print all matching networks
445 for n in db.search_networks(asn=asn, family=ns.family):
446 f.write(n)
447
448 f.finish()
449
450 def handle_list_networks_by_cc(self, db, ns):
451 writer = self.__get_output_formatter(ns)
452
453 for country_code in ns.country_code:
454 # Open standard output
455 f = writer(sys.stdout, prefix=country_code)
456
457 # Print all matching networks
458 for n in db.search_networks(country_code=country_code, family=ns.family):
459 f.write(n)
460
461 f.finish()
462
463 def handle_list_networks_by_flags(self, db, ns):
464 flags = 0
465
466 if ns.anonymous_proxy:
467 flags |= location.NETWORK_FLAG_ANONYMOUS_PROXY
468
469 if ns.satellite_provider:
470 flags |= location.NETWORK_FLAG_SATELLITE_PROVIDER
471
472 if ns.anycast:
473 flags |= location.NETWORK_FLAG_ANYCAST
474
475 if not flags:
476 raise ValueError(_("You must at least pass one flag"))
477
478 writer = self.__get_output_formatter(ns)
479 f = writer(sys.stdout, prefix="custom")
480
481 for n in db.search_networks(flags=flags, family=ns.family):
482 f.write(n)
483
484 f.finish()
485
486 def handle_export(self, db, ns):
487 countries, asns = [], []
488
489 # Translate family
490 if ns.family == "ipv6":
491 families = [ socket.AF_INET6 ]
492 elif ns.family == "ipv4":
493 families = [ socket.AF_INET ]
494 else:
495 families = [ socket.AF_INET6, socket.AF_INET ]
496
497 for object in ns.objects:
498 m = re.match("^AS(\d+)$", object)
499 if m:
500 object = int(m.group(1))
501
502 asns.append(object)
503
504 elif location.country_code_is_valid(object) \
505 or object in ("A1", "A2", "A3"):
506 countries.append(object)
507
508 else:
509 log.warning("Invalid argument: %s" % object)
510 continue
511
512 if not countries and not asns:
513 log.error("Nothing to export")
514 return 2
515
516 # Select the output format
517 writer = self.__get_output_formatter(ns)
518
519 e = location.export.Exporter(db, writer)
520 e.export(ns.directory, countries=countries, asns=asns, families=families)
521
522
523 def main():
524 # Run the command line interface
525 c = CLI()
526 c.run()
527
528 main()