1 commit 88ef7e9cd4b3a1a5662c7dc071bd7a44e1242cba
2 Author: Michael Tremer <michael.tremer@ipfire.org>
3 Date: Wed Jun 3 18:36:28 2020 +0000
5 Merge location-exporter(8) into location(8)
7 Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
9 diff --git a/Makefile.am b/Makefile.am
10 index 59870b1..9f520cc 100644
13 @@ -150,6 +150,7 @@ dist_pkgpython_PYTHON = \
14 src/python/__init__.py \
15 src/python/database.py \
16 src/python/downloader.py \
17 + src/python/export.py \
19 src/python/importer.py \
21 @@ -239,17 +240,14 @@ uninstall-perl:
25 - src/python/location-exporter \
26 src/python/location-importer
29 src/python/location.in \
30 - src/python/location-exporter.in \
31 src/python/location-importer.in
35 - src/python/location-exporter \
36 src/python/location-importer
38 # ------------------------------------------------------------------------------
39 diff --git a/src/python/export.py b/src/python/export.py
41 index 0000000..69fe964
43 +++ b/src/python/export.py
46 +###############################################################################
48 +# libloc - A library to determine the location of someone on the Internet #
50 +# Copyright (C) 2020 IPFire Development Team <info@ipfire.org> #
52 +# This library is free software; you can redistribute it and/or #
53 +# modify it under the terms of the GNU Lesser General Public #
54 +# License as published by the Free Software Foundation; either #
55 +# version 2.1 of the License, or (at your option) any later version. #
57 +# This library is distributed in the hope that it will be useful, #
58 +# but WITHOUT ANY WARRANTY; without even the implied warranty of #
59 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
60 +# Lesser General Public License for more details. #
62 +###############################################################################
71 +log = logging.getLogger("location.export")
74 +class OutputWriter(object):
78 + def __init__(self, f, prefix=None):
79 + self.f, self.prefix = f, prefix
81 + # Immediately write the header
82 + self._write_header()
85 + def open(cls, filename, **kwargs):
87 + Convenience function to open a file
89 + f = open(filename, cls.mode)
91 + return cls(f, **kwargs)
94 + return "<%s f=%s>" % (self.__class__.__name__, self.f)
96 + def _write_header(self):
98 + The header of the file
102 + def _write_footer(self):
104 + The footer of the file
108 + def write(self, network):
109 + self.f.write("%s\n" % network)
113 + Called when all data has been written
115 + self._write_footer()
121 +class IpsetOutputWriter(OutputWriter):
127 + def _write_header(self):
128 + self.f.write("create %s hash:net family inet hashsize 1024 maxelem 65536\n" % self.prefix)
130 + def write(self, network):
131 + self.f.write("add %s %s\n" % (self.prefix, network))
134 +class NftablesOutputWriter(OutputWriter):
140 + def _write_header(self):
141 + self.f.write("define %s = {\n" % self.prefix)
143 + def _write_footer(self):
144 + self.f.write("}\n")
146 + def write(self, network):
147 + self.f.write(" %s,\n" % network)
150 +class XTGeoIPOutputWriter(OutputWriter):
152 + Formats the output in that way, that it can be loaded by
153 + the xt_geoip kernel module from xtables-addons.
158 + def write(self, network):
159 + n = ipaddress.ip_network("%s" % network)
161 + for address in (n.network_address, n.broadcast_address):
162 + bytes = socket.inet_pton(
163 + socket.AF_INET6 if address.version == 6 else socket.AF_INET,
167 + self.f.write(bytes)
171 + "ipset" : IpsetOutputWriter,
172 + "list" : OutputWriter,
173 + "nftables" : NftablesOutputWriter,
174 + "xt_geoip" : XTGeoIPOutputWriter,
177 +class Exporter(object):
178 + def __init__(self, db, writer):
179 + self.db, self.writer = db, writer
181 + def export(self, directory, families, countries, asns):
182 + for family in families:
183 + log.debug("Exporting family %s" % family)
187 + # Create writers for countries
188 + for country_code in countries:
189 + filename = self._make_filename(
190 + directory, prefix=country_code, suffix=self.writer.suffix, family=family,
193 + writers[country_code] = self.writer.open(filename, prefix="CC_%s" % country_code)
195 + # Create writers for ASNs
197 + filename = self._make_filename(
198 + directory, "AS%s" % asn, suffix=self.writer.suffix, family=family,
201 + writers[asn] = self.writer.open(filename, prefix="AS%s" % asn)
203 + # Get all networks that match the family
204 + networks = self.db.search_networks(family=family)
206 + # Walk through all networks
207 + for network in networks:
208 + # Write matching countries
210 + writers[network.country_code].write(network)
214 + # Write matching ASNs
216 + writers[network.asn].write(network)
220 + # Write everything to the filesystem
221 + for writer in writers.values():
224 + def _make_filename(self, directory, prefix, suffix, family):
225 + filename = "%s.%s%s" % (
226 + prefix, suffix, "6" if family == socket.AF_INET6 else "4"
229 + return os.path.join(directory, filename)
230 diff --git a/src/python/location-exporter.in b/src/python/location-exporter.in
231 deleted file mode 100644
232 index d82f1d3..0000000
233 --- a/src/python/location-exporter.in
237 -###############################################################################
239 -# libloc - A library to determine the location of someone on the Internet #
241 -# Copyright (C) 2019 IPFire Development Team <info@ipfire.org> #
243 -# This library is free software; you can redistribute it and/or #
244 -# modify it under the terms of the GNU Lesser General Public #
245 -# License as published by the Free Software Foundation; either #
246 -# version 2.1 of the License, or (at your option) any later version. #
248 -# This library is distributed in the hope that it will be useful, #
249 -# but WITHOUT ANY WARRANTY; without even the implied warranty of #
250 -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU #
251 -# Lesser General Public License for more details. #
253 -###############################################################################
264 -# Load our location module
266 -from location.i18n import _
268 -# Initialise logging
269 -log = logging.getLogger("location.exporter")
272 -class OutputWriter(object):
273 - suffix = "networks"
275 - def __init__(self, family, country_code=None, asn=None):
276 - self.family, self.country_code, self.asn = family, country_code, asn
278 - self.f = io.BytesIO()
280 - def write_out(self, directory):
281 - # Make the output filename
282 - filename = os.path.join(
283 - directory, self._make_filename(),
286 - with open(filename, "wb") as f:
287 - self._write_header(f)
289 - # Copy all data into the file
290 - f.write(self.f.getbuffer())
292 - self._write_footer(f)
294 - def _make_filename(self):
295 - return "%s.%s%s" % (
296 - self.country_code or "AS%s" % self.asn,
298 - "6" if self.family == socket.AF_INET6 else "4"
303 - if self.country_code:
304 - return "CC_%s" % self.country_code
307 - return "AS%s" % self.asn
309 - def _write_header(self, f):
311 - The header of the file
315 - def _write_footer(self, f):
317 - The footer of the file
321 - def write(self, network):
322 - s = "%s\n" % network
324 - self.f.write(s.encode("ascii"))
327 -class IpsetOutputWriter(OutputWriter):
333 - def _write_header(self, f):
334 - h = "create %s hash:net family inet hashsize 1024 maxelem 65536\n" % self.name
336 - f.write(h.encode("ascii"))
338 - def write(self, network):
339 - s = "add %s %s\n" % (self.name, network)
341 - self.f.write(s.encode("ascii"))
344 -class NftablesOutputWriter(OutputWriter):
350 - def _write_header(self, f):
351 - h = "define %s = {\n" % self.name
353 - f.write(h.encode("ascii"))
355 - def _write_footer(self, f):
358 - def write(self, network):
359 - s = " %s,\n" % network
361 - self.f.write(s.encode("ascii"))
364 -class XTGeoIPOutputWriter(OutputWriter):
366 - Formats the output in that way, that it can be loaded by
367 - the xt_geoip kernel module from xtables-addons.
371 - def write(self, network):
372 - n = ipaddress.ip_network("%s" % network)
374 - for address in (n.network_address, n.broadcast_address):
375 - bytes = socket.inet_pton(
376 - socket.AF_INET6 if address.version == 6 else socket.AF_INET,
380 - self.f.write(bytes)
383 -class Exporter(object):
384 - def __init__(self, db, writer):
386 - self.writer = writer
388 - def export(self, directory, families, countries, asns):
389 - for family in families:
390 - log.debug("Exporting family %s" % family)
394 - # Create writers for countries
395 - for country_code in countries:
396 - writers[country_code] = self.writer(family, country_code=country_code)
398 - # Create writers for ASNs
400 - writers[asn] = self.writer(family, asn=asn)
402 - # Get all networks that match the family
403 - networks = self.db.search_networks(family=family)
405 - # Walk through all networks
406 - for network in networks:
407 - # Write matching countries
408 - if network.country_code in countries:
409 - writers[network.country_code].write(network)
411 - # Write matching ASNs
412 - if network.asn in asns:
413 - writers[network.asn].write(network)
415 - # Write everything to the filesystem
416 - for writer in writers.values():
417 - writer.write_out(directory)
422 - "ipset" : IpsetOutputWriter,
423 - "list" : OutputWriter,
424 - "nftables" : NftablesOutputWriter,
425 - "xt_geoip" : XTGeoIPOutputWriter,
428 - def parse_cli(self):
429 - parser = argparse.ArgumentParser(
430 - description=_("Location Exporter Command Line Interface"),
433 - # Global configuration flags
434 - parser.add_argument("--debug", action="store_true",
435 - help=_("Enable debug output"))
436 - parser.add_argument("--quiet", action="store_true",
437 - help=_("Enable quiet mode"))
440 - parser.add_argument("--version", action="version",
441 - version="%(prog)s @VERSION@")
444 - parser.add_argument("--database", "-d",
445 - default="@databasedir@/database.db", help=_("Path to database"),
449 - parser.add_argument("--format", help=_("Output format"),
450 - default="list", choices=self.output_formats.keys())
453 - parser.add_argument("--directory", help=_("Output directory"), required=True)
456 - parser.add_argument("--family", help=_("Specify address family"), choices=("ipv6", "ipv4"))
458 - # Countries and Autonomous Systems
459 - parser.add_argument("objects", nargs="+")
461 - args = parser.parse_args()
463 - # Configure logging
465 - location.logger.set_level(logging.DEBUG)
467 - location.logger.set_level(logging.WARNING)
472 - # Parse command line arguments
473 - args = self.parse_cli()
476 - ret = self.handle_export(args)
478 - # Return with exit code
482 - # Otherwise just exit
485 - def handle_export(self, ns):
486 - countries, asns = [], []
489 - if ns.family == "ipv6":
490 - families = [ socket.AF_INET6 ]
491 - elif ns.family == "ipv4":
492 - families = [ socket.AF_INET ]
494 - families = [ socket.AF_INET6, socket.AF_INET ]
496 - for object in ns.objects:
497 - m = re.match("^AS(\d+)$", object)
499 - object = int(m.group(1))
501 - asns.append(object)
503 - elif location.country_code_is_valid(object) \
504 - or object in ("A1", "A2", "A3"):
505 - countries.append(object)
508 - log.warning("Invalid argument: %s" % object)
511 - if not countries and not asns:
512 - log.error("Nothing to export")
515 - # Open the database
517 - db = location.Database(ns.database)
518 - except FileNotFoundError as e:
519 - log.error("Count not open database: %s" % ns.database)
522 - # Select the output format
523 - writer = self.output_formats.get(ns.format)
526 - e = Exporter(db, writer)
527 - e.export(ns.directory, countries=countries, asns=asns, families=families)
531 - # Run the command line interface
536 diff --git a/src/python/location.in b/src/python/location.in
537 index 10618e2..7614cae 100644
538 --- a/src/python/location.in
539 +++ b/src/python/location.in
540 @@ -22,6 +22,7 @@ import datetime
548 @@ -30,6 +31,8 @@ import time
549 # Load our location module
551 import location.downloader
552 +import location.export
554 from location.i18n import _
557 @@ -37,88 +40,7 @@ log = logging.getLogger("location")
561 -class OutputFormatter(object):
562 - def __init__(self, ns):
565 - def __enter__(self):
571 - def __exit__(self, type, value, tb):
577 - if "country_code" in self.ns:
578 - return "networks_country_%s" % self.ns.country_code[0]
580 - elif "asn" in self.ns:
581 - return "networks_AS%s" % self.ns.asn[0]
589 - def network(self, network):
593 -class IpsetOutputFormatter(OutputFormatter):
598 - print("create %s hash:net family inet hashsize 1024 maxelem 65536" % self.name)
600 - def network(self, network):
601 - print("add %s %s" % (self.name, network))
604 -class NftablesOutputFormatter(OutputFormatter):
609 - print("define %s = {" % self.name)
614 - def network(self, network):
615 - print(" %s," % network)
618 -class XTGeoIPOutputFormatter(OutputFormatter):
620 - Formats the output in that way, that it can be loaded by
621 - the xt_geoip kernel module from xtables-addons.
623 - def network(self, network):
624 - n = ipaddress.ip_network("%s" % network)
626 - for address in (n.network_address, n.broadcast_address):
627 - bytes = socket.inet_pton(
628 - socket.AF_INET6 if address.version == 6 else socket.AF_INET,
637 - "ipset" : IpsetOutputFormatter,
638 - "list" : OutputFormatter,
639 - "nftables" : NftablesOutputFormatter,
640 - "xt_geoip" : XTGeoIPOutputFormatter,
644 parser = argparse.ArgumentParser(
645 description=_("Location Database Command Line Interface"),
646 @@ -193,8 +115,8 @@ class CLI(object):
648 list_networks_by_as.add_argument("asn", nargs=1, type=int)
649 list_networks_by_as.add_argument("--family", choices=("ipv6", "ipv4"))
650 - list_networks_by_as.add_argument("--output-format",
651 - choices=self.output_formats.keys(), default="list")
652 + list_networks_by_as.add_argument("--format",
653 + choices=location.export.formats.keys(), default="list")
654 list_networks_by_as.set_defaults(func=self.handle_list_networks_by_as)
656 # List all networks in a country
657 @@ -203,8 +125,8 @@ class CLI(object):
659 list_networks_by_cc.add_argument("country_code", nargs=1)
660 list_networks_by_cc.add_argument("--family", choices=("ipv6", "ipv4"))
661 - list_networks_by_cc.add_argument("--output-format",
662 - choices=self.output_formats.keys(), default="list")
663 + list_networks_by_cc.add_argument("--format",
664 + choices=location.export.formats.keys(), default="list")
665 list_networks_by_cc.set_defaults(func=self.handle_list_networks_by_cc)
667 # List all networks with flags
668 @@ -221,10 +143,23 @@ class CLI(object):
669 action="store_true", help=_("Anycasts"),
671 list_networks_by_flags.add_argument("--family", choices=("ipv6", "ipv4"))
672 - list_networks_by_flags.add_argument("--output-format",
673 - choices=self.output_formats.keys(), default="list")
674 + list_networks_by_flags.add_argument("--format",
675 + choices=location.export.formats.keys(), default="list")
676 list_networks_by_flags.set_defaults(func=self.handle_list_networks_by_flags)
679 + export = subparsers.add_parser("export",
680 + help=_("Exports data in many formats to load it into packet filters"),
682 + export.add_argument("--format", help=_("Output format"),
683 + choices=location.export.formats.keys(), default="list")
684 + export.add_argument("--directory", help=_("Output directory"), required=True)
685 + export.add_argument("--family",
686 + help=_("Specify address family"), choices=("ipv6", "ipv4"),
688 + export.add_argument("objects", nargs="+", help=_("List country codes or ASNs to export"))
689 + export.set_defaults(func=self.handle_export)
691 args = parser.parse_args()
694 @@ -494,25 +429,36 @@ class CLI(object):
696 def __get_output_formatter(self, ns):
698 - cls = self.output_formats[ns.output_format]
699 + cls = location.export.formats[ns.format]
701 - cls = OutputFormatter
702 + cls = location.export.OutputFormatter
707 def handle_list_networks_by_as(self, db, ns):
708 - with self.__get_output_formatter(ns) as f:
710 - # Print all matching networks
711 - for n in db.search_networks(asn=asn, family=ns.family):
713 + writer = self.__get_output_formatter(ns)
716 + f = writer(sys.stdout, prefix="AS%s" % asn)
718 + # Print all matching networks
719 + for n in db.search_networks(asn=asn, family=ns.family):
724 def handle_list_networks_by_cc(self, db, ns):
725 - with self.__get_output_formatter(ns) as f:
726 - for country_code in ns.country_code:
727 - # Print all matching networks
728 - for n in db.search_networks(country_code=country_code, family=ns.family):
730 + writer = self.__get_output_formatter(ns)
732 + for country_code in ns.country_code:
733 + # Open standard output
734 + f = writer(sys.stdout, prefix=country_code)
736 + # Print all matching networks
737 + for n in db.search_networks(country_code=country_code, family=ns.family):
742 def handle_list_networks_by_flags(self, db, ns):
744 @@ -529,9 +475,49 @@ class CLI(object):
746 raise ValueError(_("You must at least pass one flag"))
748 - with self.__get_output_formatter(ns) as f:
749 - for n in db.search_networks(flags=flags, family=ns.family):
751 + writer = self.__get_output_formatter(ns)
752 + f = writer(sys.stdout, prefix="custom")
754 + for n in db.search_networks(flags=flags, family=ns.family):
759 + def handle_export(self, db, ns):
760 + countries, asns = [], []
763 + if ns.family == "ipv6":
764 + families = [ socket.AF_INET6 ]
765 + elif ns.family == "ipv4":
766 + families = [ socket.AF_INET ]
768 + families = [ socket.AF_INET6, socket.AF_INET ]
770 + for object in ns.objects:
771 + m = re.match("^AS(\d+)$", object)
773 + object = int(m.group(1))
775 + asns.append(object)
777 + elif location.country_code_is_valid(object) \
778 + or object in ("A1", "A2", "A3"):
779 + countries.append(object)
782 + log.warning("Invalid argument: %s" % object)
785 + if not countries and not asns:
786 + log.error("Nothing to export")
789 + # Select the output format
790 + writer = self.__get_output_formatter(ns)
792 + e = location.export.Exporter(db, writer)
793 + e.export(ns.directory, countries=countries, asns=asns, families=families)