ipv6.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. # Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
  2. # Copyright (C) 2003-2017 Nominum, Inc.
  3. #
  4. # Permission to use, copy, modify, and distribute this software and its
  5. # documentation for any purpose with or without fee is hereby granted,
  6. # provided that the above copyright notice and this permission notice
  7. # appear in all copies.
  8. #
  9. # THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
  10. # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  11. # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR
  12. # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  13. # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  14. # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
  15. # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  16. """IPv6 helper functions."""
  17. import binascii
  18. import re
  19. from typing import List, Union
  20. import dns.exception
  21. import dns.ipv4
  22. _leading_zero = re.compile(r"0+([0-9a-f]+)")
  23. def inet_ntoa(address: bytes) -> str:
  24. """Convert an IPv6 address in binary form to text form.
  25. *address*, a ``bytes``, the IPv6 address in binary form.
  26. Raises ``ValueError`` if the address isn't 16 bytes long.
  27. Returns a ``str``.
  28. """
  29. if len(address) != 16:
  30. raise ValueError("IPv6 addresses are 16 bytes long")
  31. hex = binascii.hexlify(address)
  32. chunks = []
  33. i = 0
  34. l = len(hex)
  35. while i < l:
  36. chunk = hex[i : i + 4].decode()
  37. # strip leading zeros. we do this with an re instead of
  38. # with lstrip() because lstrip() didn't support chars until
  39. # python 2.2.2
  40. m = _leading_zero.match(chunk)
  41. if m is not None:
  42. chunk = m.group(1)
  43. chunks.append(chunk)
  44. i += 4
  45. #
  46. # Compress the longest subsequence of 0-value chunks to ::
  47. #
  48. best_start = 0
  49. best_len = 0
  50. start = -1
  51. last_was_zero = False
  52. for i in range(8):
  53. if chunks[i] != "0":
  54. if last_was_zero:
  55. end = i
  56. current_len = end - start
  57. if current_len > best_len:
  58. best_start = start
  59. best_len = current_len
  60. last_was_zero = False
  61. elif not last_was_zero:
  62. start = i
  63. last_was_zero = True
  64. if last_was_zero:
  65. end = 8
  66. current_len = end - start
  67. if current_len > best_len:
  68. best_start = start
  69. best_len = current_len
  70. if best_len > 1:
  71. if best_start == 0 and (best_len == 6 or best_len == 5 and chunks[5] == "ffff"):
  72. # We have an embedded IPv4 address
  73. if best_len == 6:
  74. prefix = "::"
  75. else:
  76. prefix = "::ffff:"
  77. thex = prefix + dns.ipv4.inet_ntoa(address[12:])
  78. else:
  79. thex = (
  80. ":".join(chunks[:best_start])
  81. + "::"
  82. + ":".join(chunks[best_start + best_len :])
  83. )
  84. else:
  85. thex = ":".join(chunks)
  86. return thex
  87. _v4_ending = re.compile(rb"(.*):(\d+\.\d+\.\d+\.\d+)$")
  88. _colon_colon_start = re.compile(rb"::.*")
  89. _colon_colon_end = re.compile(rb".*::$")
  90. def inet_aton(text: Union[str, bytes], ignore_scope: bool = False) -> bytes:
  91. """Convert an IPv6 address in text form to binary form.
  92. *text*, a ``str`` or ``bytes``, the IPv6 address in textual form.
  93. *ignore_scope*, a ``bool``. If ``True``, a scope will be ignored.
  94. If ``False``, the default, it is an error for a scope to be present.
  95. Returns a ``bytes``.
  96. """
  97. #
  98. # Our aim here is not something fast; we just want something that works.
  99. #
  100. if not isinstance(text, bytes):
  101. btext = text.encode()
  102. else:
  103. btext = text
  104. if ignore_scope:
  105. parts = btext.split(b"%")
  106. l = len(parts)
  107. if l == 2:
  108. btext = parts[0]
  109. elif l > 2:
  110. raise dns.exception.SyntaxError
  111. if btext == b"":
  112. raise dns.exception.SyntaxError
  113. elif btext.endswith(b":") and not btext.endswith(b"::"):
  114. raise dns.exception.SyntaxError
  115. elif btext.startswith(b":") and not btext.startswith(b"::"):
  116. raise dns.exception.SyntaxError
  117. elif btext == b"::":
  118. btext = b"0::"
  119. #
  120. # Get rid of the icky dot-quad syntax if we have it.
  121. #
  122. m = _v4_ending.match(btext)
  123. if m is not None:
  124. b = dns.ipv4.inet_aton(m.group(2))
  125. btext = (
  126. f"{m.group(1).decode()}:{b[0]:02x}{b[1]:02x}:{b[2]:02x}{b[3]:02x}"
  127. ).encode()
  128. #
  129. # Try to turn '::<whatever>' into ':<whatever>'; if no match try to
  130. # turn '<whatever>::' into '<whatever>:'
  131. #
  132. m = _colon_colon_start.match(btext)
  133. if m is not None:
  134. btext = btext[1:]
  135. else:
  136. m = _colon_colon_end.match(btext)
  137. if m is not None:
  138. btext = btext[:-1]
  139. #
  140. # Now canonicalize into 8 chunks of 4 hex digits each
  141. #
  142. chunks = btext.split(b":")
  143. l = len(chunks)
  144. if l > 8:
  145. raise dns.exception.SyntaxError
  146. seen_empty = False
  147. canonical: List[bytes] = []
  148. for c in chunks:
  149. if c == b"":
  150. if seen_empty:
  151. raise dns.exception.SyntaxError
  152. seen_empty = True
  153. for _ in range(0, 8 - l + 1):
  154. canonical.append(b"0000")
  155. else:
  156. lc = len(c)
  157. if lc > 4:
  158. raise dns.exception.SyntaxError
  159. if lc != 4:
  160. c = (b"0" * (4 - lc)) + c
  161. canonical.append(c)
  162. if l < 8 and not seen_empty:
  163. raise dns.exception.SyntaxError
  164. btext = b"".join(canonical)
  165. #
  166. # Finally we can go to binary.
  167. #
  168. try:
  169. return binascii.unhexlify(btext)
  170. except (binascii.Error, TypeError):
  171. raise dns.exception.SyntaxError
  172. _mapped_prefix = b"\x00" * 10 + b"\xff\xff"
  173. def is_mapped(address: bytes) -> bool:
  174. """Is the specified address a mapped IPv4 address?
  175. *address*, a ``bytes`` is an IPv6 address in binary form.
  176. Returns a ``bool``.
  177. """
  178. return address.startswith(_mapped_prefix)
  179. def canonicalize(text: Union[str, bytes]) -> str:
  180. """Verify that *address* is a valid text form IPv6 address and return its
  181. canonical text form. Addresses with scopes are rejected.
  182. *text*, a ``str`` or ``bytes``, the IPv6 address in textual form.
  183. Raises ``dns.exception.SyntaxError`` if the text is not valid.
  184. """
  185. return dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(text))