|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- # -*- 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
-
- _IP = namedtuple("_IP", ["family", "ip", "size"])
- _Range = namedtuple("_Range", [
- "family", "range", "low", "high", "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 or \x0306!{0} 2500:1:2:3:: "
- "2500:1:2:3:dead:beef::\x0F.")
- self.reply(data, msg.format(data.command))
- return
-
- try:
- ips = [self._parse_ip(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.family == AF_INET for ip in ips) and any(
- ip.family == 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].family, ips)
- descr = self._describe(cidr.family, cidr.size)
-
- msg = ("Smallest CIDR range is \x02{0}\x0F, covering {1} from "
- "\x0305{2}\x0F to \x0305{3}\x0F{4}.")
- self.reply(data, msg.format(
- cidr.range, cidr.addresses, cidr.low, cidr.high,
- " (\x0304{0}\x0F)".format(descr) if descr else ""))
-
- def _parse_ip(self, arg):
- """Converts an argument into an IP address object."""
- arg = self._parse_arg(arg)
- oldarg = arg
- size = None
- if "/" in arg:
- arg, size = arg.split("/", 1)
- try:
- size = int(size, 10)
- except ValueError:
- raise ValueError(oldarg)
- if size < 0 or size > 128:
- raise ValueError(oldarg)
-
- try:
- ip = _IP(AF_INET, socket.inet_pton(AF_INET, arg), size)
- except socket.error:
- try:
- return _IP(AF_INET6, socket.inet_pton(AF_INET6, arg), size)
- except socket.error:
- raise ValueError(oldarg)
- if size > 32:
- raise ValueError(oldarg)
- return ip
-
- def _parse_arg(self, arg):
- """Converts an argument into an IP address string."""
- 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)
- return 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.ip) for ip in ips]
- for i, ip in enumerate(ips):
- if ip.size is not None:
- suffix = "X" * (len(bin_ips[i]) - ip.size)
- bin_ips[i] = bin_ips[i][:ip.size] + suffix
-
- size = len(bin_ips[0])
- for i in range(len(bin_ips[0])):
- if any(ip[i] == "X" for ip in bin_ips) or (
- any(ip[i] == "0" for ip in bin_ips) and
- any(ip[i] == "1" for ip in bin_ips)):
- size = i
- break
-
- bin_low = bin_ips[0][:size].ljust(len(bin_ips[0]), "0")
- bin_high = bin_ips[0][:size].ljust(len(bin_ips[0]), "1")
- low = self._format_bin(family, bin_low)
- high = self._format_bin(family, bin_high)
-
- return _Range(
- family, low + "/" + str(size), low, high, size,
- self._format_count(2 ** (len(bin_ips[0]) - size)))
-
- @staticmethod
- def _format_bin(family, binary):
- """Convert an IP's binary representation to presentation format."""
- return socket.inet_ntop(family, "".join(
- chr(int(binary[i:i + 8], 2)) for i in range(0, len(binary), 8)))
-
- @staticmethod
- def _format_count(count):
- """Nicely format a number of addresses affected by a range block."""
- if count == 1:
- return "1 address"
- if count > 2 ** 32:
- base = "{0:.2E} addresses".format(count)
- if count == 2 ** 64:
- return base + " (1 /64 subnet)"
- 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
- 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"
|