[Zconfig] SVN: ZConfig/trunk/ support for IPv6 addresses, contributed by Martin von L?\195?\182wis

Fred Drake fdrake at gmail.com
Mon Sep 27 13:45:51 EDT 2010


Log message for revision 116987:
  support for IPv6 addresses, contributed by Martin von L?\195?\182wis

Changed:
  U   ZConfig/trunk/NEWS.txt
  U   ZConfig/trunk/ZConfig/datatypes.py
  U   ZConfig/trunk/ZConfig/tests/test_datatypes.py
  U   ZConfig/trunk/doc/zconfig.tex

-=-
Modified: ZConfig/trunk/NEWS.txt
===================================================================
--- ZConfig/trunk/NEWS.txt	2010-09-27 17:41:29 UTC (rev 116986)
+++ ZConfig/trunk/NEWS.txt	2010-09-27 17:45:51 UTC (rev 116987)
@@ -7,6 +7,7 @@
 --------------------------------
 
 - Allow identical redefinition of ``%define`` names.
+- Added support for IPv6 addresses.
 
 
 ZConfig 2.8.0 (2010-04-13)

Modified: ZConfig/trunk/ZConfig/datatypes.py
===================================================================
--- ZConfig/trunk/ZConfig/datatypes.py	2010-09-27 17:41:29 UTC (rev 116986)
+++ ZConfig/trunk/ZConfig/datatypes.py	2010-09-27 17:45:51 UTC (rev 116987)
@@ -177,9 +177,17 @@
         host = ''
         port = None
         if ":" in s:
-            host, s = s.split(":", 1)
-            if s:
-                port = port_number(s)
+            host, p = s.rsplit(":", 1)
+            if host.startswith('[') and host.endswith(']'):
+                # [IPv6]:port
+                host = host[1:-1]
+            elif ':' in host:
+                # Unbracketed IPv6 address; 
+                # last part is not the port number
+                host = s
+                p = None
+            if p: # else leave port at None
+                port = port_number(p)
             host = host.lower()
         else:
             try:
@@ -203,6 +211,14 @@
 inet_binding_address = InetAddress("")
 
 class SocketAddress:
+    # Parsing results in family and address
+    # Family can be AF_UNIX (for addresses that are path names)
+    # or AF_INET6 (for inet addresses with colons in them)
+    # or AF_INET (for all other inet addresses); 
+    # An inet address is a (host, port) pair
+    # Notice that no DNS lookup is performed, so if the host
+    # is a DNS name, DNS lookup may end up with either IPv4 or
+    # IPv6 addresses, or both
     def __init__(self, s):
         import socket
         if "/" in s or s.find(os.sep) >= 0:
@@ -211,6 +227,8 @@
         else:
             self.family = socket.AF_INET
             self.address = self._parse_address(s)
+            if ':' in self.address[0]:
+                self.family = socket.AF_INET6
 
     def _parse_address(self, s):
         return inet_address(s)
@@ -237,15 +255,28 @@
         # IP address regex from the Perl Cookbook, Recipe 6.23 (revised ed.)
         # We allow underscores in hostnames although this is considered
         # illegal according to RFC1034.
+        # Addition: IPv6 addresses are now also accepted
         expr = (r"(^(\d|[01]?\d\d|2[0-4]\d|25[0-5])\." #ipaddr
                 r"(\d|[01]?\d\d|2[0-4]\d|25[0-5])\." #ipaddr cont'd
                 r"(\d|[01]?\d\d|2[0-4]\d|25[0-5])\." #ipaddr cont'd
                 r"(\d|[01]?\d\d|2[0-4]\d|25[0-5])$)" #ipaddr cont'd
-                r"|([A-Za-z_][-A-Za-z0-9_.]*[-A-Za-z0-9_])") # or hostname
+                r"|([A-Za-z_][-A-Za-z0-9_.]*[-A-Za-z0-9_])" # or hostname
+                r"|([0-9A-Fa-f:.]+:[0-9A-Fa-f:.]*)" # or superset of IPv6 addresses
+                                     # (requiring at least one colon)
+                ) 
         RegularExpressionConversion.__init__(self, expr)
 
     def __call__(self, value):
-        return RegularExpressionConversion.__call__(self, value).lower()
+        result = RegularExpressionConversion.__call__(self, value).lower()
+        # Use C library to validate IPv6 addresses, in particular wrt.
+        # number of colons and number of digits per group
+        if ':' in result:
+            import socket
+            try:
+                socket.inet_pton(socket.AF_INET6, result)
+            except socket.error:
+                raise ValueError('%r is not a valid IPv6 address' % value)
+        return result
 
 def existing_directory(v):
     nv = os.path.expanduser(v)

Modified: ZConfig/trunk/ZConfig/tests/test_datatypes.py
===================================================================
--- ZConfig/trunk/ZConfig/tests/test_datatypes.py	2010-09-27 17:41:29 UTC (rev 116986)
+++ ZConfig/trunk/ZConfig/tests/test_datatypes.py	2010-09-27 17:45:51 UTC (rev 116987)
@@ -185,7 +185,9 @@
         eq(convert("Host.Example.Com:80"), ("host.example.com", 80))
         eq(convert(":80"),                 (defhost, 80))
         eq(convert("80"),                  (defhost, 80))
+        eq(convert("[::1]:80"),            ("::1", 80))
         eq(convert("host.EXAMPLE.com"),    ("host.example.com", None))
+        eq(convert("2001::ABCD"),             ("2001::abcd", None))
         self.assertRaises(ValueError, convert, "40 # foo")
 
     def test_datatype_inet_binding_address(self):
@@ -258,6 +260,7 @@
         convert = self.types.get("socket-address")
         eq = self.assertEqual
         AF_INET = socket.AF_INET
+        AF_INET6 = socket.AF_INET6
         defhost = ZConfig.datatypes.DEFAULT_HOST
 
         def check(value, family, address, self=self, convert=convert):
@@ -269,6 +272,8 @@
         check(":80",                 AF_INET, (defhost, 80))
         check("80",                  AF_INET, (defhost, 80))
         check("host.EXAMPLE.com",    AF_INET, ("host.example.com",None))
+        check("::1",                 AF_INET6,("::1", None))
+        check("[::]:80",             AF_INET6,("::", 80))
         a1 = convert("/tmp/var/@345.4")
         a2 = convert("/tmp/var/@345.4:80")
         self.assertEqual(a1.address, "/tmp/var/@345.4")
@@ -291,11 +296,16 @@
         eq(convert('HOSTNAME.COM'),      'hostname.com')
         eq(convert('WWW.HOSTNAME.COM'),  'www.hostname.com')
         eq(convert('127.0.0.1'),         '127.0.0.1')
+        eq(convert('::1'),               '::1')
+        eq(convert('2001:DB8:1234:4567:89AB:cdef:0:1'), '2001:db8:1234:4567:89ab:cdef:0:1')
+        eq(convert('2001:DB8:1234:4567::10.11.12.13'), '2001:db8:1234:4567::10.11.12.13')
         raises(ValueError, convert,  '1hostnamewithleadingnumeric')
         raises(ValueError, convert,  '255.255')
         raises(ValueError, convert,  '12345678')
         raises(ValueError, convert,  '999.999.999.999')
         raises(ValueError, convert,  'a!badhostname')
+        raises(ValueError, convert,  '2001:DB8:0123:4567:89AB:cdef:0:1:2')
+        raises(ValueError, convert,  '2001:DB8:0123:4567::10.11.12.13.14')
 
     def test_existing_directory(self):
         convert = self.types.get('existing-directory')

Modified: ZConfig/trunk/doc/zconfig.tex
===================================================================
--- ZConfig/trunk/doc/zconfig.tex	2010-09-27 17:41:29 UTC (rev 116986)
+++ ZConfig/trunk/doc/zconfig.tex	2010-09-27 17:45:51 UTC (rev 116987)
@@ -905,7 +905,9 @@
   will be returned for \var{hostname}.  The default host is
   \code{localhost} on Windows and the empty string on all other
   platforms.  If the port is omitted, \code{None} will be returned for
-  \var{port}.
+  \var{port}. IPv6 addresses can be specified in colon-separated notation;
+  if both host and port need to be specified, the bracketed form
+  (\code{[addr]:port}) must be used.
 
 \term{\datatype{inet-binding-address}}
   An Internet address expressed as a \code{(\var{hostname},
@@ -931,7 +933,8 @@
   Validates a valid IP address or hostname.  If the first 
   character is a digit, the value is assumed to be an IP 
   address.  If the first character is not a digit, the value 
-  is assumed to be a hostname.  Hostnames are converted to lower
+  is assumed to be a hostname.  Strings containing colons are
+  considered IPv6 address.  Hostnames are converted to lower
   case.
 
 \term{\datatype{locale}}



More information about the ZConfig mailing list