#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2008-2010, Bryan Davis
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# - Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Utitlities for dealing with ip addresses.
Functions:
- validate_ip: Validate a dotted-quad ip address.
- ip2long: Convert a dotted-quad ip address to a network byte order 32-bit
integer.
- long2ip: Convert a network byte order 32-bit integer to a dotted quad ip
address.
- ip2hex: Convert a dotted-quad ip address to a hex encoded network byte
order 32-bit integer.
- hex2ip: Convert a hex encoded network byte order 32-bit integer to a
dotted-quad ip address.
- validate_cidr: Validate a CIDR notation ip address.
- cidr2block: Convert a CIDR notation ip address into a tuple containing
network block start and end addresses.
Objects:
- IpRange: Range of ip addresses providing ``in`` and iteration.
- IpRangeList: List of IpRange objects providing ``in`` and iteration.
The IpRangeList object can be used in a django settings file to allow CIDR
notation and/or (start, end) ranges to be used in the INTERNAL_IPS list.
Example:
INTERNAL_IPS = IpRangeList(
'127.0.0.1',
'192.168/16',
('10.0.0.1', '10.0.0.19'),
)
"""
__version__ = '0.4.0'
__all__ = (
'validate_ip', 'ip2long', 'long2ip', 'ip2hex', 'hex2ip',
'validate_cidr', 'cidr2block',
'IpRange', 'IpRangeList',
)
import re
# sniff for python2.x / python3k compatibility "fixes'
try:
basestring = basestring
except NameError:
# 'basestring' is undefined, must be python3k
basestring = str
try:
next = next
except NameError:
# builtin next function doesn't exist
def next (iterable):
return iterable.next()
_DOTTED_QUAD_RE = re.compile(r'^(\d{1,3}\.){0,3}\d{1,3}$')
def validate_ip (s):
"""Validate a dotted-quad ip address.
The string is considered a valid dotted-quad address if it consists of
one to four octets (0-255) seperated by periods (.).
>>> validate_ip('127.0.0.1')
True
>>> validate_ip('127.0')
True
>>> validate_ip('127.0.0.256')
False
>>> validate_ip(None)
Traceback (most recent call last):
...
TypeError: expected string or buffer
Args:
s: String to validate as a dotted-quad ip address
Returns:
True if str is a valid dotted-quad ip address, False otherwise
"""
if _DOTTED_QUAD_RE.match(s):
quads = s.split('.')
for q in quads:
if int(q) > 255:
return False
return True
return False
#end validate_ip
_CIDR_RE = re.compile(r'^(\d{1,3}\.){0,3}\d{1,3}/\d{1,2}$')
def validate_cidr (s):
"""Validate a CIDR notation ip address.
The string is considered a valid CIDR address if it consists of one to
four octets (0-255) seperated by periods (.) followed by a forward slash
(/) and a bit mask length (1-32).
>>> validate_cidr('127.0.0.1/32')
True
>>> validate_cidr('127.0/8')
True
>>> validate_cidr('127.0.0.256/32')
False
>>> validate_cidr('127.0.0.0')
False
>>> validate_cidr(None)
Traceback (most recent call last):
...
TypeError: expected string or buffer
Args:
str: String to validate as a CIDR ip address
Returns:
True if str is a valid CIDR address, False otherwise
"""
if _CIDR_RE.match(s):
ip, mask = s.split('/')
if validate_ip(ip):
if int(mask) > 32:
return False
else:
return False
return True
return False
#end validate_cidr
def ip2long (ip):
"""
Convert a dotted-quad ip address to a network byte order 32-bit integer.
>>> ip2long('127.0.0.1')
2130706433
>>> ip2long('127.1')
2130706433
>>> ip2long('127')
2130706432
>>> ip2long('127.0.0.256') is None
True
Args:
ip: Dotted-quad ip address (eg. '127.0.0.1')
Returns:
Network byte order 32-bit integer or None if ip is invalid
"""
if not validate_ip(ip):
return None
quads = ip.split('.')
if len(quads) == 1:
# only a network quad
quads = quads + [0, 0, 0]
elif len(quads) < 4:
# partial form, last supplied quad is host address, rest is network
host = quads[-1:]
quads = quads[:-1] + [0,] * (4 - len(quads)) + host
lngip = 0
for q in quads:
lngip = (lngip << 8) | int(q)
return lngip
#end ip2long
_MAX_IP = 0xffffffff
_MIN_IP = 0x0
def long2ip (l):
"""
Convert a network byte order 32-bit integer to a dotted quad ip address.
>>> long2ip(2130706433)
'127.0.0.1'
>>> long2ip(_MIN_IP)
'0.0.0.0'
>>> long2ip(_MAX_IP)
'255.255.255.255'
>>> long2ip(None) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for >>: 'NoneType' and 'int'
>>> long2ip(-1) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected int between 0 and 4294967295 inclusive
>>> long2ip(374297346592387463875L) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected int between 0 and 4294967295 inclusive
>>> long2ip(_MAX_IP + 1) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError: expected int between 0 and 4294967295 inclusive
Args:
l: Network byte order 32-bit integer
Returns:
Dotted-quad ip address (eg. '127.0.0.1')
"""
if _MAX_IP < l or l < 0:
raise TypeError("expected int between 0 and %d inclusive" % _MAX_IP)
return '%d.%d.%d.%d' % (l>>24 & 255, l>>16 & 255, l>>8 & 255, l & 255)
#end long2ip
def ip2hex (addr):
"""
Convert a dotted-quad ip address to a hex encoded number.
>>> ip2hex('0.0.0.1')
'00000001'
>>> ip2hex('127.0.0.1')
'7f000001'
>>> ip2hex('127.255.255.255')
'7fffffff'
>>> ip2hex('128.0.0.1')
'80000001'
>>> ip2hex('128.1')
'80000001'
>>> ip2hex('255.255.255.255')
'ffffffff'
"""
netip = ip2long(addr)
if netip is None:
return None
return "%08x" % netip
#end ip2hex
def hex2ip (hex_str):
"""
Convert a hex encoded integer to a dotted-quad ip address.
>>> hex2ip('00000001')
'0.0.0.1'
>>> hex2ip('7f000001')
'127.0.0.1'
>>> hex2ip('7fffffff')
'127.255.255.255'
>>> hex2ip('80000001')
'128.0.0.1'
>>> hex2ip('ffffffff')
'255.255.255.255'
"""
try:
netip = int(hex_str, 16)
except ValueError:
return None
return long2ip(netip)
#end hex2ip
def cidr2block (cidr):
"""
Convert a CIDR notation ip address into a tuple containing the network
block start and end addresses.
>>> cidr2block('127.0.0.1/32')
('127.0.0.1', '127.0.0.1')
>>> cidr2block('127/8')
('127.0.0.0', '127.255.255.255')
>>> cidr2block('127.0.1/16')
('127.0.0.0', '127.0.255.255')
>>> cidr2block('127.1/24')
('127.1.0.0', '127.1.0.255')
>>> cidr2block('127.0.0.3/29')
('127.0.0.0', '127.0.0.7')
>>> cidr2block('127/0')
('0.0.0.0', '255.255.255.255')
Args:
cidr: CIDR notation ip address (eg. '127.0.0.1/8')
Returns:
Tuple of block (start, end) or None if invalid
"""
if not validate_cidr(cidr):
return None
ip, prefix = cidr.split('/')
prefix = int(prefix)
# convert dotted-quad ip to base network number
# can't use ip2long because partial addresses are treated as all network
# instead of network plus host (eg. '127.1' expands to '127.1.0.0')
quads = ip.split('.')
baseIp = 0
for i in range(4):
baseIp = (baseIp << 8) | int(len(quads) > i and quads[i] or 0)
# keep left most prefix bits of baseIp
shift = 32 - prefix
start = baseIp >> shift << shift
# expand right most 32 - prefix bits to 1
mask = (1 << shift) - 1
end = start | mask
return (long2ip(start), long2ip(end))
#end cidr2block
class IpRange (object):
"""
Range of ip addresses.
Converts a CIDR notation address, tuple of ip addresses or start and end
addresses into a smart object which can perform ``in`` and ``not in``
tests and iterate all of the addresses in the range.
>>> r = IpRange('127.0.0.1', '127.255.255.255')
>>> '127.127.127.127' in r
True
>>> '10.0.0.1' in r
False
>>> 2130706433 in r
True
>>> r = IpRange('127/24')
>>> print(r)
('127.0.0.0', '127.0.0.255')
>>> r = IpRange('127/30')
>>> for ip in r:
... print(ip)
127.0.0.0
127.0.0.1
127.0.0.2
127.0.0.3
>>> print(IpRange('127.0.0.255', '127.0.0.0'))
('127.0.0.0', '127.0.0.255')
"""
def __init__ (self, start, end=None):
"""
Args:
start: Ip address in dotted quad format or CIDR notation or tuple
of ip addresses in dotted quad format
end: Ip address in dotted quad format or None
"""
if end is None:
if isinstance(start, tuple):
# occurs when IpRangeList calls via map to pass start and end
start, end = start
elif validate_cidr(start):
# CIDR notation range
start, end = cidr2block(start)
else:
# degenerate range
end = start
start = ip2long(start)
end = ip2long(end)
self.startIp = min(start, end)
self.endIp = max(start, end)
#end __init__
def __repr__ (self):
"""
>>> print(IpRange('127.0.0.1'))
('127.0.0.1', '127.0.0.1')
>>> print(IpRange('10/8'))
('10.0.0.0', '10.255.255.255')
>>> print(IpRange('127.0.0.255', '127.0.0.0'))
('127.0.0.0', '127.0.0.255')
"""
return (long2ip(self.startIp), long2ip(self.endIp)).__repr__()
#end __repr__
def __contains__ (self, item):
"""
Implements membership test operators `in` and `not in` for the address
range.
>>> r = IpRange('127.0.0.1', '127.255.255.255')
>>> '127.127.127.127' in r
True
>>> '10.0.0.1' in r
False
>>> 2130706433 in r
True
>>> 'invalid' in r
Traceback (most recent call last):
...
TypeError: expected dotted-quad ip address or 32-bit integer
Args:
item: Dotted-quad ip address
Returns:
True if address is in range, False otherwise
"""
if isinstance(item, basestring):
item = ip2long(item)
if type(item) not in [type(1), type(_MAX_IP)]:
raise TypeError(
"expected dotted-quad ip address or 32-bit integer")
return self.startIp <= item <= self.endIp
#end __contains__
def __iter__ (self):
"""
Return an iterator over the range.
>>> iter = IpRange('127/31').__iter__()
>>> next(iter)
'127.0.0.0'
>>> next(iter)
'127.0.0.1'
>>> next(iter)
Traceback (most recent call last):
...
StopIteration
"""
i = self.startIp
while i <= self.endIp:
yield long2ip(i)
i += 1
#end __iter__
#end class IpRange
class IpRangeList (object):
"""
List of IpRange objects.
Converts a list of dotted quad ip address and/or CIDR addresses into a
list of IpRange objects. This list can perform ``in`` and ``not in`` tests
and iterate all of the addresses in the range.
This can be used to convert django's conf.settings.INTERNAL_IPS list into
a smart object which allows CIDR notation.
>>> INTERNAL_IPS = IpRangeList('127.0.0.1','10/8',('192.168.0.1','192.168.255.255'))
>>> '127.0.0.1' in INTERNAL_IPS
True
>>> '10.10.10.10' in INTERNAL_IPS
True
>>> '192.168.192.168' in INTERNAL_IPS
True
>>> '172.16.0.1' in INTERNAL_IPS
False
"""
def __init__ (self, *args):
self.ips = tuple(map(IpRange, args))
#end __init__
def __repr__ (self):
"""
>>> print(IpRangeList('127.0.0.1', '10/8', '192.168/16'))
(('127.0.0.1', '127.0.0.1'), ('10.0.0.0', '10.255.255.255'), ('192.168.0.0', '192.168.255.255'))
"""
return self.ips.__repr__()
#end __repr__
def __contains__ (self, item):
"""
Implements membership test operators `in` and `not in` for the address
range.
>>> r = IpRangeList('127.0.0.1', '10/8', '192.168/16')
>>> '127.0.0.1' in r
True
>>> '10.0.0.1' in r
True
>>> 2130706433 in r
True
>>> 'invalid' in r
Traceback (most recent call last):
...
TypeError: expected dotted-quad ip address or 32-bit integer
Args:
item: Dotted-quad ip address
Returns:
True if address is in range, False otherwise
"""
for r in self.ips:
if item in r:
return True
return False
#end __contains__
def __iter__ (self):
"""
>>> iter = IpRangeList('127.0.0.1').__iter__()
>>> next(iter)
'127.0.0.1'
>>> next(iter)
Traceback (most recent call last):
...
StopIteration
>>> iter = IpRangeList('127.0.0.1', '10/31').__iter__()
>>> next(iter)
'127.0.0.1'
>>> next(iter)
'10.0.0.0'
>>> next(iter)
'10.0.0.1'
>>> next(iter)
Traceback (most recent call last):
...
StopIteration
"""
for r in self.ips:
for ip in r:
yield ip
#end __iter__
#end class IpRangeList
def iptools_test ():
import doctest
doctest.testmod()
#end iptools_test
if __name__ == '__main__':
iptools_test()
# vim: set sw=4 ts=4 sts=4 et :