diff --git a/gluon/validators.py b/gluon/validators.py index fdecac34..79228832 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -3529,6 +3529,216 @@ class IS_IPV6(Validator): return (value, translate(self.error_message)) +class IS_IPADDRESS(Validator): + """ + Checks if field's value is an IP Address (v4 or v6). Can be set to force + addresses from within a specific range. Checks are done with the correct + IS_IPV4 and IS_IPV6 validators. + + Uses ipaddress library if found, falls back to PEP-3144 ipaddr.py from + Google (in contrib). + + Universal arguments: + + minip: lowest allowed address; accepts: + str, eg. 192.168.0.1 + list or tuple of octets, eg. [192, 168, 0, 1] + maxip: highest allowed address; same as above + invert: True to allow addresses only from outside of given range; note + that range boundaries are not matched this way + + IPv4 specific arguments: + + is_localhost: localhost address treatment: + None (default): indifferent + True (enforce): query address must match localhost address + (127.0.0.1) + False (forbid): query address must not match localhost + address + is_private: same as above, except that query address is checked against + two address ranges: 172.16.0.0 - 172.31.255.255 and + 192.168.0.0 - 192.168.255.255 + is_automatic: same as above, except that query address is checked against + one address range: 169.254.0.0 - 169.254.255.255 + is_ipv4: None (default): indifferent + True (enforce): must be an IPv4 address + False (forbid): must NOT be an IPv4 address + + IPv6 specific arguments: + + is_link_local: Same as above but uses fe80::/10 range + is_reserved: Same as above but uses IETF reserved range + is_mulicast: Same as above but uses ff00::/8 range + is_routeable: Similar to above but enforces not private, link_local, + reserved or multicast + is_6to4: Same as above but uses 2002::/16 range + is_teredo: Same as above but uses 2001::/32 range + subnets: value must be a member of at least one from list of subnets + is_ipv6: None (default): indifferent + True (enforce): must be an IPv6 address + False (forbid): must NOT be an IPv6 address + + Minip and maxip may also be lists or tuples of addresses in all above + forms (str, int, list / tuple), allowing setup of multiple address ranges: + + minip = (minip1, minip2, ... minipN) + | | | + | | | + maxip = (maxip1, maxip2, ... maxipN) + + Longer iterable will be truncated to match length of shorter one. + + >>> IS_IPADDRESS()('192.168.1.5') + ('192.168.1.5', None) + >>> IS_IPADDRESS(is_ipv6=False)('192.168.1.5') + ('192.168.1.5', None) + >>> IS_IPADDRESS()('255.255.255.255') + ('255.255.255.255', None) + >>> IS_IPADDRESS()('192.168.1.5 ') + ('192.168.1.5 ', 'enter valid IP address') + >>> IS_IPADDRESS()('192.168.1.1.5') + ('192.168.1.1.5', 'enter valid IP address') + >>> IS_IPADDRESS()('123.123') + ('123.123', 'enter valid IP address') + >>> IS_IPADDRESS()('1111.2.3.4') + ('1111.2.3.4', 'enter valid IP address') + >>> IS_IPADDRESS()('0111.2.3.4') + ('0111.2.3.4', 'enter valid IP address') + >>> IS_IPADDRESS()('256.2.3.4') + ('256.2.3.4', 'enter valid IP address') + >>> IS_IPADDRESS()('300.2.3.4') + ('300.2.3.4', 'enter valid IP address') + >>> IS_IPADDRESS(minip='192.168.1.0', maxip='192.168.1.255')('192.168.1.100') + ('192.168.1.100', None) + >>> IS_IPADDRESS(minip='1.2.3.5', maxip='1.2.3.9', error_message='bad ip')('1.2.3.4') + ('1.2.3.4', 'bad ip') + >>> IS_IPADDRESS(maxip='1.2.3.4', invert=True)('127.0.0.1') + ('127.0.0.1', None) + >>> IS_IPADDRESS(maxip='192.168.1.4', invert=True)('192.168.1.4') + ('192.168.1.4', 'enter valid IP address') + >>> IS_IPADDRESS(is_localhost=True)('127.0.0.1') + ('127.0.0.1', None) + >>> IS_IPADDRESS(is_localhost=True)('192.168.1.10') + ('192.168.1.10', 'enter valid IP address') + >>> IS_IPADDRESS(is_localhost=False)('127.0.0.1') + ('127.0.0.1', 'enter valid IP address') + >>> IS_IPADDRESS(maxip='100.0.0.0', is_localhost=True)('127.0.0.1') + ('127.0.0.1', 'enter valid IP address') + + >>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af') + ('fe80::126c:8ffa:fe22:b3af', None) + >>> IS_IPADDRESS(is_ipv4=False)('fe80::126c:8ffa:fe22:b3af') + ('fe80::126c:8ffa:fe22:b3af', None) + >>> IS_IPADDRESS()('fe80::126c:8ffa:fe22:b3af ') + ('fe80::126c:8ffa:fe22:b3af ', 'enter valid IP address') + >>> IS_IPADDRESS(is_ipv4=True)('fe80::126c:8ffa:fe22:b3af') + ('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address') + >>> IS_IPADDRESS(is_ipv6=True)('192.168.1.1') + ('192.168.1.1', 'enter valid IP address') + >>> IS_IPADDRESS(is_ipv6=True, error_message='bad ip')('192.168.1.1') + ('192.168.1.1', 'bad ip') + >>> IS_IPADDRESS(is_link_local=True)('fe80::126c:8ffa:fe22:b3af') + ('fe80::126c:8ffa:fe22:b3af', None) + >>> IS_IPADDRESS(is_link_local=False)('fe80::126c:8ffa:fe22:b3af') + ('fe80::126c:8ffa:fe22:b3af', 'enter valid IP address') + >>> IS_IPADDRESS(is_link_local=True)('2001::126c:8ffa:fe22:b3af') + ('2001::126c:8ffa:fe22:b3af', 'enter valid IP address') + >>> IS_IPADDRESS(is_multicast=True)('2001::126c:8ffa:fe22:b3af') + ('2001::126c:8ffa:fe22:b3af', 'enter valid IP address') + >>> IS_IPADDRESS(is_multicast=True)('ff00::126c:8ffa:fe22:b3af') + ('ff00::126c:8ffa:fe22:b3af', None) + >>> IS_IPADDRESS(is_routeable=True)('2001::126c:8ffa:fe22:b3af') + ('2001::126c:8ffa:fe22:b3af', None) + >>> IS_IPADDRESS(is_routeable=True)('ff00::126c:8ffa:fe22:b3af') + ('ff00::126c:8ffa:fe22:b3af', 'enter valid IP address') + >>> IS_IPADDRESS(subnets='2001::/32')('2001::8ffa:fe22:b3af') + ('2001::8ffa:fe22:b3af', None) + >>> IS_IPADDRESS(subnets='fb00::/8')('2001::8ffa:fe22:b3af') + ('2001::8ffa:fe22:b3af', 'enter valid IP address') + >>> IS_IPADDRESS(subnets=['fc00::/8','2001::/32'])('2001::8ffa:fe22:b3af') + ('2001::8ffa:fe22:b3af', None) + >>> IS_IPADDRESS(subnets='invalidsubnet')('2001::8ffa:fe22:b3af') + ('2001::8ffa:fe22:b3af', 'invalid subnet provided') + """ + def __init__( + self, + minip='0.0.0.0', + maxip='255.255.255.255', + invert=False, + is_localhost=None, + is_private=None, + is_automatic=None, + is_ipv4=None, + is_link_local=None, + is_reserved=None, + is_multicast=None, + is_routeable=None, + is_6to4=None, + is_teredo=None, + subnets=None, + is_ipv6=None, + error_message='enter valid IP address'): + self.minip = minip, + self.maxip = maxip, + self.invert = invert + self.is_localhost = is_localhost + self.is_private = is_private + self.is_automatic = is_automatic + self.is_ipv4 = is_ipv4 + self.is_private = is_private + self.is_link_local = is_link_local + self.is_reserved = is_reserved + self.is_multicast = is_multicast + self.is_routeable = is_routeable + self.is_6to4 = is_6to4 + self.is_teredo = is_teredo + self.subnets = subnets + self.is_ipv6 = is_ipv6 + self.error_message = error_message + + def __call__(self, value): + try: + import ipaddress + except ImportError: + from contrib import ipaddr as ipaddress + + try: + ip = ipaddress.ip_address(value) + except ValueError, e: + return (value, translate(self.error_message)) + + if self.is_ipv4 and isinstance(ip, ipaddress.IPv6Address): + retval = (value, translate(self.error_message)) + elif self.is_ipv6 and isinstance(ip, ipaddress.IPv4Address): + retval = (value, translate(self.error_message)) + elif self.is_ipv4 or isinstance(ip, ipaddress.IPv4Address): + retval = IS_IPV4( + minip=self.minip, + maxip=self.maxip, + invert=self.invert, + is_localhost=self.is_localhost, + is_private=self.is_private, + is_automatic=self.is_automatic, + error_message=self.error_message + )(value) + elif self.is_ipv6 or isinstance(ip, ipaddress.IPv6Address): + retval = IS_IPV6( + is_private=self.is_private, + is_link_local=self.is_link_local, + is_reserved=self.is_reserved, + is_multicast=self.is_multicast, + is_routeable=self.is_routeable, + is_6to4=self.is_6to4, + is_teredo=self.is_teredo, + subnets=self.subnets, + error_message=self.error_message + )(value) + else: + retval = (value, translate(self.error_message)) + + return retval + + if __name__ == '__main__': import doctest doctest.testmod(