From dc4da527db3bbc3c9907ee0f72f5f48d3d4b4e2a Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Wed, 2 Mar 2016 12:12:05 +0800 Subject: [PATCH] Added --ssl= Closes #98 --- CHANGELOG.rst | 2 ++ README.rst | 16 ++++++++++++ httpie/cli.py | 65 ++++++++++++++++++++++++++++++----------------- httpie/client.py | 26 +++++++++++++++++-- httpie/input.py | 15 +++++++++++ tests/test_ssl.py | 21 +++++++++++++++ 6 files changed, 119 insertions(+), 26 deletions(-) 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):