/django/utils/ipv6.py
Python | 271 lines | 260 code | 2 blank | 9 comment | 0 complexity | 5fe27629d09c02c1ba66b20ddf464e53 MD5 | raw file
Possible License(s): BSD-3-Clause
- # This code was mostly based on ipaddr-py
- # Copyright 2007 Google Inc. http://code.google.com/p/ipaddr-py/
- # Licensed under the Apache License, Version 2.0 (the "License").
- from django.core.exceptions import ValidationError
- from django.utils.translation import ugettext_lazy as _
- from django.utils.six.moves import xrange
- def clean_ipv6_address(ip_str, unpack_ipv4=False,
- error_message=_("This is not a valid IPv6 address.")):
- """
- Cleans an IPv6 address string.
- Validity is checked by calling is_valid_ipv6_address() - if an
- invalid address is passed, ValidationError is raised.
- Replaces the longest continuous zero-sequence with "::" and
- removes leading zeroes and makes sure all hextets are lowercase.
- Args:
- ip_str: A valid IPv6 address.
- unpack_ipv4: if an IPv4-mapped address is found,
- return the plain IPv4 address (default=False).
- error_message: A error message for in the ValidationError.
- Returns:
- A compressed IPv6 address, or the same value
- """
- best_doublecolon_start = -1
- best_doublecolon_len = 0
- doublecolon_start = -1
- doublecolon_len = 0
- if not is_valid_ipv6_address(ip_str):
- raise ValidationError(error_message, code='invalid')
- # This algorithm can only handle fully exploded
- # IP strings
- ip_str = _explode_shorthand_ip_string(ip_str)
- ip_str = _sanitize_ipv4_mapping(ip_str)
- # If needed, unpack the IPv4 and return straight away
- # - no need in running the rest of the algorithm
- if unpack_ipv4:
- ipv4_unpacked = _unpack_ipv4(ip_str)
- if ipv4_unpacked:
- return ipv4_unpacked
- hextets = ip_str.split(":")
- for index in range(len(hextets)):
- # Remove leading zeroes
- hextets[index] = hextets[index].lstrip('0')
- if not hextets[index]:
- hextets[index] = '0'
- # Determine best hextet to compress
- if hextets[index] == '0':
- doublecolon_len += 1
- if doublecolon_start == -1:
- # Start of a sequence of zeros.
- doublecolon_start = index
- if doublecolon_len > best_doublecolon_len:
- # This is the longest sequence of zeros so far.
- best_doublecolon_len = doublecolon_len
- best_doublecolon_start = doublecolon_start
- else:
- doublecolon_len = 0
- doublecolon_start = -1
- # Compress the most suitable hextet
- if best_doublecolon_len > 1:
- best_doublecolon_end = (best_doublecolon_start +
- best_doublecolon_len)
- # For zeros at the end of the address.
- if best_doublecolon_end == len(hextets):
- hextets += ['']
- hextets[best_doublecolon_start:best_doublecolon_end] = ['']
- # For zeros at the beginning of the address.
- if best_doublecolon_start == 0:
- hextets = [''] + hextets
- result = ":".join(hextets)
- return result.lower()
- def _sanitize_ipv4_mapping(ip_str):
- """
- Sanitize IPv4 mapping in an expanded IPv6 address.
- This converts ::ffff:0a0a:0a0a to ::ffff:10.10.10.10.
- If there is nothing to sanitize, returns an unchanged
- string.
- Args:
- ip_str: A string, the expanded IPv6 address.
- Returns:
- The sanitized output string, if applicable.
- """
- if not ip_str.lower().startswith('0000:0000:0000:0000:0000:ffff:'):
- # not an ipv4 mapping
- return ip_str
- hextets = ip_str.split(':')
- if '.' in hextets[-1]:
- # already sanitized
- return ip_str
- ipv4_address = "%d.%d.%d.%d" % (
- int(hextets[6][0:2], 16),
- int(hextets[6][2:4], 16),
- int(hextets[7][0:2], 16),
- int(hextets[7][2:4], 16),
- )
- result = ':'.join(hextets[0:6])
- result += ':' + ipv4_address
- return result
- def _unpack_ipv4(ip_str):
- """
- Unpack an IPv4 address that was mapped in a compressed IPv6 address.
- This converts 0000:0000:0000:0000:0000:ffff:10.10.10.10 to 10.10.10.10.
- If there is nothing to sanitize, returns None.
- Args:
- ip_str: A string, the expanded IPv6 address.
- Returns:
- The unpacked IPv4 address, or None if there was nothing to unpack.
- """
- if not ip_str.lower().startswith('0000:0000:0000:0000:0000:ffff:'):
- return None
- return ip_str.rsplit(':', 1)[1]
- def is_valid_ipv6_address(ip_str):
- """
- Ensure we have a valid IPv6 address.
- Args:
- ip_str: A string, the IPv6 address.
- Returns:
- A boolean, True if this is a valid IPv6 address.
- """
- from django.core.validators import validate_ipv4_address
- # We need to have at least one ':'.
- if ':' not in ip_str:
- return False
- # We can only have one '::' shortener.
- if ip_str.count('::') > 1:
- return False
- # '::' should be encompassed by start, digits or end.
- if ':::' in ip_str:
- return False
- # A single colon can neither start nor end an address.
- if ((ip_str.startswith(':') and not ip_str.startswith('::')) or
- (ip_str.endswith(':') and not ip_str.endswith('::'))):
- return False
- # We can never have more than 7 ':' (1::2:3:4:5:6:7:8 is invalid)
- if ip_str.count(':') > 7:
- return False
- # If we have no concatenation, we need to have 8 fields with 7 ':'.
- if '::' not in ip_str and ip_str.count(':') != 7:
- # We might have an IPv4 mapped address.
- if ip_str.count('.') != 3:
- return False
- ip_str = _explode_shorthand_ip_string(ip_str)
- # Now that we have that all squared away, let's check that each of the
- # hextets are between 0x0 and 0xFFFF.
- for hextet in ip_str.split(':'):
- if hextet.count('.') == 3:
- # If we have an IPv4 mapped address, the IPv4 portion has to
- # be at the end of the IPv6 portion.
- if not ip_str.split(':')[-1] == hextet:
- return False
- try:
- validate_ipv4_address(hextet)
- except ValidationError:
- return False
- else:
- try:
- # a value error here means that we got a bad hextet,
- # something like 0xzzzz
- if int(hextet, 16) < 0x0 or int(hextet, 16) > 0xFFFF:
- return False
- except ValueError:
- return False
- return True
- def _explode_shorthand_ip_string(ip_str):
- """
- Expand a shortened IPv6 address.
- Args:
- ip_str: A string, the IPv6 address.
- Returns:
- A string, the expanded IPv6 address.
- """
- if not _is_shorthand_ip(ip_str):
- # We've already got a longhand ip_str.
- return ip_str
- new_ip = []
- hextet = ip_str.split('::')
- # If there is a ::, we need to expand it with zeroes
- # to get to 8 hextets - unless there is a dot in the last hextet,
- # meaning we're doing v4-mapping
- if '.' in ip_str.split(':')[-1]:
- fill_to = 7
- else:
- fill_to = 8
- if len(hextet) > 1:
- sep = len(hextet[0].split(':')) + len(hextet[1].split(':'))
- new_ip = hextet[0].split(':')
- for __ in xrange(fill_to - sep):
- new_ip.append('0000')
- new_ip += hextet[1].split(':')
- else:
- new_ip = ip_str.split(':')
- # Now need to make sure every hextet is 4 lower case characters.
- # If a hextet is < 4 characters, we've got missing leading 0's.
- ret_ip = []
- for hextet in new_ip:
- ret_ip.append(('0' * (4 - len(hextet)) + hextet).lower())
- return ':'.join(ret_ip)
- def _is_shorthand_ip(ip_str):
- """Determine if the address is shortened.
- Args:
- ip_str: A string, the IPv6 address.
- Returns:
- A boolean, True if the address is shortened.
- """
- if ip_str.count('::') == 1:
- return True
- if any(len(x) < 4 for x in ip_str.split(':')):
- return True
- return False