You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

268 lines
7.8 KiB

  1. # This code was mostly based on ipaddr-py
  2. # Copyright 2007 Google Inc. https://github.com/google/ipaddr-py
  3. # Licensed under the Apache License, Version 2.0 (the "License").
  4. from django.core.exceptions import ValidationError
  5. from django.utils.six.moves import range
  6. from django.utils.translation import ugettext_lazy as _
  7. def clean_ipv6_address(ip_str, unpack_ipv4=False,
  8. error_message=_("This is not a valid IPv6 address.")):
  9. """
  10. Cleans an IPv6 address string.
  11. Validity is checked by calling is_valid_ipv6_address() - if an
  12. invalid address is passed, ValidationError is raised.
  13. Replaces the longest continuous zero-sequence with "::" and
  14. removes leading zeroes and makes sure all hextets are lowercase.
  15. Args:
  16. ip_str: A valid IPv6 address.
  17. unpack_ipv4: if an IPv4-mapped address is found,
  18. return the plain IPv4 address (default=False).
  19. error_message: An error message used in the ValidationError.
  20. Returns:
  21. A compressed IPv6 address, or the same value
  22. """
  23. best_doublecolon_start = -1
  24. best_doublecolon_len = 0
  25. doublecolon_start = -1
  26. doublecolon_len = 0
  27. if not is_valid_ipv6_address(ip_str):
  28. raise ValidationError(error_message, code='invalid')
  29. # This algorithm can only handle fully exploded
  30. # IP strings
  31. ip_str = _explode_shorthand_ip_string(ip_str)
  32. ip_str = _sanitize_ipv4_mapping(ip_str)
  33. # If needed, unpack the IPv4 and return straight away
  34. # - no need in running the rest of the algorithm
  35. if unpack_ipv4:
  36. ipv4_unpacked = _unpack_ipv4(ip_str)
  37. if ipv4_unpacked:
  38. return ipv4_unpacked
  39. hextets = ip_str.split(":")
  40. for index in range(len(hextets)):
  41. # Remove leading zeroes
  42. hextets[index] = hextets[index].lstrip('0')
  43. if not hextets[index]:
  44. hextets[index] = '0'
  45. # Determine best hextet to compress
  46. if hextets[index] == '0':
  47. doublecolon_len += 1
  48. if doublecolon_start == -1:
  49. # Start of a sequence of zeros.
  50. doublecolon_start = index
  51. if doublecolon_len > best_doublecolon_len:
  52. # This is the longest sequence of zeros so far.
  53. best_doublecolon_len = doublecolon_len
  54. best_doublecolon_start = doublecolon_start
  55. else:
  56. doublecolon_len = 0
  57. doublecolon_start = -1
  58. # Compress the most suitable hextet
  59. if best_doublecolon_len > 1:
  60. best_doublecolon_end = (best_doublecolon_start +
  61. best_doublecolon_len)
  62. # For zeros at the end of the address.
  63. if best_doublecolon_end == len(hextets):
  64. hextets += ['']
  65. hextets[best_doublecolon_start:best_doublecolon_end] = ['']
  66. # For zeros at the beginning of the address.
  67. if best_doublecolon_start == 0:
  68. hextets = [''] + hextets
  69. result = ":".join(hextets)
  70. return result.lower()
  71. def _sanitize_ipv4_mapping(ip_str):
  72. """
  73. Sanitize IPv4 mapping in an expanded IPv6 address.
  74. This converts ::ffff:0a0a:0a0a to ::ffff:10.10.10.10.
  75. If there is nothing to sanitize, returns an unchanged
  76. string.
  77. Args:
  78. ip_str: A string, the expanded IPv6 address.
  79. Returns:
  80. The sanitized output string, if applicable.
  81. """
  82. if not ip_str.lower().startswith('0000:0000:0000:0000:0000:ffff:'):
  83. # not an ipv4 mapping
  84. return ip_str
  85. hextets = ip_str.split(':')
  86. if '.' in hextets[-1]:
  87. # already sanitized
  88. return ip_str
  89. ipv4_address = "%d.%d.%d.%d" % (
  90. int(hextets[6][0:2], 16),
  91. int(hextets[6][2:4], 16),
  92. int(hextets[7][0:2], 16),
  93. int(hextets[7][2:4], 16),
  94. )
  95. result = ':'.join(hextets[0:6])
  96. result += ':' + ipv4_address
  97. return result
  98. def _unpack_ipv4(ip_str):
  99. """
  100. Unpack an IPv4 address that was mapped in a compressed IPv6 address.
  101. This converts 0000:0000:0000:0000:0000:ffff:10.10.10.10 to 10.10.10.10.
  102. If there is nothing to sanitize, returns None.
  103. Args:
  104. ip_str: A string, the expanded IPv6 address.
  105. Returns:
  106. The unpacked IPv4 address, or None if there was nothing to unpack.
  107. """
  108. if not ip_str.lower().startswith('0000:0000:0000:0000:0000:ffff:'):
  109. return None
  110. return ip_str.rsplit(':', 1)[1]
  111. def is_valid_ipv6_address(ip_str):
  112. """
  113. Ensure we have a valid IPv6 address.
  114. Args:
  115. ip_str: A string, the IPv6 address.
  116. Returns:
  117. A boolean, True if this is a valid IPv6 address.
  118. """
  119. from django.core.validators import validate_ipv4_address
  120. # We need to have at least one ':'.
  121. if ':' not in ip_str:
  122. return False
  123. # We can only have one '::' shortener.
  124. if ip_str.count('::') > 1:
  125. return False
  126. # '::' should be encompassed by start, digits or end.
  127. if ':::' in ip_str:
  128. return False
  129. # A single colon can neither start nor end an address.
  130. if ((ip_str.startswith(':') and not ip_str.startswith('::')) or
  131. (ip_str.endswith(':') and not ip_str.endswith('::'))):
  132. return False
  133. # We can never have more than 7 ':' (1::2:3:4:5:6:7:8 is invalid)
  134. if ip_str.count(':') > 7:
  135. return False
  136. # If we have no concatenation, we need to have 8 fields with 7 ':'.
  137. if '::' not in ip_str and ip_str.count(':') != 7:
  138. # We might have an IPv4 mapped address.
  139. if ip_str.count('.') != 3:
  140. return False
  141. ip_str = _explode_shorthand_ip_string(ip_str)
  142. # Now that we have that all squared away, let's check that each of the
  143. # hextets are between 0x0 and 0xFFFF.
  144. for hextet in ip_str.split(':'):
  145. if hextet.count('.') == 3:
  146. # If we have an IPv4 mapped address, the IPv4 portion has to
  147. # be at the end of the IPv6 portion.
  148. if not ip_str.split(':')[-1] == hextet:
  149. return False
  150. try:
  151. validate_ipv4_address(hextet)
  152. except ValidationError:
  153. return False
  154. else:
  155. try:
  156. # a value error here means that we got a bad hextet,
  157. # something like 0xzzzz
  158. if int(hextet, 16) < 0x0 or int(hextet, 16) > 0xFFFF:
  159. return False
  160. except ValueError:
  161. return False
  162. return True
  163. def _explode_shorthand_ip_string(ip_str):
  164. """
  165. Expand a shortened IPv6 address.
  166. Args:
  167. ip_str: A string, the IPv6 address.
  168. Returns:
  169. A string, the expanded IPv6 address.
  170. """
  171. if not _is_shorthand_ip(ip_str):
  172. # We've already got a longhand ip_str.
  173. return ip_str
  174. new_ip = []
  175. hextet = ip_str.split('::')
  176. # If there is a ::, we need to expand it with zeroes
  177. # to get to 8 hextets - unless there is a dot in the last hextet,
  178. # meaning we're doing v4-mapping
  179. if '.' in ip_str.split(':')[-1]:
  180. fill_to = 7
  181. else:
  182. fill_to = 8
  183. if len(hextet) > 1:
  184. sep = len(hextet[0].split(':')) + len(hextet[1].split(':'))
  185. new_ip = hextet[0].split(':')
  186. for __ in range(fill_to - sep):
  187. new_ip.append('0000')
  188. new_ip += hextet[1].split(':')
  189. else:
  190. new_ip = ip_str.split(':')
  191. # Now need to make sure every hextet is 4 lower case characters.
  192. # If a hextet is < 4 characters, we've got missing leading 0's.
  193. ret_ip = []
  194. for hextet in new_ip:
  195. ret_ip.append(('0' * (4 - len(hextet)) + hextet).lower())
  196. return ':'.join(ret_ip)
  197. def _is_shorthand_ip(ip_str):
  198. """Determine if the address is shortened.
  199. Args:
  200. ip_str: A string, the IPv6 address.
  201. Returns:
  202. A boolean, True if the address is shortened.
  203. """
  204. if ip_str.count('::') == 1:
  205. return True
  206. if any(len(x) < 4 for x in ip_str.split(':')):
  207. return True
  208. return False