]> git.ipfire.org Git - people/ms/libloc.git/blame - src/python/location-importer.in
python: Drop unused list of invalid IP addresses
[people/ms/libloc.git] / src / python / location-importer.in
CommitLineData
78ff0cf2
MT
1#!/usr/bin/python3
2###############################################################################
3# #
4# libloc - A library to determine the location of someone on the Internet #
5# #
6# Copyright (C) 2020 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
6ffd06b5 21import ipaddress
78ff0cf2 22import logging
6ffd06b5
MT
23import math
24import re
78ff0cf2
MT
25import sys
26
27# Load our location module
28import location
29c6fa22 29import location.database
3192b66c 30import location.importer
78ff0cf2
MT
31from location.i18n import _
32
33# Initialise logging
34log = logging.getLogger("location.importer")
35log.propagate = 1
36
37class CLI(object):
38 def parse_cli(self):
39 parser = argparse.ArgumentParser(
40 description=_("Location Importer Command Line Interface"),
41 )
6ffd06b5 42 subparsers = parser.add_subparsers()
78ff0cf2
MT
43
44 # Global configuration flags
45 parser.add_argument("--debug", action="store_true",
46 help=_("Enable debug output"))
47
48 # version
49 parser.add_argument("--version", action="version",
50 version="%(prog)s @VERSION@")
51
29c6fa22
MT
52 # Database
53 parser.add_argument("--database-host", required=True,
54 help=_("Database Hostname"), metavar=_("HOST"))
55 parser.add_argument("--database-name", required=True,
56 help=_("Database Name"), metavar=_("NAME"))
57 parser.add_argument("--database-username", required=True,
58 help=_("Database Username"), metavar=_("USERNAME"))
59 parser.add_argument("--database-password", required=True,
60 help=_("Database Password"), metavar=_("PASSWORD"))
61
6ffd06b5
MT
62 # Update WHOIS
63 update_whois = subparsers.add_parser("update-whois", help=_("Update WHOIS Information"))
64 update_whois.set_defaults(func=self.handle_update_whois)
65
78ff0cf2
MT
66 args = parser.parse_args()
67
68 # Enable debug logging
69 if args.debug:
70 log.setLevel(logging.DEBUG)
71
6ffd06b5
MT
72 # Print usage if no action was given
73 if not "func" in args:
74 parser.print_usage()
75 sys.exit(2)
76
78ff0cf2
MT
77 return args
78
79 def run(self):
80 # Parse command line arguments
81 args = self.parse_cli()
82
29c6fa22 83 # Initialise database
6ffd06b5 84 self.db = self._setup_database(args)
29c6fa22 85
78ff0cf2 86 # Call function
6ffd06b5 87 ret = args.func(args)
78ff0cf2
MT
88
89 # Return with exit code
90 if ret:
91 sys.exit(ret)
92
93 # Otherwise just exit
94 sys.exit(0)
95
29c6fa22
MT
96 def _setup_database(self, ns):
97 """
98 Initialise the database
99 """
100 # Connect to database
101 db = location.database.Connection(
102 host=ns.database_host, database=ns.database_name,
103 user=ns.database_username, password=ns.database_password,
104 )
105
106 with db.transaction():
107 db.execute("""
6ffd06b5 108 -- autnums
0365119d 109 CREATE TABLE IF NOT EXISTS autnums(number integer, name text);
6ffd06b5
MT
110 CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number);
111
429a43d1
MT
112 -- networks
113 CREATE TABLE IF NOT EXISTS networks(network inet, autnum integer, country text);
114 CREATE UNIQUE INDEX IF NOT EXISTS networks_network ON networks(network);
29c6fa22
MT
115 """)
116
117 return db
118
6ffd06b5
MT
119 def handle_update_whois(self, ns):
120 downloader = location.importer.Downloader()
121
122 # Download all sources
0365119d
MT
123 with self.db.transaction():
124 # Create some temporary tables to store parsed data
125 self.db.execute("""
126 CREATE TEMPORARY TABLE _autnums(number integer, organization text)
127 ON COMMIT DROP;
128 CREATE UNIQUE INDEX _autnums_number ON _autnums(number);
129
130 CREATE TEMPORARY TABLE _organizations(handle text, name text)
131 ON COMMIT DROP;
132 CREATE UNIQUE INDEX _organizations_handle ON _organizations(handle);
133 """)
134
135 for source in location.importer.WHOIS_SOURCES:
6ffd06b5
MT
136 with downloader.request(source, return_blocks=True) as f:
137 for block in f:
138 self._parse_block(block)
139
0365119d
MT
140 self.db.execute("""
141 INSERT INTO autnums(number, name)
142 SELECT _autnums.number, _organizations.name FROM _autnums
143 LEFT JOIN _organizations ON _autnums.organization = _organizations.handle
144 ON CONFLICT (number) DO UPDATE SET name = excluded.name;
145 """)
146
429a43d1
MT
147 # Download all extended sources
148 for source in location.importer.EXTENDED_SOURCES:
149 with self.db.transaction():
429a43d1
MT
150 # Download data
151 with downloader.request(source) as f:
152 for line in f:
153 self._parse_line(line)
154
6ffd06b5
MT
155 def _parse_block(self, block):
156 # Get first line to find out what type of block this is
157 line = block[0]
158
6ffd06b5 159 # aut-num
429a43d1 160 if line.startswith("aut-num:"):
6ffd06b5
MT
161 return self._parse_autnum_block(block)
162
163 # organisation
164 elif line.startswith("organisation:"):
165 return self._parse_org_block(block)
166
6ffd06b5 167 def _parse_autnum_block(self, block):
6ffd06b5
MT
168 autnum = {}
169 for line in block:
170 # Split line
171 key, val = split_line(line)
172
173 if key == "aut-num":
174 m = re.match(r"^(AS|as)(\d+)", val)
175 if m:
176 autnum["asn"] = m.group(2)
177
0365119d 178 elif key == "org":
6ffd06b5
MT
179 autnum[key] = val
180
181 # Skip empty objects
182 if not autnum:
183 return
184
185 # Insert into database
0365119d
MT
186 self.db.execute("INSERT INTO _autnums(number, organization) \
187 VALUES(%s, %s) ON CONFLICT (number) DO UPDATE SET \
188 organization = excluded.organization",
189 autnum.get("asn"), autnum.get("org"),
6ffd06b5
MT
190 )
191
6ffd06b5
MT
192 def _parse_org_block(self, block):
193 org = {}
194 for line in block:
195 # Split line
196 key, val = split_line(line)
197
0365119d 198 if key in ("organisation", "org-name"):
6ffd06b5
MT
199 org[key] = val
200
201 # Skip empty objects
202 if not org:
203 return
204
0365119d
MT
205 self.db.execute("INSERT INTO _organizations(handle, name) \
206 VALUES(%s, %s) ON CONFLICT (handle) DO \
207 UPDATE SET name = excluded.name",
208 org.get("organisation"), org.get("org-name"),
6ffd06b5
MT
209 )
210
429a43d1
MT
211 def _parse_line(self, line):
212 # Skip version line
213 if line.startswith("2"):
214 return
6ffd06b5 215
429a43d1
MT
216 # Skip comments
217 if line.startswith("#"):
218 return
6ffd06b5 219
429a43d1
MT
220 try:
221 registry, country_code, type, line = line.split("|", 3)
222 except:
223 log.warning("Could not parse line: %s" % line)
224 return
6ffd06b5 225
429a43d1
MT
226 # Skip any lines that are for stats only
227 if country_code == "*":
6ffd06b5
MT
228 return
229
429a43d1
MT
230 if type in ("ipv6", "ipv4"):
231 return self._parse_ip_line(country_code, type, line)
232
429a43d1
MT
233 def _parse_ip_line(self, country, type, line):
234 try:
235 address, prefix, date, status, organization = line.split("|")
236 except ValueError:
237 organization = None
238
239 # Try parsing the line without organization
240 try:
241 address, prefix, date, status = line.split("|")
242 except ValueError:
243 log.warning("Unhandled line format: %s" % line)
244 return
245
246 # Skip anything that isn't properly assigned
247 if not status in ("assigned", "allocated"):
248 return
249
250 # Cast prefix into an integer
251 try:
252 prefix = int(prefix)
253 except:
254 log.warning("Invalid prefix: %s" % prefix)
255
256 # Fix prefix length for IPv4
257 if type == "ipv4":
258 prefix = 32 - int(math.log(prefix, 2))
259
260 # Try to parse the address
261 try:
262 network = ipaddress.ip_network("%s/%s" % (address, prefix), strict=False)
263 except ValueError:
264 log.warning("Invalid IP address: %s" % address)
265 return
266
87b3e102
MT
267 self.db.execute("INSERT INTO networks(network, country) \
268 VALUES(%s, %s) ON CONFLICT (network) DO \
269 UPDATE SET country = excluded.country",
270 "%s" % network, country,
6ffd06b5
MT
271 )
272
273
274def split_line(line):
275 key, colon, val = line.partition(":")
276
277 # Strip any excess space
278 key = key.strip()
279 val = val.strip()
78ff0cf2 280
6ffd06b5 281 return key, val
78ff0cf2
MT
282
283def main():
284 # Run the command line interface
285 c = CLI()
286 c.run()
287
288main()