| @@ -0,0 +1,137 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # | |||||
| # Copyright (C) 2009-2016 Ben Kurtovic <ben.kurtovic@gmail.com> | |||||
| # | |||||
| # Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
| # of this software and associated documentation files (the "Software"), to deal | |||||
| # in the Software without restriction, including without limitation the rights | |||||
| # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
| # copies of the Software, and to permit persons to whom the Software is | |||||
| # furnished to do so, subject to the following conditions: | |||||
| # | |||||
| # The above copyright notice and this permission notice shall be included in | |||||
| # all copies or substantial portions of the Software. | |||||
| # | |||||
| # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
| # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
| # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
| # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
| # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
| # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
| # SOFTWARE. | |||||
| from collections import namedtuple | |||||
| import re | |||||
| import socket | |||||
| from socket import AF_INET, AF_INET6 | |||||
| from earwigbot.commands import Command | |||||
| _Range = namedtuple("_Range", ["family", "range", "size", "addresses"]) | |||||
| class CIDR(Command): | |||||
| """Calculates the smallest CIDR range that encompasses a list of IP | |||||
| addresses. Used to make range blocks.""" | |||||
| name = "cidr" | |||||
| commands = ["cidr", "range", "rangeblock", "rangecalc", "blockcalc", | |||||
| "iprange", "cdir"] | |||||
| # https://www.mediawiki.org/wiki/Manual:$wgBlockCIDRLimit | |||||
| LIMIT_IPv4 = 16 | |||||
| LIMIT_IPv6 = 19 | |||||
| def process(self, data): | |||||
| if not data.args: | |||||
| msg = ("Specify a list of IP addresses to calculate a CIDR range " | |||||
| "for. For example, \x0306!{0} 192.168.0.3 192.168.0.15 " | |||||
| "192.168.1.4\x0F.") | |||||
| self.reply(data, msg.format(data.command)) | |||||
| return | |||||
| try: | |||||
| ips = [self._parse_arg(arg) for arg in data.args] | |||||
| except ValueError as exc: | |||||
| msg = "Can't parse IP address \x0302{0}\x0F." | |||||
| self.reply(data, msg.format(exc.message)) | |||||
| return | |||||
| if any(ip[0] == AF_INET for ip in ips) and any(ip[0] == AF_INET6 for ip in ips): | |||||
| msg = "Can't calculate a range for both IPv4 and IPv6 addresses." | |||||
| self.reply(data, msg) | |||||
| return | |||||
| cidr = self._calculate_range(ips[0][0], [ip[1] for ip in ips]) | |||||
| descr = self._describe(cidr.family, cidr.size) | |||||
| msg = "Smallest CIDR range is \x02{0}\x0F ({1}){2}" | |||||
| self.reply(data, msg.format( | |||||
| cidr.range, cidr.addresses, " – " + descr if descr else "")) | |||||
| def _parse_arg(self, arg): | |||||
| """Converts an argument into an IP address.""" | |||||
| if "[[" in arg and "]]" in arg: | |||||
| regex = r"\[\[\s*(?:User(?:\stalk)?:)?(.*?)(?:\|.*?)?\s*\]\]" | |||||
| match = re.search(regex, arg, re.I) | |||||
| if not match: | |||||
| raise ValueError(arg) | |||||
| arg = match.group(1) | |||||
| if re.match(r"https?://", arg): | |||||
| if "target=" in arg: | |||||
| regex = r"target=(.*?)(?:&|$)" | |||||
| elif "page=" in arg: | |||||
| regex = r"page=(?:User(?:(?:\s|_)talk)?(?::|%3A))?(.*?)(?:&|$)" | |||||
| elif re.search(r"Special(:|%3A)Contributions/", arg, re.I): | |||||
| regex = r"Special(?:\:|%3A)Contributions/(.*?)(?:\&|\?|$)" | |||||
| elif re.search(r"User((\s|_)talk)?(:|%3A)", arg, re.I): | |||||
| regex = r"User(?:(?:\s|_)talk)?(?:\:|%3A)(.*?)(?:\&|\?|$)" | |||||
| else: | |||||
| raise ValueError(arg) | |||||
| match = re.search(regex, arg, re.I) | |||||
| if not match: | |||||
| raise ValueError(arg) | |||||
| arg = match.group(1) | |||||
| try: | |||||
| return (AF_INET, socket.inet_pton(AF_INET, arg)) | |||||
| except socket.error: | |||||
| try: | |||||
| return (AF_INET6, socket.inet_pton(AF_INET6, arg)) | |||||
| except socket.error: | |||||
| raise ValueError(arg) | |||||
| def _calculate_range(self, family, ips): | |||||
| """Calculate the smallest CIDR range encompassing a list of IPs.""" | |||||
| bin_ips = ["".join( | |||||
| bin(ord(octet))[2:].zfill(8) for octet in ip) for ip in ips] | |||||
| size = len(bin_ips[0]) | |||||
| for i in xrange(len(bin_ips[0])): | |||||
| if any(ip[i] == "0" for ip in bin_ips) and any( | |||||
| ip[i] == "1" for ip in bin_ips): | |||||
| size = i | |||||
| break | |||||
| mask = bin_ips[0][:size].ljust(len(bin_ips[0]), "0") | |||||
| return _Range( | |||||
| family, | |||||
| socket.inet_ntop(family, int(mask, 2)) + "/" + str(size), | |||||
| size, | |||||
| self._format_count(2 ** (len(bin_ips[0]) - size))) | |||||
| def _format_count(self, count): | |||||
| """Nicely format a number of addresses affected by a range block.""" | |||||
| if count > 2 ** 32: | |||||
| base = "{0:.2E} addresses".format(count) | |||||
| if count > 2 ** 96: | |||||
| return base + ", {0:.2E} /64 subnets".format(count >> 64) | |||||
| if count > 2 ** 63: | |||||
| return base + ", {0} /64 subnets".format(count >> 64) | |||||
| return base | |||||
| if count == 1: | |||||
| return "1 address" | |||||
| return "{0:,} addresses".format(count) | |||||
| def _describe(self, family, size): | |||||
| """Return an optional English description of a range.""" | |||||
| if (family == AF_INET and size < self.LIMIT_IPv4) or ( | |||||
| family == AF_INET6 and size < self.LIMIT_IPv6): | |||||
| return "too large to block" | |||||