]> git.ipfire.org Git - ipfire-2.x.git/blob - src/patches/libloc-0.9.1-merge-location-exporter-into-location.patch
Add convert-to-location converter.
[ipfire-2.x.git] / src / patches / libloc-0.9.1-merge-location-exporter-into-location.patch
1 commit 88ef7e9cd4b3a1a5662c7dc071bd7a44e1242cba
2 Author: Michael Tremer <michael.tremer@ipfire.org>
3 Date: Wed Jun 3 18:36:28 2020 +0000
4
5 Merge location-exporter(8) into location(8)
6
7 Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
8
9 diff --git a/Makefile.am b/Makefile.am
10 index 59870b1..9f520cc 100644
11 --- a/Makefile.am
12 +++ b/Makefile.am
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 \
18 src/python/i18n.py \
19 src/python/importer.py \
20 src/python/logger.py
21 @@ -239,17 +240,14 @@ uninstall-perl:
22
23 bin_SCRIPTS = \
24 src/python/location \
25 - src/python/location-exporter \
26 src/python/location-importer
27
28 EXTRA_DIST += \
29 src/python/location.in \
30 - src/python/location-exporter.in \
31 src/python/location-importer.in
32
33 CLEANFILES += \
34 src/python/location \
35 - src/python/location-exporter \
36 src/python/location-importer
37
38 # ------------------------------------------------------------------------------
39 diff --git a/src/python/export.py b/src/python/export.py
40 new file mode 100644
41 index 0000000..69fe964
42 --- /dev/null
43 +++ b/src/python/export.py
44 @@ -0,0 +1,185 @@
45 +#!/usr/bin/python3
46 +###############################################################################
47 +# #
48 +# libloc - A library to determine the location of someone on the Internet #
49 +# #
50 +# Copyright (C) 2020 IPFire Development Team <info@ipfire.org> #
51 +# #
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. #
56 +# #
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. #
61 +# #
62 +###############################################################################
63 +
64 +import io
65 +import ipaddress
66 +import logging
67 +import os
68 +import socket
69 +
70 +# Initialise logging
71 +log = logging.getLogger("location.export")
72 +log.propagate = 1
73 +
74 +class OutputWriter(object):
75 + suffix = "networks"
76 + mode = "w"
77 +
78 + def __init__(self, f, prefix=None):
79 + self.f, self.prefix = f, prefix
80 +
81 + # Immediately write the header
82 + self._write_header()
83 +
84 + @classmethod
85 + def open(cls, filename, **kwargs):
86 + """
87 + Convenience function to open a file
88 + """
89 + f = open(filename, cls.mode)
90 +
91 + return cls(f, **kwargs)
92 +
93 + def __repr__(self):
94 + return "<%s f=%s>" % (self.__class__.__name__, self.f)
95 +
96 + def _write_header(self):
97 + """
98 + The header of the file
99 + """
100 + pass
101 +
102 + def _write_footer(self):
103 + """
104 + The footer of the file
105 + """
106 + pass
107 +
108 + def write(self, network):
109 + self.f.write("%s\n" % network)
110 +
111 + def finish(self):
112 + """
113 + Called when all data has been written
114 + """
115 + self._write_footer()
116 +
117 + # Close the file
118 + self.f.close()
119 +
120 +
121 +class IpsetOutputWriter(OutputWriter):
122 + """
123 + For ipset
124 + """
125 + suffix = "ipset"
126 +
127 + def _write_header(self):
128 + self.f.write("create %s hash:net family inet hashsize 1024 maxelem 65536\n" % self.prefix)
129 +
130 + def write(self, network):
131 + self.f.write("add %s %s\n" % (self.prefix, network))
132 +
133 +
134 +class NftablesOutputWriter(OutputWriter):
135 + """
136 + For nftables
137 + """
138 + suffix = "set"
139 +
140 + def _write_header(self):
141 + self.f.write("define %s = {\n" % self.prefix)
142 +
143 + def _write_footer(self):
144 + self.f.write("}\n")
145 +
146 + def write(self, network):
147 + self.f.write(" %s,\n" % network)
148 +
149 +
150 +class XTGeoIPOutputWriter(OutputWriter):
151 + """
152 + Formats the output in that way, that it can be loaded by
153 + the xt_geoip kernel module from xtables-addons.
154 + """
155 + suffix = "iv"
156 + mode = "wb"
157 +
158 + def write(self, network):
159 + n = ipaddress.ip_network("%s" % network)
160 +
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,
164 + "%s" % address,
165 + )
166 +
167 + self.f.write(bytes)
168 +
169 +
170 +formats = {
171 + "ipset" : IpsetOutputWriter,
172 + "list" : OutputWriter,
173 + "nftables" : NftablesOutputWriter,
174 + "xt_geoip" : XTGeoIPOutputWriter,
175 +}
176 +
177 +class Exporter(object):
178 + def __init__(self, db, writer):
179 + self.db, self.writer = db, writer
180 +
181 + def export(self, directory, families, countries, asns):
182 + for family in families:
183 + log.debug("Exporting family %s" % family)
184 +
185 + writers = {}
186 +
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,
191 + )
192 +
193 + writers[country_code] = self.writer.open(filename, prefix="CC_%s" % country_code)
194 +
195 + # Create writers for ASNs
196 + for asn in asns:
197 + filename = self._make_filename(
198 + directory, "AS%s" % asn, suffix=self.writer.suffix, family=family,
199 + )
200 +
201 + writers[asn] = self.writer.open(filename, prefix="AS%s" % asn)
202 +
203 + # Get all networks that match the family
204 + networks = self.db.search_networks(family=family)
205 +
206 + # Walk through all networks
207 + for network in networks:
208 + # Write matching countries
209 + try:
210 + writers[network.country_code].write(network)
211 + except KeyError:
212 + pass
213 +
214 + # Write matching ASNs
215 + try:
216 + writers[network.asn].write(network)
217 + except KeyError:
218 + pass
219 +
220 + # Write everything to the filesystem
221 + for writer in writers.values():
222 + writer.finish()
223 +
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"
227 + )
228 +
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
234 +++ /dev/null
235 @@ -1,300 +0,0 @@
236 -#!/usr/bin/python3
237 -###############################################################################
238 -# #
239 -# libloc - A library to determine the location of someone on the Internet #
240 -# #
241 -# Copyright (C) 2019 IPFire Development Team <info@ipfire.org> #
242 -# #
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. #
247 -# #
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. #
252 -# #
253 -###############################################################################
254 -
255 -import argparse
256 -import io
257 -import ipaddress
258 -import logging
259 -import os.path
260 -import re
261 -import socket
262 -import sys
263 -
264 -# Load our location module
265 -import location
266 -from location.i18n import _
267 -
268 -# Initialise logging
269 -log = logging.getLogger("location.exporter")
270 -log.propagate = 1
271 -
272 -class OutputWriter(object):
273 - suffix = "networks"
274 -
275 - def __init__(self, family, country_code=None, asn=None):
276 - self.family, self.country_code, self.asn = family, country_code, asn
277 -
278 - self.f = io.BytesIO()
279 -
280 - def write_out(self, directory):
281 - # Make the output filename
282 - filename = os.path.join(
283 - directory, self._make_filename(),
284 - )
285 -
286 - with open(filename, "wb") as f:
287 - self._write_header(f)
288 -
289 - # Copy all data into the file
290 - f.write(self.f.getbuffer())
291 -
292 - self._write_footer(f)
293 -
294 - def _make_filename(self):
295 - return "%s.%s%s" % (
296 - self.country_code or "AS%s" % self.asn,
297 - self.suffix,
298 - "6" if self.family == socket.AF_INET6 else "4"
299 - )
300 -
301 - @property
302 - def name(self):
303 - if self.country_code:
304 - return "CC_%s" % self.country_code
305 -
306 - if self.asn:
307 - return "AS%s" % self.asn
308 -
309 - def _write_header(self, f):
310 - """
311 - The header of the file
312 - """
313 - pass
314 -
315 - def _write_footer(self, f):
316 - """
317 - The footer of the file
318 - """
319 - pass
320 -
321 - def write(self, network):
322 - s = "%s\n" % network
323 -
324 - self.f.write(s.encode("ascii"))
325 -
326 -
327 -class IpsetOutputWriter(OutputWriter):
328 - """
329 - For ipset
330 - """
331 - suffix = "ipset"
332 -
333 - def _write_header(self, f):
334 - h = "create %s hash:net family inet hashsize 1024 maxelem 65536\n" % self.name
335 -
336 - f.write(h.encode("ascii"))
337 -
338 - def write(self, network):
339 - s = "add %s %s\n" % (self.name, network)
340 -
341 - self.f.write(s.encode("ascii"))
342 -
343 -
344 -class NftablesOutputWriter(OutputWriter):
345 - """
346 - For nftables
347 - """
348 - suffix = "set"
349 -
350 - def _write_header(self, f):
351 - h = "define %s = {\n" % self.name
352 -
353 - f.write(h.encode("ascii"))
354 -
355 - def _write_footer(self, f):
356 - f.write(b"}")
357 -
358 - def write(self, network):
359 - s = " %s,\n" % network
360 -
361 - self.f.write(s.encode("ascii"))
362 -
363 -
364 -class XTGeoIPOutputWriter(OutputWriter):
365 - """
366 - Formats the output in that way, that it can be loaded by
367 - the xt_geoip kernel module from xtables-addons.
368 - """
369 - suffix = "iv"
370 -
371 - def write(self, network):
372 - n = ipaddress.ip_network("%s" % network)
373 -
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,
377 - "%s" % address,
378 - )
379 -
380 - self.f.write(bytes)
381 -
382 -
383 -class Exporter(object):
384 - def __init__(self, db, writer):
385 - self.db = db
386 - self.writer = writer
387 -
388 - def export(self, directory, families, countries, asns):
389 - for family in families:
390 - log.debug("Exporting family %s" % family)
391 -
392 - writers = {}
393 -
394 - # Create writers for countries
395 - for country_code in countries:
396 - writers[country_code] = self.writer(family, country_code=country_code)
397 -
398 - # Create writers for ASNs
399 - for asn in asns:
400 - writers[asn] = self.writer(family, asn=asn)
401 -
402 - # Get all networks that match the family
403 - networks = self.db.search_networks(family=family)
404 -
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)
410 -
411 - # Write matching ASNs
412 - if network.asn in asns:
413 - writers[network.asn].write(network)
414 -
415 - # Write everything to the filesystem
416 - for writer in writers.values():
417 - writer.write_out(directory)
418 -
419 -
420 -class CLI(object):
421 - output_formats = {
422 - "ipset" : IpsetOutputWriter,
423 - "list" : OutputWriter,
424 - "nftables" : NftablesOutputWriter,
425 - "xt_geoip" : XTGeoIPOutputWriter,
426 - }
427 -
428 - def parse_cli(self):
429 - parser = argparse.ArgumentParser(
430 - description=_("Location Exporter Command Line Interface"),
431 - )
432 -
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"))
438 -
439 - # version
440 - parser.add_argument("--version", action="version",
441 - version="%(prog)s @VERSION@")
442 -
443 - # database
444 - parser.add_argument("--database", "-d",
445 - default="@databasedir@/database.db", help=_("Path to database"),
446 - )
447 -
448 - # format
449 - parser.add_argument("--format", help=_("Output format"),
450 - default="list", choices=self.output_formats.keys())
451 -
452 - # directory
453 - parser.add_argument("--directory", help=_("Output directory"), required=True)
454 -
455 - # family
456 - parser.add_argument("--family", help=_("Specify address family"), choices=("ipv6", "ipv4"))
457 -
458 - # Countries and Autonomous Systems
459 - parser.add_argument("objects", nargs="+")
460 -
461 - args = parser.parse_args()
462 -
463 - # Configure logging
464 - if args.debug:
465 - location.logger.set_level(logging.DEBUG)
466 - elif args.quiet:
467 - location.logger.set_level(logging.WARNING)
468 -
469 - return args
470 -
471 - def run(self):
472 - # Parse command line arguments
473 - args = self.parse_cli()
474 -
475 - # Call function
476 - ret = self.handle_export(args)
477 -
478 - # Return with exit code
479 - if ret:
480 - sys.exit(ret)
481 -
482 - # Otherwise just exit
483 - sys.exit(0)
484 -
485 - def handle_export(self, ns):
486 - countries, asns = [], []
487 -
488 - # Translate family
489 - if ns.family == "ipv6":
490 - families = [ socket.AF_INET6 ]
491 - elif ns.family == "ipv4":
492 - families = [ socket.AF_INET ]
493 - else:
494 - families = [ socket.AF_INET6, socket.AF_INET ]
495 -
496 - for object in ns.objects:
497 - m = re.match("^AS(\d+)$", object)
498 - if m:
499 - object = int(m.group(1))
500 -
501 - asns.append(object)
502 -
503 - elif location.country_code_is_valid(object) \
504 - or object in ("A1", "A2", "A3"):
505 - countries.append(object)
506 -
507 - else:
508 - log.warning("Invalid argument: %s" % object)
509 - continue
510 -
511 - if not countries and not asns:
512 - log.error("Nothing to export")
513 - return 2
514 -
515 - # Open the database
516 - try:
517 - db = location.Database(ns.database)
518 - except FileNotFoundError as e:
519 - log.error("Count not open database: %s" % ns.database)
520 - return 1
521 -
522 - # Select the output format
523 - writer = self.output_formats.get(ns.format)
524 - assert writer
525 -
526 - e = Exporter(db, writer)
527 - e.export(ns.directory, countries=countries, asns=asns, families=families)
528 -
529 -
530 -def main():
531 - # Run the command line interface
532 - c = CLI()
533 - c.run()
534 -
535 -main()
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
541 import ipaddress
542 import logging
543 import os
544 +import re
545 import shutil
546 import socket
547 import sys
548 @@ -30,6 +31,8 @@ import time
549 # Load our location module
550 import location
551 import location.downloader
552 +import location.export
553 +
554 from location.i18n import _
555
556 # Setup logging
557 @@ -37,88 +40,7 @@ log = logging.getLogger("location")
558
559 # Output formatters
560
561 -class OutputFormatter(object):
562 - def __init__(self, ns):
563 - self.ns = ns
564 -
565 - def __enter__(self):
566 - # Open the output
567 - self.open()
568 -
569 - return self
570 -
571 - def __exit__(self, type, value, tb):
572 - if tb is None:
573 - self.close()
574 -
575 - @property
576 - def name(self):
577 - if "country_code" in self.ns:
578 - return "networks_country_%s" % self.ns.country_code[0]
579 -
580 - elif "asn" in self.ns:
581 - return "networks_AS%s" % self.ns.asn[0]
582 -
583 - def open(self):
584 - pass
585 -
586 - def close(self):
587 - pass
588 -
589 - def network(self, network):
590 - print(network)
591 -
592 -
593 -class IpsetOutputFormatter(OutputFormatter):
594 - """
595 - For nftables
596 - """
597 - def open(self):
598 - print("create %s hash:net family inet hashsize 1024 maxelem 65536" % self.name)
599 -
600 - def network(self, network):
601 - print("add %s %s" % (self.name, network))
602 -
603 -
604 -class NftablesOutputFormatter(OutputFormatter):
605 - """
606 - For nftables
607 - """
608 - def open(self):
609 - print("define %s = {" % self.name)
610 -
611 - def close(self):
612 - print("}")
613 -
614 - def network(self, network):
615 - print(" %s," % network)
616 -
617 -
618 -class XTGeoIPOutputFormatter(OutputFormatter):
619 - """
620 - Formats the output in that way, that it can be loaded by
621 - the xt_geoip kernel module from xtables-addons.
622 - """
623 - def network(self, network):
624 - n = ipaddress.ip_network("%s" % network)
625 -
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,
629 - "%s" % address,
630 - )
631 -
632 - os.write(1, bytes)
633 -
634 -
635 class CLI(object):
636 - output_formats = {
637 - "ipset" : IpsetOutputFormatter,
638 - "list" : OutputFormatter,
639 - "nftables" : NftablesOutputFormatter,
640 - "xt_geoip" : XTGeoIPOutputFormatter,
641 - }
642 -
643 def parse_cli(self):
644 parser = argparse.ArgumentParser(
645 description=_("Location Database Command Line Interface"),
646 @@ -193,8 +115,8 @@ class CLI(object):
647 )
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)
655
656 # List all networks in a country
657 @@ -203,8 +125,8 @@ class CLI(object):
658 )
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)
666
667 # List all networks with flags
668 @@ -221,10 +143,23 @@ class CLI(object):
669 action="store_true", help=_("Anycasts"),
670 )
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)
677
678 + # Export
679 + export = subparsers.add_parser("export",
680 + help=_("Exports data in many formats to load it into packet filters"),
681 + )
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"),
687 + )
688 + export.add_argument("objects", nargs="+", help=_("List country codes or ASNs to export"))
689 + export.set_defaults(func=self.handle_export)
690 +
691 args = parser.parse_args()
692
693 # Configure logging
694 @@ -494,25 +429,36 @@ class CLI(object):
695
696 def __get_output_formatter(self, ns):
697 try:
698 - cls = self.output_formats[ns.output_format]
699 + cls = location.export.formats[ns.format]
700 except KeyError:
701 - cls = OutputFormatter
702 + cls = location.export.OutputFormatter
703
704 - return cls(ns)
705 + return cls
706
707 def handle_list_networks_by_as(self, db, ns):
708 - with self.__get_output_formatter(ns) as f:
709 - for asn in ns.asn:
710 - # Print all matching networks
711 - for n in db.search_networks(asn=asn, family=ns.family):
712 - f.network(n)
713 + writer = self.__get_output_formatter(ns)
714 +
715 + for asn in ns.asn:
716 + f = writer(sys.stdout, prefix="AS%s" % asn)
717 +
718 + # Print all matching networks
719 + for n in db.search_networks(asn=asn, family=ns.family):
720 + f.write(n)
721 +
722 + f.finish()
723
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):
729 - f.network(n)
730 + writer = self.__get_output_formatter(ns)
731 +
732 + for country_code in ns.country_code:
733 + # Open standard output
734 + f = writer(sys.stdout, prefix=country_code)
735 +
736 + # Print all matching networks
737 + for n in db.search_networks(country_code=country_code, family=ns.family):
738 + f.write(n)
739 +
740 + f.finish()
741
742 def handle_list_networks_by_flags(self, db, ns):
743 flags = 0
744 @@ -529,9 +475,49 @@ class CLI(object):
745 if not flags:
746 raise ValueError(_("You must at least pass one flag"))
747
748 - with self.__get_output_formatter(ns) as f:
749 - for n in db.search_networks(flags=flags, family=ns.family):
750 - f.network(n)
751 + writer = self.__get_output_formatter(ns)
752 + f = writer(sys.stdout, prefix="custom")
753 +
754 + for n in db.search_networks(flags=flags, family=ns.family):
755 + f.write(n)
756 +
757 + f.finish()
758 +
759 + def handle_export(self, db, ns):
760 + countries, asns = [], []
761 +
762 + # Translate family
763 + if ns.family == "ipv6":
764 + families = [ socket.AF_INET6 ]
765 + elif ns.family == "ipv4":
766 + families = [ socket.AF_INET ]
767 + else:
768 + families = [ socket.AF_INET6, socket.AF_INET ]
769 +
770 + for object in ns.objects:
771 + m = re.match("^AS(\d+)$", object)
772 + if m:
773 + object = int(m.group(1))
774 +
775 + asns.append(object)
776 +
777 + elif location.country_code_is_valid(object) \
778 + or object in ("A1", "A2", "A3"):
779 + countries.append(object)
780 +
781 + else:
782 + log.warning("Invalid argument: %s" % object)
783 + continue
784 +
785 + if not countries and not asns:
786 + log.error("Nothing to export")
787 + return 2
788 +
789 + # Select the output format
790 + writer = self.__get_output_formatter(ns)
791 +
792 + e = location.export.Exporter(db, writer)
793 + e.export(ns.directory, countries=countries, asns=asns, families=families)
794
795
796 def main():