From 1a8337f2993bb426c7965e06d63178e4e7e4078a Mon Sep 17 00:00:00 2001
From: Manuel Franceschini <livewire@google.com>
Date: Fri, 13 Aug 2010 11:32:15 +0200
Subject: [PATCH] rapi.client, http.client: Format url correctly when using
 IPv6

This patch moves the FormatAddress helper function from daemon.py to
netutils.py. This enables its use in http.client as well as in
rapi.client. Furthermore this adds functionality to format IPv6
addresses according to RFC 3986.

It is required for use of literal IPv6 addresses in URLs in pycurl.
For some reason it worked also without the bracketing ("["<address>"]"),
but we do not want to rely on that.

Signed-off-by: Manuel Franceschini <livewire@google.com>
Reviewed-by: Michael Hanselmann <hansmi@google.com>
---
 lib/daemon.py                    | 23 +++--------------------
 lib/http/client.py               |  8 +++++++-
 lib/netutils.py                  | 26 ++++++++++++++++++++++++++
 lib/rapi/client.py               |  9 ++++++++-
 test/ganeti.netutils_unittest.py | 30 ++++++++++++++++++++++++++++++
 5 files changed, 74 insertions(+), 22 deletions(-)

diff --git a/lib/daemon.py b/lib/daemon.py
index 1fd1b741e..86e6c02da 100644
--- a/lib/daemon.py
+++ b/lib/daemon.py
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2010 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -100,23 +100,6 @@ class GanetiBaseAsyncoreDispatcher(asyncore.dispatcher):
     return False
 
 
-def FormatAddress(family, address):
-  """Format a client's address
-
-  @type family: integer
-  @param family: socket family (one of socket.AF_*)
-  @type address: family specific (usually tuple)
-  @param address: address, as reported by this class
-
-  """
-  if family == socket.AF_INET and len(address) == 2:
-    return "%s:%d" % address
-  elif family == socket.AF_UNIX and len(address) == 3:
-    return "pid=%s, uid=%s, gid=%s" % address
-  else:
-    return str(address)
-
-
 class AsyncStreamServer(GanetiBaseAsyncoreDispatcher):
   """A stream server to use with asyncore.
 
@@ -159,7 +142,7 @@ class AsyncStreamServer(GanetiBaseAsyncoreDispatcher):
         # is passed in from accept anyway
         client_address = netutils.GetSocketCredentials(connected_socket)
       logging.info("Accepted connection from %s",
-                   FormatAddress(self.family, client_address))
+                   netutils.FormatAddress(self.family, client_address))
       self.handle_connection(connected_socket, client_address)
 
   def handle_connection(self, connected_socket, client_address):
@@ -290,7 +273,7 @@ class AsyncTerminatedMessageStream(asynchat.async_chat):
 
   def close_log(self):
     logging.info("Closing connection from %s",
-                 FormatAddress(self.family, self.peer_address))
+                 netutils.FormatAddress(self.family, self.peer_address))
     self.close()
 
   # this method is overriding an asyncore.dispatcher method
diff --git a/lib/http/client.py b/lib/http/client.py
index 8658aed45..891865f39 100644
--- a/lib/http/client.py
+++ b/lib/http/client.py
@@ -28,6 +28,7 @@ from cStringIO import StringIO
 
 from ganeti import http
 from ganeti import compat
+from ganeti import netutils
 
 
 class HttpClientRequest(object):
@@ -104,8 +105,13 @@ class HttpClientRequest(object):
     """Returns the full URL for this requests.
 
     """
+    if netutils.IPAddress.IsValid(self.host):
+      family = netutils.IPAddress.GetAddressFamily(self.host)
+      address = netutils.FormatAddress(family, (self.host, self.port))
+    else:
+      address = "%s:%s" % (self.host, self.port)
     # TODO: Support for non-SSL requests
-    return "https://%s:%s%s" % (self.host, self.port, self.path)
+    return "https://%s%s" % (address, self.path)
 
   @property
   def identity(self):
diff --git a/lib/netutils.py b/lib/netutils.py
index edbebb4e6..3ab985467 100644
--- a/lib/netutils.py
+++ b/lib/netutils.py
@@ -472,3 +472,29 @@ class IP6Address(IPAddress):
       address_int = (address_int << 16) + int(part or '0', 16)
 
     return address_int
+
+def FormatAddress(family, address):
+  """Format a socket address
+
+  @type family: integer
+  @param family: socket family (one of socket.AF_*)
+  @type address: family specific (usually tuple)
+  @param address: address, as reported by this class
+
+  """
+  if family == socket.AF_UNIX and len(address) == 3:
+    return "pid=%s, uid=%s, gid=%s" % address
+
+  if family in (socket.AF_INET, socket.AF_INET6) and len(address) == 2:
+    host, port = address
+    if family == socket.AF_INET6:
+      res = "[%s]" % host
+    else:
+      res = host
+
+    if port is not None:
+      res += ":%s" % port
+
+    return res
+
+  raise errors.ParameterError(family, address)
diff --git a/lib/rapi/client.py b/lib/rapi/client.py
index 498983154..8c2c75c09 100644
--- a/lib/rapi/client.py
+++ b/lib/rapi/client.py
@@ -35,6 +35,7 @@
 
 import logging
 import simplejson
+import socket
 import urllib
 import threading
 import pycurl
@@ -265,7 +266,13 @@ class GanetiRapiClient(object):
     self._curl_config_fn = curl_config_fn
     self._curl_factory = curl_factory
 
-    self._base_url = "https://%s:%s" % (host, port)
+    try:
+      socket.inet_pton(socket.AF_INET6, host)
+      address = "[%s]:%s" % (host, port)
+    except socket.error:
+      address = "%s:%s" % (host, port)
+
+    self._base_url = "https://%s" % address
 
     if username is not None:
       if password is None:
diff --git a/test/ganeti.netutils_unittest.py b/test/ganeti.netutils_unittest.py
index 6e324439a..109fe44bf 100755
--- a/test/ganeti.netutils_unittest.py
+++ b/test/ganeti.netutils_unittest.py
@@ -385,5 +385,35 @@ class TestIP6TcpPingDeaf(unittest.TestCase, _BaseTcpPingDeafTest):
     _BaseTcpPingDeafTest.tearDown(self)
 
 
+class TestFormatAddress(unittest.TestCase):
+  """Testcase for FormatAddress"""
+
+  def testFormatAddressUnixSocket(self):
+    res1 = netutils.FormatAddress(socket.AF_UNIX, ("12352", 0, 0))
+    self.assertEqual(res1, "pid=12352, uid=0, gid=0")
+
+  def testFormatAddressIP4(self):
+    res1 = netutils.FormatAddress(socket.AF_INET, ("127.0.0.1", 1234))
+    self.assertEqual(res1, "127.0.0.1:1234")
+    res2 = netutils.FormatAddress(socket.AF_INET, ("192.0.2.32", None))
+    self.assertEqual(res2, "192.0.2.32")
+
+  def testFormatAddressIP6(self):
+    res1 = netutils.FormatAddress(socket.AF_INET6, ("::1", 1234))
+    self.assertEqual(res1, "[::1]:1234")
+    res2 = netutils.FormatAddress(socket.AF_INET6, ("::1", None))
+    self.assertEqual(res2, "[::1]")
+    res2 = netutils.FormatAddress(socket.AF_INET6, ("2001:db8::beef", "80"))
+    self.assertEqual(res2, "[2001:db8::beef]:80")
+
+  def testInvalidFormatAddress(self):
+    self.assertRaises(errors.ParameterError,
+                      netutils.FormatAddress, None, ("::1", None))
+    self.assertRaises(errors.ParameterError,
+                      netutils.FormatAddress, socket.AF_INET, "127.0.0.1")
+    self.assertRaises(errors.ParameterError,
+                      netutils.FormatAddress, socket.AF_INET, ("::1"))
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
-- 
GitLab