275 lines
9.1 KiB
Python
275 lines
9.1 KiB
Python
|
# -*- coding: iso-8859-15 -*-
|
||
|
"""IP4 address range set implementation.
|
||
|
|
||
|
Implements an IPv4-range type.
|
||
|
|
||
|
Copyright (C) 2006, Heiko Wundram.
|
||
|
Released under the MIT-license.
|
||
|
"""
|
||
|
|
||
|
# Version information
|
||
|
# -------------------
|
||
|
|
||
|
__author__ = "Heiko Wundram <me@modelnine.org>"
|
||
|
__version__ = "0.2"
|
||
|
__revision__ = "3"
|
||
|
__date__ = "2006-01-20"
|
||
|
|
||
|
|
||
|
# Imports
|
||
|
# -------
|
||
|
|
||
|
from paste.util import intset
|
||
|
import socket
|
||
|
import six
|
||
|
|
||
|
|
||
|
# IP4Range class
|
||
|
# --------------
|
||
|
|
||
|
class IP4Range(intset.IntSet):
|
||
|
"""IP4 address range class with efficient storage of address ranges.
|
||
|
Supports all set operations."""
|
||
|
|
||
|
_MINIP4 = 0
|
||
|
_MAXIP4 = (1<<32) - 1
|
||
|
_UNITYTRANS = "".join([chr(n) for n in range(256)])
|
||
|
_IPREMOVE = "0123456789."
|
||
|
|
||
|
def __init__(self,*args):
|
||
|
"""Initialize an ip4range class. The constructor accepts an unlimited
|
||
|
number of arguments that may either be tuples in the form (start,stop),
|
||
|
integers, longs or strings, where start and stop in a tuple may
|
||
|
also be of the form integer, long or string.
|
||
|
|
||
|
Passing an integer or long means passing an IPv4-address that's already
|
||
|
been converted to integer notation, whereas passing a string specifies
|
||
|
an address where this conversion still has to be done. A string
|
||
|
address may be in the following formats:
|
||
|
|
||
|
- 1.2.3.4 - a plain address, interpreted as a single address
|
||
|
- 1.2.3 - a set of addresses, interpreted as 1.2.3.0-1.2.3.255
|
||
|
- localhost - hostname to look up, interpreted as single address
|
||
|
- 1.2.3<->5 - a set of addresses, interpreted as 1.2.3.0-1.2.5.255
|
||
|
- 1.2.0.0/16 - a set of addresses, interpreted as 1.2.0.0-1.2.255.255
|
||
|
|
||
|
Only the first three notations are valid if you use a string address in
|
||
|
a tuple, whereby notation 2 is interpreted as 1.2.3.0 if specified as
|
||
|
lower bound and 1.2.3.255 if specified as upper bound, not as a range
|
||
|
of addresses.
|
||
|
|
||
|
Specifying a range is done with the <-> operator. This is necessary
|
||
|
because '-' might be present in a hostname. '<->' shouldn't be, ever.
|
||
|
"""
|
||
|
|
||
|
# Special case copy constructor.
|
||
|
if len(args) == 1 and isinstance(args[0],IP4Range):
|
||
|
super(IP4Range,self).__init__(args[0])
|
||
|
return
|
||
|
|
||
|
# Convert arguments to tuple syntax.
|
||
|
args = list(args)
|
||
|
for i in range(len(args)):
|
||
|
argval = args[i]
|
||
|
if isinstance(argval,str):
|
||
|
if "<->" in argval:
|
||
|
# Type 4 address.
|
||
|
args[i] = self._parseRange(*argval.split("<->",1))
|
||
|
continue
|
||
|
elif "/" in argval:
|
||
|
# Type 5 address.
|
||
|
args[i] = self._parseMask(*argval.split("/",1))
|
||
|
else:
|
||
|
# Type 1, 2 or 3.
|
||
|
args[i] = self._parseAddrRange(argval)
|
||
|
elif isinstance(argval,tuple):
|
||
|
if len(tuple) != 2:
|
||
|
raise ValueError("Tuple is of invalid length.")
|
||
|
addr1, addr2 = argval
|
||
|
if isinstance(addr1,str):
|
||
|
addr1 = self._parseAddrRange(addr1)[0]
|
||
|
elif not isinstance(addr1, six.integer_types):
|
||
|
raise TypeError("Invalid argument.")
|
||
|
if isinstance(addr2,str):
|
||
|
addr2 = self._parseAddrRange(addr2)[1]
|
||
|
elif not isinstance(addr2, six.integer_types):
|
||
|
raise TypeError("Invalid argument.")
|
||
|
args[i] = (addr1,addr2)
|
||
|
elif not isinstance(argval, six.integer_types):
|
||
|
raise TypeError("Invalid argument.")
|
||
|
|
||
|
# Initialize the integer set.
|
||
|
super(IP4Range,self).__init__(min=self._MINIP4,max=self._MAXIP4,*args)
|
||
|
|
||
|
# Parsing functions
|
||
|
# -----------------
|
||
|
|
||
|
def _parseRange(self,addr1,addr2):
|
||
|
naddr1, naddr1len = _parseAddr(addr1)
|
||
|
naddr2, naddr2len = _parseAddr(addr2)
|
||
|
if naddr2len < naddr1len:
|
||
|
naddr2 += naddr1&(((1<<((naddr1len-naddr2len)*8))-1)<<
|
||
|
(naddr2len*8))
|
||
|
naddr2len = naddr1len
|
||
|
elif naddr2len > naddr1len:
|
||
|
raise ValueError("Range has more dots than address.")
|
||
|
naddr1 <<= (4-naddr1len)*8
|
||
|
naddr2 <<= (4-naddr2len)*8
|
||
|
naddr2 += (1<<((4-naddr2len)*8))-1
|
||
|
return (naddr1,naddr2)
|
||
|
|
||
|
def _parseMask(self,addr,mask):
|
||
|
naddr, naddrlen = _parseAddr(addr)
|
||
|
naddr <<= (4-naddrlen)*8
|
||
|
try:
|
||
|
if not mask:
|
||
|
masklen = 0
|
||
|
else:
|
||
|
masklen = int(mask)
|
||
|
if not 0 <= masklen <= 32:
|
||
|
raise ValueError
|
||
|
except ValueError:
|
||
|
try:
|
||
|
mask = _parseAddr(mask,False)
|
||
|
except ValueError:
|
||
|
raise ValueError("Mask isn't parseable.")
|
||
|
remaining = 0
|
||
|
masklen = 0
|
||
|
if not mask:
|
||
|
masklen = 0
|
||
|
else:
|
||
|
while not (mask&1):
|
||
|
remaining += 1
|
||
|
while (mask&1):
|
||
|
mask >>= 1
|
||
|
masklen += 1
|
||
|
if remaining+masklen != 32:
|
||
|
raise ValueError("Mask isn't a proper host mask.")
|
||
|
naddr1 = naddr & (((1<<masklen)-1)<<(32-masklen))
|
||
|
naddr2 = naddr1 + (1<<(32-masklen)) - 1
|
||
|
return (naddr1,naddr2)
|
||
|
|
||
|
def _parseAddrRange(self,addr):
|
||
|
naddr, naddrlen = _parseAddr(addr)
|
||
|
naddr1 = naddr<<((4-naddrlen)*8)
|
||
|
naddr2 = ( (naddr<<((4-naddrlen)*8)) +
|
||
|
(1<<((4-naddrlen)*8)) - 1 )
|
||
|
return (naddr1,naddr2)
|
||
|
|
||
|
# Utility functions
|
||
|
# -----------------
|
||
|
|
||
|
def _int2ip(self,num):
|
||
|
rv = []
|
||
|
for i in range(4):
|
||
|
rv.append(str(num&255))
|
||
|
num >>= 8
|
||
|
return ".".join(reversed(rv))
|
||
|
|
||
|
# Iterating
|
||
|
# ---------
|
||
|
|
||
|
def iteraddresses(self):
|
||
|
"""Returns an iterator which iterates over ips in this iprange. An
|
||
|
IP is returned in string form (e.g. '1.2.3.4')."""
|
||
|
|
||
|
for v in super(IP4Range,self).__iter__():
|
||
|
yield self._int2ip(v)
|
||
|
|
||
|
def iterranges(self):
|
||
|
"""Returns an iterator which iterates over ip-ip ranges which build
|
||
|
this iprange if combined. An ip-ip pair is returned in string form
|
||
|
(e.g. '1.2.3.4-2.3.4.5')."""
|
||
|
|
||
|
for r in self._ranges:
|
||
|
if r[1]-r[0] == 1:
|
||
|
yield self._int2ip(r[0])
|
||
|
else:
|
||
|
yield '%s-%s' % (self._int2ip(r[0]),self._int2ip(r[1]-1))
|
||
|
|
||
|
def itermasks(self):
|
||
|
"""Returns an iterator which iterates over ip/mask pairs which build
|
||
|
this iprange if combined. An IP/Mask pair is returned in string form
|
||
|
(e.g. '1.2.3.0/24')."""
|
||
|
|
||
|
for r in self._ranges:
|
||
|
for v in self._itermasks(r):
|
||
|
yield v
|
||
|
|
||
|
def _itermasks(self,r):
|
||
|
ranges = [r]
|
||
|
while ranges:
|
||
|
cur = ranges.pop()
|
||
|
curmask = 0
|
||
|
while True:
|
||
|
curmasklen = 1<<(32-curmask)
|
||
|
start = (cur[0]+curmasklen-1)&(((1<<curmask)-1)<<(32-curmask))
|
||
|
if start >= cur[0] and start+curmasklen <= cur[1]:
|
||
|
break
|
||
|
else:
|
||
|
curmask += 1
|
||
|
yield "%s/%s" % (self._int2ip(start),curmask)
|
||
|
if cur[0] < start:
|
||
|
ranges.append((cur[0],start))
|
||
|
if cur[1] > start+curmasklen:
|
||
|
ranges.append((start+curmasklen,cur[1]))
|
||
|
|
||
|
__iter__ = iteraddresses
|
||
|
|
||
|
# Printing
|
||
|
# --------
|
||
|
|
||
|
def __repr__(self):
|
||
|
"""Returns a string which can be used to reconstruct this iprange."""
|
||
|
|
||
|
rv = []
|
||
|
for start, stop in self._ranges:
|
||
|
if stop-start == 1:
|
||
|
rv.append("%r" % (self._int2ip(start),))
|
||
|
else:
|
||
|
rv.append("(%r,%r)" % (self._int2ip(start),
|
||
|
self._int2ip(stop-1)))
|
||
|
return "%s(%s)" % (self.__class__.__name__,",".join(rv))
|
||
|
|
||
|
def _parseAddr(addr,lookup=True):
|
||
|
if lookup and any(ch not in IP4Range._IPREMOVE for ch in addr):
|
||
|
try:
|
||
|
addr = socket.gethostbyname(addr)
|
||
|
except socket.error:
|
||
|
raise ValueError("Invalid Hostname as argument.")
|
||
|
naddr = 0
|
||
|
for naddrpos, part in enumerate(addr.split(".")):
|
||
|
if naddrpos >= 4:
|
||
|
raise ValueError("Address contains more than four parts.")
|
||
|
try:
|
||
|
if not part:
|
||
|
part = 0
|
||
|
else:
|
||
|
part = int(part)
|
||
|
if not 0 <= part < 256:
|
||
|
raise ValueError
|
||
|
except ValueError:
|
||
|
raise ValueError("Address part out of range.")
|
||
|
naddr <<= 8
|
||
|
naddr += part
|
||
|
return naddr, naddrpos+1
|
||
|
|
||
|
def ip2int(addr, lookup=True):
|
||
|
return _parseAddr(addr, lookup=lookup)[0]
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
# Little test script.
|
||
|
x = IP4Range("172.22.162.250/24")
|
||
|
y = IP4Range("172.22.162.250","172.22.163.250","172.22.163.253<->255")
|
||
|
print(x)
|
||
|
for val in x.itermasks():
|
||
|
print(val)
|
||
|
for val in y.itermasks():
|
||
|
print(val)
|
||
|
for val in (x|y).itermasks():
|
||
|
print(val)
|
||
|
for val in (x^y).iterranges():
|
||
|
print(val)
|
||
|
for val in x:
|
||
|
print(val)
|