From 790f087a67fcc0cc7730cedd1a225d58a82ddf5d Mon Sep 17 00:00:00 2001
From: Stuart McLaren <stuart.mclaren@hp.com>
Date: Fri, 18 Jan 2013 14:17:21 +0000
Subject: [PATCH] Add option to disable SSL compression

Allows optionally disabling SSL compression. This can significantly
improve HTTPS upload/download performance in some cases -- in particular
when the object is not compressible and you have very high network
bandwidth.

Implements blueprint ssl-compression.

Change-Id: I1260055f9c2e83cdabfeb51aed11b3899bed4d55
---
 bin/swift                       |  8 ++-
 swiftclient/client.py           | 31 +++++++++--
 swiftclient/https_connection.py | 95 +++++++++++++++++++++++++++++++++
 tests/test_swiftclient.py       |  8 +--
 4 files changed, 133 insertions(+), 9 deletions(-)
 create mode 100644 swiftclient/https_connection.py

diff --git a/bin/swift b/bin/swift
index e30d697d..7fdaf390 100755
--- a/bin/swift
+++ b/bin/swift
@@ -50,7 +50,8 @@ def get_conn(options):
                       os_options=options.os_options,
                       snet=options.snet,
                       cacert=options.os_cacert,
-                      insecure=options.insecure)
+                      insecure=options.insecure,
+                      ssl_compression=options.ssl_compression)
 
 
 def mkdirs(path):
@@ -1268,6 +1269,11 @@ Examples:
                            'be verified. '
                            'Defaults to env[SWIFTCLIENT_INSECURE] '
                            '(set to \'true\' to enable).')
+    parser.add_option('--no-ssl-compression',
+                      action='store_false', dest='ssl_compression',
+                      default=True,
+                      help='Disable SSL compression when using https. '
+                           'This may increase performance.')
     parser.disable_interspersed_args()
     (options, args) = parse_args(parser, argv[1:], enforce_requires=False)
     parser.enable_interspersed_args()
diff --git a/swiftclient/client.py b/swiftclient/client.py
index afd85f0f..cb09a37f 100644
--- a/swiftclient/client.py
+++ b/swiftclient/client.py
@@ -28,6 +28,10 @@ from urlparse import urlparse, urlunparse
 from httplib import HTTPException, HTTPConnection, HTTPSConnection
 from time import sleep
 
+try:
+    from swiftclient.https_connection import HTTPSConnectionNoSSLComp
+except ImportError:
+    HTTPSConnectionNoSSLComp = HTTPSConnection
 
 logger = logging.getLogger("swiftclient")
 
@@ -141,23 +145,32 @@ class ClientException(Exception):
         return b and '%s: %s' % (a, b) or a
 
 
-def http_connection(url, proxy=None):
+def http_connection(url, proxy=None, ssl_compression=True):
     """
     Make an HTTPConnection or HTTPSConnection
 
     :param url: url to connect to
     :param proxy: proxy to connect through, if any; None by default; str of the
                   format 'http://127.0.0.1:8888' to set one
+    :param ssl_compression: Whether to enable compression at the SSL layer.
+                            If set to 'False' and the pyOpenSSL library is
+                            present an attempt to disable SSL compression
+                            will be made. This may provide a performance
+                            increase for https upload/download operations.
     :returns: tuple of (parsed url, connection object)
     :raises ClientException: Unable to handle protocol scheme
     """
     url = encode_utf8(url)
     parsed = urlparse(url)
     proxy_parsed = urlparse(proxy) if proxy else None
+    host = proxy_parsed if proxy else parsed.netloc
     if parsed.scheme == 'http':
-        conn = HTTPConnection((proxy_parsed if proxy else parsed).netloc)
+        conn = HTTPConnection(host)
     elif parsed.scheme == 'https':
-        conn = HTTPSConnection((proxy_parsed if proxy else parsed).netloc)
+        if ssl_compression is True:
+            conn = HTTPSConnection(host)
+        else:
+            conn = HTTPSConnectionNoSSLComp(host)
     else:
         raise ClientException('Cannot handle protocol scheme %s for url %s' %
                               (parsed.scheme, repr(url)))
@@ -956,7 +969,8 @@ class Connection(object):
     def __init__(self, authurl=None, user=None, key=None, retries=5,
                  preauthurl=None, preauthtoken=None, snet=False,
                  starting_backoff=1, tenant_name=None, os_options=None,
-                 auth_version="1", cacert=None, insecure=False):
+                 auth_version="1", cacert=None, insecure=False,
+                 ssl_compression=True):
         """
         :param authurl: authentication URL
         :param user: user name to authenticate as
@@ -975,6 +989,11 @@ class Connection(object):
                            tenant_name, object_storage_url, region_name
         :param insecure: Allow to access insecure keystone server.
                          The keystone's certificate will not be verified.
+        :param ssl_compression: Whether to enable compression at the SSL layer.
+                                If set to 'False' and the pyOpenSSL library is
+                                present an attempt to disable SSL compression
+                                will be made. This may provide a performance
+                                increase for https upload/download operations.
         """
         self.authurl = authurl
         self.user = user
@@ -992,6 +1011,7 @@ class Connection(object):
             self.os_options['tenant_name'] = tenant_name
         self.cacert = cacert
         self.insecure = insecure
+        self.ssl_compression = ssl_compression
 
     def get_auth(self):
         return get_auth(self.authurl,
@@ -1004,7 +1024,8 @@ class Connection(object):
                         insecure=self.insecure)
 
     def http_connection(self):
-        return http_connection(self.url)
+        return http_connection(self.url,
+                               ssl_compression=self.ssl_compression)
 
     def _retry(self, reset_func, func, *args, **kwargs):
         self.attempts = 0
diff --git a/swiftclient/https_connection.py b/swiftclient/https_connection.py
new file mode 100644
index 00000000..2a2dc1f0
--- /dev/null
+++ b/swiftclient/https_connection.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2013 OpenStack, LLC.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+HTTPS/SSL related functionality
+"""
+
+import socket
+
+from httplib import HTTPSConnection
+
+import OpenSSL
+
+try:
+    from eventlet.green.OpenSSL.SSL import GreenConnection
+    from eventlet.greenio import GreenSocket
+    from eventlet.patcher import is_monkey_patched
+
+    def getsockopt(self, *args, **kwargs):
+        return self.fd.getsockopt(*args, **kwargs)
+    # The above is a workaround for an eventlet bug in getsockopt.
+    # TODO(mclaren): Workaround can be removed when this fix lands:
+    # https://bitbucket.org/eventlet/eventlet/commits/609f230
+    GreenSocket.getsockopt = getsockopt
+except ImportError:
+    def is_monkey_patched(*args):
+        return False
+
+
+class HTTPSConnectionNoSSLComp(HTTPSConnection):
+    """
+    Extended HTTPSConnection which uses the OpenSSL library
+    for disabling SSL compression.
+    Note: This functionality can eventually be replaced
+          with native Python 3.3 code.
+    """
+    def __init__(self, host):
+        HTTPSConnection.__init__(self, host)
+        self.setcontext()
+
+    def setcontext(self):
+        """
+        Set up the OpenSSL context.
+        """
+        self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
+        # Disable SSL layer compression.
+        self.context.set_options(0x20000)  # SSL_OP_NO_COMPRESSION
+
+    def connect(self):
+        """
+        Connect to an SSL port using the OpenSSL library and apply
+        per-connection parameters.
+        """
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self.sock = OpenSSLConnectionDelegator(self.context, sock)
+        self.sock.connect((self.host, self.port))
+
+
+class OpenSSLConnectionDelegator(object):
+    """
+    An OpenSSL.SSL.Connection delegator.
+
+    Supplies an additional 'makefile' method which httplib requires
+    and is not present in OpenSSL.SSL.Connection.
+
+    Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
+    a delegator must be used.
+    """
+    def __init__(self, *args, **kwargs):
+        if is_monkey_patched('socket'):
+            # If we are running in a monkey patched environment
+            # use eventlet's GreenConnection -- it handles eventlet's
+            # non-blocking sockets correctly.
+            Connection = GreenConnection
+        else:
+            Connection = OpenSSL.SSL.Connection
+        self.connection = Connection(*args, **kwargs)
+
+    def __getattr__(self, name):
+        return getattr(self.connection, name)
+
+    def makefile(self, *args, **kwargs):
+        return socket._fileobject(self.connection, *args, **kwargs)
diff --git a/tests/test_swiftclient.py b/tests/test_swiftclient.py
index aa0e1590..84d165bd 100644
--- a/tests/test_swiftclient.py
+++ b/tests/test_swiftclient.py
@@ -14,6 +14,7 @@
 # limitations under the License.
 
 # TODO: More tests
+import httplib
 import socket
 import StringIO
 import testtools
@@ -123,7 +124,7 @@ class MockHttpTest(testtools.TestCase):
             return_read = kwargs.get('return_read')
             query_string = kwargs.get('query_string')
 
-            def wrapper(url, proxy=None):
+            def wrapper(url, proxy=None, ssl_compression=True):
                 parsed, _conn = _orig_http_connection(url, proxy=proxy)
                 conn = fake_http_connect(*args, **kwargs)()
 
@@ -182,7 +183,8 @@ class TestHttpHelpers(MockHttpTest):
         self.assertTrue(isinstance(conn, c.HTTPConnection))
         url = 'https://www.test.com'
         _junk, conn = c.http_connection(url)
-        self.assertTrue(isinstance(conn, c.HTTPSConnection))
+        self.assertTrue(isinstance(conn, httplib.HTTPSConnection) or
+                        isinstance(conn, c.HTTPSConnectionNoSSLComp))
         url = 'ftp://www.test.com'
         self.assertRaises(c.ClientException, c.http_connection, url)
 
@@ -775,7 +777,7 @@ class TestConnection(MockHttpTest):
             def read(self, *args, **kwargs):
                 return ''
 
-        def local_http_connection(url, proxy=None):
+        def local_http_connection(url, proxy=None, ssl_compression=True):
             parsed = urlparse(url)
             return parsed, LocalConnection()