diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 4fde5aaf..c790dfea 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -10,6 +10,8 @@ This project adheres to `Semantic Versioning `_.
-------------------------
* Added ``Content-Type`` of files uploaded in ``multipart/form-data`` requests
+* Added ``--ssl=`` to specify SSL/TLS the desired protocol version
+ to use for HTTPS requests.
* Added ``--show-redirects, -R`` to show intermediate responses with ``--follow``
* Added ``--max-redirects`` (default 30)
* Added ``-A`` as short name for ``--auth-type``
diff --git a/README.rst b/README.rst
index 4e64d0c0..8784fa36 100644
--- a/README.rst
+++ b/README.rst
@@ -730,6 +730,22 @@ path of the key file with ``--cert-key``:
$ http --cert=client.crt --cert-key=client.key https://example.org
+-----------
+SSL version
+-----------
+
+Use the ``--ssl=`` to specify the desired protocol version to use.
+This will default to SSL v2.3 which will negotiate the highest protocol that both
+the server and your installation of OpenSSL support. The available protocols
+are ``ssl2.3``, ``ssl3``, ``tls1``, ``tls1.1``, ```tls1.2``. (The actually
+available set of protocols may vary depending your on OpenSSL installation.)
+
+.. code-block:: bash
+
+ # Specify the vulnerable SSL v3 protocol to talk to an outdated server:
+ $ http --ssl=ssl3 https://vulnerable.example.org
+
+
----------------------------
SNI (Server Name Indication)
----------------------------
diff --git a/httpie/cli.py b/httpie/cli.py
index 6e4e2be3..771126cb 100644
--- a/httpie/cli.py
+++ b/httpie/cli.py
@@ -20,7 +20,7 @@ from httpie.input import (HTTPieArgumentParser,
OUT_RESP_BODY, OUTPUT_OPTIONS,
OUTPUT_OPTIONS_DEFAULT, PRETTY_MAP,
PRETTY_STDOUT_TTY_ONLY, SessionNameValidator,
- readable_file_arg)
+ readable_file_arg, SSL_VERSION_ARG_MAPPING)
class HTTPieHelpFormatter(RawDescriptionHelpFormatter):
@@ -485,29 +485,6 @@ network.add_argument(
"""
)
-network.add_argument(
- '--cert',
- default=None,
- type=readable_file_arg,
- help="""
- You can specify a local cert to use as client side SSL certificate.
- This file may either contain both private key and certificate or you may
- specify --cert-key separately.
-
- """
-)
-
-network.add_argument(
- '--cert-key',
- default=None,
- type=readable_file_arg,
- help="""
- The private key to use with SSL. Only needed if --cert is given and the
- certificate file does not contain the private key.
-
- """
-)
-
network.add_argument(
'--timeout',
type=float,
@@ -537,6 +514,46 @@ network.add_argument(
)
+#######################################################################
+# SSL
+#######################################################################
+
+ssl = parser.add_argument_group(title='SSL')
+ssl.add_argument(
+ '--ssl', # TODO: Maybe something more general, such as --secure-protocol?
+ dest='ssl_version',
+ choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())),
+ help="""
+ The desired protocol version to use. This will default to
+ SSL v2.3 which will negotiate the highest protocol that both
+ the server and your installation of OpenSSL support. Available protocols
+ may vary depending on OpenSSL installation (only the supported ones
+ are shown here).
+ """
+)
+ssl.add_argument(
+ '--cert',
+ default=None,
+ type=readable_file_arg,
+ help="""
+ You can specify a local cert to use as client side SSL certificate.
+ This file may either contain both private key and certificate or you may
+ specify --cert-key separately.
+
+ """
+)
+
+ssl.add_argument(
+ '--cert-key',
+ default=None,
+ type=readable_file_arg,
+ help="""
+ The private key to use with SSL. Only needed if --cert is given and the
+ certificate file does not contain the private key.
+
+ """
+)
+
#######################################################################
# Troubleshooting
#######################################################################
diff --git a/httpie/client.py b/httpie/client.py
index 4ff4efb8..8c0b6fe8 100644
--- a/httpie/client.py
+++ b/httpie/client.py
@@ -3,11 +3,13 @@ import sys
from pprint import pformat
import requests
+from requests.adapters import HTTPAdapter
from requests.packages import urllib3
from httpie import sessions
from httpie import __version__
from httpie.compat import str
+from httpie.input import SSL_VERSION_ARG_MAPPING
from httpie.plugins import plugin_manager
@@ -27,8 +29,23 @@ JSON = 'application/json'
DEFAULT_UA = 'HTTPie/%s' % __version__
-def get_requests_session():
+class HTTPieHTTPAdapter(HTTPAdapter):
+
+ def __init__(self, ssl_version=None, **kwargs):
+ self._ssl_version = ssl_version
+ super(HTTPieHTTPAdapter, self).__init__(**kwargs)
+
+ def init_poolmanager(self, *args, **kwargs):
+ kwargs['ssl_version'] = self._ssl_version
+ super(HTTPieHTTPAdapter, self).init_poolmanager(*args, **kwargs)
+
+
+def get_requests_session(ssl_version):
requests_session = requests.Session()
+ requests_session.mount(
+ 'https://',
+ HTTPieHTTPAdapter(ssl_version=ssl_version)
+ )
for cls in plugin_manager.get_transport_plugins():
transport_plugin = cls()
requests_session.mount(prefix=transport_plugin.prefix,
@@ -38,7 +55,12 @@ def get_requests_session():
def get_response(args, config_dir):
"""Send the request and return a `request.Response`."""
- requests_session = get_requests_session()
+
+ ssl_version = None
+ if args.ssl_version:
+ ssl_version = SSL_VERSION_ARG_MAPPING[args.ssl_version]
+
+ requests_session = get_requests_session(ssl_version)
requests_session.max_redirects = args.max_redirects
if not args.session and not args.session_read_only:
diff --git a/httpie/input.py b/httpie/input.py
index 232e2d9e..96c7945a 100644
--- a/httpie/input.py
+++ b/httpie/input.py
@@ -2,6 +2,7 @@
"""
import os
+import ssl
import sys
import re
import errno
@@ -103,6 +104,20 @@ OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
+SSL_VERSION_ARG_MAPPING = {
+ 'ssl2.3': 'PROTOCOL_SSLv23',
+ 'ssl3': 'PROTOCOL_SSLv3',
+ 'tls1': 'PROTOCOL_TLSv1',
+ 'tls1.1': 'PROTOCOL_TLSv1_1',
+ 'tls1.2': 'PROTOCOL_TLSv1_2',
+}
+SSL_VERSION_ARG_MAPPING = dict(
+ (cli_arg, getattr(ssl, ssl_constant))
+ for cli_arg, ssl_constant in SSL_VERSION_ARG_MAPPING.items()
+ if hasattr(ssl, ssl_constant)
+)
+
+
class HTTPieArgumentParser(ArgumentParser):
"""Adds additional logic to `argparse.ArgumentParser`.
diff --git a/tests/test_ssl.py b/tests/test_ssl.py
index ac2fed3b..b2c7108a 100644
--- a/tests/test_ssl.py
+++ b/tests/test_ssl.py
@@ -5,6 +5,7 @@ import pytest_httpbin.certs
from requests.exceptions import SSLError
from httpie import ExitStatus
+from httpie.input import SSL_VERSION_ARG_MAPPING
from utils import http, HTTP_OK, TESTS_ROOT
@@ -18,6 +19,26 @@ CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
CA_BUNDLE = pytest_httpbin.certs.where()
+@pytest.mark.parametrize(
+ argnames='ssl_version',
+ argvalues=SSL_VERSION_ARG_MAPPING.keys()
+)
+def test_ssl_version(httpbin_secure, ssl_version):
+ try:
+ r = http(
+ '--verify', CA_BUNDLE,
+ '--ssl', ssl_version,
+ httpbin_secure + '/get'
+ )
+ assert HTTP_OK in r
+ except SSLError as e:
+ if ssl_version == 'ssl3':
+ # pytest-httpbin doesn't support ssl3
+ assert 'SSLV3_ALERT_HANDSHAKE_FAILURE' in str(e)
+ else:
+ raise
+
+
class TestClientCert:
def test_cert_and_key(self, httpbin_secure):