2 ###############################################################################
4 # libloc - A library to determine the location of someone on the Internet #
6 # Copyright (C) 2017-2021 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 ###############################################################################
31 # Load our location module
33 import location
.downloader
34 import location
.export
36 from location
.i18n
import _
39 log
= logging
.getLogger("location")
45 parser
= argparse
.ArgumentParser(
46 description
=_("Location Database Command Line Interface"),
48 subparsers
= parser
.add_subparsers()
50 # Global configuration flags
51 parser
.add_argument("--debug", action
="store_true",
52 help=_("Enable debug output"))
53 parser
.add_argument("--quiet", action
="store_true",
54 help=_("Enable quiet mode"))
57 parser
.add_argument("--version", action
="version",
58 version
="%(prog)s @VERSION@")
61 parser
.add_argument("--database", "-d",
62 default
="@databasedir@/database.db", help=_("Path to database"),
66 parser
.add_argument("--public-key", "-k",
67 default
="@databasedir@/signing-key.pem", help=_("Public Signing Key"),
70 # Show the database version
71 version
= subparsers
.add_parser("version",
72 help=_("Show database version"))
73 version
.set_defaults(func
=self
.handle_version
)
75 # lookup an IP address
76 lookup
= subparsers
.add_parser("lookup",
77 help=_("Lookup one or multiple IP addresses"),
79 lookup
.add_argument("address", nargs
="+")
80 lookup
.set_defaults(func
=self
.handle_lookup
)
82 # Dump the whole database
83 dump
= subparsers
.add_parser("dump",
84 help=_("Dump the entire database"),
86 dump
.add_argument("output", nargs
="?", type=argparse
.FileType("w"))
87 dump
.set_defaults(func
=self
.handle_dump
)
90 update
= subparsers
.add_parser("update", help=_("Update database"))
91 update
.add_argument("--cron",
92 help=_("Update the library only once per interval"),
93 choices
=("daily", "weekly", "monthly"),
95 update
.set_defaults(func
=self
.handle_update
)
98 verify
= subparsers
.add_parser("verify",
99 help=_("Verify the downloaded database"))
100 verify
.set_defaults(func
=self
.handle_verify
)
103 get_as
= subparsers
.add_parser("get-as",
104 help=_("Get information about one or multiple Autonomous Systems"),
106 get_as
.add_argument("asn", nargs
="+")
107 get_as
.set_defaults(func
=self
.handle_get_as
)
110 search_as
= subparsers
.add_parser("search-as",
111 help=_("Search for Autonomous Systems that match the string"),
113 search_as
.add_argument("query", nargs
=1)
114 search_as
.set_defaults(func
=self
.handle_search_as
)
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"),
120 list_networks_by_as
.add_argument("asn", nargs
=1, type=int)
121 list_networks_by_as
.add_argument("--family", choices
=("ipv6", "ipv4"))
122 list_networks_by_as
.add_argument("--format",
123 choices
=location
.export
.formats
.keys(), default
="list")
124 list_networks_by_as
.set_defaults(func
=self
.handle_list_networks_by_as
)
126 # List all networks in a country
127 list_networks_by_cc
= subparsers
.add_parser("list-networks-by-cc",
128 help=_("Lists all networks in a country"),
130 list_networks_by_cc
.add_argument("country_code", nargs
=1)
131 list_networks_by_cc
.add_argument("--family", choices
=("ipv6", "ipv4"))
132 list_networks_by_cc
.add_argument("--format",
133 choices
=location
.export
.formats
.keys(), default
="list")
134 list_networks_by_cc
.set_defaults(func
=self
.handle_list_networks_by_cc
)
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"),
140 list_networks_by_flags
.add_argument("--anonymous-proxy",
141 action
="store_true", help=_("Anonymous Proxies"),
143 list_networks_by_flags
.add_argument("--satellite-provider",
144 action
="store_true", help=_("Satellite Providers"),
146 list_networks_by_flags
.add_argument("--anycast",
147 action
="store_true", help=_("Anycasts"),
149 list_networks_by_flags
.add_argument("--drop",
150 action
="store_true", help=_("Hostile Networks safe to drop"),
152 list_networks_by_flags
.add_argument("--family", choices
=("ipv6", "ipv4"))
153 list_networks_by_flags
.add_argument("--format",
154 choices
=location
.export
.formats
.keys(), default
="list")
155 list_networks_by_flags
.set_defaults(func
=self
.handle_list_networks_by_flags
)
158 list_bogons
= subparsers
.add_parser("list-bogons",
159 help=_("Lists all bogons"),
161 list_bogons
.add_argument("--family", choices
=("ipv6", "ipv4"))
162 list_bogons
.add_argument("--format",
163 choices
=location
.export
.formats
.keys(), default
="list")
164 list_bogons
.set_defaults(func
=self
.handle_list_bogons
)
167 list_countries
= subparsers
.add_parser("list-countries",
168 help=_("Lists all countries"),
170 list_countries
.add_argument("--show-name",
171 action
="store_true", help=_("Show the name of the country"),
173 list_countries
.add_argument("--show-continent",
174 action
="store_true", help=_("Show the continent"),
176 list_countries
.set_defaults(func
=self
.handle_list_countries
)
179 export
= subparsers
.add_parser("export",
180 help=_("Exports data in many formats to load it into packet filters"),
182 export
.add_argument("--format", help=_("Output format"),
183 choices
=location
.export
.formats
.keys(), default
="list")
184 export
.add_argument("--directory", help=_("Output directory"), required
=True)
185 export
.add_argument("--family",
186 help=_("Specify address family"), choices
=("ipv6", "ipv4"),
188 export
.add_argument("objects", nargs
="*", help=_("List country codes or ASNs to export"))
189 export
.set_defaults(func
=self
.handle_export
)
191 args
= parser
.parse_args()
195 location
.logger
.set_level(logging
.DEBUG
)
197 location
.logger
.set_level(logging
.WARNING
)
199 # Print usage if no action was given
200 if not "func" in args
:
207 # Parse command line arguments
208 args
= self
.parse_cli()
212 db
= location
.Database(args
.database
)
213 except FileNotFoundError
as e
:
214 # Allow continuing without a database
215 if args
.func
== self
.handle_update
:
219 sys
.stderr
.write("location: Could not open database %s: %s\n" \
220 % (args
.database
, e
))
223 # Translate family (if present)
225 if args
.family
== "ipv6":
226 args
.family
= socket
.AF_INET6
227 elif args
.family
== "ipv4":
228 args
.family
= socket
.AF_INET
234 ret
= args
.func(db
, args
)
236 # Catch invalid inputs
237 except ValueError as e
:
238 sys
.stderr
.write("%s\n" % e
)
241 # Catch any other exceptions
242 except Exception as e
:
243 sys
.stderr
.write("%s\n" % e
)
246 # Return with exit code
250 # Otherwise just exit
253 def handle_version(self
, db
, ns
):
255 Print the version of the database
258 "%a, %d %b %Y %H:%M:%S GMT", time
.gmtime(db
.created_at
),
263 def handle_lookup(self
, db
, ns
):
266 format
= " %-24s: %s"
268 for address
in ns
.address
:
270 network
= db
.lookup(address
)
272 print(_("Invalid IP address: %s") % address
, file=sys
.stderr
)
282 print(_("Nothing found for %(address)s") % args
, file=sys
.stderr
)
286 print("%s:" % address
)
287 print(format
% (_("Network"), network
))
290 if network
.country_code
:
291 country
= db
.get_country(network
.country_code
)
295 country
.name
if country
else network
.country_code
),
298 # Print AS information
300 autonomous_system
= db
.get_as(network
.asn
)
303 _("Autonomous System"),
304 autonomous_system
or "AS%s" % network
.asn
),
308 if network
.has_flag(location
.NETWORK_FLAG_ANONYMOUS_PROXY
):
310 _("Anonymous Proxy"), _("yes"),
314 if network
.has_flag(location
.NETWORK_FLAG_SATELLITE_PROVIDER
):
316 _("Satellite Provider"), _("yes"),
320 if network
.has_flag(location
.NETWORK_FLAG_ANYCAST
):
322 _("Anycast"), _("yes"),
326 if network
.has_flag(location
.NETWORK_FLAG_DROP
):
328 _("Hostile Network safe to drop"), _("yes"),
333 def handle_dump(self
, db
, ns
):
334 # Use output file or write to stdout
335 f
= ns
.output
or sys
.stdout
337 # Format everything like this
338 format
= "%-24s %s\n"
341 f
.write("#\n# Location Database Export\n#\n")
343 f
.write("# Generated: %s\n" % time
.strftime(
344 "%a, %d %b %Y %H:%M:%S GMT", time
.gmtime(db
.created_at
),
348 f
.write("# Vendor: %s\n" % db
.vendor
)
351 f
.write("# License: %s\n" % db
.license
)
356 for line
in db
.description
.splitlines():
358 f
.write("%s\n" % line
.rstrip())
362 # Iterate over all ASes
365 f
.write(format
% ("aut-num:", "AS%s" % a
.number
))
366 f
.write(format
% ("name:", a
.name
))
369 location
.NETWORK_FLAG_ANONYMOUS_PROXY
: "is-anonymous-proxy:",
370 location
.NETWORK_FLAG_SATELLITE_PROVIDER
: "is-satellite-provider:",
371 location
.NETWORK_FLAG_ANYCAST
: "is-anycast:",
372 location
.NETWORK_FLAG_DROP
: "drop:",
375 # Iterate over all networks
376 for n
in db
.networks
:
378 f
.write(format
% ("net:", n
))
381 f
.write(format
% ("country:", n
.country_code
))
384 f
.write(format
% ("aut-num:", n
.asn
))
389 f
.write(format
% (flags
[flag
], "yes"))
391 def handle_get_as(self
, db
, ns
):
393 Gets information about Autonomous Systems
401 print(_("Invalid ASN: %s") % asn
, file=sys
.stderr
)
405 # Fetch AS from database
410 print(_("Could not find AS%s") % asn
, file=sys
.stderr
)
414 print(_("AS%(asn)s belongs to %(name)s") % { "asn" : a
.number
, "name" : a
.name
})
418 def handle_search_as(self
, db
, ns
):
419 for query
in ns
.query
:
420 # Print all matches ASes
421 for a
in db
.search_as(query
):
424 def handle_update(self
, db
, ns
):
428 if ns
.cron
== "daily":
429 delta
= datetime
.timedelta(days
=1)
430 elif ns
.cron
== "weekly":
431 delta
= datetime
.timedelta(days
=7)
432 elif ns
.cron
== "monthly":
433 delta
= datetime
.timedelta(days
=30)
435 delta
= delta
.total_seconds()
437 # Check if the database has recently been updated
438 if db
.created_at
>= (now
- delta
):
440 _("The database has been updated recently"),
444 # Fetch the timestamp we need from DNS
445 t
= location
.discover_latest_version()
447 # Check the version of the local database
448 if db
and t
and db
.created_at
>= t
:
449 log
.info("Already on the latest version")
452 # Download the database into the correct directory
453 tmpdir
= os
.path
.dirname(ns
.database
)
455 # Create a downloader
456 d
= location
.downloader
.Downloader()
458 # Try downloading a new database
460 t
= d
.download(public_key
=ns
.public_key
, timestamp
=t
, tmpdir
=tmpdir
)
462 # If no file could be downloaded, log a message
463 except FileNotFoundError
as e
:
464 log
.error("Could not download a new database")
467 # If we have not received a new file, there is nothing to do
471 # Move temporary file to destination
472 shutil
.move(t
.name
, ns
.database
)
476 def handle_verify(self
, db
, ns
):
477 # Verify the database
478 with
open(ns
.public_key
, "r") as f
:
480 log
.error("Could not verify database")
484 log
.debug("Database successfully verified")
487 def __get_output_formatter(self
, ns
):
489 cls
= location
.export
.formats
[ns
.format
]
491 cls
= location
.export
.OutputFormatter
495 def handle_list_countries(self
, db
, ns
):
496 for country
in db
.countries
:
501 if ns
.show_continent
:
502 line
.append(country
.continent_code
)
505 line
.append(country
.name
)
508 line
= " ".join(line
)
513 def handle_list_networks_by_as(self
, db
, ns
):
514 writer
= self
.__get
_output
_formatter
(ns
)
517 f
= writer(sys
.stdout
, prefix
="AS%s" % asn
)
519 # Print all matching networks
520 for n
in db
.search_networks(asns
=[asn
], family
=ns
.family
):
525 def handle_list_networks_by_cc(self
, db
, ns
):
526 writer
= self
.__get
_output
_formatter
(ns
)
528 for country_code
in ns
.country_code
:
529 # Open standard output
530 f
= writer(sys
.stdout
, prefix
=country_code
)
532 # Print all matching networks
533 for n
in db
.search_networks(country_codes
=[country_code
], family
=ns
.family
):
538 def handle_list_networks_by_flags(self
, db
, ns
):
541 if ns
.anonymous_proxy
:
542 flags |
= location
.NETWORK_FLAG_ANONYMOUS_PROXY
544 if ns
.satellite_provider
:
545 flags |
= location
.NETWORK_FLAG_SATELLITE_PROVIDER
548 flags |
= location
.NETWORK_FLAG_ANYCAST
551 flags |
= location
.NETWORK_FLAG_DROP
554 raise ValueError(_("You must at least pass one flag"))
556 writer
= self
.__get
_output
_formatter
(ns
)
557 f
= writer(sys
.stdout
, prefix
="custom")
559 for n
in db
.search_networks(flags
=flags
, family
=ns
.family
):
564 def handle_list_bogons(self
, db
, ns
):
565 writer
= self
.__get
_output
_formatter
(ns
)
566 f
= writer(sys
.stdout
, prefix
="bogons")
568 for n
in db
.list_bogons(family
=ns
.family
):
573 def handle_export(self
, db
, ns
):
574 countries
, asns
= [], []
578 families
= [ ns
.family
]
580 families
= [ socket
.AF_INET6
, socket
.AF_INET
]
582 for object in ns
.objects
:
583 m
= re
.match("^AS(\d+)$", object)
585 object = int(m
.group(1))
589 elif location
.country_code_is_valid(object) \
590 or object in ("A1", "A2", "A3", "XD"):
591 countries
.append(object)
594 log
.warning("Invalid argument: %s" % object)
597 # Default to exporting all countries
598 if not countries
and not asns
:
599 countries
= ["A1", "A2", "A3", "XD"] + [country
.code
for country
in db
.countries
]
601 # Select the output format
602 writer
= self
.__get
_output
_formatter
(ns
)
604 e
= location
.export
.Exporter(db
, writer
)
605 e
.export(ns
.directory
, countries
=countries
, asns
=asns
, families
=families
)
608 def format_timedelta(t
):
613 _("One Day", "%(days)s Days", t
.days
) % { "days" : t
.days
, }
616 hours
= t
.seconds
// 3600
619 _("One Hour", "%(hours)s Hours", hours
) % { "hours" : hours
, }
622 minutes
= (t
.seconds
% 3600) // 60
625 _("One Minute", "%(minutes)s Minutes", minutes
) % { "minutes" : minutes
, }
628 seconds
= t
.seconds
% 60
631 _("One Second", "%(seconds)s Seconds", seconds
) % { "seconds" : seconds
, }
637 return _("%s ago") % ", ".join(s
)
640 # Run the command line interface