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