Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throw a more friendly error message if establishing SSL / TLS connection fails #682

Merged
merged 9 commits into from Jan 14, 2016
5 changes: 5 additions & 0 deletions CHANGES.rst
Expand Up @@ -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
~~~~~~~

Expand Down
58 changes: 52 additions & 6 deletions libcloud/httplib_ssl.py
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions libcloud/test/test_httplib_ssl.py
Expand Up @@ -16,7 +16,9 @@
import os
import sys
import os.path
import socket

import mock
from mock import patch

import libcloud.security
Expand Down Expand Up @@ -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())