Browse Source

git-svn-id: http://proj.badc.rl.ac.uk/svn/ndg-security/trunk/ndg_httpsclient@7972 051b1e3e-aa0c-0410-b6c2-bfbade6052be

pjkersha 13 years ago
commit
8ad766f537

+ 17 - 0
.project

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>ndg_httpsclient</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.python.pydev.PyDevBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.python.pydev.pythonNature</nature>
+	</natures>
+</projectDescription>

+ 10 - 0
.pydevproject

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<?eclipse-pydev version="1.0"?>
+
+<pydev_project>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
+<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.6</pydev_property>
+<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
+<path>/ndg_httpsclient</path>
+</pydev_pathproperty>
+</pydev_project>

+ 26 - 0
LICENSE

@@ -0,0 +1,26 @@
+Copyright (c) 2011, Science & Technology Facilities Council (STFC)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of the Science & Technology Facilities Council (STFC) 
+      nor the names of its contributors may be used to endorse or promote 
+      products derived from this software without specific prior written 
+      permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 39 - 0
README

@@ -0,0 +1,39 @@
+================
+urllib2pyopenssl
+================
+
+Description
+===========
+This is a library to enable urllib2 to be used with SSL sockets from pyOpenSSL instead of the built in ssl library. A script is provided to exercise it:
+
+urllib2pyopenssl_get::
+ - Utility to fetch data using HTTP or HTTPS GET from a specified URL.
+
+Prerequisites
+=============
+This has been developed and tested for Python 2.6 (compiled with HTTPS support).
+pyOpenSSL
+
+
+Installation
+============
+Installation can be performed using easy_install, e.g.::
+  easy_install urllib2pyopenssl-0.1.0-py2.6.egg
+
+Running urllib2pyopenssl_get
+============================
+Parameter::
+    url                   The URL of the resource to be fetched
+
+Options::
+    -h, --help            Show help message and exit.
+    -c FILE, --certificate=FILE
+                          Certificate file - defaults to $HOME/credentials.pem
+    -k FILE, --private-key=FILE
+                          Private key file - defaults to the certificate file
+    -t DIR, --ca-certificate-dir=DIR
+                          Trusted CA certificate file directory.
+    -d, --debug           Print debug information - this may be useful in solving problems with HTTP
+                          or HTTPS access to a server.
+    -f FILE, --fetch=FILE Output file
+    -v, --verify-peer     Verify peer certificate.

+ 19 - 0
ndg/__init__.py

@@ -0,0 +1,19 @@
+"""ndg_httpsclient - PyOpenSSL utility to make a httplib-like interface suitable
+for use with urllib2
+
+This is a setuptools namespace_package.  DO NOT place any other
+code in this file!  There is no guarantee that it will be installed
+with easy_install.  See:
+
+http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
+
+... for details.
+"""
+__author__ = "P J Kershaw"
+__date__ = "06/01/12"
+__copyright__ = "(C) 2012 Science and Technology Facilities Council"
+__license__ = "BSD - see LICENSE file in top-level directory"
+__contact__ = "Philip.Kershaw@stfc.ac.uk"
+__revision__ = '$Id$'
+
+__import__('pkg_resources').declare_namespace(__name__)

+ 9 - 0
ndg/httpsclient/__init__.py

@@ -0,0 +1,9 @@
+"""ndg_httpsclient - PyOpenSSL utility to make a httplib-like interface suitable
+for use with urllib2
+"""
+__author__ = "P J Kershaw (STFC) and Richard Wilkinson (Tessella)"
+__date__ = "09/12/11"
+__copyright__ = "(C) 2011 Science and Technology Facilities Council"
+__license__ = "BSD - see LICENSE file in top-level directory"
+__contact__ = "Philip.Kershaw@stfc.ac.uk"
+__revision__ = '$Id$'

+ 177 - 0
ndg/httpsclient/get.py

@@ -0,0 +1,177 @@
+import cookielib
+import httplib
+import logging
+from optparse import OptionParser
+import os
+import urllib2
+import urlparse
+
+from OpenSSL import SSL
+
+from urllib2pyopenssl.urllib2_build_opener import urllib2_build_opener
+from urllib2pyopenssl.https import HTTPSContextHandler
+import urllib2pyopenssl.ssl_context_util as ssl_context_util
+from urllib2pyopenssl.ssl_peer_verification import ServerSSLCertVerification
+
+def fetch_from_url(url, config):
+    """Returns data retrieved from a URL.
+    @param url - URL to attempt to open
+    @param config - configuration
+    @return data retrieved from URL or None
+    """
+    (return_code, return_message, response) = open_url(url, config)
+    if return_code and return_code == httplib.OK:
+        return_data = response.read()
+        response.close()
+        return return_data
+    else:
+        raise Exception(return_message)
+
+def fetch_from_url_to_file(url, config, output_file):
+    """Writes data retrieved from a URL to a file.
+    @param url - URL to attempt to open
+    @param config - configuration
+    @param output_file - output file
+    @return tuple (
+        returned HTTP status code or 0 if an error occurred
+        returned message
+        boolean indicating whether access was successful
+    )
+    """
+    (return_code, return_message, response) = open_url(url, config)
+    if return_code == httplib.OK:
+        return_data = response.read()
+        response.close()
+        outfile = open(output_file, "w")
+        outfile.write(return_data)
+        outfile.close()
+    return (return_code, return_message, (return_code == httplib.OK))
+
+def open_url(url, config):
+    """Attempts to open a connection to a specified URL.
+    @param url - URL to attempt to open
+    @param config - configuration
+    @return tuple (
+        returned HTTP status code or 0 if an error occurred
+        returned message or error description
+        response object
+    )
+    """
+    debuglevel = 1 if config.debug else 0
+
+    # Set up handlers for URL opener.
+    cj = cookielib.CookieJar()
+    cookie_handler = urllib2.HTTPCookieProcessor(cj)
+
+    handlers = [cookie_handler]
+
+    if config.debug:
+        http_handler = urllib2.HTTPHandler(debuglevel=debuglevel)
+        https_handler = HTTPSContextHandler(config.ssl_context, debuglevel=debuglevel)
+        handlers.extend([http_handler, https_handler])
+
+    # Explicitly remove proxy handling if the host is one listed in the value of the no_proxy
+    # environment variable because urllib2 does use proxy settings set via http_proxy and
+    # https_proxy, but does not take the no_proxy value into account.
+    if not _should_use_proxy(url):
+        handlers.append(urllib2.ProxyHandler({}))
+        if config.debug:
+            print "Not using proxy"
+
+    opener = urllib2_build_opener(config.ssl_context, *handlers)
+
+    # Open the URL and check the response.
+    return_code = 0
+    return_message = ''
+    response = None
+    try:
+        response = opener.open(url)
+        if response.url == url:
+            return_message = response.msg
+            return_code = response.code
+        else:
+            return_message = ('Redirected (%s  %s)' % (response.code, response.url))
+        if config.debug:
+            for index, cookie in enumerate(cj):
+                print index, '  :  ', cookie        
+    except urllib2.HTTPError, exc:
+        return_code = exc.code
+        return_message = ("Error: %s" % exc.msg)
+        if config.debug:
+            print exc.code, exc.msg
+    except Exception, exc:
+        return_message = ("Error: %s" % exc.__str__())
+        if config.debug:
+            print exc.__class__, exc.__str__()
+    return (return_code, return_message, response)
+
+def _should_use_proxy(url):
+    """Determines whether a proxy should be used to open a connection to the specified URL, based on
+        the value of the no_proxy environment variable.
+    @param url - URL string
+    """
+    no_proxy = os.environ.get('no_proxy', '')
+
+    urlObj = urlparse.urlparse(url)
+    for np in [h.strip() for h in no_proxy.split(',')]:
+        if urlObj.hostname == np:
+            return False
+
+    return True
+
+class Configuration(object):
+    """Checker configuration.
+    """
+    def __init__(self, ssl_context, debug):
+        """
+        @param key_file - file containing the user's private key
+        @param cert_file - file containing the user's certificate
+        @param debug - if True, output debugging information
+        """
+        self.ssl_context = ssl_context
+        self.debug = debug
+
+def main():
+    '''Utility to fetch data using HTTP or HTTPS GET from a specified URL.
+    '''
+    parser = OptionParser(usage="%prog [options] url")
+    parser.add_option("-k", "--private-key", dest="key_file", metavar="FILE",
+                      default=None,
+                      help="Private key file.")
+    parser.add_option("-c", "--certificate", dest="cert_file", metavar="FILE",
+                      default=os.path.expanduser("~/credentials.pem"),
+                      help="Certificate file.")
+    parser.add_option("-t", "--ca-certificate-dir", dest="ca_dir", metavar="PATH",
+                      default=None,
+                      help="Trusted CA certificate file directory.")
+    parser.add_option("-d", "--debug", action="store_true", dest="debug", default=False,
+                      help="Print debug information.")
+    parser.add_option("-f", "--fetch", dest="output_file", metavar="FILE",
+                      default=None, help="Output file.")
+    parser.add_option("-v", "--verify-peer", action="store_true", dest="verify_peer", default=False,
+                      help="Verify peer certificate.")
+    (options, args) = parser.parse_args()
+    if len(args) != 1:
+        parser.error("Incorrect number of arguments")
+
+    url = args[0]
+    # If a private key file is not specified, the key is assumed to be stored in the certificate file.
+    ssl_context = ssl_context_util.make_ssl_context(
+        options.key_file if options.key_file and os.path.exists(options.key_file) else None,
+        options.cert_file if options.cert_file and os.path.exists(options.cert_file) else None,
+        None,
+        options.ca_dir if options.ca_dir and os.path.exists(options.ca_dir) else None,
+        options.verify_peer, url)
+
+    config = Configuration(ssl_context, options.debug)
+    if options.output_file:
+        (return_code, return_message, success) = fetch_from_url_to_file(url, config,
+            options.output_file)
+        print return_code, return_message
+    else:
+        data = fetch_from_url(url, config)
+        print data
+
+if __name__=='__main__':
+    logging.basicConfig()
+    main()

+ 100 - 0
ndg/httpsclient/https.py

@@ -0,0 +1,100 @@
+"""urllib2pyopenssl HTTPS module containing PyOpenSSL implementation of
+httplib.HTTPSConnection
+
+PyOpenSSL utility to make a httplib-like interface suitable for use with 
+urllib2
+"""
+__author__ = "P J Kershaw (STFC)"
+__date__ = "09/12/11"
+__copyright__ = "(C) 2012 Science and Technology Facilities Council"
+__license__ = "BSD - see LICENSE file in top-level directory"
+__contact__ = "Philip.Kershaw@stfc.ac.uk"
+__revision__ = '$Id$'
+import logging
+import socket
+from httplib import HTTPConnection, HTTPS_PORT
+from urllib2 import AbstractHTTPHandler
+
+from OpenSSL import SSL
+
+from urllib2pyopenssl.ssl_socket import SSLSocket
+
+log = logging.getLogger(__name__)
+
+
+class HTTPSConnection(HTTPConnection):
+    """This class allows communication via SSL using PyOpenSSL.
+    It is based on httplib.HTTPSConnection, modified to use PyOpenSSL.
+
+    Note: This uses the constructor inherited from HTTPConnection to allow it to
+    be used with httplib and HTTPSContextHandler. To use the class directly with
+    an SSL context set ssl_context after construction.
+    @cvar default_port: default port for this class (443)
+    @type default_port: int
+    """
+    default_port = HTTPS_PORT
+
+    def __init__(self, host, port=None, strict=None,
+                 timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
+        HTTPConnection.__init__(self, host, port, strict, timeout)
+        if not hasattr(self, 'ssl_context'):
+            self.ssl_context = None
+
+    def connect(self):
+        """Create SSL socket and connect to peer
+        """ 
+        if getattr(self, 'ssl_context', None):
+            if not isinstance(self.ssl_context, SSL.Context):
+                raise TypeError('Expecting OpenSSL.SSL.Context type for "'
+                                'ssl_context" keyword; got %r instead' %
+                                self.ssl_context)
+            ssl_context = self.ssl_context
+        else:
+            ssl_context = SSL.Context(SSL.SSLv23_METHOD)
+
+        sock = socket.create_connection((self.host, self.port), self.timeout)
+        if getattr(self, '_tunnel_host', None):
+            self.sock = sock
+            self._tunnel()
+        self.sock = SSLSocket(ssl_context, sock)
+        # Go to client mode.
+        self.sock.set_connect_state()
+
+    def close(self):
+        """Close socket and shut down SSL connection"""
+        self.sock.close()
+        
+
+class HTTPSContextHandler(AbstractHTTPHandler):
+    '''HTTPS handler that provides allows a SSL context to be set for the SSL
+    connections.
+    '''
+    https_request = AbstractHTTPHandler.do_request_
+
+    def __init__(self, ssl_context, debuglevel=0):
+        """
+        @param ssl_context - SSL context
+        @param debuglevel - debug level for HTTPSHandler
+        """
+        AbstractHTTPHandler.__init__(self, debuglevel)
+
+        if ssl_context is not None:
+            if not isinstance(ssl_context, SSL.Context):
+                raise TypeError('Expecting OpenSSL.SSL.Context type for "'
+                                'ssl_context" keyword; got %r instead' %
+                                ssl_context)
+            self.ssl_context = ssl_context
+        else:
+            self.ssl_context = SSL.Context(SSL.SSLv23_METHOD)
+
+    def https_open(self, req):
+        """Opens HTTPS request
+        @param req - HTTP request
+        @return HTTP Response object
+        """
+        # Make a custom class extending HTTPSConnection, with the SSL context
+        # set as a class variable so that it is available to the connect method.
+        customHTTPSContextConnection = type('CustomHTTPSContextConnection',
+                                            (HTTPSConnection, object),
+                                            {'ssl_context': self.ssl_context})
+        return self.do_open(customHTTPSContextConnection, req)

+ 65 - 0
ndg/httpsclient/ssl_context_util.py

@@ -0,0 +1,65 @@
+import urlparse
+
+from OpenSSL import SSL
+
+from urllib2pyopenssl.ssl_peer_verification import ServerSSLCertVerification
+
+class SSlContextConfig(object):
+    """
+    Holds configuration options for creating a SSL context. This is used as a
+    template to create the contexts with specific verification callbacks.
+    """
+    def __init__(self, key_file=None, cert_file=None, pem_file=None, ca_dir=None,
+                 verify_peer=False):
+        self.key_file = key_file
+        self.cert_file = cert_file
+        self.pem_file = pem_file
+        self.ca_dir = ca_dir
+        self.verify_peer = verify_peer
+
+def make_ssl_context_from_config(ssl_config=False, url=None):
+    return make_ssl_context(ssl_config.key_file, ssl_config.cert_file,
+                            ssl_config.pem_file, ssl_config.ca_dir,
+                            ssl_config.verify_peer, url)
+
+def make_ssl_context(key_file=None, cert_file=None, pem_file=None, ca_dir=None,
+                     verify_peer=False, url=None):
+    """
+    Creates SSL context containing certificate and key file locations.
+    """
+    ssl_context = SSL.Context(SSL.SSLv23_METHOD)
+    # Key file defaults to certificate file if present.
+    if cert_file:
+        ssl_context.use_certificate_file(cert_file)
+    if key_file:
+        ssl_context.use_privatekey_file(key_file)
+    else:
+        if cert_file:
+            ssl_context.use_privatekey_file(cert_file)
+
+    if ca_dir:
+        ssl_context.load_verify_locations(pem_file, ca_dir)
+
+    def _callback(conn, x509, errnum, errdepth, preverify_ok):
+        """Default certification verification callback.
+        Performs no checks and returns the status passed in.
+        """
+        return preverify_ok
+    verify_callback = _callback
+
+    if verify_peer:
+        ssl_context.set_verify_depth(9)
+        if url:
+            set_peer_verification_for_url_hostname(ssl_context, url)
+        else:
+            ssl_context.set_verify(SSL.VERIFY_PEER, verify_callback)
+    else:
+        ssl_context.set_verify(SSL.VERIFY_NONE, verify_callback)
+    return ssl_context
+
+def set_peer_verification_for_url_hostname(ssl_context, url, if_verify_enabled=False):
+    if not if_verify_enabled or (ssl_context.get_verify_mode() & SSL.VERIFY_PEER):
+        urlObj = urlparse.urlparse(url)
+        hostname = urlObj.hostname
+        verify_callback = ServerSSLCertVerification(hostname=hostname)
+        ssl_context.set_verify(SSL.VERIFY_PEER, verify_callback)

+ 182 - 0
ndg/httpsclient/ssl_peer_verification.py

@@ -0,0 +1,182 @@
+"""
+"""
+__author__ = "P J Kershaw"
+__date__ = "02/06/05"
+__copyright__ = "(C) 2010 Science and Technology Facilities Council"
+__license__ = """BSD - See LICENSE file in top-level directory"""
+__contact__ = "Philip.Kershaw@stfc.ac.uk"
+__revision__ = '$Id: client.py 7928 2011-08-12 13:16:26Z pjkersha $'
+
+import logging
+log = logging.getLogger(__name__)
+
+import re
+
+class ServerSSLCertVerification(object):
+    """Check server identity.  If hostname doesn't match, allow match of
+    host's Distinguished Name against server DN setting"""
+    DN_LUT = {
+        'commonName':               'CN',
+        'organisationalUnitName':   'OU',
+        'organisation':             'O',
+        'countryName':              'C',
+        'emailAddress':             'EMAILADDRESS',
+        'localityName':             'L',
+        'stateOrProvinceName':      'ST',
+        'streetAddress':            'STREET',
+        'domainComponent':          'DC',
+        'userid':                   'UID'
+    }
+    PARSER_RE_STR = '/(%s)=' % '|'.join(DN_LUT.keys() + DN_LUT.values())
+    PARSER_RE = re.compile(PARSER_RE_STR)
+
+    __slots__ = ('__hostname', '__certDN', '__serverCNPrefixes')
+
+    def __init__(self, certDN=None, hostname=None, serverCNPrefixes=None):
+        """Override parent class __init__ to enable setting of certDN
+        setting
+
+        @type certDN: string
+        @param certDN: Set the expected Distinguished Name of the
+        server to avoid errors matching hostnames.  This is useful
+        where the hostname is not fully qualified
+        """
+        self.__certDN = None
+        self.__hostname = None
+        self.__serverCNPrefixes = None
+
+        if certDN is not None:
+            self.certDN = certDN
+
+        if hostname is not None:
+            self.hostname = hostname
+
+        if serverCNPrefixes is not None:
+            self.serverCNPrefixes = serverCNPrefixes
+        else:
+            self.serverCNPrefixes = ['']
+
+    def __call__(self, connection, peerCert, errorStatus, errorDepth,
+                 preverifyOK):
+        """Verify server certificate
+
+        @type connection: OpenSSL.SSL.Connection
+        @param connection: SSL connection object
+        @type peerCert: basestring
+        @param peerCert: server host certificate as OpenSSL.crypto.X509
+        instance
+        @type errorStatus: int
+        @param errorStatus: error status passed from caller.  This is the value
+        returned by the OpenSSL C function X509_STORE_CTX_get_error().  Look-up
+        x509_vfy.h in the OpenSSL source to get the meanings of the different
+        codes.  PyOpenSSL doesn't help you!
+        @type errorDepth: int
+        @param errorDepth: a non-negative integer representing where in the
+        certificate chain the error occurred. If it is zero it occured in the
+        end entity certificate, one if it is the certificate which signed the
+        end entity certificate and so on.
+
+        @type preverifyOK: int
+        @param preverifyOK: the error status - 0 = Error, 1 = OK of the current
+        SSL context irrespective of any verification checks done here.  If this
+        function yields an OK status, it should enforce the preverifyOK value
+        so that any error set upstream overrides and is honoured.
+        @rtype: int
+        @return: status code - 0/False = Error, 1/True = OK
+        """
+        if peerCert.has_expired():
+            # Any expired certificate in the chain should result in an error
+            log.error('Certificate %r in peer certificate chain has expired',
+                      peerCert.get_subject())
+
+            return False
+
+        elif errorDepth == 0:
+            # Only interested in DN of last certificate in the chain - this must
+            # match the expected Server DN setting
+            peerCertSubj = peerCert.get_subject()
+            peerCertDN = peerCertSubj.get_components()
+            peerCertDN.sort()
+
+            if self.certDN is None:
+                # Check hostname against peer certificate CN field instead:
+                if self.hostname is None:
+                    log.error('No "hostname" or "certDN" set to check peer '
+                              'certificate against')
+                    return False
+
+                acceptableCNs = [pfx + self.hostname
+                                 for pfx in self.serverCNPrefixes]
+                if peerCertSubj.commonName in acceptableCNs:
+                    return preverifyOK
+                else:
+                    log.error('Peer certificate CN %r doesn\'t match the '
+                              'expected CN %r', peerCertSubj.commonName,
+                              acceptableCNs)
+                    return False
+            else:
+                if peerCertDN == self.certDN:
+                    return preverifyOK
+                else:
+                    log.error('Peer certificate DN %r doesn\'t match the '
+                              'expected DN %r', peerCertDN, self.certDN)
+                    return False
+        else:
+            return preverifyOK
+
+    def _getCertDN(self):
+        return self.__certDN
+
+    def _setCertDN(self, val):
+        if isinstance(val, basestring):
+            # Allow for quoted DN
+            certDN = val.strip('"')
+
+            dnFields = self.__class__.PARSER_RE.split(certDN)
+            if len(dnFields) < 2:
+                raise TypeError('Error parsing DN string: "%s"' % certDN)
+
+            self.__certDN = zip(dnFields[1::2], dnFields[2::2])
+            self.__certDN.sort()
+
+        elif not isinstance(val, list):
+            for i in val:
+                if not len(i) == 2:
+                    raise TypeError('Expecting list of two element DN field, '
+                                    'DN field value pairs for "certDN" '
+                                    'attribute')
+            self.__certDN = val
+        else:
+            raise TypeError('Expecting list or string type for "certDN" '
+                            'attribute')
+
+    certDN = property(fget=_getCertDN,
+                      fset=_setCertDN,
+                      doc="Distinguished Name for Server Certificate")
+
+    # Get/Set Property methods
+    def _getHostname(self):
+        return self.__hostname
+
+    def _setHostname(self, val):
+        if not isinstance(val, basestring):
+            raise TypeError("Expecting string type for hostname "
+                                 "attribute")
+        self.__hostname = val
+
+    hostname = property(fget=_getHostname,
+                        fset=_setHostname,
+                        doc="hostname of server")
+
+    def _getServerCNPrefixes(self):
+        return self.__serverCNPrefixes
+
+    def _setServerCNPrefixes(self, val):
+        if not isinstance(val, list):
+            raise TypeError("Expecting string type for ServerCNPrefixes "
+                            "attribute")
+        self.__serverCNPrefixes = val
+
+    serverCNPrefixes = property(fget=_getServerCNPrefixes,
+                                fset=_setServerCNPrefixes,
+                                doc="Server CN Prefixes")

+ 278 - 0
ndg/httpsclient/ssl_socket.py

@@ -0,0 +1,278 @@
+"""PyOpenSSL utilities including HTTPSSocket class which wraps PyOpenSSL
+SSL connection into a httplib-like interface suitable for use with urllib2
+
+"""
+__author__ = "P J Kershaw"
+__date__ = "21/12/10"
+__copyright__ = "(C) 2012 Science and Technology Facilities Council"
+__license__ = "BSD - see LICENSE file in top-level directory"
+__contact__ = "Philip.Kershaw@stfc.ac.uk"
+__revision__ = '$Id: pyopenssl.py 7929 2011-08-16 16:39:13Z pjkersha $'
+
+from datetime import datetime
+import logging
+import socket
+from cStringIO import StringIO
+
+from OpenSSL import SSL
+
+log = logging.getLogger(__name__)
+
+
+class SSLSocket(object):
+    """SSL Socket class wraps pyOpenSSL's SSL.Connection class implementing
+    the makefile method so that it is compatible with the standard socket
+    interface and usable with httplib.
+
+    @cvar default_buf_size: default buffer size for recv operations in the
+    makefile method
+    @type default_buf_size: int
+    """
+    default_buf_size = 8192
+
+    def __init__(self, ctx, sock=None):
+        """Create SSL socket object
+
+        @param ctx: SSL context
+        @type ctx: OpenSSL.SSL.Context
+        @param sock: underlying socket object
+        @type sock: socket.socket
+        """
+        if sock is not None:
+            self.socket = sock
+        else:
+            self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+        self.__ssl_conn = SSL.Connection(ctx, self.socket)
+        self.buf_size = self.__class__.default_buf_size
+
+    def __del__(self):
+        """Close underlying socket when this object goes out of scope
+        """
+        self.close()
+
+    @property
+    def buf_size(self):
+        """Buffer size for makefile method recv() operations"""
+        return self.__buf_size
+
+    @buf_size.setter
+    def buf_size(self, value):
+        """Buffer size for makefile method recv() operations"""
+        if not isinstance(value, (int, long)):
+            raise TypeError('Expecting int or long type for "buf_size"; '
+                            'got %r instead' % type(value))
+        self.__buf_size = value
+
+    def close(self):
+        """Shutdown the SSL connection and call the close method of the
+        underlying socket"""
+        try:
+            self.__ssl_conn.shutdown()
+        except SSL.Error, e:
+            # Make errors on shutdown non-fatal
+            log.warning('Connection shutdown failed: %r', e)
+
+        self.__ssl_conn.close()
+
+    def set_shutdown(self, mode):
+        """Set the shutdown state of the Connection.
+        @param mode: bit vector of either or both of SENT_SHUTDOWN and
+        RECEIVED_SHUTDOWN
+        """
+        self.__ssl_conn.set_shutdown(mode)
+
+    def get_shutdown(self):
+        """Get the shutdown state of the Connection.
+        @return: bit vector of either or both of SENT_SHUTDOWN and
+        RECEIVED_SHUTDOWN
+        """
+        return self.__ssl_conn.get_shutdown()
+
+    def bind(self, addr):
+        """bind to the given address - calls method of the underlying socket
+        @param addr: address/port number tuple
+        @type addr: tuple"""
+        self.__ssl_conn.bind(addr)
+
+    def listen(self, backlog):
+        """Listen for connections made to the socket.
+
+        @param backlog: specifies the maximum number of queued connections and
+        should be at least 1; the maximum value is system-dependent (usually 5).
+        @param backlog: int
+        """
+        self.__ssl_conn.listen(backlog)
+
+    def set_accept_state(self):
+        """Set the connection to work in server mode. The handshake will be
+        handled automatically by read/write"""
+        self.__ssl_conn.set_accept_state()
+
+    def accept(self):
+        """Accept an SSL connection.
+
+        @return: pair (ssl, addr) where ssl is a new SSL connection object and
+        addr is the address bound to the other end of the SSL connection.
+        @rtype: tuple
+        """
+        return self.__ssl_conn.accept()
+
+    def set_connect_state(self):
+        """Set the connection to work in client mode. The handshake will be
+        handled automatically by read/write"""
+        self.__ssl_conn.set_connect_state()
+
+    def connect(self, addr):
+        """Call the connect method of the underlying socket and set up SSL on
+        the socket, using the Context object supplied to this Connection object
+        at creation.
+
+        @param addr: address/port number pair
+        @type addr: tuple
+        """
+        self.__ssl_conn.connect(addr)
+
+    def shutdown(self, how):
+        """Send the shutdown message to the Connection.
+
+        @param how: for socket.socket this flag determines whether read, write
+        or both type operations are supported.  OpenSSL.SSL.Connection doesn't
+        support this so this parameter is IGNORED
+        @return: true if the shutdown message exchange is completed and false
+        otherwise (in which case you call recv() or send() when the connection
+        becomes readable/writeable.
+        @rtype: bool
+        """
+        return self.__ssl_conn.shutdown()
+
+    def renegotiate(self):
+        """Renegotiate this connection's SSL parameters."""
+        return self.__ssl_conn.renegotiate()
+
+    def pending(self):
+        """@return: numbers of bytes that can be safely read from the SSL
+        buffer.
+        @rtype: int
+        """
+        return self.__ssl_conn.pending()
+
+    def send(self, data, *flags_arg):
+        """Send data to the socket. Nb. The optional flags argument is ignored.
+        - retained for compatibility with socket.socket interface
+
+        @param data: data to send down the socket
+        @type data: string
+        """
+        return self.__ssl_conn.send(data)
+
+    def sendall(self, data):
+        self.__ssl_conn.sendall(data)
+
+    def recv(self, size=default_buf_size):
+        """Receive data from the Connection.
+
+        @param size: The maximum amount of data to be received at once
+        @type size: int
+        @return: data received.
+        @rtype: string
+        """
+        return self.__ssl_conn.recv(size)
+
+    def setblocking(self, mode):
+        """Set this connection's underlying socket blocking _mode_.
+
+        @param mode: blocking mode
+        @type mode: int
+        """
+        self.__ssl_conn.setblocking(mode)
+
+    def fileno(self):
+        """
+        @return: file descriptor number for the underlying socket
+        @rtype: int
+        """
+        return self.__ssl_conn.fileno()
+
+    def getsockopt(self, *args):
+        """See socket.socket.getsockopt
+        """
+        return self.__ssl_conn.getsockopt(*args)
+
+    def setsockopt(self, *args):
+        """See socket.socket.setsockopt
+
+        @return: value of the given socket option
+        @rtype: int/string
+        """
+        return self.__ssl_conn.setsockopt(*args)
+
+    def state_string(self):
+        """Return the SSL state of this connection."""
+        return self.__ssl_conn.state_string()
+
+    def makefile(self, *args):
+        """Specific to Python socket API and required by httplib: convert
+        response into a file-like object.  This implementation reads using recv
+        and copies the output into a StringIO buffer to simulate a file object
+        for consumption by httplib
+
+        Nb. Ignoring optional file open mode (StringIO is generic and will
+        open for read and write unless a string is passed to the constructor)
+        and buffer size - httplib set a zero buffer size which results in recv
+        reading nothing
+
+        @return: file object for data returned from socket
+        @rtype: cStringIO.StringO
+        """
+        # Optimisation
+        _buf_size = self.buf_size
+
+        i=0
+        stream = StringIO()
+        startTime = datetime.utcnow()
+        try:
+            dat = self.__ssl_conn.recv(_buf_size)
+            while dat:
+                i+=1
+                stream.write(dat)
+                dat = self.__ssl_conn.recv(_buf_size)
+
+        except (SSL.ZeroReturnError, SSL.SysCallError):
+            # Connection is closed - assuming here that all is well and full
+            # response has been received.  httplib will catch an error in
+            # incomplete content since it checks the content-length header
+            # against the actual length of data received
+            pass
+
+        if log.getEffectiveLevel() <= logging.DEBUG:
+            log.debug("Socket.makefile %d recv calls completed in %s", i,
+                      datetime.utcnow() - startTime)
+
+        # Make sure to rewind the buffer otherwise consumers of the content will
+        # read from the end of the buffer
+        stream.seek(0)
+
+        return stream
+
+    def getsockname(self):
+        """
+        @return: the socket's own address
+        @rtype:
+        """
+        return self.__ssl_conn.getsockname()
+
+    def getpeername(self):
+        """
+        @return: remote address to which the socket is connected
+        """
+        return self.__ssl_conn.getpeername()
+
+    def get_context(self):
+        '''Retrieve the Context object associated with this Connection. '''
+        return self.__ssl_conn.get_context()
+
+    def get_peer_certificate(self):
+        '''Retrieve the other side's certificate (if any)  '''
+        return self.__ssl_conn.get_peer_certificate()

+ 11 - 0
ndg/httpsclient/test/__init__.py

@@ -0,0 +1,11 @@
+"""unit tests package for urllib2pyopenssl
+
+PyOpenSSL utility to make a httplib-like interface suitable for use with 
+urllib2
+"""
+__author__ = "P J Kershaw (STFC)"
+__date__ = "05/01/12"
+__copyright__ = "(C) 2012 Science and Technology Facilities Council"
+__license__ = "BSD - see LICENSE file in top-level directory"
+__contact__ = "Philip.Kershaw@stfc.ac.uk"
+__revision__ = '$Id$'

+ 14 - 0
ndg/httpsclient/test/pki/ca/d573507a.0

@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICITCCAYqgAwIBAgIBADANBgkqhkiG9w0BAQQFADAzMQwwCgYDVQQKEwNOREcx
+ETAPBgNVBAsTCFNlY3VyaXR5MRAwDgYDVQQDEwdUZXN0IENBMB4XDTA5MTIwOTE0
+MjgyNVoXDTE0MTIwODE0MjgyNVowMzEMMAoGA1UEChMDTkRHMREwDwYDVQQLEwhT
+ZWN1cml0eTEQMA4GA1UEAxMHVGVzdCBDQTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+gYkCgYEAlTKr9U+5qatwtMXVLPJzpGOTbWJTl1to81v7h6K8twu/l27zwfkerneh
+bvN04P7WyAdcY451N7B2L8WNdyR7p75EpggEnIOXBqjAwvBFiedEWPM3m9YDv9sk
+Mlz3NjnvKHzsLUaj0uSd29LwuFBEMAh5sOWDwrUhynO9zEOyc6cCAwEAAaNFMEMw
+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUJ1Sm8y9h3jmb7I5cwivGK8QWYMsw
+EQYJYIZIAYb4QgEBBAQDAgAHMA0GCSqGSIb3DQEBBAUAA4GBADFRnM0jO41wdNpo
+itXbPv4VZOm70xOS3LoaDlw+6/cSUMu0lwz4YKY/5NgJLy0c3amQV/Hv/fJlBTZX
+PS2lpF5ORUa4y8HN/Pxh6fBpJY76GW4JovXYS1yl1FJowHlBOuP1yJEZAwZt/h7E
+MS2chcBR+pCeDEC8v3tG9ZBJwWVF
+-----END CERTIFICATE-----

+ 61 - 0
ndg/httpsclient/test/pki/localhost.crt

@@ -0,0 +1,61 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 40 (0x28)
+        Signature Algorithm: md5WithRSAEncryption
+        Issuer: O=NDG, OU=Security, CN=Test CA
+        Validity
+            Not Before: Jul  6 11:32:04 2011 GMT
+            Not After : Jul  5 11:32:04 2012 GMT
+        Subject: O=NDG, OU=Security, CN=localhost
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+            RSA Public Key: (2048 bit)
+                Modulus (2048 bit):
+                    00:c0:e0:94:c2:e9:c0:df:96:36:ba:4d:0e:3f:bc:
+                    41:51:7b:4f:fe:d8:82:47:52:f8:36:57:35:15:3b:
+                    83:77:ba:84:aa:a3:48:f0:03:0b:5a:b7:31:40:8d:
+                    3f:87:05:9e:06:c6:72:1f:ca:7d:ed:73:3c:d0:76:
+                    4d:3b:32:89:e7:1f:5b:84:27:bf:8b:72:09:2a:d4:
+                    40:8d:2c:c4:c3:23:68:6c:f4:62:55:a8:e0:2e:8a:
+                    c8:b4:5f:bf:e1:18:d7:6a:a6:1a:90:6d:e6:83:17:
+                    10:3a:95:b5:da:0d:44:3f:df:fb:6c:c5:9e:bf:1d:
+                    04:5f:1c:16:ce:9e:f7:9d:81:f8:fe:ca:43:82:5f:
+                    6b:c8:70:17:f8:e8:f3:74:d9:fb:ac:14:af:99:8f:
+                    fd:f3:ad:e2:7e:8d:f3:bd:89:15:f0:39:ea:51:7c:
+                    29:81:8c:bc:ba:63:6b:69:e0:c1:46:2f:27:93:83:
+                    85:be:8b:bb:aa:b8:76:c3:ec:8a:f6:50:e1:c3:90:
+                    9e:47:1d:19:68:40:62:59:13:8b:eb:e8:89:64:20:
+                    ec:a3:b7:e7:b2:8f:98:f2:64:b4:aa:6e:d0:f1:73:
+                    fc:ea:ed:19:19:67:98:11:f5:95:ca:76:0b:c7:43:
+                    35:3c:53:23:b5:67:b0:b5:26:59:d1:c5:3e:4a:d1:
+                    70:dd
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            Netscape Cert Type: 
+                SSL Client, SSL Server, S/MIME, Object Signing
+    Signature Algorithm: md5WithRSAEncryption
+        5a:2d:a4:5d:b2:b6:37:60:ed:16:62:88:a7:26:b7:d6:10:b4:
+        d0:f1:25:08:5f:b3:54:34:14:1b:3e:4e:b7:17:a5:6e:54:42:
+        a0:99:0b:41:ad:dc:e9:59:0f:c1:19:db:54:ba:a8:c8:09:44:
+        43:37:62:f9:59:7f:6a:e6:ff:db:4c:7c:68:d5:e8:0d:2f:58:
+        a9:64:a6:57:82:45:d1:41:a9:38:cb:29:70:13:eb:ac:1f:1a:
+        46:b6:b4:bb:a0:ed:0d:12:7c:3e:10:21:a3:62:1d:e2:ec:9c:
+        3a:a0:ca:ce:d2:ed:4a:61:14:19:c2:b5:bd:b6:4d:29:cd:3d:
+        8d:18
+-----BEGIN CERTIFICATE-----
+MIICdzCCAeCgAwIBAgIBKDANBgkqhkiG9w0BAQQFADAzMQwwCgYDVQQKEwNOREcx
+ETAPBgNVBAsTCFNlY3VyaXR5MRAwDgYDVQQDEwdUZXN0IENBMB4XDTExMDcwNjEx
+MzIwNFoXDTEyMDcwNTExMzIwNFowNTEMMAoGA1UEChMDTkRHMREwDwYDVQQLEwhT
+ZWN1cml0eTESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAwOCUwunA35Y2uk0OP7xBUXtP/tiCR1L4Nlc1FTuDd7qEqqNI
+8AMLWrcxQI0/hwWeBsZyH8p97XM80HZNOzKJ5x9bhCe/i3IJKtRAjSzEwyNobPRi
+VajgLorItF+/4RjXaqYakG3mgxcQOpW12g1EP9/7bMWevx0EXxwWzp73nYH4/spD
+gl9ryHAX+OjzdNn7rBSvmY/9863ifo3zvYkV8DnqUXwpgYy8umNraeDBRi8nk4OF
+vou7qrh2w+yK9lDhw5CeRx0ZaEBiWROL6+iJZCDso7fnso+Y8mS0qm7Q8XP86u0Z
+GWeYEfWVynYLx0M1PFMjtWewtSZZ0cU+StFw3QIDAQABoxUwEzARBglghkgBhvhC
+AQEEBAMCBPAwDQYJKoZIhvcNAQEEBQADgYEAWi2kXbK2N2DtFmKIpya31hC00PEl
+CF+zVDQUGz5OtxelblRCoJkLQa3c6VkPwRnbVLqoyAlEQzdi+Vl/aub/20x8aNXo
+DS9YqWSmV4JF0UGpOMspcBPrrB8aRra0u6DtDRJ8PhAho2Id4uycOqDKztLtSmEU
+GcK1vbZNKc09jRg=
+-----END CERTIFICATE-----

+ 27 - 0
ndg/httpsclient/test/pki/localhost.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAwOCUwunA35Y2uk0OP7xBUXtP/tiCR1L4Nlc1FTuDd7qEqqNI
+8AMLWrcxQI0/hwWeBsZyH8p97XM80HZNOzKJ5x9bhCe/i3IJKtRAjSzEwyNobPRi
+VajgLorItF+/4RjXaqYakG3mgxcQOpW12g1EP9/7bMWevx0EXxwWzp73nYH4/spD
+gl9ryHAX+OjzdNn7rBSvmY/9863ifo3zvYkV8DnqUXwpgYy8umNraeDBRi8nk4OF
+vou7qrh2w+yK9lDhw5CeRx0ZaEBiWROL6+iJZCDso7fnso+Y8mS0qm7Q8XP86u0Z
+GWeYEfWVynYLx0M1PFMjtWewtSZZ0cU+StFw3QIDAQABAoIBABL7h2iPfO4EaKp5
+PvfC3WLZkgvw3xGJ9ufSOWU3kD7OWpQ4scr9Ybax5OUHgcMWKHFeQokicrZV6xxR
+KFS1KqFWkrk0+EMPHBvc++VpDIxwBa0DeFIZ5sZt3kbyTX4n5buXUXH5fwBT2sMJ
+TPaC676lmqcf4/nHL1D1AMGhH5merxGFGBapMLaMYQDP0dFF6av1q11IAtT8CWjt
+dTJhKsRwoPkkjjDLvVdGXuud+CE8fFUJwS73OacXxqG2CHAjUkDM89QQtHsb/Wfo
+crF6JfH2vErBoTw4LVyJ8qeUGmkvRoInmJd+M/MT0fMtOLJe8Sj8h3LAvfGxtke2
+a6j/sLkCgYEA+IAvE8reQ+3yroIGKees1S2+b0qqi0vKYjqD+qdpawc+GwbuMtHc
+4fOo9VqjknLJwgpZDu6H5+8s+XJLGJbz8H3N9dgVhGsBd4KwNeVB//oKC5aNYQc7
+oDYbpHqGFA1f+bQa1qPd2AF00jXqU+Lk1/l3z87hK3sLcrAWG5Dms+MCgYEAxrKs
+RMMfYnI4hy9Aqy9cL2OcsB3dRGrPo/LMhy7L4Fkr25qq4mQKa+v4DVulf4HZOpw2
+rghHbyVNL/GiUGQiqYIe31L8w8BXviaRReoy+47lBLaQ2NVX4Lv+y7N9p6U4xxEc
+m+CCtSN74YvYzRpIKBRaPSWf+UNTqRRSDEjn5D8CgYEAu9YYv7s+2tYH9MSv6AkI
+6XLUWcd0tiop5qoYjOTymEY3ObK5Zoyfi+PkOOG0dsRxoUy1GMZQ0I5Hzp4ICaRA
+6+4MOuKFETzZNP8CNxr+EoFsCmpYn5kaBvYfWuqKYqnhfBwZlVj0HYysQyEh6Rq+
+pEobuGbGaVluw9g6Pcf/usUCgYAv6k5YlqUu3FR9ZQu3PEiCtQbIAaumIAvKgXaI
+8uP/SgGlh3rF9VH+DH1Y20zhnrJ/y8Pz29M+HkSq1x5JPJyPO+2t9Rk3K179X9eQ
+gJWizCa2KEBtyaTTcQJUpQgcMV+rwZigjld2zwPEtDCn5TqZT68jJ7uYJIA8OcY/
+aCVjiQKBgFSIhub8r68DnC/UQUh2ktTTB+0VbqMxPWVohs9PJPE2fInUcei1KPHL
+MceNKch+bfbJr64Ru3JKgqFQKsmhVGzLQXX49qDRHT30yGKpcGw2b6H4DUB2FR9L
+6tVZKWeKspmaMGrdDs5/XuK4soE60oPO0nUiB7Sh70shN9/lWAfw
+-----END RSA PRIVATE KEY-----

+ 2 - 0
ndg/httpsclient/test/scripts/openssl_https_server.sh

@@ -0,0 +1,2 @@
+#!/bin/sh
+openssl s_server -www -cert pki/localhost.crt -key pki/localhost.key -accept 4443

+ 32 - 0
ndg/httpsclient/test/test.py

@@ -0,0 +1,32 @@
+'''
+Created on Jan 5, 2012
+
+@author: philipkershaw
+'''
+import unittest
+from urllib2pyopenssl.urllib2_build_opener import urllib2_build_opener
+from urllib2pyopenssl.https import HTTPSConnection
+
+
+class Urllib2PyOpenSslTestCase(unittest.TestCase):
+    """Unit tests for PyOpenSSL HTTPS interface for urllib2"""
+    TEST_URI = 'https://localhost:4443'
+    
+    def test01_httpsconnection(self):
+        conn = HTTPSConnection('localhost', port=4443)
+        conn.connect()
+        
+        conn.close()
+        
+    def test02_urllib2_build_opener(self):     
+        opener = urllib2_build_opener()
+        self.assert_(opener)
+
+    def test03_open(self):
+        opener = urllib2_build_opener()
+        res = opener.open(self.__class__.TEST_URI)
+        self.assert_(res)
+        print("res = %s" % res.read())
+
+if __name__ == "__main__":
+    unittest.main()

+ 62 - 0
ndg/httpsclient/urllib2_build_opener.py

@@ -0,0 +1,62 @@
+"""PyOpenSSL utilities including HTTPSSocket class which wraps PyOpenSSL
+SSL connection into a httplib-like interface suitable for use with urllib2
+"""
+__author__ = "P J Kershaw"
+__date__ = "21/12/10"
+__copyright__ = "(C) 2011 Science and Technology Facilities Council"
+__license__ = "BSD - see LICENSE file in top-level directory"
+__contact__ = "Philip.Kershaw@stfc.ac.uk"
+__revision__ = '$Id: pyopenssl.py 7929 2011-08-16 16:39:13Z pjkersha $'
+
+import logging
+from urllib2 import (OpenerDirector, ProxyHandler, UnknownHandler, HTTPHandler,
+                     HTTPDefaultErrorHandler, HTTPRedirectHandler,
+                     FTPHandler, FileHandler, HTTPErrorProcessor)
+
+from urllib2pyopenssl.https import HTTPSContextHandler
+
+log = logging.getLogger(__name__)
+
+# Copied from urllib2 with modifications for ssl
+def urllib2_build_opener(ssl_context=None, *handlers):
+    """Create an opener object from a list of handlers.
+
+    The opener will use several default handlers, including support
+    for HTTP and FTP.
+
+    If any of the handlers passed as arguments are subclasses of the
+    default handlers, the default handlers will not be used.
+    """
+    import types
+    def isclass(obj):
+        return isinstance(obj, types.ClassType) or hasattr(obj, "__bases__")
+
+    opener = OpenerDirector()
+    default_classes = [ProxyHandler, UnknownHandler, HTTPHandler,
+                       HTTPDefaultErrorHandler, HTTPRedirectHandler,
+                       FTPHandler, FileHandler, HTTPErrorProcessor]
+    check_classes = list(default_classes)
+    check_classes.append(HTTPSContextHandler)
+    skip = []
+    for klass in check_classes:
+        for check in handlers:
+            if isclass(check):
+                if issubclass(check, klass):
+                    skip.append(klass)
+            elif isinstance(check, klass):
+                skip.append(klass)
+
+    for klass in default_classes:
+        if klass not in skip:
+            opener.add_handler(klass())
+
+    # Add the HTTPS handler with ssl_context
+    if HTTPSContextHandler not in skip:
+        opener.add_handler(HTTPSContextHandler(ssl_context))
+
+    for h in handlers:
+        if isclass(h):
+            h = h()
+        opener.add_handler(h)
+
+    return opener

+ 22 - 0
setup.py

@@ -0,0 +1,22 @@
+try:
+    from setuptools import setup, find_packages
+except ImportError:
+    from ez_setup import use_setuptools
+    use_setuptools()
+    from setuptools import setup, find_packages
+
+setup(
+    name='urllib2pyopenssl',
+    version="0.1.0",
+    description='Provides HTTPS with urllib2 using PyOpenSSL',
+    author='Richard Wilkinson',
+    long_description=open('README').read(),
+    license='BSD - See LICENCE file for details',
+    namespace_packages=['ndg'],
+    packages=find_packages(),
+    entry_points = {
+        'console_scripts': [
+#            'urllib2pyopenssl_get = urllib2pyopenssl.get:main'
+            ]
+        }
+)