From fedcfafe7e4753991c5fec68c473fe284f76569b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Honza=20Kr=C3=A1l?= Date: Tue, 11 Nov 2014 03:20:16 +0100 Subject: [PATCH] Proper SSL implementation, including optional certificate verification --- docs/connection.rst | 6 +++--- elasticsearch/connection/http_requests.py | 17 ++++++++++++--- elasticsearch/connection/http_urllib3.py | 25 +++++++++++++++++++---- elasticsearch/exceptions.py | 15 ++++++++++++++ 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/docs/connection.rst b/docs/connection.rst index 2e22f1ee..c7f5c6de 100644 --- a/docs/connection.rst +++ b/docs/connection.rst @@ -36,8 +36,8 @@ Connection Selector :members: -Connection ----------- +Urllib3HttpConnection (default connection_class) +------------------------------------------------ -.. autoclass:: Connection +.. autoclass:: Urllib3HttpConnection :members: diff --git a/elasticsearch/connection/http_requests.py b/elasticsearch/connection/http_requests.py index 372ce375..61760396 100644 --- a/elasticsearch/connection/http_requests.py +++ b/elasticsearch/connection/http_requests.py @@ -6,7 +6,7 @@ except ImportError: REQUESTS_AVAILABLE = False from .base import Connection -from ..exceptions import ConnectionError, ImproperlyConfigured, ConnectionTimeout +from ..exceptions import ConnectionError, ImproperlyConfigured, ConnectionTimeout, SSLError from ..compat import urlencode class RequestsHttpConnection(Connection): @@ -16,8 +16,12 @@ class RequestsHttpConnection(Connection): :arg http_auth: optional http auth information as either ':' separated string or a tuple :arg use_ssl: use ssl for the connection if `True` + :arg verify_certs: whether to verify SSL certificates + :arg ca_certs: optional path to CA bundle. By default standard requests' + bundle will be used. """ - def __init__(self, host='localhost', port=9200, http_auth=None, use_ssl=False, **kwargs): + def __init__(self, host='localhost', port=9200, http_auth=None, + use_ssl=False, verify_certs=False, ca_certs=None, **kwargs): if not REQUESTS_AVAILABLE: raise ImproperlyConfigured("Please install requests to use RequestsHttpConnection.") @@ -32,7 +36,11 @@ class RequestsHttpConnection(Connection): 's' if use_ssl else '', host, port, self.url_prefix ) - + self.session.verify = verify_certs + if ca_certs: + if not verify_certs: + raise ImproperlyConfigured("You cannot pass CA certificates when verify SSL is off.") + self.session.verify = ca_certs def perform_request(self, method, url, params=None, body=None, timeout=None, ignore=()): url = self.base_url + url @@ -44,6 +52,9 @@ class RequestsHttpConnection(Connection): response = self.session.request(method, url, data=body, timeout=timeout or self.timeout) duration = time.time() - start raw_data = response.text + except requests.exceptions.SSLError as e: + self.log_request_fail(method, url, body, time.time() - start, exception=e) + raise SSLError('N/A', str(e), e) except requests.Timeout as e: self.log_request_fail(method, url, body, time.time() - start, exception=e) raise ConnectionTimeout('TIMEOUT', str(e), e) diff --git a/elasticsearch/connection/http_urllib3.py b/elasticsearch/connection/http_urllib3.py index 68207d7b..7829ddd4 100644 --- a/elasticsearch/connection/http_urllib3.py +++ b/elasticsearch/connection/http_urllib3.py @@ -1,9 +1,9 @@ import time import urllib3 -from urllib3.exceptions import ReadTimeoutError +from urllib3.exceptions import ReadTimeoutError, SSLError as UrllibSSLError from .base import Connection -from ..exceptions import ConnectionError, ConnectionTimeout +from ..exceptions import ConnectionError, ImproperlyConfigured, ConnectionTimeout, SSLError from ..compat import urlencode class Urllib3HttpConnection(Connection): @@ -13,10 +13,17 @@ class Urllib3HttpConnection(Connection): :arg http_auth: optional http auth information as either ':' separated string or a tuple :arg use_ssl: use ssl for the connection if `True` + :arg verify_certs: whether to verify SSL certificates + :arg ca_certs: optional path to CA bundle. See + http://urllib3.readthedocs.org/en/latest/security.html#using-certifi-with-urllib3 + for instructions how to get default set :arg maxsize: the maximum number of connections which will be kept open to this host. """ - def __init__(self, host='localhost', port=9200, http_auth=None, use_ssl=False, maxsize=10, **kwargs): + def __init__(self, host='localhost', port=9200, http_auth=None, + use_ssl=False, verify_certs=False, ca_certs=None, maxsize=10, + **kwargs): + super(Urllib3HttpConnection, self).__init__(host=host, port=port, **kwargs) self.headers = {} if http_auth is not None: @@ -25,10 +32,17 @@ class Urllib3HttpConnection(Connection): self.headers = urllib3.make_headers(basic_auth=http_auth) pool_class = urllib3.HTTPConnectionPool + kw = {} if use_ssl: pool_class = urllib3.HTTPSConnectionPool - self.pool = pool_class(host, port=port, timeout=self.timeout, maxsize=maxsize) + if verify_certs: + kw['cert_reqs'] = 'CERT_REQUIRED' + kw['ca_certs'] = ca_certs + elif ca_certs: + raise ImproperlyConfigured("You cannot pass CA certificates when verify SSL is off.") + + self.pool = pool_class(host, port=port, timeout=self.timeout, maxsize=maxsize, **kw) def perform_request(self, method, url, params=None, body=None, timeout=None, ignore=()): url = self.url_prefix + url @@ -50,6 +64,9 @@ class Urllib3HttpConnection(Connection): response = self.pool.urlopen(method, url, body, retries=False, headers=self.headers, **kw) duration = time.time() - start raw_data = response.data.decode('utf-8') + except UrllibSSLError as e: + self.log_request_fail(method, full_url, body, time.time() - start, exception=e) + raise SSLError('N/A', str(e), e) except ReadTimeoutError as e: self.log_request_fail(method, full_url, body, time.time() - start, exception=e) raise ConnectionTimeout('TIMEOUT', str(e), e) diff --git a/elasticsearch/exceptions.py b/elasticsearch/exceptions.py index d62fb5c2..2a98a0fa 100644 --- a/elasticsearch/exceptions.py +++ b/elasticsearch/exceptions.py @@ -8,6 +8,7 @@ class ImproperlyConfigured(Exception): Exception raised when the config passed to the client is inconsistent or invalid. """ + class ElasticsearchException(Exception): """ Base class for all exceptions raised by this package's operations (doesn't @@ -61,6 +62,10 @@ class ConnectionError(TransportError): self.error, self.info.__class__.__name__, self.info) +class SSLError(ConnectionError): + """ Error raised when encountering SSL errors. """ + + class ConnectionTimeout(ConnectionError): """ A network timeout. """ def __str__(self): @@ -79,9 +84,19 @@ class ConflictError(TransportError): class RequestError(TransportError): """ Exception representing a 400 status code. """ + +class AuthenticationException(TransportError): + """ Exception representing a 401 status code. """ + + +class AuthorizationException(TransportError): + """ Exception representing a 403 status code. """ + # more generic mappings from status_code to python exceptions HTTP_EXCEPTIONS = { 400: RequestError, + 401: AuthenticationException, + 403: AuthorizationException, 404: NotFoundError, 409: ConflictError, }