blob: bbcf752055f4259c8b5f191c5bc0e4a2bf8e4da0 [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (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.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Subclass for httplib.HTTPSConnection with optional certificate name
verification, depending on libcloud.security settings.
"""
import os
import sys
import socket
import ssl
import base64
import warnings
import libcloud.security
from libcloud.utils.py3 import b
from libcloud.utils.py3 import httplib
from libcloud.utils.py3 import urlparse
from libcloud.utils.py3 import urlunquote
from libcloud.utils.py3 import match_hostname
from libcloud.utils.py3 import CertificateError
__all__ = [
'LibcloudBaseConnection',
'LibcloudHTTPConnection',
'LibcloudHTTPSConnection'
]
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):
"""
Base connection class to inherit from.
Note: This class should not be instantiated directly.
"""
proxy_scheme = None
proxy_host = None
proxy_port = None
proxy_username = None
proxy_password = None
http_proxy_used = False
def set_http_proxy(self, proxy_url):
"""
Set a HTTP proxy which will be used with this connection.
:param proxy_url: Proxy URL (e.g. http://<hostname>:<port> without
authentication and
http://<username>:<password>@<hostname>:<port> for
basic auth authentication information.
:type proxy_url: ``str``
"""
result = self._parse_proxy_url(proxy_url=proxy_url)
scheme = result[0]
host = result[1]
port = result[2]
username = result[3]
password = result[4]
self.proxy_scheme = scheme
self.proxy_host = host
self.proxy_port = port
self.proxy_username = username
self.proxy_password = password
self.http_proxy_used = True
self._setup_http_proxy()
def _parse_proxy_url(self, proxy_url):
"""
Parse and validate a proxy URL.
:param proxy_url: Proxy URL (e.g. http://hostname:3128)
:type proxy_url: ``str``
:rtype: ``tuple`` (``scheme``, ``hostname``, ``port``)
"""
parsed = urlparse.urlparse(proxy_url)
if parsed.scheme != 'http':
raise ValueError('Only http proxies are supported')
if not parsed.hostname or not parsed.port:
raise ValueError('proxy_url must be in the following format: '
'http://<proxy host>:<proxy port>')
proxy_scheme = parsed.scheme
proxy_host, proxy_port = parsed.hostname, parsed.port
netloc = parsed.netloc
if '@' in netloc:
username_password = netloc.split('@', 1)[0]
split = username_password.split(':', 1)
if len(split) < 2:
raise ValueError('URL is in an invalid format')
proxy_username, proxy_password = split[0], split[1]
else:
proxy_username = None
proxy_password = None
return (proxy_scheme, proxy_host, proxy_port, proxy_username,
proxy_password)
def _setup_http_proxy(self):
"""
Set up HTTP proxy.
:param proxy_url: Proxy URL (e.g. http://<host>:3128)
:type proxy_url: ``str``
"""
headers = {}
if self.proxy_username and self.proxy_password:
# Include authentication header
user_pass = '%s:%s' % (self.proxy_username, self.proxy_password)
encoded = base64.encodestring(b(urlunquote(user_pass))).strip()
auth_header = 'Basic %s' % (encoded.decode('utf-8'))
headers['Proxy-Authorization'] = auth_header
if hasattr(self, 'set_tunnel'):
# Python 2.7 and higher
# pylint: disable=no-member
self.set_tunnel(host=self.host, port=self.port, headers=headers)
elif hasattr(self, '_set_tunnel'):
# Python 2.6
# pylint: disable=no-member
self._set_tunnel(host=self.host, port=self.port, headers=headers)
else:
raise ValueError('Unsupported Python version')
self._set_hostport(host=self.proxy_host, port=self.proxy_port)
def _activate_http_proxy(self, sock):
self.sock = sock
self._tunnel() # pylint: disable=no-member
def _set_hostport(self, host, port):
"""
Backported from Python stdlib so Proxy support also works with
Python 3.4.
"""
if port is None:
i = host.rfind(':')
j = host.rfind(']') # ipv6 addresses have [...]
if i > j:
try:
port = int(host[i + 1:])
except ValueError:
msg = "nonnumeric port: '%s'" % (host[i + 1:])
raise httplib.InvalidURL(msg)
host = host[:i]
else:
port = self.default_port # pylint: disable=no-member
if host and host[0] == '[' and host[-1] == ']':
host = host[1:-1]
self.host = host
self.port = port
class LibcloudHTTPConnection(httplib.HTTPConnection, LibcloudBaseConnection):
def __init__(self, *args, **kwargs):
# Support for HTTP proxy
proxy_url_env = os.environ.get(HTTP_PROXY_ENV_VARIABLE_NAME, None)
proxy_url = kwargs.pop('proxy_url', proxy_url_env)
super(LibcloudHTTPConnection, self).__init__(*args, **kwargs)
if proxy_url:
self.set_http_proxy(proxy_url=proxy_url)
class LibcloudHTTPSConnection(httplib.HTTPSConnection, LibcloudBaseConnection):
"""
LibcloudHTTPSConnection
Subclass of HTTPSConnection which verifies certificate names
if and only if CA certificates are available.
"""
verify = True # verify by default
ca_cert = None # no default CA Certificate
def __init__(self, *args, **kwargs):
"""
Constructor
"""
self._setup_verify()
# Support for HTTP proxy
proxy_url_env = os.environ.get(HTTP_PROXY_ENV_VARIABLE_NAME, None)
proxy_url = kwargs.pop('proxy_url', proxy_url_env)
super(LibcloudHTTPSConnection, self).__init__(*args, **kwargs)
if proxy_url:
self.set_http_proxy(proxy_url=proxy_url)
def _setup_verify(self):
"""
Setup Verify SSL or not
Reads security module's VERIFY_SSL_CERT and toggles whether
the class overrides the connect() class method or runs the
inherited httplib.HTTPSConnection connect()
"""
self.verify = libcloud.security.VERIFY_SSL_CERT
if self.verify:
self._setup_ca_cert()
else:
warnings.warn(libcloud.security.VERIFY_SSL_DISABLED_MSG)
def _setup_ca_cert(self):
"""
Setup CA Certs
Search in CA_CERTS_PATH for valid candidates and
return first match. Otherwise, complain about certs
not being available.
"""
if not self.verify:
return
ca_certs_available = [cert
for cert in libcloud.security.CA_CERTS_PATH
if os.path.exists(cert) and os.path.isfile(cert)]
if ca_certs_available:
# use first available certificate
self.ca_cert = ca_certs_available[0]
else:
raise RuntimeError(
libcloud.security.CA_CERTS_UNAVAILABLE_ERROR_MSG)
def connect(self):
"""
Connect
Checks if verification is toggled; if not, just call
httplib.HTTPSConnection's connect
"""
if not self.verify:
return httplib.HTTPSConnection.connect(self)
# otherwise, create a connection and verify the hostname
# use socket.create_connection (in 2.6+) if possible
if getattr(socket, 'create_connection', None):
sock = socket.create_connection((self.host, self.port),
self.timeout)
else:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.host, self.port))
# Activate the HTTP proxy
if self.http_proxy_used:
self._activate_http_proxy(sock=sock)
ssl_version = libcloud.security.SSL_VERSION
# If we support SNI, use SSLContext and tls_context.wrap_socket()
# else revert to older behaviour with ssl.wrap_socket()
# Note: This feature is only available in Python 2.7.9 and
# Python >= 3.2
has_sni = getattr(ssl, 'HAS_SNI', False)
if has_sni:
self.tls_context = ssl.SSLContext(ssl_version)
self.tls_context.verify_mode = ssl.CERT_REQUIRED
if self.cert_file and self.key_file:
self.tls_context.load_cert_chain(
certfile=self.cert_file,
keyfile=self.key_file,
password=None)
if self.ca_cert:
self.tls_context.load_verify_locations(cafile=self.ca_cert)
try:
self.sock = self.tls_context.wrap_socket(
sock,
server_hostname=self.host,
)
except:
exc = sys.exc_info()[1]
exc = get_socket_error_exception(ssl_version=ssl_version,
exc=exc)
raise exc
else:
# SNI support not available
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=ssl_version
)
except:
exc = sys.exc_info()[1]
exc = get_socket_error_exception(ssl_version=ssl_version,
exc=exc)
raise exc
cert = self.sock.getpeercert()
# Verify Hostname
try:
match_hostname(cert, self.host)
except CertificateError:
e = sys.exc_info()[1]
raise ssl.SSLError('Failed to verify hostname: %s' % (str(e)))
def get_socket_error_exception(ssl_version, exc):
"""
Function which intercepts socket.error exceptions and re-throws an
exception with a more user-friendly message in case server doesn't support
requested SSL version.
"""
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_name = SSL_CONSTANT_TO_TLS_VERSION_MAP[ssl_version]
msg = (UNSUPPORTED_TLS_VERSION_ERROR_MSG %
(exc_msg, ssl_version_name))
# 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
return new_exc
else:
return exc