NEW: Support for specifying the SSL protocol version (Python v2) (#9)
* Support for specifying the SSL protocol version (Python v2)
This fixes #329.
diff --git a/python2/httplib2/__init__.py b/python2/httplib2/__init__.py
index a64d8b4..b6307d3 100644
--- a/python2/httplib2/__init__.py
+++ b/python2/httplib2/__init__.py
@@ -20,7 +20,8 @@
"Jonathan Feinberg",
"Blair Zajac",
"Sam Ruby",
- "Louis Nyffenegger"]
+ "Louis Nyffenegger",
+ "Alex Yu"]
__license__ = "MIT"
__version__ = "0.9.2"
@@ -68,16 +69,16 @@
import ssl # python 2.6
ssl_SSLError = ssl.SSLError
def _ssl_wrap_socket(sock, key_file, cert_file,
- disable_validation, ca_certs):
+ disable_validation, ca_certs, ssl_version):
if disable_validation:
cert_reqs = ssl.CERT_NONE
else:
cert_reqs = ssl.CERT_REQUIRED
- # We should be specifying SSL version 3 or TLS v1, but the ssl module
- # doesn't expose the necessary knobs. So we need to go with the default
- # of SSLv23.
+ if ssl_version is None:
+ ssl_version = ssl.PROTOCOL_SSLv23
return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file,
- cert_reqs=cert_reqs, ca_certs=ca_certs)
+ cert_reqs=cert_reqs, ca_certs=ca_certs,
+ ssl_version=ssl_version)
except (AttributeError, ImportError):
ssl_SSLError = None
def _ssl_wrap_socket(sock, key_file, cert_file,
@@ -938,7 +939,8 @@
"""
def __init__(self, host, port=None, key_file=None, cert_file=None,
strict=None, timeout=None, proxy_info=None,
- ca_certs=None, disable_ssl_certificate_validation=False):
+ ca_certs=None, disable_ssl_certificate_validation=False,
+ ssl_version=None):
httplib.HTTPSConnection.__init__(self, host, port=port,
key_file=key_file,
cert_file=cert_file, strict=strict)
@@ -949,6 +951,7 @@
self.ca_certs = ca_certs
self.disable_ssl_certificate_validation = \
disable_ssl_certificate_validation
+ self.ssl_version = ssl_version
# The following two methods were adapted from https_wrapper.py, released
# with the Google Appengine SDK at
@@ -1033,7 +1036,8 @@
sock.connect((self.host, self.port))
self.sock =_ssl_wrap_socket(
sock, self.key_file, self.cert_file,
- self.disable_ssl_certificate_validation, self.ca_certs)
+ self.disable_ssl_certificate_validation, self.ca_certs,
+ self.ssl_version)
if self.debuglevel > 0:
print "connect: (%s, %s)" % (self.host, self.port)
if use_proxy:
@@ -1155,7 +1159,8 @@
"""
def __init__(self, cache=None, timeout=None,
proxy_info=proxy_info_from_environment,
- ca_certs=None, disable_ssl_certificate_validation=False):
+ ca_certs=None, disable_ssl_certificate_validation=False,
+ ssl_version=None):
"""If 'cache' is a string then it is used as a directory name for
a disk cache. Otherwise it must be an object that supports the
same interface as FileCache.
@@ -1178,11 +1183,14 @@
If disable_ssl_certificate_validation is true, SSL cert validation will
not be performed.
+
+ By default, ssl.PROTOCOL_SSLv23 will be used for the ssl version.
"""
self.proxy_info = proxy_info
self.ca_certs = ca_certs
self.disable_ssl_certificate_validation = \
disable_ssl_certificate_validation
+ self.ssl_version = ssl_version
# Map domain name to an httplib connection
self.connections = {}
@@ -1478,14 +1486,16 @@
proxy_info=proxy_info,
ca_certs=self.ca_certs,
disable_ssl_certificate_validation=
- self.disable_ssl_certificate_validation)
+ self.disable_ssl_certificate_validation,
+ ssl_version=self.ssl_version)
else:
conn = self.connections[conn_key] = connection_type(
authority, timeout=self.timeout,
proxy_info=proxy_info,
ca_certs=self.ca_certs,
disable_ssl_certificate_validation=
- self.disable_ssl_certificate_validation)
+ self.disable_ssl_certificate_validation,
+ ssl_version=self.ssl_version)
else:
conn = self.connections[conn_key] = connection_type(
authority, timeout=self.timeout,
diff --git a/python2/httplib2/test/other_cacerts.txt b/python2/httplib2/test/other_cacerts.txt
index 360954a..b2c2488 100644
--- a/python2/httplib2/test/other_cacerts.txt
+++ b/python2/httplib2/test/other_cacerts.txt
@@ -1,70 +1,19 @@
-# Certifcate Authority certificates for validating SSL connections.
-#
-# This file contains PEM format certificates generated from
-# http://mxr.mozilla.org/seamonkey/source/security/nss/lib/ckfw/builtins/certdata.txt
-#
-# ***** BEGIN LICENSE BLOCK *****
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1
-#
-# The contents of this file are subject to the Mozilla Public License Version
-# 1.1 (the "License"); you may not use this file except in compliance with
-# the License. You may obtain a copy of the License at
-# http://www.mozilla.org/MPL/
-#
-# Software distributed under the License is distributed on an "AS IS" basis,
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
-# for the specific language governing rights and limitations under the
-# License.
-#
-# The Original Code is the Netscape security libraries.
-#
-# The Initial Developer of the Original Code is
-# Netscape Communications Corporation.
-# Portions created by the Initial Developer are Copyright (C) 1994-2000
-# the Initial Developer. All Rights Reserved.
-#
-# Contributor(s):
-#
-# Alternatively, the contents of this file may be used under the terms of
-# either the GNU General Public License Version 2 or later (the "GPL"), or
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
-# in which case the provisions of the GPL or the LGPL are applicable instead
-# of those above. If you wish to allow use of your version of this file only
-# under the terms of either the GPL or the LGPL, and not to allow others to
-# use your version of this file under the terms of the MPL, indicate your
-# decision by deleting the provisions above and replace them with the notice
-# and other provisions required by the GPL or the LGPL. If you do not delete
-# the provisions above, a recipient may use your version of this file under
-# the terms of any one of the MPL, the GPL or the LGPL.
-#
-# ***** END LICENSE BLOCK *****
-
-
-Comodo CA Limited, CN=Trusted Certificate Services
-==================================================
-
-----BEGIN CERTIFICATE-----
-MIIEQzCCAyugAwIBAgIBATANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJHQjEb
-MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
-GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDElMCMGA1UEAwwcVHJ1c3RlZCBDZXJ0
-aWZpY2F0ZSBTZXJ2aWNlczAeFw0wNDAxMDEwMDAwMDBaFw0yODEyMzEyMzU5NTla
-MH8xCzAJBgNVBAYTAkdCMRswGQYDVQQIDBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO
-BgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoMEUNvbW9kbyBDQSBMaW1pdGVkMSUwIwYD
-VQQDDBxUcnVzdGVkIENlcnRpZmljYXRlIFNlcnZpY2VzMIIBIjANBgkqhkiG9w0B
-AQEFAAOCAQ8AMIIBCgKCAQEA33FvNlhTWvI2VFeAxHQIIO0Yfyod5jWaHiWsnOWW
-fnJSoBVC21ndZHoa0Lh73TkVvFVIxO06AOoxEbrycXQaZ7jPM8yoMa+j49d/vzMt
-TGo87IvDktJTdyR0nAducPy9C1t2ul/y/9c3S0pgePfw+spwtOpZqqPOSC+pw7IL
-fhdyFgymBwwbOM/JYrc/oJOlh0Hyt3BAd9i+FHzjqMB6juljatEPmsbS9Is6FARW
-1O24zG71++IsWL1/T2sr92AkWCTOJu80kTrV44HQsvAEAtdbtz6SrGsSivnkBbA7
-kUlcsutT6vifR4buv5XAwAaf0lteERv0xwQ1KdJVXOTt6wIDAQABo4HJMIHGMB0G
-A1UdDgQWBBTFe1i97doladL3WRaoszLAeydb9DAOBgNVHQ8BAf8EBAMCAQYwDwYD
-VR0TAQH/BAUwAwEB/zCBgwYDVR0fBHwwejA8oDqgOIY2aHR0cDovL2NybC5jb21v
-ZG9jYS5jb20vVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMuY3JsMDqgOKA2hjRo
-dHRwOi8vY3JsLmNvbW9kby5uZXQvVHJ1c3RlZENlcnRpZmljYXRlU2VydmljZXMu
-Y3JsMA0GCSqGSIb3DQEBBQUAA4IBAQDIk4E7ibSvuIQSTI3S8NtwuleGFTQQuS9/
-HrCoiWChisJ3DFBKmwCL2Iv0QeLQg4pKHBQGsKNoBXAxMKdTmw7pSqBYaWcOrp32
-pSxBvzwGa+RZzG0Q8ZZvH9/0BAKkn0U+yNj6NkZEUD+Cl5EfKNsYEYwq5GWDVxIS
-jBc/lDb+XbDABHcTuPQV1T84zJQ6VdCsmPW6AF/ghhmBeC8owH7TzEIK9a5QoNE+
-xqFx7D+gIIxmOom0jtTYsU0lR+4viMi14QVFwL4Ucd56/Y57fU0IlqUSc/Atyjcn
-dBInTMu2l+nZrghtWjlA3QVHdWpaIbOjGM9O9y5Xt5hwXsjEeLBi
+MIIDBzCCAe+gAwIBAgIJAIw94zvO7fk1MA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
+BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNjA2MDQwMjMxMTRaFw0yNjA2MDIwMjMx
+MTRaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBAK3YNcDIwK/wlTa0/iBARvDFOncQ6Jkk+Ymql1HXny7v
+mWPFWeLXEW+Zw1NrQEx/SIUGvxpRA+QyhTOhu2Gcwvtqilix/dHgaKgqWEcRYu8m
+L70uVDPVgB/kfNI8bpXM1Mz8Crjo0tHw5oUSD3wny8SyT6CYlXVmF923L8c2zdN9
+n9blFgYwxBq2+q+mqOiDErMFbwHES8FNBSWGBXdE1xjBdITtlfeHezmJhj/ylPW1
+7v8HInsv/WqU9DcJYlFxSnK0SZCLFBM/31Ez8O1gCfMlDUFvJoo59GyFqukUjuO1
+uB85wpu27gtcLm/J9X1Md71IxbDupV7a0dDoTvbhO4kCAwEAAaNQME4wHQYDVR0O
+BBYEFIHgAmwppZSKLz2peyFSO2kwVobNMB8GA1UdIwQYMBaAFIHgAmwppZSKLz2p
+eyFSO2kwVobNMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAJxz+AU/
+Iq8fMEStJ0BgPP1N86W9Jpb7aPMFCYTEZ+nd8hFPhPs4//55J0yIve+1I43MNFFz
+yflwwCzrIIhZdkvbsyea6CmlTo4jBc4+ihaDGobYnoNzFhavC47n5kYqJ8Ikyb2W
+OMrmNRiaTeSBl0wQmftnnQCbonenjmE1LDuJtE6bCwfFjfLbMxwdWtp/ymOlXsb5
+80XcWwcqc12UHWexYwHFzEJmDfncak/8tjHBsLWMJg5p2sVTY9kVt7TYgSIl+mFb
+4WVGrqZd2uTlJkRQQ4pCl+D+PKwadHuV6YI7oxkeajjcHCgbK/ANwW28MXYho6t6
+aWVIN4bWHrZ38kE=
-----END CERTIFICATE-----
diff --git a/python2/httplib2test.py b/python2/httplib2test.py
index d9bfdb6..0536512 100755
--- a/python2/httplib2test.py
+++ b/python2/httplib2test.py
@@ -511,25 +511,6 @@
self.assertEqual(200, response.status)
self.assertNotEqual(None, response.previous)
- def testSslCertValidation(self):
- if sys.version_info >= (2, 6):
- # Test that we get an ssl.SSLError when specifying a non-existent CA
- # certs file.
- http = httplib2.Http(ca_certs='/nosuchfile')
- self.assertRaises(ssl.SSLError,
- http.request, "https://www.google.com/", "GET")
-
- # Test that we get a SSLHandshakeError if we try to access
- # https;//www.google.com, using a CA cert file that doesn't contain
- # the CA Gogole uses (i.e., simulating a cert that's not signed by a
- # trusted CA).
- other_ca_certs = os.path.join(
- os.path.dirname(os.path.abspath(httplib2.__file__ )),
- "test", "other_cacerts.txt")
- http = httplib2.Http(ca_certs=other_ca_certs)
- self.assertRaises(httplib2.SSLHandshakeError,
- http.request, "https://www.google.com/", "GET")
-
def testSslCertValidationDoubleDots(self):
pass
# No longer a valid test.
diff --git a/python2/ssl_protocol_test.py b/python2/ssl_protocol_test.py
new file mode 100755
index 0000000..bac84c0
--- /dev/null
+++ b/python2/ssl_protocol_test.py
@@ -0,0 +1,57 @@
+"""Tests for SSL handling in httplib2."""
+
+import httplib2
+import os
+import ssl
+import sys
+import unittest
+
+
+class TestSslProtocol(unittest.TestCase):
+
+ def testSslCertValidationWithInvalidCaCert(self):
+ if sys.version_info >= (2, 6):
+ http = httplib2.Http(ca_certs='/nosuchfile')
+ if sys.version_info >= (2, 7):
+ with self.assertRaises(IOError):
+ http.request('https://www.google.com/', 'GET')
+ else:
+ self.assertRaises(
+ ssl.SSLError, http.request, 'https://www.google.com/', 'GET')
+
+ def testSslCertValidationWithSelfSignedCaCert(self):
+ if sys.version_info >= (2, 7):
+ other_ca_certs = os.path.join(
+ os.path.dirname(os.path.abspath(httplib2.__file__ )), 'test',
+ 'other_cacerts.txt')
+ http = httplib2.Http(ca_certs=other_ca_certs)
+ if sys.platform != 'darwin':
+ with self.assertRaises(httplib2.SSLHandshakeError):
+ http.request('https://www.google.com/', 'GET')
+
+ def testSslProtocolTlsV1AndShouldPass(self):
+ http = httplib2.Http(ssl_version=ssl.PROTOCOL_TLSv1)
+ urls = ['https://www.amazon.com',
+ 'https://www.apple.com',
+ 'https://www.twitter.com']
+ for url in urls:
+ if sys.version_info >= (2, 7):
+ self.assertIsNotNone(http.request(uri=url))
+
+ def testSslProtocolV3AndShouldFailDueToPoodle(self):
+ http = httplib2.Http(ssl_version=ssl.PROTOCOL_SSLv3)
+ urls = ['https://www.amazon.com',
+ 'https://www.apple.com',
+ 'https://www.twitter.com']
+ for url in urls:
+ if sys.version_info >= (2, 7):
+ with self.assertRaises(httplib2.SSLHandshakeError):
+ http.request(url)
+ try:
+ http.request(url)
+ except httplib2.SSLHandshakeError as e:
+ self.assertTrue('sslv3 alert handshake failure' in str(e))
+
+
+if __name__ == '__main__':
+ unittest.main()