RF/ENH: 1st wave of IPAddr pythonization - properties, logical statements, etc

# Conflicts:
#	fail2ban/server/ipdns.py
pull/1414/head
Yaroslav Halchenko 2016-04-23 20:05:27 -04:00 committed by sebres
parent dbd7e347b1
commit c1a54974e9
5 changed files with 126 additions and 168 deletions

View File

@ -201,6 +201,7 @@ fail2ban/tests/actionstestcase.py
fail2ban/tests/actiontestcase.py fail2ban/tests/actiontestcase.py
fail2ban/tests/banmanagertestcase.py fail2ban/tests/banmanagertestcase.py
fail2ban/tests/clientreadertestcase.py fail2ban/tests/clientreadertestcase.py
fail2ban/tests/clientbeautifiertestcase.py
fail2ban/tests/config/action.d/brokenaction.conf fail2ban/tests/config/action.d/brokenaction.conf
fail2ban/tests/config/fail2ban.conf fail2ban/tests/config/fail2ban.conf
fail2ban/tests/config/filter.d/simple.conf fail2ban/tests/config/filter.d/simple.conf

View File

@ -152,8 +152,9 @@ class BanManager:
for banData in self.__banList: for banData in self.__banList:
ip = banData.getIP() ip = banData.getIP()
# Reference: http://www.team-cymru.org/Services/ip-to-asn.html#dns # Reference: http://www.team-cymru.org/Services/ip-to-asn.html#dns
question = ip.getPTR("origin.asn.cymru.com" if ip.isIPv4() question = ip.getPTR(
else "origin6.asn.cymru.com" "origin.asn.cymru.com" if ip.isIPv4
else "origin6.asn.cymru.com"
) )
try: try:
answers = dns.resolver.query(question, "TXT") answers = dns.resolver.query(question, "TXT")

View File

@ -383,7 +383,7 @@ class Filter(JailThread):
for net in self.__ignoreIpList: for net in self.__ignoreIpList:
# check if the IP is covered by ignore IP # check if the IP is covered by ignore IP
if ip.isInNet(net): if ip.isInNet(net):
self.logIgnoreIp(ip, log_ignore, ignore_source=("ip" if net.isValidIP() else "dns")) self.logIgnoreIp(ip, log_ignore, ignore_source=("ip" if net.isValid else "dns"))
return True return True
if self.__ignoreCommand: if self.__ignoreCommand:

View File

@ -17,13 +17,13 @@
# along with Fail2Ban; if not, write to the Free Software # along with Fail2Ban; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
__author__ = "Fail2Ban Developers, Alexander Koeppe, Serg G. Brester" __author__ = "Fail2Ban Developers, Alexander Koeppe, Serg G. Brester, Yaroslav Halchenko"
__copyright__ = "Copyright (c) 2004-2016 Fail2ban Developers" __copyright__ = "Copyright (c) 2004-2016 Fail2ban Developers"
__license__ = "GPL" __license__ = "GPL"
import re
import socket import socket
import struct import struct
import re
from .utils import Utils from .utils import Utils
from ..helpers import getLogger from ..helpers import getLogger
@ -38,9 +38,7 @@ logSys = getLogger(__name__)
# #
def asip(ip): def asip(ip):
"""A little helper to guarantee ip being an IPAddr instance""" """A little helper to guarantee ip being an IPAddr instance"""
if isinstance(ip, IPAddr): return ip if isinstance(ip, IPAddr) or ip is None else IPAddr(ip)
return ip
return IPAddr(ip)
## ##
@ -68,7 +66,7 @@ class DNSUtils:
ips = list() ips = list()
for result in socket.getaddrinfo(dns, None, 0, 0, socket.IPPROTO_TCP): for result in socket.getaddrinfo(dns, None, 0, 0, socket.IPPROTO_TCP):
ip = IPAddr(result[4][0]) ip = IPAddr(result[4][0])
if ip.isValidIP(): if ip.isValid:
ips.append(ip) ips.append(ip)
except socket.error, e: except socket.error, e:
# todo: make configurable the expired time of cache entry: # todo: make configurable the expired time of cache entry:
@ -88,7 +86,7 @@ class DNSUtils:
if not isinstance(ip, IPAddr): if not isinstance(ip, IPAddr):
v = socket.gethostbyaddr(ip)[0] v = socket.gethostbyaddr(ip)[0]
else: else:
v = socket.gethostbyaddr(ip.ntoa())[0] v = socket.gethostbyaddr(ip.ntoa)[0]
except socket.error, e: except socket.error, e:
logSys.debug("Unable to find a name for the IP %s: %s", ip, e) logSys.debug("Unable to find a name for the IP %s: %s", ip, e)
v = None v = None
@ -104,7 +102,7 @@ class DNSUtils:
plainIP = IPAddr.searchIP(text) plainIP = IPAddr.searchIP(text)
if plainIP is not None: if plainIP is not None:
ip = IPAddr(plainIP.group(0)) ip = IPAddr(plainIP.group(0))
if ip.isValidIP(): if ip.isValid:
ipList.append(ip) ipList.append(ip)
# If we are allowed to resolve -- give it a try if nothing was found # If we are allowed to resolve -- give it a try if nothing was found
@ -123,134 +121,132 @@ class DNSUtils:
# Class for IP address handling. # Class for IP address handling.
# #
# This class contains methods for handling IPv4 and IPv6 addresses. # This class contains methods for handling IPv4 and IPv6 addresses.
class IPAddr(object): #
""" provide functions to handle IPv4 and IPv6 addresses class IPAddr:
"""Encapsulate functionality for IPv4 and IPv6 addresses
""" """
IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$") IP_CRE = re.compile("^(?:\d{1,3}\.){3}\d{1,3}$")
IP6_CRE = re.compile("^[0-9a-fA-F]{4}[0-9a-fA-F:]+:[0-9a-fA-F]{1,4}|::1$") IP6_CRE = re.compile("^[0-9a-fA-F]{4}[0-9a-fA-F:]+:[0-9a-fA-F]{1,4}|::1$")
# object attributes # object attributes
addr = 0 _addr = 0
family = socket.AF_UNSPEC _family = socket.AF_UNSPEC
plen = 0 _plen = 0
valid = False _isValid = False
raw = "" _raw = ""
# todo: make configurable the expired time and max count of cache entries:
CACHE_OBJ = Utils.Cache(maxCount=1000, maxTime=5*60)
def __new__(cls, ipstring, cidr=-1):
# already correct IPAddr
args = (ipstring, cidr)
ip = IPAddr.CACHE_OBJ.get(args)
if ip is not None:
return ip
ip = super(IPAddr, cls).__new__(cls)
ip.__init(ipstring, cidr)
IPAddr.CACHE_OBJ.set(args, ip)
return ip
# object methods # object methods
def __init(self, ipstring, cidr=-1): def __init__(self, ipstring, cidr=-1):
""" initialize IP object by converting IP address string """ initialize IP object by converting IP address string
to binary to integer to binary to integer
""" """
for family in [socket.AF_INET, socket.AF_INET6]: for family in [socket.AF_INET, socket.AF_INET6]:
try: try:
binary = socket.inet_pton(family, ipstring) binary = socket.inet_pton(family, ipstring)
self.valid = True
break
except socket.error: except socket.error:
continue continue
else:
self._isValid = True
break
if self.valid and family == socket.AF_INET: if self.isValid and family == socket.AF_INET:
# convert host to network byte order # convert host to network byte order
self.addr, = struct.unpack("!L", binary) self._addr, = struct.unpack("!L", binary)
self.family = family self._family = family
self.plen = 32 self._plen = 32
# mask out host portion if prefix length is supplied # mask out host portion if prefix length is supplied
if cidr != None and cidr >= 0: if cidr is not None and cidr >= 0:
mask = ~(0xFFFFFFFFL >> cidr) mask = ~(0xFFFFFFFFL >> cidr)
self.addr = self.addr & mask self._addr &= mask
self.plen = cidr self._plen = cidr
elif self.valid and family == socket.AF_INET6: elif self.isValid and family == socket.AF_INET6:
# convert host to network byte order # convert host to network byte order
hi, lo = struct.unpack("!QQ", binary) hi, lo = struct.unpack("!QQ", binary)
self.addr = (hi << 64) | lo self._addr = (hi << 64) | lo
self.family = family self._family = family
self.plen = 128 self._plen = 128
# mask out host portion if prefix length is supplied # mask out host portion if prefix length is supplied
if cidr != None and cidr >= 0: if cidr is not None and cidr >= 0:
mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> cidr) mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> cidr)
self.addr = self.addr & mask self._addr &= mask
self.plen = cidr self._plen = cidr
# if IPv6 address is a IPv4-compatible, make instance a IPv4 # if IPv6 address is a IPv4-compatible, make instance a IPv4
elif self.isInNet(IPAddr("::ffff:0:0", 96)): elif self.isInNet(_IPv6_v4COMPAT):
self.addr = lo & 0xFFFFFFFFL self._addr = lo & 0xFFFFFFFFL
self.family = socket.AF_INET self._family = socket.AF_INET
self.plen = 32 self._plen = 32
else: else:
# string couldn't be converted neither to a IPv4 nor # string couldn't be converted neither to a IPv4 nor
# to a IPv6 address - retain raw input for later use # to a IPv6 address - retain raw input for later use
# (e.g. DNS resolution) # (e.g. DNS resolution)
self.raw = ipstring self._raw = ipstring
def __repr__(self): def __repr__(self):
return self.ntoa() return self.ntoa
def __str__(self): def __str__(self):
return self.ntoa() return self.ntoa
@property
def addr(self):
return self._addr
@property
def family(self):
return self._family
@property
def plen(self):
return self._plen
@property
def raw(self):
"""The raw address
Should only be set to a non-empty string if prior address
conversion wasn't possible
"""
return self._raw
@property
def isValid(self):
"""Either the object corresponds to a valid IP address
"""
return self._isValid
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, IPAddr): if not (self.isValid or other.isValid):
if other is None: return False return self.raw == other.raw
other = IPAddr(other) return (
if not self.valid and not other.valid: return self.raw == other.raw (self.isValid and other.isValid) and
if not self.valid or not other.valid: return False (self.addr == other.addr) and
if self.addr != other.addr: return False (self.family == other.family) and
if self.family != other.family: return False (self.plen == other.plen)
if self.plen != other.plen: return False )
return True
def __ne__(self, other): def __ne__(self, other):
if not isinstance(other, IPAddr): return not (self == other)
if other is None: return True
other = IPAddr(other)
if not self.valid and not other.valid: return self.raw != other.raw
if self.addr != other.addr: return True
if self.family != other.family: return True
if self.plen != other.plen: return True
return False
def __lt__(self, other): def __lt__(self, other):
if not isinstance(other, IPAddr):
if other is None: return False
other = IPAddr(other)
return self.family < other.family or self.addr < other.addr return self.family < other.family or self.addr < other.addr
def __add__(self, other): def __add__(self, other):
if not isinstance(other, IPAddr):
other = IPAddr(other)
return "%s%s" % (self, other) return "%s%s" % (self, other)
def __radd__(self, other): def __radd__(self, other):
if not isinstance(other, IPAddr):
other = IPAddr(other)
return "%s%s" % (other, self) return "%s%s" % (other, self)
def __hash__(self): def __hash__(self):
# should be the same as by string (because of possible compare with string): return hash(self.addr) ^ hash((self.plen << 16) | self.family)
return hash(self.ntoa())
#return hash(self.addr)^hash((self.plen<<16)|self.family)
@property
def hexdump(self): def hexdump(self):
""" dump the ip address in as a hex sequence in """Hex representation of the IP address (for debug purposes)
network byte order - for debug purpose
""" """
if self.family == socket.AF_INET: if self.family == socket.AF_INET:
return "%08x" % self.addr return "%08x" % self.addr
@ -258,132 +254,94 @@ class IPAddr(object):
return "%032x" % self.addr return "%032x" % self.addr
else: else:
return "" return ""
# TODO: could be lazily evaluated
@property
def ntoa(self): def ntoa(self):
""" represent IP object as text like the depricated """ represent IP object as text like the deprecated
C pendant inet_ntoa() but address family independent C pendant inet.ntoa but address family independent
""" """
if self.family == socket.AF_INET: if self.isIPv4:
# convert network to host byte order # convert network to host byte order
binary = struct.pack("!L", self.addr) binary = struct.pack("!L", self._addr)
elif self.family == socket.AF_INET6: elif self.isIPv6:
# convert network to host byte order # convert network to host byte order
hi = self.addr >> 64 hi = self.addr >> 64
lo = self.addr & 0xFFFFFFFFFFFFFFFFL lo = self.addr & 0xFFFFFFFFFFFFFFFFL
binary = struct.pack("!QQ", hi, lo) binary = struct.pack("!QQ", hi, lo)
else: else:
return self.getRaw() return self.raw
return socket.inet_ntop(self.family, binary) return socket.inet_ntop(self.family, binary)
def getPTR(self, suffix=""): def getPTR(self, suffix=""):
""" generates the DNS PTR string of the provided IP address object """ return the DNS PTR string of the provided IP address object
if "suffix" is provided it will be appended as the second and top
If "suffix" is provided it will be appended as the second and top
level reverse domain. level reverse domain.
if omitted it is implicitely set to the second and top level reverse If omitted it is implicitly set to the second and top level reverse
domain of the according IP address family domain of the according IP address family
""" """
if self.family == socket.AF_INET: if self.isIPv4:
reversed_ip = ".".join(reversed(self.ntoa().split("."))) exploded_ip = self.ntoa.split(".")
if not suffix: if not suffix:
suffix = "in-addr.arpa." suffix = "in-addr.arpa."
elif self.isIPv6:
return "%s.%s" % (reversed_ip, suffix) exploded_ip = self.hexdump()
elif self.family == socket.AF_INET6:
reversed_ip = ".".join(reversed(self.hexdump()))
if not suffix: if not suffix:
suffix = "ip6.arpa." suffix = "ip6.arpa."
return "%s.%s" % (reversed_ip, suffix)
else: else:
return "" return ""
return "%s.%s" % (".".join(reversed(exploded_ip)), suffix)
@property
def isIPv4(self): def isIPv4(self):
""" return true if the IP object is of address family AF_INET """Either the IP object is of address family AF_INET
""" """
return self.family == socket.AF_INET return self.family == socket.AF_INET
@property
def isIPv6(self): def isIPv6(self):
""" return true if the IP object is of address family AF_INET6 """Either the IP object is of address family AF_INET6
""" """
return self.family == socket.AF_INET6 return self.family == socket.AF_INET6
def getRaw(self):
""" returns the raw attribute - should only be set
to a non-empty string if prior address conversion
wasn't possible
"""
return self.raw
def isValidIP(self):
""" returns true if the IP object has been created
from a valid IP address or false if not
"""
return self.valid
def isInNet(self, net): def isInNet(self, net):
""" returns true if the IP object is in the provided """Return either the IP object is in the provided network
network (object)
""" """
# if it isn't a valid IP address, try DNS resolution
if not net.isValidIP() and net.getRaw() != "":
# Check if IP in DNS
return self in DNSUtils.dnsToIp(net.getRaw())
if self.family != net.family: if self.family != net.family:
return False return False
if self.isIPv4:
if self.family == socket.AF_INET:
mask = ~(0xFFFFFFFFL >> net.plen) mask = ~(0xFFFFFFFFL >> net.plen)
elif self.isIPv6:
elif self.family == socket.AF_INET6:
mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> net.plen) mask = ~(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFL >> net.plen)
else: else:
return False return False
if self.addr & mask == net.addr: return self.addr & mask == net.addr
return True
return False
@property
def maskplen(self):
plen = 0
if (hasattr(self, '_maskplen')):
return self._plen
maddr = self.addr
while maddr:
if not (maddr & 0x80000000):
raise ValueError("invalid mask %r, no plen representation" % (self.ntoa(),))
maddr = (maddr << 1) & 0xFFFFFFFFL
plen += 1
self._maskplen = plen
return plen
@staticmethod @staticmethod
def masktoplen(maskstr): def masktoplen(mask):
""" converts mask string to prefix length """Convert mask string to prefix length
only used for IPv4 masks
"""
return IPAddr(maskstr).maskplen
To be used only for IPv4 masks
"""
mask = mask.addr # to avoid side-effect within original mask
plen = 0
while mask:
mask = (mask << 1) & 0xFFFFFFFFL
plen += 1
return plen
@staticmethod @staticmethod
def searchIP(text): def searchIP(text):
""" Search if an IP address if directly available and return """Search if text is an IP address, and return it if so, else None
it.
""" """
match = IPAddr.IP_CRE.match(text) match = IPAddr.IP_CRE.match(text)
if match: if not match:
return match
else:
match = IPAddr.IP6_CRE.match(text) match = IPAddr.IP6_CRE.match(text)
if match: return match if match else None
return match
else:
return None
# An IPv4 compatible IPv6 to be reused
_IPv6_v4COMPAT = IPAddr("::ffff:0:0", 96)

View File

@ -107,5 +107,3 @@ class BeautifierTest(unittest.TestCase):
self.assertEqual(self.b.beautify(response), output) self.assertEqual(self.b.beautify(response), output)