]> git.ipfire.org Git - location/libloc.git/blame - src/python/location.in
Fix timezone-dependant interpretation of database timestamp
[location/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"))
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
fadc1af0
MT
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
da3e360e
MT
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
43154ed7
MT
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)
44e5ef71 117 list_networks_by_as.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
118 list_networks_by_as.add_argument("--format",
119 choices=location.export.formats.keys(), default="list")
43154ed7
MT
120 list_networks_by_as.set_defaults(func=self.handle_list_networks_by_as)
121
ccc7ab4e 122 # List all networks in a country
b5cdfad7 123 list_networks_by_cc = subparsers.add_parser("list-networks-by-cc",
ccc7ab4e
MT
124 help=_("Lists all networks in a country"),
125 )
b5cdfad7 126 list_networks_by_cc.add_argument("country_code", nargs=1)
44e5ef71 127 list_networks_by_cc.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
128 list_networks_by_cc.add_argument("--format",
129 choices=location.export.formats.keys(), default="list")
b5cdfad7 130 list_networks_by_cc.set_defaults(func=self.handle_list_networks_by_cc)
ccc7ab4e 131
bbdb2e0a
MT
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 )
44e5ef71 145 list_networks_by_flags.add_argument("--family", choices=("ipv6", "ipv4"))
88ef7e9c
MT
146 list_networks_by_flags.add_argument("--format",
147 choices=location.export.formats.keys(), default="list")
bbdb2e0a
MT
148 list_networks_by_flags.set_defaults(func=self.handle_list_networks_by_flags)
149
fa9a3663
MT
150 # List countries
151 list_countries = subparsers.add_parser("list-countries",
152 help=_("Lists all countries"),
153 )
154 list_countries.add_argument("--show-name",
155 action="store_true", help=_("Show the name of the country"),
156 )
157 list_countries.add_argument("--show-continent",
158 action="store_true", help=_("Show the continent"),
159 )
160 list_countries.set_defaults(func=self.handle_list_countries)
161
88ef7e9c
MT
162 # Export
163 export = subparsers.add_parser("export",
164 help=_("Exports data in many formats to load it into packet filters"),
165 )
166 export.add_argument("--format", help=_("Output format"),
167 choices=location.export.formats.keys(), default="list")
168 export.add_argument("--directory", help=_("Output directory"), required=True)
169 export.add_argument("--family",
170 help=_("Specify address family"), choices=("ipv6", "ipv4"),
171 )
10fa313b 172 export.add_argument("objects", nargs="*", help=_("List country codes or ASNs to export"))
88ef7e9c
MT
173 export.set_defaults(func=self.handle_export)
174
78f37815
MT
175 args = parser.parse_args()
176
bc1f5f53 177 # Configure logging
f9de5e61
MT
178 if args.debug:
179 location.logger.set_level(logging.DEBUG)
bc1f5f53
MT
180 elif args.quiet:
181 location.logger.set_level(logging.WARNING)
f9de5e61 182
78f37815
MT
183 # Print usage if no action was given
184 if not "func" in args:
185 parser.print_usage()
186 sys.exit(2)
187
188 return args
5118a4b8
MT
189
190 def run(self):
191 # Parse command line arguments
192 args = self.parse_cli()
193
2538ed9a
MT
194 # Open database
195 try:
196 db = location.Database(args.database)
197 except FileNotFoundError as e:
1d237439 198 sys.stderr.write("location: Could not open database %s: %s\n" \
2538ed9a
MT
199 % (args.database, e))
200 sys.exit(1)
201
6961aaf3
MT
202 # Translate family (if present)
203 if "family" in args:
204 if args.family == "ipv6":
205 args.family = socket.AF_INET6
206 elif args.family == "ipv4":
207 args.family = socket.AF_INET
208 else:
209 args.family = 0
44e5ef71 210
5118a4b8 211 # Call function
228d0e74
MT
212 try:
213 ret = args.func(db, args)
214
215 # Catch invalid inputs
216 except ValueError as e:
217 sys.stderr.write("%s\n" % e)
218 ret = 2
5118a4b8
MT
219
220 # Return with exit code
221 if ret:
222 sys.exit(ret)
223
224 # Otherwise just exit
225 sys.exit(0)
226
9f64f1eb
MT
227 def handle_version(self, db, ns):
228 """
229 Print the version of the database
230 """
231 t = time.strftime(
232 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
233 )
234
235 print(t)
236
2538ed9a 237 def handle_lookup(self, db, ns):
5118a4b8
MT
238 ret = 0
239
fbf925c8
MT
240 format = " %-24s: %s"
241
5118a4b8
MT
242 for address in ns.address:
243 try:
fbf925c8 244 network = db.lookup(address)
5118a4b8 245 except ValueError:
9f2f5d13 246 print(_("Invalid IP address: %s") % address, file=sys.stderr)
5118a4b8
MT
247
248 args = {
249 "address" : address,
fbf925c8 250 "network" : network,
5118a4b8
MT
251 }
252
253 # Nothing found?
fbf925c8 254 if not network:
9f2f5d13 255 print(_("Nothing found for %(address)s") % args, file=sys.stderr)
5118a4b8
MT
256 ret = 1
257 continue
258
fbf925c8
MT
259 print("%s:" % address)
260 print(format % (_("Network"), network))
5118a4b8 261
fbf925c8
MT
262 # Print country
263 if network.country_code:
072ca100
MT
264 country = db.get_country(network.country_code)
265
266 print(format % (
267 _("Country"),
268 country.name if country else network.country_code),
269 )
5118a4b8 270
fbf925c8
MT
271 # Print AS information
272 if network.asn:
273 autonomous_system = db.get_as(network.asn)
5118a4b8 274
fbf925c8
MT
275 print(format % (
276 _("Autonomous System"),
277 autonomous_system or "AS%s" % network.asn),
278 )
5118a4b8 279
ee83fe2e
MT
280 # Anonymous Proxy
281 if network.has_flag(location.NETWORK_FLAG_ANONYMOUS_PROXY):
282 print(format % (
283 _("Anonymous Proxy"), _("yes"),
284 ))
285
286 # Satellite Provider
287 if network.has_flag(location.NETWORK_FLAG_SATELLITE_PROVIDER):
288 print(format % (
289 _("Satellite Provider"), _("yes"),
290 ))
291
292 # Anycast
293 if network.has_flag(location.NETWORK_FLAG_ANYCAST):
294 print(format % (
295 _("Anycast"), _("yes"),
296 ))
297
5118a4b8
MT
298 return ret
299
a68a46f5
MT
300 def handle_dump(self, db, ns):
301 # Use output file or write to stdout
302 f = ns.output or sys.stdout
303
d6d06375
MT
304 # Format everything like this
305 format = "%-24s %s\n"
306
a68a46f5
MT
307 # Write metadata
308 f.write("#\n# Location Database Export\n#\n")
309
310 f.write("# Generated: %s\n" % time.strftime(
311 "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(db.created_at),
312 ))
313
314 if db.vendor:
315 f.write("# Vendor: %s\n" % db.vendor)
316
317 if db.license:
318 f.write("# License: %s\n" % db.license)
319
320 f.write("#\n")
321
322 if db.description:
323 for line in db.description.splitlines():
324 f.write("# %s\n" % line)
325
326 f.write("#\n")
327
328 # Iterate over all ASes
329 for a in db.ases:
330 f.write("\n")
d6d06375
MT
331 f.write(format % ("aut-num:", "AS%s" % a.number))
332 f.write(format % ("name:", a.name))
333
334 flags = {
335 location.NETWORK_FLAG_ANONYMOUS_PROXY : "is-anonymous-proxy:",
336 location.NETWORK_FLAG_SATELLITE_PROVIDER : "is-satellite-provider:",
337 location.NETWORK_FLAG_ANYCAST : "is-anycast:",
338 }
a68a46f5
MT
339
340 # Iterate over all networks
341 for n in db.networks:
342 f.write("\n")
d6d06375 343 f.write(format % ("net:", n))
a68a46f5
MT
344
345 if n.country_code:
d6d06375 346 f.write(format % ("country:", n.country_code))
a68a46f5
MT
347
348 if n.asn:
03cd8096 349 f.write(format % ("aut-num:", n.asn))
d6d06375
MT
350
351 # Print all flags
352 for flag in flags:
353 if n.has_flag(flag):
354 f.write(format % (flags[flag], "yes"))
a68a46f5 355
2538ed9a 356 def handle_get_as(self, db, ns):
fadc1af0
MT
357 """
358 Gets information about Autonomous Systems
359 """
360 ret = 0
361
362 for asn in ns.asn:
363 try:
364 asn = int(asn)
365 except ValueError:
9f2f5d13 366 print(_("Invalid ASN: %s") % asn, file=sys.stderr)
fadc1af0
MT
367 ret = 1
368 continue
369
370 # Fetch AS from database
2538ed9a 371 a = db.get_as(asn)
fadc1af0
MT
372
373 # Nothing found
374 if not a:
9f2f5d13 375 print(_("Could not find AS%s") % asn, file=sys.stderr)
fadc1af0
MT
376 ret = 1
377 continue
378
379 print(_("AS%(asn)s belongs to %(name)s") % { "asn" : a.number, "name" : a.name })
380
381 return ret
5118a4b8 382
2538ed9a 383 def handle_search_as(self, db, ns):
da3e360e
MT
384 for query in ns.query:
385 # Print all matches ASes
2538ed9a 386 for a in db.search_as(query):
da3e360e
MT
387 print(a)
388
a6f1e346
MT
389 def handle_update(self, db, ns):
390 # Fetch the timestamp we need from DNS
391 t = location.discover_latest_version()
392
393 # Parse timestamp into datetime format
394 timestamp = datetime.datetime.fromtimestamp(t) if t else None
395
396 # Check the version of the local database
397 if db and timestamp and db.created_at >= timestamp.timestamp():
398 log.info("Already on the latest version")
399 return
400
401 # Download the database into the correct directory
402 tmpdir = os.path.dirname(ns.database)
403
404 # Create a downloader
405 d = location.downloader.Downloader()
406
407 # Try downloading a new database
408 try:
409 t = d.download(public_key=ns.public_key, timestamp=timestamp, tmpdir=tmpdir)
410
411 # If no file could be downloaded, log a message
412 except FileNotFoundError as e:
413 log.error("Could not download a new database")
414 return 1
415
416 # If we have not received a new file, there is nothing to do
417 if not t:
418 return 3
419
420 # Move temporary file to destination
421 shutil.move(t.name, ns.database)
422
423 return 0
424
425 def handle_verify(self, ns):
426 try:
427 db = location.Database(ns.database)
428 except FileNotFoundError as e:
429 log.error("%s: %s" % (ns.database, e))
430 return 127
431
432 # Verify the database
433 with open(ns.public_key, "r") as f:
434 if not db.verify(f):
435 log.error("Could not verify database")
436 return 1
437
438 # Success
439 log.debug("Database successfully verified")
440 return 0
441
4439e317
MT
442 def __get_output_formatter(self, ns):
443 try:
88ef7e9c 444 cls = location.export.formats[ns.format]
4439e317 445 except KeyError:
88ef7e9c 446 cls = location.export.OutputFormatter
4439e317 447
88ef7e9c 448 return cls
4439e317 449
fa9a3663
MT
450 def handle_list_countries(self, db, ns):
451 for country in db.countries:
452 line = [
453 country.code,
454 ]
455
456 if ns.show_continent:
457 line.append(country.continent_code)
458
459 if ns.show_name:
460 line.append(country.name)
461
462 # Format the output
463 line = " ".join(line)
464
465 # Print the output
466 print(line)
467
43154ed7 468 def handle_list_networks_by_as(self, db, ns):
88ef7e9c
MT
469 writer = self.__get_output_formatter(ns)
470
471 for asn in ns.asn:
472 f = writer(sys.stdout, prefix="AS%s" % asn)
473
474 # Print all matching networks
475 for n in db.search_networks(asn=asn, family=ns.family):
476 f.write(n)
477
478 f.finish()
43154ed7 479
ccc7ab4e 480 def handle_list_networks_by_cc(self, db, ns):
88ef7e9c
MT
481 writer = self.__get_output_formatter(ns)
482
483 for country_code in ns.country_code:
484 # Open standard output
485 f = writer(sys.stdout, prefix=country_code)
486
487 # Print all matching networks
488 for n in db.search_networks(country_code=country_code, family=ns.family):
489 f.write(n)
490
491 f.finish()
4439e317 492
bbdb2e0a
MT
493 def handle_list_networks_by_flags(self, db, ns):
494 flags = 0
495
496 if ns.anonymous_proxy:
497 flags |= location.NETWORK_FLAG_ANONYMOUS_PROXY
498
499 if ns.satellite_provider:
500 flags |= location.NETWORK_FLAG_SATELLITE_PROVIDER
501
502 if ns.anycast:
503 flags |= location.NETWORK_FLAG_ANYCAST
504
228d0e74
MT
505 if not flags:
506 raise ValueError(_("You must at least pass one flag"))
507
88ef7e9c
MT
508 writer = self.__get_output_formatter(ns)
509 f = writer(sys.stdout, prefix="custom")
510
511 for n in db.search_networks(flags=flags, family=ns.family):
512 f.write(n)
513
514 f.finish()
515
516 def handle_export(self, db, ns):
517 countries, asns = [], []
518
519 # Translate family
520 if ns.family == "ipv6":
521 families = [ socket.AF_INET6 ]
522 elif ns.family == "ipv4":
523 families = [ socket.AF_INET ]
524 else:
525 families = [ socket.AF_INET6, socket.AF_INET ]
526
527 for object in ns.objects:
528 m = re.match("^AS(\d+)$", object)
529 if m:
530 object = int(m.group(1))
531
532 asns.append(object)
533
534 elif location.country_code_is_valid(object) \
535 or object in ("A1", "A2", "A3"):
536 countries.append(object)
537
538 else:
539 log.warning("Invalid argument: %s" % object)
540 continue
541
10fa313b 542 # Default to exporting all countries
88ef7e9c 543 if not countries and not asns:
10fa313b 544 countries = ["A1", "A2", "A3"] + [country.code for country in db.countries]
88ef7e9c
MT
545
546 # Select the output format
547 writer = self.__get_output_formatter(ns)
548
549 e = location.export.Exporter(db, writer)
550 e.export(ns.directory, countries=countries, asns=asns, families=families)
bbdb2e0a 551
ccc7ab4e 552
5118a4b8
MT
553def main():
554 # Run the command line interface
555 c = CLI()
556 c.run()
557
558main()