Home | History | Annotate | Download | only in util
      1 # -*- coding: iso-8859-15 -*-
      2 """IP4 address range set implementation.
      3 
      4 Implements an IPv4-range type.
      5 
      6 Copyright (C) 2006, Heiko Wundram.
      7 Released under the MIT-license.
      8 """
      9 
     10 # Version information
     11 # -------------------
     12 
     13 __author__ = "Heiko Wundram <me (at] modelnine.org>"
     14 __version__ = "0.2"
     15 __revision__ = "3"
     16 __date__ = "2006-01-20"
     17 
     18 
     19 # Imports
     20 # -------
     21 
     22 from paste.util import intset
     23 import socket
     24 import six
     25 
     26 
     27 # IP4Range class
     28 # --------------
     29 
     30 class IP4Range(intset.IntSet):
     31     """IP4 address range class with efficient storage of address ranges.
     32     Supports all set operations."""
     33 
     34     _MINIP4 = 0
     35     _MAXIP4 = (1<<32) - 1
     36     _UNITYTRANS = "".join([chr(n) for n in range(256)])
     37     _IPREMOVE = "0123456789."
     38 
     39     def __init__(self,*args):
     40         """Initialize an ip4range class. The constructor accepts an unlimited
     41         number of arguments that may either be tuples in the form (start,stop),
     42         integers, longs or strings, where start and stop in a tuple may
     43         also be of the form integer, long or string.
     44 
     45         Passing an integer or long means passing an IPv4-address that's already
     46         been converted to integer notation, whereas passing a string specifies
     47         an address where this conversion still has to be done. A string
     48         address may be in the following formats:
     49 
     50         - 1.2.3.4    - a plain address, interpreted as a single address
     51         - 1.2.3      - a set of addresses, interpreted as 1.2.3.0-1.2.3.255
     52         - localhost  - hostname to look up, interpreted as single address
     53         - 1.2.3<->5  - a set of addresses, interpreted as 1.2.3.0-1.2.5.255
     54         - 1.2.0.0/16 - a set of addresses, interpreted as 1.2.0.0-1.2.255.255
     55 
     56         Only the first three notations are valid if you use a string address in
     57         a tuple, whereby notation 2 is interpreted as 1.2.3.0 if specified as
     58         lower bound and 1.2.3.255 if specified as upper bound, not as a range
     59         of addresses.
     60 
     61         Specifying a range is done with the <-> operator. This is necessary
     62         because '-' might be present in a hostname. '<->' shouldn't be, ever.
     63         """
     64 
     65         # Special case copy constructor.
     66         if len(args) == 1 and isinstance(args[0],IP4Range):
     67             super(IP4Range,self).__init__(args[0])
     68             return
     69 
     70         # Convert arguments to tuple syntax.
     71         args = list(args)
     72         for i in range(len(args)):
     73             argval = args[i]
     74             if isinstance(argval,str):
     75                 if "<->" in argval:
     76                     # Type 4 address.
     77                     args[i] = self._parseRange(*argval.split("<->",1))
     78                     continue
     79                 elif "/" in argval:
     80                     # Type 5 address.
     81                     args[i] = self._parseMask(*argval.split("/",1))
     82                 else:
     83                     # Type 1, 2 or 3.
     84                     args[i] = self._parseAddrRange(argval)
     85             elif isinstance(argval,tuple):
     86                 if len(tuple) != 2:
     87                     raise ValueError("Tuple is of invalid length.")
     88                 addr1, addr2 = argval
     89                 if isinstance(addr1,str):
     90                     addr1 = self._parseAddrRange(addr1)[0]
     91                 elif not isinstance(addr1, six.integer_types):
     92                     raise TypeError("Invalid argument.")
     93                 if isinstance(addr2,str):
     94                     addr2 = self._parseAddrRange(addr2)[1]
     95                 elif not isinstance(addr2, six.integer_types):
     96                     raise TypeError("Invalid argument.")
     97                 args[i] = (addr1,addr2)
     98             elif not isinstance(argval, six.integer_types):
     99                 raise TypeError("Invalid argument.")
    100 
    101         # Initialize the integer set.
    102         super(IP4Range,self).__init__(min=self._MINIP4,max=self._MAXIP4,*args)
    103 
    104     # Parsing functions
    105     # -----------------
    106 
    107     def _parseRange(self,addr1,addr2):
    108         naddr1, naddr1len = _parseAddr(addr1)
    109         naddr2, naddr2len = _parseAddr(addr2)
    110         if naddr2len < naddr1len:
    111             naddr2 += naddr1&(((1<<((naddr1len-naddr2len)*8))-1)<<
    112                               (naddr2len*8))
    113             naddr2len = naddr1len
    114         elif naddr2len > naddr1len:
    115             raise ValueError("Range has more dots than address.")
    116         naddr1 <<= (4-naddr1len)*8
    117         naddr2 <<= (4-naddr2len)*8
    118         naddr2 += (1<<((4-naddr2len)*8))-1
    119         return (naddr1,naddr2)
    120 
    121     def _parseMask(self,addr,mask):
    122         naddr, naddrlen = _parseAddr(addr)
    123         naddr <<= (4-naddrlen)*8
    124         try:
    125             if not mask:
    126                 masklen = 0
    127             else:
    128                 masklen = int(mask)
    129             if not 0 <= masklen <= 32:
    130                 raise ValueError
    131         except ValueError:
    132             try:
    133                 mask = _parseAddr(mask,False)
    134             except ValueError:
    135                 raise ValueError("Mask isn't parseable.")
    136             remaining = 0
    137             masklen = 0
    138             if not mask:
    139                 masklen = 0
    140             else:
    141                 while not (mask&1):
    142                     remaining += 1
    143                 while (mask&1):
    144                     mask >>= 1
    145                     masklen += 1
    146                 if remaining+masklen != 32:
    147                     raise ValueError("Mask isn't a proper host mask.")
    148         naddr1 = naddr & (((1<<masklen)-1)<<(32-masklen))
    149         naddr2 = naddr1 + (1<<(32-masklen)) - 1
    150         return (naddr1,naddr2)
    151 
    152     def _parseAddrRange(self,addr):
    153         naddr, naddrlen = _parseAddr(addr)
    154         naddr1 = naddr<<((4-naddrlen)*8)
    155         naddr2 = ( (naddr<<((4-naddrlen)*8)) +
    156                    (1<<((4-naddrlen)*8)) - 1 )
    157         return (naddr1,naddr2)
    158 
    159     # Utility functions
    160     # -----------------
    161 
    162     def _int2ip(self,num):
    163         rv = []
    164         for i in range(4):
    165             rv.append(str(num&255))
    166             num >>= 8
    167         return ".".join(reversed(rv))
    168 
    169     # Iterating
    170     # ---------
    171 
    172     def iteraddresses(self):
    173         """Returns an iterator which iterates over ips in this iprange. An
    174         IP is returned in string form (e.g. '1.2.3.4')."""
    175 
    176         for v in super(IP4Range,self).__iter__():
    177             yield self._int2ip(v)
    178 
    179     def iterranges(self):
    180         """Returns an iterator which iterates over ip-ip ranges which build
    181         this iprange if combined. An ip-ip pair is returned in string form
    182         (e.g. '1.2.3.4-2.3.4.5')."""
    183 
    184         for r in self._ranges:
    185             if r[1]-r[0] == 1:
    186                 yield self._int2ip(r[0])
    187             else:
    188                 yield '%s-%s' % (self._int2ip(r[0]),self._int2ip(r[1]-1))
    189 
    190     def itermasks(self):
    191         """Returns an iterator which iterates over ip/mask pairs which build
    192         this iprange if combined. An IP/Mask pair is returned in string form
    193         (e.g. '1.2.3.0/24')."""
    194 
    195         for r in self._ranges:
    196             for v in self._itermasks(r):
    197                 yield v
    198 
    199     def _itermasks(self,r):
    200         ranges = [r]
    201         while ranges:
    202             cur = ranges.pop()
    203             curmask = 0
    204             while True:
    205                 curmasklen = 1<<(32-curmask)
    206                 start = (cur[0]+curmasklen-1)&(((1<<curmask)-1)<<(32-curmask))
    207                 if start >= cur[0] and start+curmasklen <= cur[1]:
    208                     break
    209                 else:
    210                     curmask += 1
    211             yield "%s/%s" % (self._int2ip(start),curmask)
    212             if cur[0] < start:
    213                 ranges.append((cur[0],start))
    214             if cur[1] > start+curmasklen:
    215                 ranges.append((start+curmasklen,cur[1]))
    216 
    217     __iter__ = iteraddresses
    218 
    219     # Printing
    220     # --------
    221 
    222     def __repr__(self):
    223         """Returns a string which can be used to reconstruct this iprange."""
    224 
    225         rv = []
    226         for start, stop in self._ranges:
    227             if stop-start == 1:
    228                 rv.append("%r" % (self._int2ip(start),))
    229             else:
    230                 rv.append("(%r,%r)" % (self._int2ip(start),
    231                                        self._int2ip(stop-1)))
    232         return "%s(%s)" % (self.__class__.__name__,",".join(rv))
    233 
    234 def _parseAddr(addr,lookup=True):
    235     if lookup and any(ch not in IP4Range._IPREMOVE for ch in addr):
    236         try:
    237             addr = socket.gethostbyname(addr)
    238         except socket.error:
    239             raise ValueError("Invalid Hostname as argument.")
    240     naddr = 0
    241     for naddrpos, part in enumerate(addr.split(".")):
    242         if naddrpos >= 4:
    243             raise ValueError("Address contains more than four parts.")
    244         try:
    245             if not part:
    246                 part = 0
    247             else:
    248                 part = int(part)
    249             if not 0 <= part < 256:
    250                 raise ValueError
    251         except ValueError:
    252             raise ValueError("Address part out of range.")
    253         naddr <<= 8
    254         naddr += part
    255     return naddr, naddrpos+1
    256 
    257 def ip2int(addr, lookup=True):
    258     return _parseAddr(addr, lookup=lookup)[0]
    259 
    260 if __name__ == "__main__":
    261     # Little test script.
    262     x = IP4Range("172.22.162.250/24")
    263     y = IP4Range("172.22.162.250","172.22.163.250","172.22.163.253<->255")
    264     print(x)
    265     for val in x.itermasks():
    266         print(val)
    267     for val in y.itermasks():
    268         print(val)
    269     for val in (x|y).itermasks():
    270         print(val)
    271     for val in (x^y).iterranges():
    272         print(val)
    273     for val in x:
    274         print(val)
    275