]> git.ipfire.org Git - people/ms/libloc.git/blame - src/python/location.in
location(8): Allow limiting updates to once a day, week, month
[people/ms/libloc.git] / src / python / location.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
a6f1e346 21import datetime
4439e317 22import ipaddress
a6f1e346 23import logging
4439e317 24import os
88ef7e9c 25import re
a6f1e346 26import shutil
4439e317 27import socket
5118a4b8 28import sys
a68a46f5 29import time
5118a4b8
MT
30
31# Load our location module
32import location
a6f1e346 33import location.downloader
88ef7e9c
MT
34import location.export
35
7dccb767 36from location.i18n import _
5118a4b8 37
a6f1e346
MT
38# Setup logging
39log = logging.getLogger("location")
40
4439e317
MT
41# Output formatters
42
5118a4b8 43class CLI(object):
5118a4b8
MT
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"))
bc1f5f53
MT
53 parser.add_argument("--quiet", action="store_true",
54 help=_("Enable quiet mode"))
5118a4b8 55
ddb184be
MT
56 # version
57 parser.add_argument("--version", action="version",
d2714e4a 58 version="%(prog)s @VERSION@")
ddb184be 59
2538ed9a
MT
60 # database
61 parser.add_argument("--database", "-d",
62 default="@databasedir@/database.db", help=_("Path to database"),
63 )
64
726f9984
MT
65 # public key
66 parser.add_argument("--public-key", "-k",
67 default="@databasedir@/signing-key.pem", help=_("Public Signing Key"),
68 )
69
9f64f1eb
MT
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
5118a4b8
MT
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
a68a46f5
MT
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
a6f1e346
MT
89 # Update
90 update = subparsers.add_parser("update", help=_("Update database"))
97769987
MT
91 update.add_argument("--cron",
92 help=_("Update the library only once per interval"),
93 choices=("daily", "weekly", "monthly"),
94 )
a6f1e346
MT
95 update.set_defaults(func=self.handle_update)
96
97 # Verify
98 verify = subparsers.add_parser("verify",
99 help=_("Verify the downloaded database"))
100 verify.set_defaults(func=self.handle_verify)
101
fadc1af0
MT
102 # Get AS
103 get_as = subparsers.add_parser("get-as",
104 help=_("Get information about one or multiple Autonomous Systems"),
105 )
106 get_as.add_argument("asn", nargs="+")
107 get_as.set_defaults(func=self.handle_get_as)
108
da3e360e
MT
109 # Search for AS
110 search_as = subparsers.add_parser("search-as",
111 help=_("Search for Autonomous Systems that match the string"),
112 )
113 search_as.add_argument("query", nargs=1)
114 search_as.set_defaults(func=self.handle_search_as)
115
43154ed7
MT
116 # List all networks in an AS
117 list_networks_by_as = subparsers.add_parser("list-networks-by-as",
118 help=_("Lists all networks in an AS"),
119 )
120 list_networks_by_as.add_argument("asn", nargs=1, type=int)
44e5ef71 121 list_networks_by_as.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
122 list_networks_by_as.add_argument("--format",
123 choices=location.export.formats.keys(), default="list")
43154ed7
MT
124 list_networks_by_as.set_defaults(func=self.handle_list_networks_by_as)
125
ccc7ab4e 126 # List all networks in a country
b5cdfad7 127 list_networks_by_cc = subparsers.add_parser("list-networks-by-cc",
ccc7ab4e
MT
128 help=_("Lists all networks in a country"),
129 )
b5cdfad7 130 list_networks_by_cc.add_argument("country_code", nargs=1)
44e5ef71 131 list_networks_by_cc.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
132 list_networks_by_cc.add_argument("--format",
133 choices=location.export.formats.keys(), default="list")
b5cdfad7 134 list_networks_by_cc.set_defaults(func=self.handle_list_networks_by_cc)
ccc7ab4e 135
bbdb2e0a
MT
136 # List all networks with flags
137 list_networks_by_flags = subparsers.add_parser("list-networks-by-flags",
138 help=_("Lists all networks with flags"),
139 )
140 list_networks_by_flags.add_argument("--anonymous-proxy",
141 action="store_true", help=_("Anonymous Proxies"),
142 )
143 list_networks_by_flags.add_argument("--satellite-provider",
144 action="store_true", help=_("Satellite Providers"),
145 )
146 list_networks_by_flags.add_argument("--anycast",
147 action="store_true", help=_("Anycasts"),
148 )
44e5ef71 149 list_networks_by_flags.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
150 list_networks_by_flags.add_argument("--format",
151 choices=location.export.formats.keys(), default="list")
bbdb2e0a
MT
152 list_networks_by_flags.set_defaults(func=self.handle_list_networks_by_flags)
153
fa9a3663
MT
154 # List countries
155 list_countries = subparsers.add_parser("list-countries",
156 help=_("Lists all countries"),
157 )
158 list_countries.add_argument("--show-name",
159 action="store_true", help=_("Show the name of the country"),
160 )
161 list_countries.add_argument("--show-continent",
162 action="store_true", help=_("Show the continent"),
163 )
164 list_countries.set_defaults(func=self.handle_list_countries)
165
88ef7e9c
MT
166 # Export
167 export = subparsers.add_parser("export",
168 help=_("Exports data in many formats to load it into packet filters"),
169 )
170 export.add_argument("--format", help=_("Output format"),
171 choices=location.export.formats.keys(), default="list")
172 export.add_argument("--directory", help=_("Output directory"), required=True)
173 export.add_argument("--family",
174 help=_("Specify address family"), choices=("ipv6", "ipv4"),
175 )
10fa313b 176 export.add_argument("objects", nargs="*", help=_("List country codes or ASNs to export"))
88ef7e9c
MT
177 export.set_defaults(func=self.handle_export)
178
78f37815
MT
179 args = parser.parse_args()
180
bc1f5f53 181 # Configure logging
f9de5e61
MT
182 if args.debug:
183 location.logger.set_level(logging.DEBUG)
bc1f5f53
MT
184 elif args.quiet:
185 location.logger.set_level(logging.WARNING)
f9de5e61 186
78f37815
MT
187 # Print usage if no action was given
188 if not "func" in args:
189 parser.print_usage()
190 sys.exit(2)
191
192 return args
5118a4b8
MT
193
194 def run(self):
195 # Parse command line arguments
196 args = self.parse_cli()
197
2538ed9a
MT
198 # Open database
199 try:
200 db = location.Database(args.database)
201 except FileNotFoundError as e:
1d237439 202 sys.stderr.write("location: Could not open database %s: %s\n" \
2538ed9a
MT
203 % (args.database, e))
204 sys.exit(1)
205
6961aaf3
MT
206 # Translate family (if present)
207 if "family" in args:
208 if args.family == "ipv6":
209 args.family = socket.AF_INET6
210 elif args.family == "ipv4":
211 args.family = socket.AF_INET
212 else:
213 args.family = 0
44e5ef71 214
5118a4b8 215 # Call function
228d0e74
MT
216 try:
217 ret = args.func(db, args)
218
219 # Catch invalid inputs
220 except ValueError as e:
221 sys.stderr.write("%s\n" % e)
222 ret = 2
5118a4b8
MT
223
224 # Return with exit code
225 if ret:
226 sys.exit(ret)
227
228 # Otherwise just exit
229 sys.exit(0)
230
9f64f1eb
MT
231 def handle_version(self, db, ns):
232 """
233 Print the version of the database
234 """
235 t = time.strftime(
236 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
237 )
238
239 print(t)
240
2538ed9a 241 def handle_lookup(self, db, ns):
5118a4b8
MT
242 ret = 0
243
fbf925c8
MT
244 format = " %-24s: %s"
245
5118a4b8
MT
246 for address in ns.address:
247 try:
fbf925c8 248 network = db.lookup(address)
5118a4b8 249 except ValueError:
9f2f5d13 250 print(_("Invalid IP address: %s") % address, file=sys.stderr)
5118a4b8
MT
251
252 args = {
253 "address" : address,
fbf925c8 254 "network" : network,
5118a4b8
MT
255 }
256
257 # Nothing found?
fbf925c8 258 if not network:
9f2f5d13 259 print(_("Nothing found for %(address)s") % args, file=sys.stderr)
5118a4b8
MT
260 ret = 1
261 continue
262
fbf925c8
MT
263 print("%s:" % address)
264 print(format % (_("Network"), network))
5118a4b8 265
fbf925c8
MT
266 # Print country
267 if network.country_code:
072ca100
MT
268 country = db.get_country(network.country_code)
269
270 print(format % (
271 _("Country"),
272 country.name if country else network.country_code),
273 )
5118a4b8 274
fbf925c8
MT
275 # Print AS information
276 if network.asn:
277 autonomous_system = db.get_as(network.asn)
5118a4b8 278
fbf925c8
MT
279 print(format % (
280 _("Autonomous System"),
281 autonomous_system or "AS%s" % network.asn),
282 )
5118a4b8 283
ee83fe2e
MT
284 # Anonymous Proxy
285 if network.has_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY):
286 print(format % (
287 _("Anonymous Proxy"), _("yes"),
288 ))
289
290 # Satellite Provider
291 if network.has_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER):
292 print(format % (
293 _("Satellite Provider"), _("yes"),
294 ))
295
296 # Anycast
297 if network.has_flag(location.NETWORK_FLAG_ANYCAST):
298 print(format % (
299 _("Anycast"), _("yes"),
300 ))
301
5118a4b8
MT
302 return ret
303
a68a46f5
MT
304 def handle_dump(self, db, ns):
305 # Use output file or write to stdout
306 f = ns.output or sys.stdout
307
d6d06375
MT
308 # Format everything like this
309 format = "%-24s %s\n"
310
a68a46f5
MT
311 # Write metadata
312 f.write("#\n# Location Database Export\n#\n")
313
314 f.write("# Generated: %s\n" % time.strftime(
315 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
316 ))
317
318 if db.vendor:
319 f.write("# Vendor: %s\n" % db.vendor)
320
321 if db.license:
322 f.write("# License: %s\n" % db.license)
323
324 f.write("#\n")
325
326 if db.description:
327 for line in db.description.splitlines():
cfbae90f
PM
328 line = "# %s" % line
329 f.write("%s\n" % line.rstrip())
a68a46f5
MT
330
331 f.write("#\n")
332
333 # Iterate over all ASes
334 for a in db.ases:
335 f.write("\n")
d6d06375
MT
336 f.write(format % ("aut-num:", "AS%s" % a.number))
337 f.write(format % ("name:", a.name))
338
339 flags = {
340 location.NETWORK_FLAG_ANONYMOUS_PROXY : "is-anonymous-proxy:",
341 location.NETWORK_FLAG_SATELLITE_PROVIDER : "is-satellite-provider:",
342 location.NETWORK_FLAG_ANYCAST : "is-anycast:",
343 }
a68a46f5
MT
344
345 # Iterate over all networks
346 for n in db.networks:
347 f.write("\n")
d6d06375 348 f.write(format % ("net:", n))
a68a46f5
MT
349
350 if n.country_code:
d6d06375 351 f.write(format % ("country:", n.country_code))
a68a46f5
MT
352
353 if n.asn:
03cd8096 354 f.write(format % ("aut-num:", n.asn))
d6d06375
MT
355
356 # Print all flags
357 for flag in flags:
358 if n.has_flag(flag):
359 f.write(format % (flags[flag], "yes"))
a68a46f5 360
2538ed9a 361 def handle_get_as(self, db, ns):
fadc1af0
MT
362 """
363 Gets information about Autonomous Systems
364 """
365 ret = 0
366
367 for asn in ns.asn:
368 try:
369 asn = int(asn)
370 except ValueError:
9f2f5d13 371 print(_("Invalid ASN: %s") % asn, file=sys.stderr)
fadc1af0
MT
372 ret = 1
373 continue
374
375 # Fetch AS from database
2538ed9a 376 a = db.get_as(asn)
fadc1af0
MT
377
378 # Nothing found
379 if not a:
9f2f5d13 380 print(_("Could not find AS%s") % asn, file=sys.stderr)
fadc1af0
MT
381 ret = 1
382 continue
383
384 print(_("AS%(asn)s belongs to %(name)s") % { "asn" : a.number, "name" : a.name })
385
386 return ret
5118a4b8 387
2538ed9a 388 def handle_search_as(self, db, ns):
da3e360e
MT
389 for query in ns.query:
390 # Print all matches ASes
2538ed9a 391 for a in db.search_as(query):
da3e360e
MT
392 print(a)
393
a6f1e346 394 def handle_update(self, db, ns):
97769987
MT
395 if ns.cron and db:
396 now = datetime.datetime.utcnow()
397
398 # Parse the database timestamp
399 t = datetime.datetime.fromtimestamp(db.created_at)
400
401 if ns.cron == "daily":
402 delta = datetime.timedelta(days=1)
403 elif ns.cron == "weekly":
404 delta = datetime.timedelta(days=7)
405 elif ns.cron == "monthly":
406 delta = datetime.timedelta(days=30)
407
408 # Check if the database has recently been updated
409 if t >= (now - delta):
410 log.info(
411 _("The datase has recently be updated recently (%s)") % \
412 format_timedelta(now - t),
413 )
414 return
415
a6f1e346
MT
416 # Fetch the timestamp we need from DNS
417 t = location.discover_latest_version()
418
419 # Parse timestamp into datetime format
420 timestamp = datetime.datetime.fromtimestamp(t) if t else None
421
422 # Check the version of the local database
423 if db and timestamp and db.created_at >= timestamp.timestamp():
424 log.info("Already on the latest version")
425 return
426
427 # Download the database into the correct directory
428 tmpdir = os.path.dirname(ns.database)
429
430 # Create a downloader
431 d = location.downloader.Downloader()
432
433 # Try downloading a new database
434 try:
435 t = d.download(public_key=ns.public_key, timestamp=timestamp, tmpdir=tmpdir)
436
437 # If no file could be downloaded, log a message
438 except FileNotFoundError as e:
439 log.error("Could not download a new database")
440 return 1
441
442 # If we have not received a new file, there is nothing to do
443 if not t:
444 return 3
445
446 # Move temporary file to destination
447 shutil.move(t.name, ns.database)
448
449 return 0
450
451 def handle_verify(self, ns):
452 try:
453 db = location.Database(ns.database)
454 except FileNotFoundError as e:
455 log.error("%s: %s" % (ns.database, e))
456 return 127
457
458 # Verify the database
459 with open(ns.public_key, "r") as f:
460 if not db.verify(f):
461 log.error("Could not verify database")
462 return 1
463
464 # Success
465 log.debug("Database successfully verified")
466 return 0
467
4439e317
MT
468 def __get_output_formatter(self, ns):
469 try:
88ef7e9c 470 cls = location.export.formats[ns.format]
4439e317 471 except KeyError:
88ef7e9c 472 cls = location.export.OutputFormatter
4439e317 473
88ef7e9c 474 return cls
4439e317 475
fa9a3663
MT
476 def handle_list_countries(self, db, ns):
477 for country in db.countries:
478 line = [
479 country.code,
480 ]
481
482 if ns.show_continent:
483 line.append(country.continent_code)
484
485 if ns.show_name:
486 line.append(country.name)
487
488 # Format the output
489 line = " ".join(line)
490
491 # Print the output
492 print(line)
493
43154ed7 494 def handle_list_networks_by_as(self, db, ns):
88ef7e9c
MT
495 writer = self.__get_output_formatter(ns)
496
497 for asn in ns.asn:
498 f = writer(sys.stdout, prefix="AS%s" % asn)
499
500 # Print all matching networks
501 for n in db.search_networks(asn=asn, family=ns.family):
502 f.write(n)
503
504 f.finish()
43154ed7 505
ccc7ab4e 506 def handle_list_networks_by_cc(self, db, ns):
88ef7e9c
MT
507 writer = self.__get_output_formatter(ns)
508
509 for country_code in ns.country_code:
510 # Open standard output
511 f = writer(sys.stdout, prefix=country_code)
512
513 # Print all matching networks
514 for n in db.search_networks(country_code=country_code, family=ns.family):
515 f.write(n)
516
517 f.finish()
4439e317 518
bbdb2e0a
MT
519 def handle_list_networks_by_flags(self, db, ns):
520 flags = 0
521
522 if ns.anonymous_proxy:
523 flags |= location.NETWORK_FLAG_ANONYMOUS_PROXY
524
525 if ns.satellite_provider:
526 flags |= location.NETWORK_FLAG_SATELLITE_PROVIDER
527
528 if ns.anycast:
529 flags |= location.NETWORK_FLAG_ANYCAST
530
228d0e74
MT
531 if not flags:
532 raise ValueError(_("You must at least pass one flag"))
533
88ef7e9c
MT
534 writer = self.__get_output_formatter(ns)
535 f = writer(sys.stdout, prefix="custom")
536
537 for n in db.search_networks(flags=flags, family=ns.family):
538 f.write(n)
539
540 f.finish()
541
542 def handle_export(self, db, ns):
543 countries, asns = [], []
544
545 # Translate family
df072005
MT
546 if ns.family:
547 families = [ ns.family ]
88ef7e9c
MT
548 else:
549 families = [ socket.AF_INET6, socket.AF_INET ]
550
551 for object in ns.objects:
552 m = re.match("^AS(\d+)$", object)
553 if m:
554 object = int(m.group(1))
555
556 asns.append(object)
557
558 elif location.country_code_is_valid(object) \
559 or object in ("A1", "A2", "A3"):
560 countries.append(object)
561
562 else:
563 log.warning("Invalid argument: %s" % object)
564 continue
565
10fa313b 566 # Default to exporting all countries
88ef7e9c 567 if not countries and not asns:
10fa313b 568 countries = ["A1", "A2", "A3"] + [country.code for country in db.countries]
88ef7e9c
MT
569
570 # Select the output format
571 writer = self.__get_output_formatter(ns)
572
573 e = location.export.Exporter(db, writer)
574 e.export(ns.directory, countries=countries, asns=asns, families=families)
bbdb2e0a 575
ccc7ab4e 576
97769987
MT
577def format_timedelta(t):
578 s = []
579
580 if t.days:
581 s.append(
582 _("One Day", "%(days)s Days", t.days) % { "days" : t.days, }
583 )
584
585 hours = t.seconds // 3600
586 if hours:
587 s.append(
588 _("One Hour", "%(hours)s Hours", hours) % { "hours" : hours, }
589 )
590
591 minutes = (t.seconds % 3600) // 60
592 if minutes:
593 s.append(
594 _("One Minute", "%(minutes)s Minutes", minutes) % { "minutes" : minutes, }
595 )
596
597 seconds = t.seconds % 60
598 if t.seconds:
599 s.append(
600 _("One Second", "%(seconds)s Seconds", seconds) % { "seconds" : seconds, }
601 )
602
603 if not s:
604 return _("Now")
605
606 return _("%s ago") % ", ".join(s)
607
5118a4b8
MT
608def main():
609 # Run the command line interface
610 c = CLI()
611 c.run()
612
613main()