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