diff --git a/CHANGES.rst b/CHANGES.rst index ed5e40777a..c991fd9e03 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -17,6 +17,11 @@ General (GITHUB-556, LIBCLOUD-728) [Scott Kruger] +- Throw a more user-friendly exception if a client fails to establish SSL / TLS + connection with a server because of an unsupported SSL / TLS version. + (GITHUB-682) + [Tomaz Muraus] + Compute ~~~~~~~ diff --git a/libcloud/httplib_ssl.py b/libcloud/httplib_ssl.py index cac4098b49..5f11d05fc8 100644 --- a/libcloud/httplib_ssl.py +++ b/libcloud/httplib_ssl.py @@ -40,6 +40,24 @@ HTTP_PROXY_ENV_VARIABLE_NAME = 'http_proxy' +# Error message which is thrown when establishing SSL / TLS connection fails +UNSUPPORTED_TLS_VERSION_ERROR_MSG = """ +Failed to establish SSL / TLS connection (%s). It is possible that the server \ +doesn't support requested SSL / TLS version (%s). +For information on how to work around this issue, please see \ +https://libcloud.readthedocs.org/en/latest/other/\ +ssl-certificate-validation.html#changing-used-ssl-tls-version +""".strip() + +# Maps ssl.PROTOCOL_* constant to the actual SSL / TLS version name +SSL_CONSTANT_TO_TLS_VERSION_MAP = { + 0: 'SSL v2', + 2: 'SSLv3, TLS v1.0, TLS v1.1, TLS v1.2', + 3: 'TLS v1.0', + 4: 'TLS v1.1', + 5: 'TLS v1.2' +} + class LibcloudBaseConnection(object): """ @@ -272,12 +290,40 @@ def connect(self): if self.http_proxy_used: self._activate_http_proxy(sock=sock) - self.sock = ssl.wrap_socket(sock, - self.key_file, - self.cert_file, - cert_reqs=ssl.CERT_REQUIRED, - ca_certs=self.ca_cert, - ssl_version=libcloud.security.SSL_VERSION) + try: + self.sock = ssl.wrap_socket( + sock, + self.key_file, + self.cert_file, + cert_reqs=ssl.CERT_REQUIRED, + ca_certs=self.ca_cert, + ssl_version=libcloud.security.SSL_VERSION) + except socket.error: + exc = sys.exc_info()[1] + exc_msg = str(exc) + + # Re-throw an exception with a more friendly error message + if 'connection reset by peer' in exc_msg.lower(): + ssl_version = libcloud.security.SSL_VERSION + ssl_version = SSL_CONSTANT_TO_TLS_VERSION_MAP[ssl_version] + msg = (UNSUPPORTED_TLS_VERSION_ERROR_MSG % + (exc_msg, ssl_version)) + + # Note: In some cases arguments are (errno, message) and in + # other it's just (message,) + exc_args = getattr(exc, 'args', []) + + if len(exc_args) == 2: + new_exc_args = [exc.args[0], msg] + else: + new_exc_args = [msg] + + new_exc = socket.error(*new_exc_args) + new_exc.original_exc = exc + raise new_exc + + raise exc + cert = self.sock.getpeercert() try: match_hostname(cert, self.host) diff --git a/libcloud/test/test_httplib_ssl.py b/libcloud/test/test_httplib_ssl.py index 65e676c9d9..28eed69bbe 100644 --- a/libcloud/test/test_httplib_ssl.py +++ b/libcloud/test/test_httplib_ssl.py @@ -16,7 +16,9 @@ import os import sys import os.path +import socket +import mock from mock import patch import libcloud.security @@ -105,6 +107,41 @@ def test_setup_ca_cert(self, _): self.assertRaisesRegexp(RuntimeError, expected_msg, self.httplib_object._setup_ca_cert) + @mock.patch('socket.create_connection', mock.MagicMock()) + @mock.patch('socket.socket', mock.MagicMock()) + @mock.patch('ssl.wrap_socket') + def test_connect_throws_friendly_error_message_on_ssl_wrap_connection_reset_by_peer(self, mock_wrap_socket): + # Test that we re-throw a more friendly error message in case + # "connection reset by peer" error occurs when trying to establish a + # SSL connection + libcloud.security.VERIFY_SSL_CERT = True + self.httplib_object.verify = True + self.httplib_object.http_proxy_used = False + + # No connection reset by peer, original exception should be thrown + mock_wrap_socket.side_effect = Exception('foo bar fail') + + expected_msg = 'foo bar fail' + self.assertRaisesRegexp(Exception, expected_msg, + self.httplib_object.connect) + + # Connection reset by peer, wrapped exception with friendly error + # message should be thrown + mock_wrap_socket.side_effect = socket.error('Connection reset by peer') + + expected_msg = 'Failed to establish SSL / TLS connection' + self.assertRaisesRegexp(socket.error, expected_msg, + self.httplib_object.connect) + + # Same error but including errno + with self.assertRaises(socket.error) as cm: + mock_wrap_socket.side_effect = socket.error(104, 'Connection reset by peer') + self.httplib_object.connect() + + e = cm.exception + self.assertEqual(e.errno, 104) + self.assertTrue(expected_msg in str(e)) + if __name__ == '__main__': sys.exit(unittest.main())