1# -*- coding: iso-8859-15 -*- 2"""IP4 address range set implementation. 3 4Implements an IPv4-range type. 5 6Copyright (C) 2006, Heiko Wundram. 7Released under the MIT-license. 8""" 9 10# Version information 11# ------------------- 12 13__author__ = "Heiko Wundram <me@modelnine.org>" 14__version__ = "0.2" 15__revision__ = "3" 16__date__ = "2006-01-20" 17 18 19# Imports 20# ------- 21 22from paste.util import intset 23import socket 24import six 25 26 27# IP4Range class 28# -------------- 29 30class 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 234def _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 257def ip2int(addr, lookup=True): 258 return _parseAddr(addr, lookup=lookup)[0] 259 260if __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