From 7339a272b58536921cc6a4f50d0aa12c67483566 Mon Sep 17 00:00:00 2001 From: Nick Lang Date: Wed, 16 Aug 2017 21:39:32 -0600 Subject: [PATCH] Add SSL context for versions of Python that support SSL Context. Instead of passing parameters manually, you can construct an SSL context. This allows for greater flexibility in using self signed certs. --- .travis.yml | 4 +- docs/connection.rst | 10 +++ elasticsearch/connection/http_urllib3.py | 70 +++++++++++++------ test_elasticsearch/test_connection.py | 23 +++++- test_elasticsearch/test_server/test_common.py | 6 +- 5 files changed, 88 insertions(+), 25 deletions(-) diff --git a/.travis.yml b/.travis.yml index 98c60f31..9c95d79a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,9 +24,9 @@ before_install: - java -version install: - - curl -L -o /tmp/es-snap.zip https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.0.0-beta2.zip + - curl -L -o /tmp/es-snap.zip https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.0.0-rc1.zip - unzip /tmp/es-snap.zip -d /tmp/ - - /tmp/elasticsearch-*/bin/elasticsearch -E script.inline=true -E path.repo=/tmp -E repositories.url.allowed_urls='http://*' -E node.attr.testattr=test -d + - /tmp/elasticsearch-*/bin/elasticsearch -E path.repo=/tmp -E repositories.url.allowed_urls='http://*' -E node.attr.testattr=test -d - git clone https://github.com/elastic/elasticsearch.git ../elasticsearch - pip install . diff --git a/docs/connection.rst b/docs/connection.rst index 1c42f0c2..7f5743e2 100644 --- a/docs/connection.rst +++ b/docs/connection.rst @@ -45,5 +45,15 @@ Connection Selector Urllib3HttpConnection (default connection_class) ------------------------------------------------ +Deprecation Notice: `use_ssl`, `verify_certs`, `ca_certs` and `ssl_version` are being +deprecated in favor of using a `SSLContext` (https://docs.python.org/3/library/ssl.html#ssl.SSLContext) object. + +You can continue to use the deprecated parameters and an `SSLContext` will be created for you. + +If you want to create your own `SSLContext` object you can create one natively using the +python SSL library with the `create_default_context` (https://docs.python.org/3/library/ssl.html#ssl.create_default_context) method +or you can use the wrapper function :function:`~elasticsearch.connection.http_urllib3.create_ssl_context`. + + .. autoclass:: Urllib3HttpConnection :members: diff --git a/elasticsearch/connection/http_urllib3.py b/elasticsearch/connection/http_urllib3.py index 7b4e6c79..879ab685 100644 --- a/elasticsearch/connection/http_urllib3.py +++ b/elasticsearch/connection/http_urllib3.py @@ -1,4 +1,5 @@ import time +import ssl import urllib3 from urllib3.exceptions import ReadTimeoutError, SSLError as UrllibSSLError import warnings @@ -15,6 +16,19 @@ from .base import Connection from ..exceptions import ConnectionError, ImproperlyConfigured, ConnectionTimeout, SSLError from ..compat import urlencode + +def create_ssl_context(**kwargs): + """ + A helper function around creating an SSL context + + https://docs.python.org/3/library/ssl.html#context-creation + + Accepts kwargs in the same manner as `create_default_context`. + """ + ctx = ssl.create_default_context(**kwargs) + return ctx + + class Urllib3HttpConnection(Connection): """ Default connection class using the `urllib3` library and the http protocol. @@ -47,7 +61,7 @@ class Urllib3HttpConnection(Connection): def __init__(self, host='localhost', port=9200, http_auth=None, use_ssl=False, verify_certs=True, ca_certs=None, client_cert=None, client_key=None, ssl_version=None, ssl_assert_hostname=None, - ssl_assert_fingerprint=None, maxsize=10, headers=None, **kwargs): + ssl_assert_fingerprint=None, maxsize=10, headers=None, ssl_context=None, **kwargs): super(Urllib3HttpConnection, self).__init__(host=host, port=port, use_ssl=use_ssl, **kwargs) self.headers = urllib3.make_headers(keep_alive=True) @@ -62,33 +76,49 @@ class Urllib3HttpConnection(Connection): self.headers[k.lower()] = headers[k] self.headers.setdefault('content-type', 'application/json') - ca_certs = CA_CERTS if ca_certs is None else ca_certs pool_class = urllib3.HTTPConnectionPool kw = {} - if use_ssl: + + # if providing an SSL context, raise error if any other SSL related flag is used + if ssl_context and (verify_certs or ca_certs or ssl_version): + raise ImproperlyConfigured("When using `ssl_context`, `use_ssl`, `verify_certs`, `ca_certs` and `ssl_version` are not permitted") + + # if ssl_context provided use SSL by default + if use_ssl or ssl_context: + if not ca_certs and not ssl_context and verify_certs: + # If no ca_certs and no sslcontext passed and asking to verify certs + # raise error + raise ImproperlyConfigured("Root certificates are missing for certificate " + "validation. Either pass them in using the ca_certs parameter or " + "install certifi to use it automatically.") + if verify_certs or ca_certs or ssl_version: + warnings.warn('Use of `verify_certs`, `ca_certs`, `ssl_version` have been deprecated in favor of using SSLContext`', DeprecationWarning) pool_class = urllib3.HTTPSConnectionPool + + if not ssl_context: + # if SSLContext hasn't been passed in, create one. + cafile = CA_CERTS if ca_certs is None else ca_certs + # need to skip if sslContext isn't avail + try: + ssl_context = create_ssl_context(cafile=cafile) + except AttributeError: + ssl_context = None + + if not verify_certs and ssl_context is not None: + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + warnings.warn( + 'Connecting to %s using SSL with verify_certs=False is insecure.' % host) + kw.update({ 'ssl_version': ssl_version, 'assert_hostname': ssl_assert_hostname, 'assert_fingerprint': ssl_assert_fingerprint, + 'ssl_context': ssl_context, + 'cert_file': client_cert, + 'ca_certs': ca_certs, + 'key_file': client_key, }) - - if verify_certs: - if not ca_certs: - raise ImproperlyConfigured("Root certificates are missing for certificate " - "validation. Either pass them in using the ca_certs parameter or " - "install certifi to use it automatically.") - - kw.update({ - 'cert_reqs': 'CERT_REQUIRED', - 'ca_certs': ca_certs, - 'cert_file': client_cert, - 'key_file': client_key, - }) - else: - warnings.warn( - 'Connecting to %s using SSL with verify_certs=False is insecure.' % host) - 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=()): diff --git a/test_elasticsearch/test_connection.py b/test_elasticsearch/test_connection.py index b2d84996..5dd7176c 100644 --- a/test_elasticsearch/test_connection.py +++ b/test_elasticsearch/test_connection.py @@ -1,4 +1,6 @@ +import sys import re +import ssl from mock import Mock, patch import urllib3 import warnings @@ -7,8 +9,9 @@ from requests.auth import AuthBase from elasticsearch.exceptions import TransportError, ConflictError, RequestError, NotFoundError from elasticsearch.connection import RequestsHttpConnection, \ Urllib3HttpConnection - -from .test_cases import TestCase +from elasticsearch.exceptions import ImproperlyConfigured +from elasticsearch.connection.http_urllib3 import create_ssl_context +from .test_cases import TestCase, SkipTest class TestUrllib3Connection(TestCase): @@ -42,6 +45,12 @@ class TestUrllib3Connection(TestCase): 'connection': 'keep-alive'}, con.headers) def test_uses_https_if_verify_certs_is_off(self): + if ( + sys.version_info >= (3,0) and sys.version_info <= (3,4) + ) or ( + sys.version_info >= (2,6) and sys.version_info <= (2,7) + ): + raise SkipTest("SSL Context not supported in this version of python") with warnings.catch_warnings(record=True) as w: con = Urllib3HttpConnection(use_ssl=True, verify_certs=False) self.assertEquals(1, len(w)) @@ -53,6 +62,16 @@ class TestUrllib3Connection(TestCase): con = Urllib3HttpConnection() self.assertIsInstance(con.pool, urllib3.HTTPConnectionPool) + def test_ssl_context_and_depreicated_values(self): + try: + ctx = create_ssl_context() + except AttributeError: + raise SkipTest("SSL Context not supported in this version of python") + self.assertRaises(ImproperlyConfigured, Urllib3HttpConnection, ssl_context=ctx, use_ssl=True) + self.assertRaises(ImproperlyConfigured, Urllib3HttpConnection, ssl_context=ctx, verify_certs=True) + self.assertRaises(ImproperlyConfigured, Urllib3HttpConnection, ssl_context=ctx, ca_certs="/some/path/to/cert.crt") + self.assertRaises(ImproperlyConfigured, Urllib3HttpConnection, ssl_context=ctx, ssl_version=ssl.PROTOCOL_SSLv23) + class TestRequestsConnection(TestCase): def _get_mock_connection(self, connection_params={}, status_code=200, response_body='{}'): con = RequestsHttpConnection(**connection_params) diff --git a/test_elasticsearch/test_server/test_common.py b/test_elasticsearch/test_server/test_common.py index 6c0a896c..dbb94d41 100644 --- a/test_elasticsearch/test_server/test_common.py +++ b/test_elasticsearch/test_server/test_common.py @@ -40,7 +40,11 @@ SKIP_TESTS = { '*': set(('TestBulk10Basic', 'TestCatSnapshots10Basic', 'TestClusterPutSettings10Basic', 'TestIndex10WithId', 'TestClusterPutScript10Basic', 'TestIndicesPutMapping10Basic', - 'TestIndicesPutTemplate10Basic', 'TestMsearch10Basic')) + 'TestIndicesPutTemplate10Basic', 'TestMsearch10Basic', + # skip these two till https://github.com/elastic/elasticsearch/pull/26905 has been merged + 'TestSearchAggregation190PercentilesHdrMetric', + 'TestSearchAggregation180PercentilesTdigestMetric', + )) }