diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a5d5de7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/ndg_httpsclient.egg-info/ +/dist/ +*.pyc +.eggs/ +build/ diff --git a/.pydevproject b/.pydevproject index 6a7714c..85f561e 100644 --- a/.pydevproject +++ b/.pydevproject @@ -1,10 +1,14 @@ - - - -ndg-httpsclient-py2.7 -python 2.7 - -/ndg_httpsclient - + + + Default + + python 3.6 + + + + /ndg_httpsclient + + + diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f0f7398 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: python +python: + - 2.7 + - 3.6 + - 3.7 + - 3.8 + - 3.9 + - nightly +script: + - python setup.py install +jobs: + allow_failures: + - python: nightly diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..ca23198 --- /dev/null +++ b/INSTALL @@ -0,0 +1,8 @@ +Installation notes +================== + +For install on Mac Yosemite, special header file include paths are needed for +PyOpenSSL dependency source builds. Install XCode and add explicit include path +for ffi package and generic include: + +export CPATH=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/usr/include/ffi:/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/usr/include/ \ No newline at end of file diff --git a/README.md b/README.md index 7e1d7cc..7193452 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,139 @@ -A HTTPS client implementation for httplib and urllib2 based on -PyOpenSSL. PyOpenSSL provides a more fully featured SSL implementation over the -default provided with Python and importantly enables full verification of the -SSL peer. +A HTTPS client implementation for + * ``httplib`` (Python 2), ``http.client`` (Python 3) and + * ``urllib2`` (Python 2) and ``urllib`` (Python 3) + +... based on PyOpenSSL. PyOpenSSL provides a more fully featured SSL implementation +over the default provided with Python and importantly enables full verification +of the SSL peer using ``pyasn1``. + +Releases +======== +0.5.1 +----- + * Clean up handling for description file - pull in content from this file into setup() + * Allows the nightly build to fail + * Add Trove version classifiers to make it explicit what is supported + * Add python_requires to help pip + * Drop support for EOL Python 2.6 and 3.3 + +Thanks to @hugovk for contributions + +0.5.0 +----- + * Fix to Subject Alternative Name handling to allow for certificates with + more than 64 names (max now 1024). Thanks to Matt Pegler + * Fix to subjectAltName string to use byte type for correct matching + * Updated SSL Context objects to default to TLS 1.2 + +0.4.4 +----- + * Updated test certificates + +0.4.3 +----- + * Fix to ``ndg`` namespace package warning issue (https://github.com/cedadev/ndg_httpsclient/issues/3). + ``__init__.py`` file now included in ``ndg`` directory so that there are no longer warnings with imports + when using Python 2.x. Thanks to Max Mauntner for fix. + * Minor fix for installation: set minimum release for ``pyasn1`` to avoid conflicts with Ubuntu + install - see https://github.com/cedadev/ndg_httpsclient/issues/5 and + https://github.com/cedadev/ndg_httpsclient/pull/10. ``pyasn1`` also becomes mandatory rather + than optional package for install. - It required by ``cryptography`` anyway which is a + dependency for ``pyOpenSSL`` from version 0.14. + +0.4.2 +----- + * Fix to bug in ``ndg.httpsclient.utils.open_url`` - duplicate open call. + Nb. This bug and the fix DO NOT affect the ``httplib``and ``urllib2`` + interfaces that this package provides. + +0.4.1 +----- + * Added explicit ref to Python 3 in classifier text for Python 3 checking tools. + * Moved LICENSE file into package + +0.4.0 +----- + * Made dual compatible with Python 2 / 3. + +0.3.3 +----- + * Fix to add in AnotherName for ``subjectAltNames`` field - added for support for CACert issued + certs (thanks to Gu1). + * Fix to HTTP Basic Auth option for ``ndg.httpsclient.utils.main`` + * Fix to ``ServerSSLCertVerification`` so that it can pass a function-based callback instead of using ``__call__``. In newer versions of OpenSSL (>= 0.14) the latter failed because of a request for ``__name__`` attribute. + +0.3.2 +----- + * Fix to SubjectAltNames support check - should only be enabled if pyasn1 is + installed. + * Fix to open_url: HTTP Request object was being created inside if headers is + None block - now corrected to create regardless. + * Added http basic auth support to script. (Thanks to Willem van Engen) + +0.3.1 +----- + * extended utils functions to support keyword for passing additional ``urllib2`` + handlers. + +0.3.0 +----- + * Added ``ndg.httpsclient.utils.fetch_stream_from_url`` function and added + parameter for data to post in ``open_url`` and ``fetch_*`` methods. + * fix to ndg.httpsclient.utils module _should_use_proxy and open_url functions + +0.2.0 +----- + * added support for SSL verification with subjectAltNames using pyasn1 + * fixed minor bug - SSL cert DN prefix matching + +0.1.0 +----- +Initial release + +Prerequisites +============= +This has been developed and tested for Python 2.7 with pyOpenSSL 0.13 +and 0.14. Version 0.4.0 tested with ``pyOpenSSL`` 0.15.1 and Python 2.7 and +3.4. +``pyasn1`` is required for correct SSL verification with ``subjectAltNames``. + +Installation +============ +Installation can be performed using ``easy_install`` or ``pip``. + +Running ndg_httpclient +====================== +A simple script for fetching data using HTTP or HTTPS GET from a specified URL. + +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. + +``-p FILE, --post-data-file=FILE`` + POST data file + +``-f FILE, --fetch=FILE`` + Output file + +``-n, --no-verify-peer`` + Skip verification of peer certificate. + diff --git a/ndg/__init__.py b/ndg/__init__.py index 3b01e15..69e3be5 100644 --- a/ndg/__init__.py +++ b/ndg/__init__.py @@ -1,19 +1 @@ -"""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__) \ No newline at end of file +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/LICENSE b/ndg/httpsclient/LICENSE similarity index 100% rename from LICENSE rename to ndg/httpsclient/LICENSE diff --git a/ndg/httpsclient/https.py b/ndg/httpsclient/https.py index f7a9bdf..aaa0bb3 100644 --- a/ndg/httpsclient/https.py +++ b/ndg/httpsclient/https.py @@ -12,9 +12,18 @@ __revision__ = '$Id$' import logging import socket -from httplib import HTTPS_PORT -from httplib import HTTPConnection -from urllib2 import AbstractHTTPHandler +import sys + +if sys.version_info[0] > 2: + from http.client import HTTPS_PORT + from http.client import HTTPConnection + + from urllib.request import AbstractHTTPHandler +else: + from httplib import HTTPS_PORT + from httplib import HTTPConnection + + from urllib2 import AbstractHTTPHandler from OpenSSL import SSL @@ -39,7 +48,7 @@ class HTTPSConnection(HTTPConnection): @type default_ssl_method: int """ default_port = HTTPS_PORT - default_ssl_method = SSL.SSLv23_METHOD + default_ssl_method = SSL.TLSv1_2_METHOD def __init__(self, host, port=None, strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, ssl_context=None): @@ -69,7 +78,7 @@ def connect(self): sock = socket.create_connection((self.host, self.port), self.timeout) - # Tunnel if using a proxy - ONLY available for Python 2.6.2 and above + # Tunnel if using a proxy - ONLY available for Python 2.6.2 and above if getattr(self, '_tunnel_host', None): self.sock = sock self._tunnel() @@ -81,7 +90,8 @@ def connect(self): def close(self): """Close socket and shut down SSL connection""" - self.sock.close() + if hasattr(self.sock, "close"): + self.sock.close() class HTTPSContextHandler(AbstractHTTPHandler): @@ -90,6 +100,8 @@ class HTTPSContextHandler(AbstractHTTPHandler): ''' https_request = AbstractHTTPHandler.do_request_ + SSL_METHOD = SSL.TLSv1_2_METHOD + def __init__(self, ssl_context, debuglevel=0): """ @param ssl_context:SSL context @@ -106,7 +118,7 @@ def __init__(self, ssl_context, debuglevel=0): ssl_context) self.ssl_context = ssl_context else: - self.ssl_context = SSL.Context(SSL.SSLv23_METHOD) + self.ssl_context = SSL.Context(self.__class__.SSL_METHOD) def https_open(self, req): """Opens HTTPS request diff --git a/ndg/httpsclient/ssl_context_util.py b/ndg/httpsclient/ssl_context_util.py index 5e11501..810e3c8 100644 --- a/ndg/httpsclient/ssl_context_util.py +++ b/ndg/httpsclient/ssl_context_util.py @@ -8,7 +8,12 @@ __license__ = "BSD - see LICENSE file in top-level directory" __contact__ = "Philip.Kershaw@stfc.ac.uk" __revision__ = '$Id$' -import urlparse +import sys + +if sys.version_info[0] > 2: + import urllib.parse as urlparse_ +else: + import urlparse as urlparse_ from OpenSSL import SSL @@ -36,7 +41,7 @@ def make_ssl_context_from_config(ssl_config=False, url=None): def make_ssl_context(key_file=None, cert_file=None, pem_file=None, ca_dir=None, - verify_peer=False, url=None, method=SSL.TLSv1_METHOD, + verify_peer=False, url=None, method=SSL.TLSv1_2_METHOD, key_file_passphrase=None): """ Creates SSL context containing certificate and key file locations. @@ -59,6 +64,8 @@ def make_ssl_context(key_file=None, cert_file=None, pem_file=None, ca_dir=None, if pem_file or ca_dir: ssl_context.load_verify_locations(pem_file, ca_dir) + else: + ssl_context.set_default_verify_paths() # Use OS CA bundle def _callback(conn, x509, errnum, errdepth, preverify_ok): """Default certification verification callback. @@ -85,7 +92,7 @@ def set_peer_verification_for_url_hostname(ssl_context, url, '''Convenience routine to set peer verification callback based on ServerSSLCertVerification class''' if not if_verify_enabled or (ssl_context.get_verify_mode() & SSL.VERIFY_PEER): - urlObj = urlparse.urlparse(url) + urlObj = urlparse_.urlparse(url) hostname = urlObj.hostname server_ssl_cert_verif = ServerSSLCertVerification(hostname=hostname) verify_callback_ = server_ssl_cert_verif.get_verify_server_cert_func() diff --git a/ndg/httpsclient/ssl_peer_verification.py b/ndg/httpsclient/ssl_peer_verification.py index 57f65e2..b02b6f8 100644 --- a/ndg/httpsclient/ssl_peer_verification.py +++ b/ndg/httpsclient/ssl_peer_verification.py @@ -14,7 +14,8 @@ from ndg.httpsclient.subj_alt_name import SubjectAltName from pyasn1.codec.der import decoder as der_decoder SUBJ_ALT_NAME_SUPPORT = True -except ImportError, e: + +except ImportError as e: SUBJ_ALT_NAME_SUPPORT = False SUBJ_ALT_NAME_SUPPORT_MSG = ( 'SubjectAltName support is disabled - check pyasn1 package ' @@ -39,8 +40,9 @@ class ServerSSLCertVerification(object): 'domainComponent': 'DC', 'userid': 'UID' } - SUBJ_ALT_NAME_EXT_NAME = 'subjectAltName' - PARSER_RE_STR = '/(%s)=' % '|'.join(DN_LUT.keys() + DN_LUT.values()) + SUBJ_ALT_NAME_EXT_NAME = b'subjectAltName' + PARSER_RE_STR = '/(%s)=' % '|'.join(list(DN_LUT.keys()) + \ + list(DN_LUT.values())) PARSER_RE = re.compile(PARSER_RE_STR) __slots__ = ('__hostname', '__certDN', '__subj_alt_name_match') @@ -156,12 +158,12 @@ def __call__(self, connection, peerCert, errorStatus, errorDepth, return preverifyOK def get_verify_server_cert_func(self): - def verify_server_cert(connection, peerCert, errorStatus, errorDepth, - preverifyOK): - return self.__call__(connection, peerCert, errorStatus, - errorDepth, preverifyOK) - - return verify_server_cert + def verify_server_cert(connection, peerCert, errorStatus, errorDepth, + preverifyOK): + return self.__call__(connection, peerCert, errorStatus, + errorDepth, preverifyOK) + + return verify_server_cert @classmethod def _get_subj_alt_name(cls, peer_cert): @@ -195,7 +197,7 @@ def _getCertDN(self): return self.__certDN def _setCertDN(self, val): - if isinstance(val, basestring): + if isinstance(val, str): # Allow for quoted DN certDN = val.strip('"') @@ -203,7 +205,7 @@ def _setCertDN(self, val): if len(dnFields) < 2: raise TypeError('Error parsing DN string: "%s"' % certDN) - self.__certDN = zip(dnFields[1::2], dnFields[2::2]) + self.__certDN = list(zip(dnFields[1::2], dnFields[2::2])) self.__certDN.sort() elif not isinstance(val, list): @@ -226,7 +228,7 @@ def _getHostname(self): return self.__hostname def _setHostname(self, val): - if not isinstance(val, basestring): + if not isinstance(val, str): raise TypeError("Expecting string type for hostname " "attribute") self.__hostname = val diff --git a/ndg/httpsclient/ssl_socket.py b/ndg/httpsclient/ssl_socket.py index ab7f216..7780314 100644 --- a/ndg/httpsclient/ssl_socket.py +++ b/ndg/httpsclient/ssl_socket.py @@ -12,7 +12,7 @@ from datetime import datetime import logging import socket -from cStringIO import StringIO +from io import BytesIO from OpenSSL import SSL @@ -61,22 +61,20 @@ def buf_size(self): @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"; ' + if not isinstance(value, int): + raise TypeError('Expecting int 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: -# # Make errors on shutdown non-fatal -# pass - if self._makefile_refs < 1: - self.__ssl_conn.shutdown() + try: + self.__ssl_conn.shutdown() + except (SSL.Error, SSL.SysCallError): + # Make errors on shutdown non-fatal + pass else: self._makefile_refs -= 1 @@ -236,7 +234,7 @@ def makefile(self, *args): _buf_size = self.buf_size i=0 - stream = StringIO() + stream = BytesIO() startTime = datetime.utcnow() try: dat = self.__ssl_conn.recv(_buf_size) @@ -261,17 +259,6 @@ def makefile(self, *args): stream.seek(0) return stream - -# def makefile(self, mode='r', bufsize=-1): -# -# """Make and return a file-like object that -# works with the SSL connection. Just use the code -# from the socket module.""" -# -# self._makefile_refs += 1 -# # close=True so as to decrement the reference count when done with -# # the file-like object. -# return socket._fileobject(self.socket, mode, bufsize, close=True) def getsockname(self): """ diff --git a/ndg/httpsclient/subj_alt_name.py b/ndg/httpsclient/subj_alt_name.py index 6bd8e0d..7c195aa 100644 --- a/ndg/httpsclient/subj_alt_name.py +++ b/ndg/httpsclient/subj_alt_name.py @@ -14,7 +14,7 @@ try: from pyasn1.type import univ, constraint, char, namedtype, tag -except ImportError, e: +except ImportError as e: import_error_msg = ('Error importing pyasn1, subjectAltName check for SSL ' 'peer verification will be disabled. Import error ' 'is: %s' % e) @@ -25,7 +25,7 @@ class Pyasn1ImportError(ImportError): raise Pyasn1ImportError(import_error_msg) -MAX = 64 +MAX = 1024 class DirectoryString(univ.Choice): @@ -69,7 +69,7 @@ class AttributeTypeAndValue(univ.Sequence): class RelativeDistinguishedName(univ.SetOf): - '''ASN.1 Realtive distinguished name''' + '''ASN.1 Relative distinguished name''' componentType = AttributeTypeAndValue() class RDNSequence(univ.SequenceOf): diff --git a/ndg/httpsclient/test/__init__.py b/ndg/httpsclient/test/__init__.py index 4e8196a..6d4ee88 100644 --- a/ndg/httpsclient/test/__init__.py +++ b/ndg/httpsclient/test/__init__.py @@ -16,11 +16,13 @@ class Constants(object): '''Convenience base class from which other unit tests can extend. Its sets the generic data directory path''' PORT = 4443 +# PORT = 443 PORT2 = 4444 HOSTNAME = 'localhost' +# HOSTNAME = 'files.pythonhosted.org' TEST_URI = 'https://%s:%d' % (HOSTNAME, PORT) TEST_URI2 = 'https://%s:%d' % (HOSTNAME, PORT2) - + UNITTEST_DIR = os.path.dirname(os.path.abspath(__file__)) CACERT_DIR = os.path.join(UNITTEST_DIR, 'pki', 'ca') SSL_CERT_FILENAME = 'localhost.crt' diff --git a/ndg/httpsclient/test/docker/Dockerfile b/ndg/httpsclient/test/docker/Dockerfile new file mode 100644 index 0000000..9ba08a2 --- /dev/null +++ b/ndg/httpsclient/test/docker/Dockerfile @@ -0,0 +1,6 @@ +FROM ubuntu:14.04 +RUN apt-get -yqq update && apt-get -yqq install python-pip python-dev libffi-dev libxmlsec1-openssl libssl-dev +WORKDIR /app/ +ADD . /app/ +RUN pip install -e . +CMD ["python", "setup.py", "test"] diff --git a/ndg/httpsclient/test/pki/ca/7e15277f.0 b/ndg/httpsclient/test/pki/ca/7e15277f.0 new file mode 100644 index 0000000..9a4ab56 --- /dev/null +++ b/ndg/httpsclient/test/pki/ca/7e15277f.0 @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFLjCCAxagAwIBAgIBATANBgkqhkiG9w0BAQsFADA3MRQwEgYDVQQDDAtOREcg +VGVzdCBDQTEMMAoGA1UECgwDTkRHMREwDwYDVQQLDAhTZWN1cml0eTAeFw0yMDAy +MjcxMDM4MDhaFw0yNTAyMjUxMDM4MDhaMDcxFDASBgNVBAMMC05ERyBUZXN0IENB +MQwwCgYDVQQKDANOREcxETAPBgNVBAsMCFNlY3VyaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAoLeKc7StJLjyWuPBc+0pg8y5QJ4VQFzOc1jXbhqo +GugSvV/BpTqAPPCbd5hkET8nMIHkZ6EkXWNlxvYvrqKOKnrTvw3WUjEmsdoHkaXA +hq+v1IHbPbCQbB+RCCObuxlZ9Y72llUrODATb7dc9Odc4LCLPR/9+FjncB1d5b4x +Qt74VolID0dCd5dmMqy+lUYjsyLSycItmWcamdMEEPCop6AXlkAq3Q3eQDxaHtly +IOmyrYv2EWnFH5FRevhJdTnb48FdUTDya3T+BWpRqzF/Rh3NmI05JTbmSutCOs8m +SgSyi0JTNgaOaLlbtYVBLEkETUHPWABWwrXghIMIHaw3BZas2a9nlKE6zQSBxjsC +M/jJopgCDjLIxCw/SZ/Koovye/yvhMKYo71YnQCIv+1pIaL7GMrxMQZzrJiUVlpY +XtCyjx+5uiD4WuKCq6/aEKUd3o6ZdmNhkG8R0uBmivwxAmuMoMssCZC2GG7dBG+a +FlAG0OW+qzez4njjVwT7ePaO/ggRm6yJRNo7s3+SlEppV/orKLH4+O7FJm2HwTIB +Z3HSObrOJtjlcjM5pzhAFC4M0ul9vnCcnaFmhzB7bH7h8X7RaqECuHIhhfWTUiEP +f+riC5C962eGMqJH3zzPaXCKFVgyYXtphRZJYzf3oU4bGn69uumPl47McLd9lseR +uLkCAwEAAaNFMEMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFIYdooV3qYYhM6aq0LTBYQWA9j1BMA0GCSqGSIb3DQEBCwUAA4IC +AQAzqykz00ztLxbAEVc8QbfxMYVa5QYp4Ljdl7Wb4IPb4WY6fDkkJ1wRNg/KWZov +CkTsiFvQSG5eHEVDLPUjW4WJ+eJI3uPeNjUTB+9i88pjbLqP1VBeiNdpTcOAu4w8 +cosK1wk6K3LNHATXZArD+8mHEFAHinuqlYwkBekIPBgMgbKr/WlaowV7fkIAUpxm +K2W7oLt3ZvWmFrsFcywLsyXmA8EC3d5svQ01YLHE7xJUcjatKSJyc+0dnMNy+ZZN +6NTPCefHwi5ShWWmkwtmvCChzePpYP/tBqkAIHLB7js2nHLZYtJYT1CqOGHdy3bL +DWHYU527Dj949K03/ZE8jkBqzQje5uAw9bqluydXXkAtMO8rMktIsEjZuMsyzD1b +Z8qdeHvwHQjnA1fvMuAO2QDTb8zGc8Mcn2/Jbj9ZD61cej75GWPIDolwSmMIA7fT +HyyD8NiOnqM1tGQZQWGTlwZidH6Z7jUASZd+J/+pVKyFmA6ecgo54/w/wTQSv/6S +ZWGp4ZYdjdpZ/coX2kbulej+Z3m0m4WB66LbvKD8zCYymRu5ULtS59QElliDKERS +U41awYtdgP/R/cz/SBoYxPvY/n1sOWT0mQp1+ctehDf+tDMirjOst71eq6n/85CR +NuP+k4yntJuzOjuxZ2MI8iuGDrqi/rFx2AhXXNCkCFDArw== +-----END CERTIFICATE----- diff --git a/ndg/httpsclient/test/pki/ca/ca.key b/ndg/httpsclient/test/pki/ca/ca.key new file mode 100644 index 0000000..4e79919 --- /dev/null +++ b/ndg/httpsclient/test/pki/ca/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +ep2Pg4lXDl4+iO5SDobGse4Ka/Pkkw== +-----END PRIVATE KEY----- diff --git a/ndg/httpsclient/test/pki/ca/d573507a.0 b/ndg/httpsclient/test/pki/ca/d573507a.0 deleted file mode 100644 index 49d7f63..0000000 --- a/ndg/httpsclient/test/pki/ca/d573507a.0 +++ /dev/null @@ -1,14 +0,0 @@ ------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----- diff --git a/ndg/httpsclient/test/pki/gen-csr.sh b/ndg/httpsclient/test/pki/gen-csr.sh new file mode 100755 index 0000000..c008668 --- /dev/null +++ b/ndg/httpsclient/test/pki/gen-csr.sh @@ -0,0 +1,3 @@ +#!/bin/bash +openssl genrsa -out localhost.key 4096 +openssl req -new -out localhost.csr -subj '/O=NDG/OU=Security/CN=localhost' -key localhost.key diff --git a/ndg/httpsclient/test/pki/localhost.crt b/ndg/httpsclient/test/pki/localhost.crt index 257a5d5..74a393a 100644 --- a/ndg/httpsclient/test/pki/localhost.crt +++ b/ndg/httpsclient/test/pki/localhost.crt @@ -1,14 +1,30 @@ -----BEGIN CERTIFICATE----- -MIICFjCCAX+gAwIBAgIBCjANBgkqhkiG9w0BAQQFADAzMQwwCgYDVQQKEwNOREcx -ETAPBgNVBAsTCFNlY3VyaXR5MRAwDgYDVQQDEwdUZXN0IENBMB4XDTEyMDIwODE2 -MTE1M1oXDTE3MDIwNjE2MTE1M1owNTERMA8GA1UECxMIU2VjdXJpdHkxEjAQBgNV -BAMTCWxvY2FsaG9zdDEMMAoGA1UEChMDTkRHMIGfMA0GCSqGSIb3DQEBAQUAA4GN -ADCBiQKBgQCdhZgzD0xusZqzdphETJPgb4QK/sdDpF8EOT/20bAuyRgGt7papJmc -6UtdgS5b9bGh6sRXx+vSKiTqq1ZFLOjnn3OQKhdrK2VU8XiD5rjuwTuNzser0uba -lTOW5/2yVab+uZ/vw4yxR64+KdyBuVopXV9STuh12Q0JSrXzdH82iQIDAQABozgw -NjAMBgNVHRMBAf8EAjAAMCYGA1UdEQQfMB2CCWxvY2FsaG9zdIIQbG9jYWxob3N0 -LmRvbWFpbjANBgkqhkiG9w0BAQQFAAOBgQBAAQCTkLfgYAjvm63KRXcE8djkYIVQ -LleHNrCad/v3zNFK0PPCjIeBSWlI/1bPhJDCpfwpvJLk86DrB97Q3IafU2ml7DkC -93bi3iaDy4jI1uskvlM516iaBQx1DCIa4gesluBAnZFvby8HX9y/A7tn5Ew2vdQJ -upkcCUswsU4MSA== +MIIFDTCCAvWgAwIBAgIBADANBgkqhkiG9w0BAQsFADA3MRQwEgYDVQQDDAtOREcg +VGVzdCBDQTEMMAoGA1UECgwDTkRHMREwDwYDVQQLDAhTZWN1cml0eTAeFw0yMDAy +MjcxMDU2NTlaFw0yMzAyMjYxMDU2NTlaMDUxDDAKBgNVBAoMA05ERzERMA8GA1UE +CwwIU2VjdXJpdHkxEjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBALAc2LCkaet7eZdOPQgSRcz2e1mJQ3qwMqT1BdMn+AG/ +9ZMInCftXmJBHpb5WKeA4hclCP+sFUfaxt2TcJ7rNsPX6rkN5JFwMAIDMzjaq8Dr +1ZYjenTvInnb6tWIK2KP9nVdJRxSCXHIdN0pvJQdyi+3LOnTHyfHKIH9lTBLf2HY +Y1tpGFDWL/ywHbh7krUkIfC3vSx6xZXBNgStIH4gjpP2DxKfuPmSw1Kae2vOKcRJ +29PwB/H1TaPr0VvQTNN+cBC2oOehXpH/QRbU8Nye7uq4z/llKD5LMZAOJe7VUU5A +zxUH7DE27824CmS1ywd5JueyednsnsTjltEayQcnvAm/OEIr6rO0aDVmZH40i/W1 ++NdiWgGxt3OrvjU71BQAlaknshv2xDx9UNsLrQ+iOpDdsYxpD84s4Za0V+qnq1J5 +QCSD7oaQLi4SG9nPLC+wpEhMCVGCHtClfL1NVSBYVycbPp1iLQP3xCiwV9058XVP +T2psI+PaPLeXp1ZNpEkRl+/UyAdDKqb3C6nE1tqNbTURhuL/ECFFHiH1k4mlGZfG +n2YL90ROqKFm6YdYE357vPrdjDbow32Cqt8wJqTE3mIaxZH+vLqKRezbU8fmo+7g +yEMePsCTWw0yXPyorioLAbFZBO5512sa09CXLGmeTaLs0215zaK81HnAgMwFWT+9 +AgMBAAGjJjAkMAwGA1UdEwEB/wQCMAAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0G +CSqGSIb3DQEBCwUAA4ICAQASq7gPmWVMjm6XzmHJ2LSIJAjGEmjWSNBf1rvbEItH +NjxzLu4VfiShgj9y53yWoNJSJGGGtoKmaNQK63x9GzTQbN60izXlW20FP32b9Idz +6y7Ta4JSqNx1Q/OW+ABsWZZ68Ob19XLr8QY5fog3Teh7Q0jzln/rxteB7OUTJ5QC +o5Nq7PDlg/R9buRZnHHb1tmBHbSbkXK9wjgSQMtbga2EQuewJ5KYHYUrdYsklg2u +wqnxtL7iND1dGcG2J3QKdC2fT1i7EdU5ZFvDxo9VOPvQmrIzzHKfyfgmOD3QhOrz +2rCcze86Vi8zlk0yk8oQNTjLndnJgSWYq0OFl7iape1ObblulILDhb80RI5N6pK2 +WIb05Kl4Sm/bNcphi5KHtQlr7HzSNojr4qgHlo5KdQ6IW/ZOIGHgq6OjcifAIsKw +wkojOqriFdeKAiU83bWvxVUW/F9Yfe/hHCjjRxp6xMRoGTh63jGmAWxu/WR9GpFP +pfVsHLHg3p8tU4TusU1I5TuHZ/8B7atpFdlS5J8mc5bVB6oHYuaJVg4bbMGQIfll +m5CTfEKfTxkLjVtUGEhmoutb60qnXECNhUYFef69hMZ5VOUj4gAC8e2JKm/8yjG+ +K9USxsK1YTAMIKBbLqNsJmOB8mz08CxPU8R6XCvzDPr4rqnG18IcUxAlbYiH77eA +qg== -----END CERTIFICATE----- diff --git a/ndg/httpsclient/test/pki/localhost.csr b/ndg/httpsclient/test/pki/localhost.csr new file mode 100644 index 0000000..a3c5181 --- /dev/null +++ b/ndg/httpsclient/test/pki/localhost.csr @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEejCCAmICAQAwNTEMMAoGA1UECgwDTkRHMREwDwYDVQQLDAhTZWN1cml0eTES +MBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC +AgEAsBzYsKRp63t5l049CBJFzPZ7WYlDerAypPUF0yf4Ab/1kwicJ+1eYkEelvlY +p4DiFyUI/6wVR9rG3ZNwnus2w9fquQ3kkXAwAgMzONqrwOvVliN6dO8iedvq1Ygr +Yo/2dV0lHFIJcch03Sm8lB3KL7cs6dMfJ8cogf2VMEt/YdhjW2kYUNYv/LAduHuS +tSQh8Le9LHrFlcE2BK0gfiCOk/YPEp+4+ZLDUpp7a84pxEnb0/AH8fVNo+vRW9BM +035wELag56Fekf9BFtTw3J7u6rjP+WUoPksxkA4l7tVRTkDPFQfsMTbvzbgKZLXL +B3km57J52eyexOOW0RrJBye8Cb84Qivqs7RoNWZkfjSL9bX412JaAbG3c6u+NTvU +FACVqSeyG/bEPH1Q2wutD6I6kN2xjGkPzizhlrRX6qerUnlAJIPuhpAuLhIb2c8s +L7CkSEwJUYIe0KV8vU1VIFhXJxs+nWItA/fEKLBX3TnxdU9Pamwj49o8t5enVk2k +SRGX79TIB0MqpvcLqcTW2o1tNRGG4v8QIUUeIfWTiaUZl8afZgv3RE6ooWbph1gT +fnu8+t2MNujDfYKq3zAmpMTeYhrFkf68uopF7NtTx+aj7uDIQx4+wJNbDTJc/Kiu +KgsBsVkE7nnXaxrT0JcsaZ5NouzTbXnNorzUecCAzAVZP70CAwEAAaAAMA0GCSqG +SIb3DQEBCwUAA4ICAQBQ+niVozKfjJP1VCTDb88/mAGEpEOGF+BXwN7EdQKAwZva +vAxQSpqt8h0EYyToPCV6P5GNzFQ/rRW2OYgWwXEHk3ptiQzcJzIF/F9CbE9vgYO5 +Xvosxy3uyJr15sAoS+qL4jEXg7dDni+LesYYR5qx6l/5nYG/6ql0Z/K9+Z/1xrci +00Ml6+xq/oT1kc0JQYuUKnMXgQb6Mishyt+ESoRDFt6oFqlza7TNhC/Q3YxuOjqw +Hr3rc1T9L8ArYFMhwjCBSitJS9ojqtPyXTkE4ZrQoaAAl/jdwbiHQ81/PQErgrk4 +3RkyokdrOkeK2CssJ402s8ybnbYzaI+wg4K7r4APsiOXAacfVObmPYzQLamqCNkE +hcGD6Yp9JpSgVWH+cTHa47slz3RjLxMW0EMfffYOlsWlxnlY+LHTMrphfd7otSlD +c3lSbUgURW6babKQBxOULR0AooaJK/uHgarb+6xNQoPTA7RSDfemfBGu+xAxIeE8 +xPRXH6DYjhzwiHB8tFKcfdXX+MZCQ+QUHsbeVLVl+0kkw3J5bRTo5lS6menuzbPA +osnOO2IMBrhctgspj6MJg6lyFyWUDYzWn0JU6+rF/NHmnrlFgJ5in1eg1u783bXJ +0jKPpw1QAOz+gI2MHKx2Tpbu43UtAb/35GtW711e6lhm3gZUxjVQj7qhfW1pcw== +-----END CERTIFICATE REQUEST----- diff --git a/ndg/httpsclient/test/pki/localhost.key b/ndg/httpsclient/test/pki/localhost.key index 17fe929..4be01f3 100644 --- a/ndg/httpsclient/test/pki/localhost.key +++ b/ndg/httpsclient/test/pki/localhost.key @@ -1,15 +1,51 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/ndg/httpsclient/test/test_https.py b/ndg/httpsclient/test/test_https.py index 8400e80..415a6bc 100644 --- a/ndg/httpsclient/test/test_https.py +++ b/ndg/httpsclient/test/test_https.py @@ -35,10 +35,10 @@ def test01_open(self): def test02_open_fails(self): conn = HTTPSConnection(Constants.HOSTNAME, port=Constants.PORT2) - self.failUnlessRaises(socket.error, conn.connect) + self.assertRaises(socket.error, conn.connect) def test03_ssl_verification_of_peer_fails(self): - ctx = SSL.Context(SSL.SSLv3_METHOD) + ctx = SSL.Context(SSL.TLSv1_2_METHOD) def verify_callback(conn, x509, errnum, errdepth, preverify_ok): log.debug('SSL peer certificate verification failed for %r', @@ -54,10 +54,10 @@ def verify_callback(conn, x509, errnum, errdepth, preverify_ok): conn = HTTPSConnection(Constants.HOSTNAME, port=Constants.PORT, ssl_context=ctx) conn.connect() - self.failUnlessRaises(SSL.Error, conn.request, 'GET', '/') + self.assertRaises(SSL.Error, conn.request, 'GET', '/') def test03_ssl_verification_of_peer_succeeds(self): - ctx = SSL.Context(SSL.SSLv3_METHOD) + ctx = SSL.Context(SSL.TLSv1_2_METHOD) verify_callback = lambda conn, x509, errnum, errdepth, preverify_ok: \ preverify_ok @@ -76,10 +76,11 @@ def test03_ssl_verification_of_peer_succeeds(self): print('Response = %s' % resp.read()) def test04_ssl_verification_with_subj_alt_name(self): - ctx = SSL.Context(SSL.SSLv3_METHOD) + ctx = SSL.Context(SSL.TLSv1_2_METHOD) + + verification = ServerSSLCertVerification(hostname='localhost') + verify_callback = verification.get_verify_server_cert_func() - verify_callback = ServerSSLCertVerification(hostname='localhost') - ctx.set_verify(SSL.VERIFY_PEER, verify_callback) ctx.set_verify_depth(9) @@ -94,13 +95,15 @@ def test04_ssl_verification_with_subj_alt_name(self): print('Response = %s' % resp.read()) def test04_ssl_verification_with_subj_common_name(self): - ctx = SSL.Context(SSL.SSLv3_METHOD) + ctx = SSL.Context(SSL.TLSv1_2_METHOD) # Explicitly set verification of peer hostname using peer certificate # subject common name - verify_callback = ServerSSLCertVerification(hostname='localhost', - subj_alt_name_match=False) + verification = ServerSSLCertVerification(hostname='localhost', + subj_alt_name_match=False) + verify_callback = verification.get_verify_server_cert_func() + ctx.set_verify(SSL.VERIFY_PEER, verify_callback) ctx.set_verify_depth(9) diff --git a/ndg/httpsclient/test/test_urllib2.py b/ndg/httpsclient/test/test_urllib2.py index 9c1ef8d..5262fe1 100644 --- a/ndg/httpsclient/test/test_urllib2.py +++ b/ndg/httpsclient/test/test_urllib2.py @@ -9,12 +9,19 @@ __license__ = "BSD - see LICENSE file in top-level directory" __contact__ = "Philip.Kershaw@stfc.ac.uk" __revision__ = '$Id$' -from urllib2 import URLError +import sys + +if sys.version_info[0] > 2: + from urllib.error import URLError as URLError +else: + from urllib2 import URLError as URLError + import unittest from OpenSSL import SSL from ndg.httpsclient.test import Constants from ndg.httpsclient.urllib2_build_opener import build_opener +from ndg.httpsclient.ssl_peer_verification import ServerSSLCertVerification class Urllib2TestCase(unittest.TestCase): @@ -22,29 +29,61 @@ class Urllib2TestCase(unittest.TestCase): def test01_urllib2_build_opener(self): opener = build_opener() - self.assert_(opener) + self.assertTrue(opener) def test02_open(self): opener = build_opener() res = opener.open(Constants.TEST_URI) - self.assert_(res) + self.assertTrue(res) print("res = %s" % res.read()) + # Skip this test for remote service as it can take a long time to timeout + @unittest.skipIf(Constants.HOSTNAME != 'localhost', 'Skip non-local host') def test03_open_fails_unknown_loc(self): opener = build_opener() - self.failUnlessRaises(URLError, opener.open, Constants.TEST_URI2) + self.assertRaises(URLError, opener.open, Constants.TEST_URI2) def test04_open_peer_cert_verification_fails(self): # Explicitly set empty CA directory to make verification fail - ctx = SSL.Context(SSL.SSLv3_METHOD) + ctx = SSL.Context(SSL.TLSv1_2_METHOD) verify_callback = lambda conn, x509, errnum, errdepth, preverify_ok: \ preverify_ok ctx.set_verify(SSL.VERIFY_PEER, verify_callback) ctx.load_verify_locations(None, './') opener = build_opener(ssl_context=ctx) - self.failUnlessRaises(SSL.Error, opener.open, Constants.TEST_URI) + self.assertRaises(SSL.Error, opener.open, Constants.TEST_URI) + + def test05_open_with_subj_alt_names_verification(self): + ctx = SSL.Context(SSL.TLSv1_2_METHOD) + + # Set wildcard hostname for subject alternative name matching - + # setting a minimum of two name components for hostname + split_hostname = Constants.HOSTNAME.split('.', 1) + if len(split_hostname) > 1: + _hostname = '*.' + split_hostname[-1] + else: + _hostname = Constants.HOSTNAME + + server_ssl_verify = ServerSSLCertVerification(hostname=_hostname) + verify_callback_ = server_ssl_verify.get_verify_server_cert_func() + ctx.set_verify(SSL.VERIFY_PEER, verify_callback_) + + # Set default verify paths if testing with peer that has corresponding + # CA cert in bundle provided with the OS. In this case, load verify + # locations is not needed. + #ctx.set_default_verify_paths() + ctx.set_verify_depth(9) + + # Set correct location for CA certs to verify with + ctx.load_verify_locations(None, Constants.CACERT_DIR) + + opener = build_opener(ssl_context=ctx) + res = opener.open(Constants.TEST_URI) + self.assertTrue(res) + print("res = %s" % res.read()) + if __name__ == "__main__": unittest.main() diff --git a/ndg/httpsclient/test/test_utils.py b/ndg/httpsclient/test/test_utils.py index fe496a6..a91045d 100644 --- a/ndg/httpsclient/test/test_utils.py +++ b/ndg/httpsclient/test/test_utils.py @@ -23,17 +23,17 @@ class TestUtilsModule(unittest.TestCase): '''Test ndg.httpsclient.utils module''' def test01_configuration(self): - config = Configuration(SSL.Context(SSL.SSLv3_METHOD), True) - self.assert_(config.ssl_context) - self.assertEquals(config.debug, True) + config = Configuration(SSL.Context(SSL.TLSv1_2_METHOD), True) + self.assertTrue(config.ssl_context) + self.assertEqual(config.debug, True) def test02_fetch_from_url(self): - config = Configuration(SSL.Context(SSL.SSLv3_METHOD), True) + config = Configuration(SSL.Context(SSL.TLSv1_2_METHOD), True) res = fetch_from_url(Constants.TEST_URI, config) - self.assert_(res) + self.assertTrue(res) def test03_open_url(self): - config = Configuration(SSL.Context(SSL.SSLv3_METHOD), True) + config = Configuration(SSL.Context(SSL.TLSv1_2_METHOD), True) res = open_url(Constants.TEST_URI, config) self.assertEqual(res[0], 200, 'open_url for %r failed' % Constants.TEST_URI) @@ -57,5 +57,6 @@ def test04__should_use_proxy(self): else: del os.environ['no_proxy'] + if __name__ == "__main__": unittest.main() \ No newline at end of file diff --git a/ndg/httpsclient/urllib2_build_opener.py b/ndg/httpsclient/urllib2_build_opener.py index 106f875..55d8632 100644 --- a/ndg/httpsclient/urllib2_build_opener.py +++ b/ndg/httpsclient/urllib2_build_opener.py @@ -8,9 +8,23 @@ __contact__ = "Philip.Kershaw@stfc.ac.uk" __revision__ = '$Id$' import logging -from urllib2 import (ProxyHandler, UnknownHandler, HTTPDefaultErrorHandler, - FTPHandler, FileHandler, HTTPErrorProcessor, HTTPHandler, - OpenerDirector, HTTPRedirectHandler) +import sys + +# Py 2 <=> 3 compatibility for class type checking +if sys.version_info[0] > 2: + class_type_ = type + from urllib.request import (ProxyHandler, UnknownHandler, + HTTPDefaultErrorHandler, FTPHandler, + FileHandler, HTTPErrorProcessor, + HTTPHandler, OpenerDirector, + HTTPRedirectHandler) +else: + import types + class_type_ = types.ClassType + + from urllib2 import (ProxyHandler, UnknownHandler, HTTPDefaultErrorHandler, + FTPHandler, FileHandler, HTTPErrorProcessor, + HTTPHandler, OpenerDirector, HTTPRedirectHandler) from ndg.httpsclient.https import HTTPSContextHandler @@ -27,9 +41,8 @@ def build_opener(*handlers, **kw): 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__") + return isinstance(obj, class_type_) or hasattr(obj, "__bases__") opener = OpenerDirector() default_classes = [ProxyHandler, UnknownHandler, HTTPHandler, diff --git a/ndg/httpsclient/utils.py b/ndg/httpsclient/utils.py index 5ad0217..7e43a73 100644 --- a/ndg/httpsclient/utils.py +++ b/ndg/httpsclient/utils.py @@ -8,16 +8,35 @@ __contact__ = "Philip.Kershaw@stfc.ac.uk" __revision__ = '$Id$' -import cookielib -import httplib import logging from optparse import OptionParser import os -import urllib2 -from urllib2 import (HTTPHandler, HTTPCookieProcessor, - HTTPBasicAuthHandler, HTTPPasswordMgrWithDefaultRealm) - -import urlparse +import sys + +if sys.version_info[0] > 2: + import http.cookiejar as cookiejar_ + import http.client as http_client_ + from urllib.request import Request as Request_ + from urllib.request import HTTPHandler as HTTPHandler_ + from urllib.request import HTTPCookieProcessor as HTTPCookieProcessor_ + from urllib.request import HTTPBasicAuthHandler as HTTPBasicAuthHandler_ + from urllib.request import HTTPPasswordMgrWithDefaultRealm as \ + HTTPPasswordMgrWithDefaultRealm_ + from urllib.request import ProxyHandler as ProxyHandler_ + from urllib.error import HTTPError as HTTPError_ + import urllib.parse as urlparse_ +else: + import cookielib as cookiejar_ + import httplib as http_client_ + from urllib2 import Request as Request_ + from urllib2 import HTTPHandler as HTTPHandler_ + from urllib2 import HTTPCookieProcessor as HTTPCookieProcessor_ + from urllib2 import HTTPBasicAuthHandler as HTTPBasicAuthHandler_ + from urllib2 import HTTPPasswordMgrWithDefaultRealm as \ + HTTPPasswordMgrWithDefaultRealm_ + from urllib2 import ProxyHandler as ProxyHandler_ + from urllib2 import HTTPError as HTTPError_ + import urlparse as urlparse_ from ndg.httpsclient.urllib2_build_opener import build_opener from ndg.httpsclient.https import HTTPSContextHandler @@ -25,7 +44,7 @@ log = logging.getLogger(__name__) -class AccumulatingHTTPCookieProcessor(HTTPCookieProcessor): +class AccumulatingHTTPCookieProcessor(HTTPCookieProcessor_): """Cookie processor that adds new cookies (instead of replacing the existing ones as HTTPCookieProcessor does) """ @@ -37,7 +56,7 @@ def http_request(self, request): @rtype: urllib2.Request """ COOKIE_HEADER_NAME = "Cookie" - tmp_request = urllib2.Request(request.get_full_url(), request.data, {}, + tmp_request = Request_(request.get_full_url(), request.data, {}, request.origin_req_host, request.unverifiable) self.cookiejar.add_cookie_header(tmp_request) @@ -73,7 +92,7 @@ def fetch_from_url(url, config, data=None, handlers=None): """ return_code, return_message, response = open_url(url, config, data=data, handlers=handlers) - if return_code and return_code == httplib.OK: + if return_code and return_code == http_client_.OK: return_data = response.read() response.close() return return_data @@ -95,14 +114,14 @@ def fetch_from_url_to_file(url, config, output_file, data=None, handlers=None): """ return_code, return_message, response = open_url(url, config, data=data, handlers=handlers) - if return_code == httplib.OK: + if return_code == http_client_.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 + return return_code, return_message, return_code == http_client_.OK def fetch_stream_from_url(url, config, data=None, handlers=None): @@ -120,7 +139,7 @@ def fetch_stream_from_url(url, config, data=None, handlers=None): """ return_code, return_message, response = open_url(url, config, data=data, handlers=handlers) - if return_code and return_code == httplib.OK: + if return_code and return_code == http_client_.OK: return response else: raise URLFetchError(return_message) @@ -146,7 +165,7 @@ def open_url(url, config, data=None, handlers=None): if config.cookie: cj = config.cookie else: - cj = cookielib.CookieJar() + cj = cookiejar_.CookieJar() # Use a cookie processor that accumulates cookies when redirects occur so # that an application can redirect for authentication and retain both any @@ -160,17 +179,17 @@ def open_url(url, config, data=None, handlers=None): handlers.append(cookie_handler) if config.debug: - http_handler = HTTPHandler(debuglevel=debuglevel) + http_handler = HTTPHandler_(debuglevel=debuglevel) https_handler = HTTPSContextHandler(config.ssl_context, debuglevel=debuglevel) handlers.extend([http_handler, https_handler]) if config.http_basicauth: # currently only supports http basic auth - auth_handler = HTTPBasicAuthHandler(HTTPPasswordMgrWithDefaultRealm()) + auth_handler = HTTPBasicAuthHandler_(HTTPPasswordMgrWithDefaultRealm_()) auth_handler.add_password(realm=None, uri=url, - user=config.httpauth[0], - passwd=config.httpauth[1]) + user=config.http_basicauth[0], + passwd=config.http_basicauth[1]) handlers.append(auth_handler) @@ -179,10 +198,10 @@ def open_url(url, config, data=None, handlers=None): # set via http_proxy and https_proxy, but does not take the no_proxy value # into account. if not _should_use_proxy(url, config.no_proxy): - handlers.append(urllib2.ProxyHandler({})) + handlers.append(ProxyHandler_({})) log.debug("Not using proxy") elif config.proxies: - handlers.append(urllib2.ProxyHandler(config.proxies)) + handlers.append(ProxyHandler_(config.proxies)) log.debug("Configuring proxies: %s" % config.proxies) opener = build_opener(*handlers, ssl_context=config.ssl_context) @@ -191,12 +210,13 @@ def open_url(url, config, data=None, handlers=None): if headers is None: headers = {} - request = urllib2.Request(url, data, headers) + request = Request_(url, data, headers) # Open the URL and check the response. return_code = 0 return_message = '' response = None + try: response = opener.open(request) return_message = response.msg @@ -205,13 +225,13 @@ def open_url(url, config, data=None, handlers=None): for index, cookie in enumerate(cj): log.debug("%s : %s", index, cookie) - except urllib2.HTTPError, exc: + except HTTPError_ as exc: return_code = exc.code return_message = "Error: %s" % exc.msg if log.isEnabledFor(logging.DEBUG): log.debug("%s %s", exc.code, exc.msg) - except Exception, exc: + except Exception as exc: return_message = "Error: %s" % exc.__str__() if log.isEnabledFor(logging.DEBUG): import traceback @@ -231,7 +251,7 @@ def _should_use_proxy(url, no_proxy=None): else: no_proxy_effective = no_proxy - urlObj = urlparse.urlparse(_url_as_string(url)) + urlObj = urlparse_.urlparse(_url_as_string(url)) for np in [h.strip() for h in no_proxy_effective.split(',')]: if urlObj.hostname == np: return False @@ -246,13 +266,13 @@ def _url_as_string(url): @return: URL string @rtype: basestring """ - if isinstance(url, urllib2.Request): + if isinstance(url, Request_): return url.get_full_url() - elif isinstance(url, basestring): + elif isinstance(url, str): return url else: raise TypeError("Expected type %r or %r" % - (basestring, urllib2.Request)) + (str, Request_)) class Configuration(object): @@ -270,7 +290,7 @@ def __init__(self, ssl_context, debug=False, proxies=None, no_proxy=None, @param no_proxy: hosts for which a proxy should not be used @type no_proxy: basestring @param cookie: cookies to set for request - @type cookie: cookielib.CookieJar + @type cookie: cookielib.CookieJar (python 3 - http.cookiejar) @param http_basicauth: http authentication, or None @type http_basicauth: tuple of (username,password) @param headers: http headers diff --git a/setup.py b/setup.py index 1de205d..90ab546 100644 --- a/setup.py +++ b/setup.py @@ -5,121 +5,36 @@ use_setuptools() from setuptools import setup, find_packages -_long_description = ''' -This is a HTTPS client implementation for httplib and urllib2 based on -PyOpenSSL. PyOpenSSL provides a more fully featured SSL implementation over the -default provided with Python and importantly enables full verification of the -SSL peer. +NAMESPACE_PKGS = ['ndg'] -Releases -======== -0.3.3 ------ - * Fix to add in AnotherName for ``subjectAltNames`` field - added for support for CACert issued - certs (thanks to Gu1). - * Fix to HTTP Basic Auth option for ``ndg.httpsclient.utils.main`` - * Fix to ``ServerSSLCertVerification`` so that it can pass a function-based callback instead of using ``__call__``. In newer versions of OpenSSL (>= 0.14) the latter failed because of a request for ``__name__`` attribute. +with open('README.md') as f: + _long_description = f.read() -0.3.2 ------ - * Fix to SubjectAltNames support check - should only be enabled if pyasn1 is - installed. - * Fix to open_url: HTTP Request object was being created inside if headers is - None block - now corrected to create regardless. - * Added http basic auth support to script. (Thanks to Willem van Engen) - -0.3.1 ------ - * extended utils functions to support keyword for passing additional urllib2 - handlers. - -0.3.0 ------ - * Added ndg.httpsclient.utils.fetch_stream_from_url function and added - parameter for data to post in open_url and fetch_* methods. - * fix to ndg.httpsclient.utils module _should_use_proxy and open_url functions - -0.2.0 ------ - * added support for SSL verification with subjectAltNames using pyasn1 - * fixed minor bug - SSL cert DN prefix matching - -0.1.0 ------ -Initial release - -Prerequisites -============= -This has been developed and tested for Python 2.6 and 2.7 with pyOpenSSL 0.13 and 0.14. -Note that proxy support is only available from Python 2.6.2 onwards. pyasn1 is -required for correct SSL verification with subjectAltNames. - -Installation -============ -Installation can be performed using easy_install or pip. - -Running ndg_httpclient -====================== -A simple script for fetching data using HTTP or HTTPS GET from a specified URL. - -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. - -``-p FILE, --post-data-file=FILE`` - POST data file - -``-f FILE, --fetch=FILE`` - Output file - -``-n, --no-verify-peer`` - Skip verification of peer certificate. -''' - setup( name='ndg_httpsclient', - version="0.3.3", + version="0.5.1", description='Provides enhanced HTTPS support for httplib and urllib2 using ' 'PyOpenSSL', author='Richard Wilkinson and Philip Kershaw', author_email='Philip.Kershaw@stfc.ac.uk', url='https://github.com/cedadev/ndg_httpsclient/', long_description=_long_description, - license='BSD - See LICENCE file for details', - namespace_packages=['ndg'], + long_description_content_type='text/markdown', + license='BSD - See ndg/httpsclient/LICENCE file for details', packages=find_packages(), - package_dir={'ndg.httpsclient': 'ndg/httpsclient'}, package_data={ 'ndg.httpsclient': [ - 'test/README', + 'LICENSE', + 'test/README', 'test/scripts/*.sh', 'test/pki/localhost.*', 'test/pki/ca/*.0' ], }, - install_requires = ['PyOpenSSL'], - extras_require = {'subjectAltName_support': 'pyasn1'}, - classifiers = [ - 'Development Status :: 3 - Alpha', + install_requires=['PyOpenSSL', 'pyasn1>=0.1.1'], + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', + classifiers=[ + 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Web Environment', 'Intended Audience :: End Users/Desktop', @@ -131,6 +46,12 @@ 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Security', 'Topic :: Internet', 'Topic :: Scientific/Engineering', @@ -138,8 +59,8 @@ 'Topic :: System :: Systems Administration :: Authentication/Directory', 'Topic :: Software Development :: Libraries :: Python Modules' ], - zip_safe = False, - entry_points = { + zip_safe=False, + entry_points={ 'console_scripts': ['ndg_httpclient = ndg.httpsclient.utils:main', ], }