]> git.ipfire.org Git - people/ms/libloc.git/blame - src/python/location.in
Makefile: Remove accidentially committed hacks for Debian
[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"))
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
88ef7e9c
MT
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
78f37815
MT
163 args = parser.parse_args()
164
bc1f5f53 165 # Configure logging
f9de5e61
MT
166 if args.debug:
167 location.logger.set_level(logging.DEBUG)
bc1f5f53
MT
168 elif args.quiet:
169 location.logger.set_level(logging.WARNING)
f9de5e61 170
78f37815
MT
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
5118a4b8
MT
177
178 def run(self):
179 # Parse command line arguments
180 args = self.parse_cli()
181
2538ed9a
MT
182 # Open database
183 try:
184 db = location.Database(args.database)
185 except FileNotFoundError as e:
1d237439 186 sys.stderr.write("location: Could not open database %s: %s\n" \
2538ed9a
MT
187 % (args.database, e))
188 sys.exit(1)
189
6961aaf3
MT
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
44e5ef71 198
5118a4b8 199 # Call function
228d0e74
MT
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
5118a4b8
MT
207
208 # Return with exit code
209 if ret:
210 sys.exit(ret)
211
212 # Otherwise just exit
213 sys.exit(0)
214
9f64f1eb
MT
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
2538ed9a 225 def handle_lookup(self, db, ns):
5118a4b8
MT
226 ret = 0
227
fbf925c8
MT
228 format = " %-24s: %s"
229
5118a4b8
MT
230 for address in ns.address:
231 try:
fbf925c8 232 network = db.lookup(address)
5118a4b8 233 except ValueError:
9f2f5d13 234 print(_("Invalid IP address: %s") % address, file=sys.stderr)
5118a4b8
MT
235
236 args = {
237 "address" : address,
fbf925c8 238 "network" : network,
5118a4b8
MT
239 }
240
241 # Nothing found?
fbf925c8 242 if not network:
9f2f5d13 243 print(_("Nothing found for %(address)s") % args, file=sys.stderr)
5118a4b8
MT
244 ret = 1
245 continue
246
fbf925c8
MT
247 print("%s:" % address)
248 print(format % (_("Network"), network))
5118a4b8 249
fbf925c8
MT
250 # Print country
251 if network.country_code:
072ca100
MT
252 country = db.get_country(network.country_code)
253
254 print(format % (
255 _("Country"),
256 country.name if country else network.country_code),
257 )
5118a4b8 258
fbf925c8
MT
259 # Print AS information
260 if network.asn:
261 autonomous_system = db.get_as(network.asn)
5118a4b8 262
fbf925c8
MT
263 print(format % (
264 _("Autonomous System"),
265 autonomous_system or "AS%s" % network.asn),
266 )
5118a4b8 267
ee83fe2e
MT
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
5118a4b8
MT
286 return ret
287
a68a46f5
MT
288 def handle_dump(self, db, ns):
289 # Use output file or write to stdout
290 f = ns.output or sys.stdout
291
d6d06375
MT
292 # Format everything like this
293 format = "%-24s %s\n"
294
a68a46f5
MT
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")
d6d06375
MT
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 }
a68a46f5
MT
327
328 # Iterate over all networks
329 for n in db.networks:
330 f.write("\n")
d6d06375 331 f.write(format % ("net:", n))
a68a46f5
MT
332
333 if n.country_code:
d6d06375 334 f.write(format % ("country:", n.country_code))
a68a46f5
MT
335
336 if n.asn:
03cd8096 337 f.write(format % ("aut-num:", n.asn))
d6d06375
MT
338
339 # Print all flags
340 for flag in flags:
341 if n.has_flag(flag):
342 f.write(format % (flags[flag], "yes"))
a68a46f5 343
2538ed9a 344 def handle_get_as(self, db, ns):
fadc1af0
MT
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:
9f2f5d13 354 print(_("Invalid ASN: %s") % asn, file=sys.stderr)
fadc1af0
MT
355 ret = 1
356 continue
357
358 # Fetch AS from database
2538ed9a 359 a = db.get_as(asn)
fadc1af0
MT
360
361 # Nothing found
362 if not a:
9f2f5d13 363 print(_("Could not find AS%s") % asn, file=sys.stderr)
fadc1af0
MT
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
5118a4b8 370
2538ed9a 371 def handle_search_as(self, db, ns):
da3e360e
MT
372 for query in ns.query:
373 # Print all matches ASes
2538ed9a 374 for a in db.search_as(query):
da3e360e
MT
375 print(a)
376
a6f1e346
MT
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
4439e317
MT
430 def __get_output_formatter(self, ns):
431 try:
88ef7e9c 432 cls = location.export.formats[ns.format]
4439e317 433 except KeyError:
88ef7e9c 434 cls = location.export.OutputFormatter
4439e317 435
88ef7e9c 436 return cls
4439e317 437
43154ed7 438 def handle_list_networks_by_as(self, db, ns):
88ef7e9c
MT
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()
43154ed7 449
ccc7ab4e 450 def handle_list_networks_by_cc(self, db, ns):
88ef7e9c
MT
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()
4439e317 462
bbdb2e0a
MT
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
228d0e74
MT
475 if not flags:
476 raise ValueError(_("You must at least pass one flag"))
477
88ef7e9c
MT
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)
bbdb2e0a 521
ccc7ab4e 522
5118a4b8
MT
523def main():
524 # Run the command line interface
525 c = CLI()
526 c.run()
527
528main()