elif af == socket.AF_INET6:
return "::"
raise NotImplementedError(f"unknown address family {af}")
+
+
+def canonicalize(text: str) -> str:
+ """Verify that *address* is a valid text form IPv4 or IPv6 address and return its
+ canonical text form. IPv6 addresses with scopes are rejected.
+
+ *text*, a ``str``, the address in textual form.
+
+ Raises ``ValueError`` if the text is not valid.
+ """
+ try:
+ return dns.ipv6.canonicalize(text)
+ except Exception:
+ try:
+ return dns.ipv4.canonicalize(text)
+ except Exception:
+ raise ValueError
return struct.pack("BBBB", *b)
except Exception:
raise dns.exception.SyntaxError
+
+
+def canonicalize(text: Union[str, bytes]) -> str:
+ """Verify that *address* is a valid text form IPv4 address and return its
+ canonical text form.
+
+ *text*, a ``str`` or ``bytes``, the IPv4 address in textual form.
+
+ Raises ``dns.exception.SyntaxError`` if the text is not valid.
+ """
+ # Note that inet_aton() only accepts canonial form, but we still run through
+ # inet_ntoa() to ensure the output is a str.
+ return dns.ipv4.inet_ntoa(dns.ipv4.inet_aton(text))
def inet_aton(text: Union[str, bytes], ignore_scope: bool = False) -> bytes:
"""Convert an IPv6 address in text form to binary form.
- *text*, a ``str``, the IPv6 address in textual form.
+ *text*, a ``str`` or ``bytes``, the IPv6 address in textual form.
*ignore_scope*, a ``bool``. If ``True``, a scope will be ignored.
If ``False``, the default, it is an error for a scope to be present.
"""
return address.startswith(_mapped_prefix)
+
+
+def canonicalize(text: Union[str, bytes]) -> str:
+ """Verify that *address* is a valid text form IPv6 address and return its
+ canonical text form. Addresses with scopes are rejected.
+
+ *text*, a ``str`` or ``bytes``, the IPv6 address in textual form.
+
+ Raises ``dns.exception.SyntaxError`` if the text is not valid.
+ """
+ return dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(text))
@classmethod
def _as_ipv4_address(cls, value):
if isinstance(value, str):
- # call to check validity
- dns.ipv4.inet_aton(value)
- return value
+ return dns.ipv4.canonicalize(value)
elif isinstance(value, bytes):
return dns.ipv4.inet_ntoa(value)
else:
@classmethod
def _as_ipv6_address(cls, value):
if isinstance(value, str):
- # call to check validity
- dns.ipv6.inet_aton(value)
- return value
+ return dns.ipv6.canonicalize(value)
elif isinstance(value, bytes):
return dns.ipv6.inet_ntoa(value)
else:
amtrelay04 3600 IN AMTRELAY 10 0 2 2001:db8::15
amtrelay05 3600 IN AMTRELAY 128 1 3 amtrelays.example.com.
apl01 3600 IN APL 1:192.168.32.0/21 !1:192.168.38.0/28
-apl02 3600 IN APL 1:224.0.0.0/4 2:FF00:0:0:0:0:0:0:0/8
+apl02 3600 IN APL 1:224.0.0.0/4 2:ff00::/8
avc01 3600 IN AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
b 300 IN CNAME foo.net.
c 300 IN A 73.80.65.49
amtrelay04.example. 3600 IN AMTRELAY 10 0 2 2001:db8::15
amtrelay05.example. 3600 IN AMTRELAY 128 1 3 amtrelays.example.com.
apl01.example. 3600 IN APL 1:192.168.32.0/21 !1:192.168.38.0/28
-apl02.example. 3600 IN APL 1:224.0.0.0/4 2:FF00:0:0:0:0:0:0:0/8
+apl02.example. 3600 IN APL 1:224.0.0.0/4 2:ff00::/8
avc01.example. 3600 IN AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
b.example. 300 IN CNAME foo.net.
c.example. 300 IN A 73.80.65.49
amtrelay04 3600 IN AMTRELAY 10 0 2 2001:db8::15
amtrelay05 3600 IN AMTRELAY 128 1 3 amtrelays.example.com.
apl01 3600 IN APL 1:192.168.32.0/21 !1:192.168.38.0/28
-apl02 3600 IN APL 1:224.0.0.0/4 2:FF00:0:0:0:0:0:0:0/8
+apl02 3600 IN APL 1:224.0.0.0/4 2:ff00::/8
avc01 3600 IN AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
b 300 IN CNAME foo.net.
c 300 IN A 73.80.65.49
amtrelay04 3600 IN AMTRELAY 10 0 2 2001:db8::15
amtrelay05 3600 IN AMTRELAY 128 1 3 amtrelays.example.com.
apl01 3600 IN APL 1:192.168.32.0/21 !1:192.168.38.0/28
-apl02 3600 IN APL 1:224.0.0.0/4 2:FF00:0:0:0:0:0:0:0/8
+apl02 3600 IN APL 1:224.0.0.0/4 2:ff00::/8
avc01 3600 IN AVC "app-name:WOLFGANG|app-class:OAM|business=yes"
b 300 IN CNAME foo.net.
c 300 IN A 73.80.65.49
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-import unittest
import binascii
+import itertools
import socket
+import unittest
import dns.exception
+import dns.inet
import dns.ipv4
import dns.ipv6
-import dns.inet
# for convenience
aton4 = dns.ipv4.inet_aton
"1.2.3.4.",
]
+v4_canonicalize_addrs = [
+ # (input, expected)
+ ("127.0.0.1", "127.0.0.1"),
+ (b"127.0.0.1", "127.0.0.1"),
+]
+
+v6_canonicalize_addrs = [
+ # (input, expected)
+ ("2001:503:83eb:0:0:0:0:30", "2001:503:83eb::30"),
+ (b"2001:503:83eb:0:0:0:0:30", "2001:503:83eb::30"),
+ ("2001:db8::1:1:1:1:1", "2001:db8:0:1:1:1:1:1"),
+ ("2001:DB8::1:1:1:1:1", "2001:db8:0:1:1:1:1:1"),
+]
+
+bad_canonicalize_addrs = [
+ "127.00.0.1",
+ "hi there",
+ "2001::db8::1:1:1:1:1",
+ "fe80::1%lo0",
+]
+
class NtoAAtoNTestCase(unittest.TestCase):
def test_aton1(self):
NotImplementedError, lambda: dns.inet.inet_ntop(12345, b"bogus")
)
+ def test_ipv4_canonicalize(self):
+ for address, expected in v4_canonicalize_addrs:
+ self.assertEqual(dns.ipv4.canonicalize(address), expected)
+ for bad_address in bad_canonicalize_addrs:
+ self.assertRaises(
+ dns.exception.SyntaxError, lambda: dns.ipv4.canonicalize(bad_address)
+ )
+
+ def test_ipv6_canonicalize(self):
+ for address, expected in v6_canonicalize_addrs:
+ self.assertEqual(dns.ipv6.canonicalize(address), expected)
+ for bad_address in bad_canonicalize_addrs:
+ self.assertRaises(
+ dns.exception.SyntaxError, lambda: dns.ipv6.canonicalize(bad_address)
+ )
+
+ def test_inet_canonicalize(self):
+ for address, expected in itertools.chain(
+ v4_canonicalize_addrs, v6_canonicalize_addrs
+ ):
+ self.assertEqual(dns.inet.canonicalize(address), expected)
+ for bad_address in bad_canonicalize_addrs:
+ self.assertRaises(ValueError, lambda: dns.inet.canonicalize(bad_address))
+
if __name__ == "__main__":
unittest.main()