]>
Commit | Line | Data |
---|---|---|
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 | ||
20 | import argparse | |
6ffd06b5 | 21 | import ipaddress |
78ff0cf2 | 22 | import logging |
6ffd06b5 MT |
23 | import math |
24 | import re | |
78ff0cf2 MT |
25 | import sys |
26 | ||
27 | # Load our location module | |
28 | import location | |
29c6fa22 | 29 | import location.database |
3192b66c | 30 | import location.importer |
78ff0cf2 MT |
31 | from location.i18n import _ |
32 | ||
33 | # Initialise logging | |
34 | log = logging.getLogger("location.importer") | |
35 | log.propagate = 1 | |
36 | ||
6ffd06b5 MT |
37 | INVALID_ADDRESSES = ( |
38 | "0.0.0.0", | |
39 | "::/0", | |
40 | "0::/0", | |
41 | ) | |
42 | ||
78ff0cf2 MT |
43 | class CLI(object): |
44 | def parse_cli(self): | |
45 | parser = argparse.ArgumentParser( | |
46 | description=_("Location Importer Command Line Interface"), | |
47 | ) | |
6ffd06b5 | 48 | subparsers = parser.add_subparsers() |
78ff0cf2 MT |
49 | |
50 | # Global configuration flags | |
51 | parser.add_argument("--debug", action="store_true", | |
52 | help=_("Enable debug output")) | |
53 | ||
54 | # version | |
55 | parser.add_argument("--version", action="version", | |
56 | version="%(prog)s @VERSION@") | |
57 | ||
29c6fa22 MT |
58 | # Database |
59 | parser.add_argument("--database-host", required=True, | |
60 | help=_("Database Hostname"), metavar=_("HOST")) | |
61 | parser.add_argument("--database-name", required=True, | |
62 | help=_("Database Name"), metavar=_("NAME")) | |
63 | parser.add_argument("--database-username", required=True, | |
64 | help=_("Database Username"), metavar=_("USERNAME")) | |
65 | parser.add_argument("--database-password", required=True, | |
66 | help=_("Database Password"), metavar=_("PASSWORD")) | |
67 | ||
6ffd06b5 MT |
68 | # Update WHOIS |
69 | update_whois = subparsers.add_parser("update-whois", help=_("Update WHOIS Information")) | |
70 | update_whois.set_defaults(func=self.handle_update_whois) | |
71 | ||
78ff0cf2 MT |
72 | args = parser.parse_args() |
73 | ||
74 | # Enable debug logging | |
75 | if args.debug: | |
76 | log.setLevel(logging.DEBUG) | |
77 | ||
6ffd06b5 MT |
78 | # Print usage if no action was given |
79 | if not "func" in args: | |
80 | parser.print_usage() | |
81 | sys.exit(2) | |
82 | ||
78ff0cf2 MT |
83 | return args |
84 | ||
85 | def run(self): | |
86 | # Parse command line arguments | |
87 | args = self.parse_cli() | |
88 | ||
29c6fa22 | 89 | # Initialise database |
6ffd06b5 | 90 | self.db = self._setup_database(args) |
29c6fa22 | 91 | |
78ff0cf2 | 92 | # Call function |
6ffd06b5 | 93 | ret = args.func(args) |
78ff0cf2 MT |
94 | |
95 | # Return with exit code | |
96 | if ret: | |
97 | sys.exit(ret) | |
98 | ||
99 | # Otherwise just exit | |
100 | sys.exit(0) | |
101 | ||
29c6fa22 MT |
102 | def _setup_database(self, ns): |
103 | """ | |
104 | Initialise the database | |
105 | """ | |
106 | # Connect to database | |
107 | db = location.database.Connection( | |
108 | host=ns.database_host, database=ns.database_name, | |
109 | user=ns.database_username, password=ns.database_password, | |
110 | ) | |
111 | ||
112 | with db.transaction(): | |
113 | db.execute(""" | |
6ffd06b5 MT |
114 | -- autnums |
115 | CREATE TABLE IF NOT EXISTS autnums(number integer, name text, organization text); | |
116 | CREATE UNIQUE INDEX IF NOT EXISTS autnums_number ON autnums(number); | |
117 | ||
118 | -- inetnums | |
119 | CREATE TABLE IF NOT EXISTS inetnums(network inet, name text, country text, description text); | |
120 | CREATE UNIQUE INDEX IF NOT EXISTS inetnums_networks ON inetnums(network); | |
121 | CREATE INDEX IF NOT EXISTS inetnums_family ON inetnums(family(network)); | |
122 | ||
123 | -- organizations | |
124 | CREATE TABLE IF NOT EXISTS organizations(handle text, name text, country text); | |
125 | CREATE UNIQUE INDEX IF NOT EXISTS organizations_handle ON organizations(handle); | |
126 | ||
127 | -- routes | |
128 | CREATE TABLE IF NOT EXISTS routes(network inet, asn integer); | |
129 | CREATE UNIQUE INDEX IF NOT EXISTS routes_network ON routes(network); | |
130 | CREATE INDEX IF NOT EXISTS routes_family ON routes(family(network)); | |
29c6fa22 MT |
131 | """) |
132 | ||
133 | return db | |
134 | ||
6ffd06b5 MT |
135 | def handle_update_whois(self, ns): |
136 | downloader = location.importer.Downloader() | |
137 | ||
138 | # Download all sources | |
139 | for source in location.importer.WHOIS_SOURCES: | |
140 | with self.db.transaction(): | |
141 | with downloader.request(source, return_blocks=True) as f: | |
142 | for block in f: | |
143 | self._parse_block(block) | |
144 | ||
145 | def _parse_block(self, block): | |
146 | # Get first line to find out what type of block this is | |
147 | line = block[0] | |
148 | ||
149 | # inetnum | |
150 | if line.startswith("inet6num:") or line.startswith("inetnum:"): | |
151 | return self._parse_inetnum_block(block) | |
152 | ||
153 | # route | |
154 | elif line.startswith("route6:") or line.startswith("route:"): | |
155 | return self._parse_route_block(block) | |
156 | ||
157 | # aut-num | |
158 | elif line.startswith("aut-num:"): | |
159 | return self._parse_autnum_block(block) | |
160 | ||
161 | # organisation | |
162 | elif line.startswith("organisation:"): | |
163 | return self._parse_org_block(block) | |
164 | ||
165 | # person (ignored) | |
166 | elif line.startswith("person:"): | |
167 | return | |
168 | ||
169 | # domain (ignored) | |
170 | elif line.startswith("domain:"): | |
171 | return | |
172 | ||
173 | # mntner (ignored) | |
174 | elif line.startswith("mntner:"): | |
175 | return | |
176 | ||
177 | # as-block (ignored) | |
178 | elif line.startswith("as-block:"): | |
179 | return | |
180 | ||
181 | # as-set (ignored) | |
182 | elif line.startswith("as-set:"): | |
183 | return | |
184 | ||
185 | # route-set (ignored) | |
186 | elif line.startswith("route-set:"): | |
187 | return | |
188 | ||
189 | # role (ignored) | |
190 | elif line.startswith("role:"): | |
191 | return | |
192 | ||
193 | # key-cert (ignored) | |
194 | elif line.startswith("key-cert:"): | |
195 | return | |
196 | ||
197 | # irt (ignored) | |
198 | elif line.startswith("irt:"): | |
199 | return | |
200 | ||
201 | # Log any unknown blocks | |
202 | else: | |
203 | log.warning("Unknown block:") | |
204 | for line in block: | |
205 | log.warning(line) | |
206 | ||
207 | def _parse_autnum_block(self, block): | |
208 | log.debug("Parsing autnum block:") | |
209 | ||
210 | autnum = {} | |
211 | for line in block: | |
212 | # Split line | |
213 | key, val = split_line(line) | |
214 | ||
215 | if key == "aut-num": | |
216 | m = re.match(r"^(AS|as)(\d+)", val) | |
217 | if m: | |
218 | autnum["asn"] = m.group(2) | |
219 | ||
220 | elif key in ("as-name", "org"): | |
221 | autnum[key] = val | |
222 | ||
223 | # Skip empty objects | |
224 | if not autnum: | |
225 | return | |
226 | ||
227 | # Insert into database | |
228 | self.db.execute("INSERT INTO autnums(number, name, organization) \ | |
229 | VALUES(%s, %s, %s) ON CONFLICT (number) DO UPDATE SET \ | |
230 | name = excluded.name, organization = excluded.organization", | |
231 | autnum.get("asn"), autnum.get("as-name"), autnum.get("org"), | |
232 | ) | |
233 | ||
234 | def _parse_inetnum_block(self, block): | |
235 | inetnum = {} | |
236 | for line in block: | |
237 | # Split line | |
238 | key, val = split_line(line) | |
239 | ||
240 | if key == "inetnum": | |
241 | start_address, delim, end_address = val.partition("-") | |
242 | ||
243 | # Strip any excess space | |
244 | start_address, end_address = start_address.rstrip(), end_address.strip() | |
245 | ||
246 | # Skip invalid blocks | |
247 | if start_address in INVALID_ADDRESSES: | |
248 | return | |
249 | ||
250 | # Convert to IP address | |
251 | try: | |
252 | start_address = ipaddress.ip_address(start_address) | |
253 | end_address = ipaddress.ip_address(end_address) | |
254 | except ValueError: | |
255 | log.warning("Could not parse line: %s" % line) | |
256 | return | |
257 | ||
258 | # Set prefix to default | |
259 | prefix = 32 | |
260 | ||
261 | # Count number of addresses in this subnet | |
262 | num_addresses = int(end_address) - int(start_address) | |
263 | if num_addresses: | |
264 | prefix -= math.log(num_addresses, 2) | |
265 | ||
266 | inetnum["inetnum"] = "%s/%.0f" % (start_address, prefix) | |
267 | ||
268 | elif key == "inet6num": | |
269 | # Skip invalid blocks | |
270 | if val in INVALID_ADDRESSES: | |
271 | return | |
272 | ||
273 | inetnum[key] = val | |
274 | ||
275 | elif key == "netname": | |
276 | inetnum[key] = val | |
277 | ||
278 | elif key == "country": | |
279 | if val == "UNITED STATES": | |
280 | val = "US" | |
281 | ||
282 | inetnum[key] = val.upper() | |
283 | ||
284 | elif key == "descr": | |
285 | if key in inetnum: | |
286 | inetnum[key] += "\n%s" % val | |
287 | else: | |
288 | inetnum[key] = val | |
289 | ||
290 | # Skip empty objects | |
291 | if not inetnum: | |
292 | return | |
293 | ||
294 | network = ipaddress.ip_network(inetnum.get("inet6num") or inetnum.get("inetnum"), strict=False) | |
295 | ||
296 | self.db.execute("INSERT INTO inetnums(network, name, country, description) \ | |
297 | VALUES(%s, %s, %s, %s) ON CONFLICT (network) DO \ | |
298 | UPDATE SET name = excluded.name, country = excluded.country, description = excluded.description", | |
299 | "%s" % network, inetnum.get("netname"), inetnum.get("country"), inetnum.get("descr"), | |
300 | ) | |
301 | ||
302 | def _parse_org_block(self, block): | |
303 | org = {} | |
304 | for line in block: | |
305 | # Split line | |
306 | key, val = split_line(line) | |
307 | ||
308 | if key in ("organisation", "org-name", "country"): | |
309 | org[key] = val | |
310 | ||
311 | # Skip empty objects | |
312 | if not org: | |
313 | return | |
314 | ||
315 | self.db.execute("INSERT INTO organizations(handle, name, country) \ | |
316 | VALUES(%s, %s, %s) ON CONFLICT (handle) DO \ | |
317 | UPDATE SET name = excluded.name, country = excluded.country", | |
318 | org.get("organisation"), org.get("org-name"), org.get("country"), | |
319 | ) | |
320 | ||
321 | def _parse_route_block(self, block): | |
322 | route = {} | |
323 | for line in block: | |
324 | # Split line | |
325 | key, val = split_line(line) | |
326 | ||
327 | # Keep any significant data | |
328 | if key in ("route6", "route"): | |
329 | route[key] = val | |
330 | ||
331 | elif key == "origin": | |
332 | m = re.match(r"^(AS|as)(\d+)", val) | |
333 | if m: | |
334 | route["asn"] = m.group(2) | |
335 | ||
336 | # Skip empty objects | |
337 | if not route: | |
338 | return | |
339 | ||
340 | network = ipaddress.ip_network(route.get("route6") or route.get("route"), strict=False) | |
341 | ||
342 | self.db.execute("INSERT INTO routes(network, asn) \ | |
343 | VALUES(%s, %s) ON CONFLICT (network) DO UPDATE SET asn = excluded.asn", | |
344 | "%s" % network, route.get("asn"), | |
345 | ) | |
346 | ||
347 | ||
348 | def split_line(line): | |
349 | key, colon, val = line.partition(":") | |
350 | ||
351 | # Strip any excess space | |
352 | key = key.strip() | |
353 | val = val.strip() | |
78ff0cf2 | 354 | |
6ffd06b5 | 355 | return key, val |
78ff0cf2 MT |
356 | |
357 | def main(): | |
358 | # Run the command line interface | |
359 | c = CLI() | |
360 | c.run() | |
361 | ||
362 | main() |