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