From 588c4bdd73bdd5e0d9f259e2438af7ef9dd23b20 Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sun, 15 Feb 2015 18:04:52 -0500 Subject: [PATCH] Add ssl_options argument for simple_httpclient. --- tornado/curl_httpclient.py | 3 +++ tornado/httpclient.py | 15 ++++++++++++--- tornado/simple_httpclient.py | 2 ++ tornado/test/simple_httpclient_test.py | 23 +++++++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/tornado/curl_httpclient.py b/tornado/curl_httpclient.py index bced9499e..87312de23 100644 --- a/tornado/curl_httpclient.py +++ b/tornado/curl_httpclient.py @@ -417,6 +417,9 @@ class CurlAsyncHTTPClient(AsyncHTTPClient): if request.client_key is not None: curl.setopt(pycurl.SSLKEY, request.client_key) + if request.ssl_options is not None: + raise ValueError("ssl_options not supported in curl_httpclient") + if threading.activeCount() > 1: # libcurl/pycurl is not thread-safe by default. When multiple threads # are used, signals should be disabled. This has the side effect diff --git a/tornado/httpclient.py b/tornado/httpclient.py index 0ae9e4802..87b21437d 100644 --- a/tornado/httpclient.py +++ b/tornado/httpclient.py @@ -310,7 +310,8 @@ class HTTPRequest(object): validate_cert=None, ca_certs=None, allow_ipv6=None, client_key=None, client_cert=None, body_producer=None, - expect_100_continue=False, decompress_response=None): + expect_100_continue=False, decompress_response=None, + ssl_options=None): r"""All parameters except ``url`` are optional. :arg string url: URL to fetch @@ -380,12 +381,16 @@ class HTTPRequest(object): :arg string ca_certs: filename of CA certificates in PEM format, or None to use defaults. See note below when used with ``curl_httpclient``. - :arg bool allow_ipv6: Use IPv6 when available? Default is false in - ``simple_httpclient`` and true in ``curl_httpclient`` :arg string client_key: Filename for client SSL key, if any. See note below when used with ``curl_httpclient``. :arg string client_cert: Filename for client SSL certificate, if any. See note below when used with ``curl_httpclient``. + :arg SSLContext ssl_options: `ssl.SSLContext` object for use in + ``simple_httpclient`` (unsupported by ``curl_httpclient``). + Overrides ``validate_cert``, ``ca_certs``, ``client_key``, + and ``client_cert``. + :arg bool allow_ipv6: Use IPv6 when available? Default is false in + ``simple_httpclient`` and true in ``curl_httpclient`` :arg bool expect_100_continue: If true, send the ``Expect: 100-continue`` header and wait for a continue response before sending the request body. Only supported with @@ -408,6 +413,9 @@ class HTTPRequest(object): .. versionadded:: 4.0 The ``body_producer`` and ``expect_100_continue`` arguments. + + .. verisonadded:: 4.2 + The ``ssl_options`` argument. """ # Note that some of these attributes go through property setters # defined below. @@ -445,6 +453,7 @@ class HTTPRequest(object): self.allow_ipv6 = allow_ipv6 self.client_key = client_key self.client_cert = client_cert + self.ssl_options = ssl_options self.expect_100_continue = expect_100_continue self.start_time = time.time() diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index 13400207a..ed9f7387e 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -220,6 +220,8 @@ class _HTTPConnection(httputil.HTTPMessageDelegate): def _get_ssl_options(self, scheme): if scheme == "https": + if self.request.ssl_options is not None: + return self.request.ssl_options ssl_options = {} if self.request.validate_cert: ssl_options["cert_reqs"] = ssl.CERT_REQUIRED diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py index bb870db3b..a852168e1 100644 --- a/tornado/test/simple_httpclient_test.py +++ b/tornado/test/simple_httpclient_test.py @@ -8,6 +8,7 @@ import logging import os import re import socket +import ssl import sys from tornado import gen @@ -432,6 +433,28 @@ class SimpleHTTPSClientTestCase(SimpleHTTPClientTestMixin, AsyncHTTPSTestCase): defaults=dict(validate_cert=False), **kwargs) + def test_ssl_options(self): + resp = self.fetch("/hello", ssl_options={}) + self.assertEqual(resp.body, b"Hello world!") + + def test_ssl_context(self): + resp = self.fetch("/hello", + ssl_options=ssl.SSLContext(ssl.PROTOCOL_SSLv23)) + self.assertEqual(resp.body, b"Hello world!") + + def test_ssl_options_handshake_fail(self): + with ExpectLog(gen_log, "SSL Error|Uncaught exception"): + resp = self.fetch( + "/hello", ssl_options=dict(cert_reqs=ssl.CERT_REQUIRED)) + self.assertRaises(ssl.SSLError, resp.rethrow) + + def test_ssl_context_handshake_fail(self): + with ExpectLog(gen_log, "SSL Error|Uncaught exception"): + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx.verify_mode = ssl.CERT_REQUIRED + resp = self.fetch("/hello", ssl_options=ctx) + self.assertRaises(ssl.SSLError, resp.rethrow) + class CreateAsyncHTTPClientTestCase(AsyncTestCase): def setUp(self): -- 2.47.2