| # 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.six.moves import xrange |
| |
| def clean_ipv6_address(ip_str, unpack_ipv4=False, |
| error_message="This is not a valid IPv6 address"): |
| """ |
| Cleans a IPv6 address string. |
| |
| Validity is checked by calling is_valid_ipv6_address() - if an |
| invalid address is passed, ValidationError is raised. |
| |
| Replaces the longest continious 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) |
| |
| # 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 a 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 |
| |
| hextets = ip_str.split(':') |
| return hextets[-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 |