2 ###############################################################################
4 # libloc - A library to determine the location of someone on the Internet #
6 # Copyright (C) 2017 IPFire Development Team <info@ipfire.org> #
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. #
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. #
18 ###############################################################################
30 # Load our location module
32 import location
.downloader
33 from location
.i18n
import _
36 log
= logging
.getLogger("location")
40 class OutputFormatter(object):
41 def __init__(self
, ns
):
50 def __exit__(self
, type, value
, tb
):
56 if "country_code" in self
.ns
:
57 return "networks_country_%s" % self
.ns
.country_code
[0]
59 elif "asn" in self
.ns
:
60 return "networks_AS%s" % self
.ns
.asn
[0]
68 def network(self
, network
):
72 class IpsetOutputFormatter(OutputFormatter
):
77 print("create %s hash:net family inet hashsize 1024 maxelem 65536" % self
.name
)
79 def network(self
, network
):
80 print("add %s %s" % (self
.name
, network
))
83 class NftablesOutputFormatter(OutputFormatter
):
88 print("define %s = {" % self
.name
)
93 def network(self
, network
):
94 print(" %s," % network
)
97 class XTGeoIPOutputFormatter(OutputFormatter
):
99 Formats the output in that way, that it can be loaded by
100 the xt_geoip kernel module from xtables-addons.
102 def network(self
, network
):
103 n
= ipaddress
.ip_network("%s" % network
)
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
,
116 "ipset" : IpsetOutputFormatter
,
117 "list" : OutputFormatter
,
118 "nftables" : NftablesOutputFormatter
,
119 "xt_geoip" : XTGeoIPOutputFormatter
,
123 parser
= argparse
.ArgumentParser(
124 description
=_("Location Database Command Line Interface"),
126 subparsers
= parser
.add_subparsers()
128 # Global configuration flags
129 parser
.add_argument("--debug", action
="store_true",
130 help=_("Enable debug output"))
131 parser
.add_argument("--quiet", action
="store_true",
132 help=_("Enable quiet mode"))
135 parser
.add_argument("--version", action
="version",
136 version
="%(prog)s @VERSION@")
139 parser
.add_argument("--database", "-d",
140 default
="@databasedir@/database.db", help=_("Path to database"),
144 parser
.add_argument("--public-key", "-k",
145 default
="@databasedir@/signing-key.pem", help=_("Public Signing Key"),
148 # Show the database version
149 version
= subparsers
.add_parser("version",
150 help=_("Show database version"))
151 version
.set_defaults(func
=self
.handle_version
)
153 # lookup an IP address
154 lookup
= subparsers
.add_parser("lookup",
155 help=_("Lookup one or multiple IP addresses"),
157 lookup
.add_argument("address", nargs
="+")
158 lookup
.set_defaults(func
=self
.handle_lookup
)
160 # Dump the whole database
161 dump
= subparsers
.add_parser("dump",
162 help=_("Dump the entire database"),
164 dump
.add_argument("output", nargs
="?", type=argparse
.FileType("w"))
165 dump
.set_defaults(func
=self
.handle_dump
)
168 update
= subparsers
.add_parser("update", help=_("Update database"))
169 update
.set_defaults(func
=self
.handle_update
)
172 verify
= subparsers
.add_parser("verify",
173 help=_("Verify the downloaded database"))
174 verify
.set_defaults(func
=self
.handle_verify
)
177 get_as
= subparsers
.add_parser("get-as",
178 help=_("Get information about one or multiple Autonomous Systems"),
180 get_as
.add_argument("asn", nargs
="+")
181 get_as
.set_defaults(func
=self
.handle_get_as
)
184 search_as
= subparsers
.add_parser("search-as",
185 help=_("Search for Autonomous Systems that match the string"),
187 search_as
.add_argument("query", nargs
=1)
188 search_as
.set_defaults(func
=self
.handle_search_as
)
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"),
194 list_networks_by_as
.add_argument("asn", nargs
=1, type=int)
195 list_networks_by_as
.add_argument("--family", choices
=("ipv6", "ipv4"))
196 list_networks_by_as
.add_argument("--output-format",
197 choices
=self
.output_formats
.keys(), default
="list")
198 list_networks_by_as
.set_defaults(func
=self
.handle_list_networks_by_as
)
200 # List all networks in a country
201 list_networks_by_cc
= subparsers
.add_parser("list-networks-by-cc",
202 help=_("Lists all networks in a country"),
204 list_networks_by_cc
.add_argument("country_code", nargs
=1)
205 list_networks_by_cc
.add_argument("--family", choices
=("ipv6", "ipv4"))
206 list_networks_by_cc
.add_argument("--output-format",
207 choices
=self
.output_formats
.keys(), default
="list")
208 list_networks_by_cc
.set_defaults(func
=self
.handle_list_networks_by_cc
)
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"),
214 list_networks_by_flags
.add_argument("--anonymous-proxy",
215 action
="store_true", help=_("Anonymous Proxies"),
217 list_networks_by_flags
.add_argument("--satellite-provider",
218 action
="store_true", help=_("Satellite Providers"),
220 list_networks_by_flags
.add_argument("--anycast",
221 action
="store_true", help=_("Anycasts"),
223 list_networks_by_flags
.add_argument("--family", choices
=("ipv6", "ipv4"))
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
)
228 args
= parser
.parse_args()
232 location
.logger
.set_level(logging
.DEBUG
)
234 location
.logger
.set_level(logging
.WARNING
)
236 # Print usage if no action was given
237 if not "func" in args
:
244 # Parse command line arguments
245 args
= self
.parse_cli()
249 db
= location
.Database(args
.database
)
250 except FileNotFoundError
as e
:
251 sys
.stderr
.write("location: Could not open database %s: %s\n" \
252 % (args
.database
, e
))
255 # Translate family (if present)
257 if args
.family
== "ipv6":
258 args
.family
= socket
.AF_INET6
259 elif args
.family
== "ipv4":
260 args
.family
= socket
.AF_INET
266 ret
= args
.func(db
, args
)
268 # Catch invalid inputs
269 except ValueError as e
:
270 sys
.stderr
.write("%s\n" % e
)
273 # Return with exit code
277 # Otherwise just exit
280 def handle_version(self
, db
, ns
):
282 Print the version of the database
285 "%a, %d %b %Y %H:%M:%S GMT", time
.gmtime(db
.created_at
),
290 def handle_lookup(self
, db
, ns
):
293 format
= " %-24s: %s"
295 for address
in ns
.address
:
297 network
= db
.lookup(address
)
299 print(_("Invalid IP address: %s") % address
, file=sys
.stderr
)
308 print(_("Nothing found for %(address)s") % args
, file=sys
.stderr
)
312 print("%s:" % address
)
313 print(format
% (_("Network"), network
))
316 if network
.country_code
:
317 country
= db
.get_country(network
.country_code
)
321 country
.name
if country
else network
.country_code
),
324 # Print AS information
326 autonomous_system
= db
.get_as(network
.asn
)
329 _("Autonomous System"),
330 autonomous_system
or "AS%s" % network
.asn
),
334 if network
.has_flag(location
.NETWORK_FLAG_ANONYMOUS_PROXY
):
336 _("Anonymous Proxy"), _("yes"),
340 if network
.has_flag(location
.NETWORK_FLAG_SATELLITE_PROVIDER
):
342 _("Satellite Provider"), _("yes"),
346 if network
.has_flag(location
.NETWORK_FLAG_ANYCAST
):
348 _("Anycast"), _("yes"),
353 def handle_dump(self
, db
, ns
):
354 # Use output file or write to stdout
355 f
= ns
.output
or sys
.stdout
357 # Format everything like this
358 format
= "%-24s %s\n"
361 f
.write("#\n# Location Database Export\n#\n")
363 f
.write("# Generated: %s\n" % time
.strftime(
364 "%a, %d %b %Y %H:%M:%S GMT", time
.gmtime(db
.created_at
),
368 f
.write("# Vendor: %s\n" % db
.vendor
)
371 f
.write("# License: %s\n" % db
.license
)
376 for line
in db
.description
.splitlines():
377 f
.write("# %s\n" % line
)
381 # Iterate over all ASes
384 f
.write(format
% ("aut-num:", "AS%s" % a
.number
))
385 f
.write(format
% ("name:", a
.name
))
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:",
393 # Iterate over all networks
394 for n
in db
.networks
:
396 f
.write(format
% ("net:", n
))
399 f
.write(format
% ("country:", n
.country_code
))
402 f
.write(format
% ("aut-num:", n
.asn
))
407 f
.write(format
% (flags
[flag
], "yes"))
409 def handle_get_as(self
, db
, ns
):
411 Gets information about Autonomous Systems
419 print(_("Invalid ASN: %s") % asn
, file=sys
.stderr
)
423 # Fetch AS from database
428 print(_("Could not find AS%s") % asn
, file=sys
.stderr
)
432 print(_("AS%(asn)s belongs to %(name)s") % { "asn" : a
.number
, "name" : a
.name
})
436 def handle_search_as(self
, db
, ns
):
437 for query
in ns
.query
:
438 # Print all matches ASes
439 for a
in db
.search_as(query
):
442 def handle_update(self
, db
, ns
):
443 # Fetch the timestamp we need from DNS
444 t
= location
.discover_latest_version()
446 # Parse timestamp into datetime format
447 timestamp
= datetime
.datetime
.fromtimestamp(t
) if t
else None
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")
454 # Download the database into the correct directory
455 tmpdir
= os
.path
.dirname(ns
.database
)
457 # Create a downloader
458 d
= location
.downloader
.Downloader()
460 # Try downloading a new database
462 t
= d
.download(public_key
=ns
.public_key
, timestamp
=timestamp
, tmpdir
=tmpdir
)
464 # If no file could be downloaded, log a message
465 except FileNotFoundError
as e
:
466 log
.error("Could not download a new database")
469 # If we have not received a new file, there is nothing to do
473 # Move temporary file to destination
474 shutil
.move(t
.name
, ns
.database
)
478 def handle_verify(self
, ns
):
480 db
= location
.Database(ns
.database
)
481 except FileNotFoundError
as e
:
482 log
.error("%s: %s" % (ns
.database
, e
))
485 # Verify the database
486 with
open(ns
.public_key
, "r") as f
:
488 log
.error("Could not verify database")
492 log
.debug("Database successfully verified")
495 def __get_output_formatter(self
, ns
):
497 cls
= self
.output_formats
[ns
.output_format
]
499 cls
= OutputFormatter
503 def handle_list_networks_by_as(self
, db
, ns
):
504 with self
.__get
_output
_formatter
(ns
) as f
:
506 # Print all matching networks
507 for n
in db
.search_networks(asn
=asn
, family
=ns
.family
):
510 def handle_list_networks_by_cc(self
, db
, ns
):
511 with self
.__get
_output
_formatter
(ns
) as f
:
512 for country_code
in ns
.country_code
:
513 # Print all matching networks
514 for n
in db
.search_networks(country_code
=country_code
, family
=ns
.family
):
517 def handle_list_networks_by_flags(self
, db
, ns
):
520 if ns
.anonymous_proxy
:
521 flags |
= location
.NETWORK_FLAG_ANONYMOUS_PROXY
523 if ns
.satellite_provider
:
524 flags |
= location
.NETWORK_FLAG_SATELLITE_PROVIDER
527 flags |
= location
.NETWORK_FLAG_ANYCAST
530 raise ValueError(_("You must at least pass one flag"))
532 with self
.__get
_output
_formatter
(ns
) as f
:
533 for n
in db
.search_networks(flags
=flags
, family
=ns
.family
):
538 # Run the command line interface