Add support for --ciphers (#870)

This commit is contained in:
Jakub Roztocil 2020-05-23 13:26:06 +02:00
parent 0a81facccf
commit d62d6a77d1
7 changed files with 115 additions and 42 deletions

View File

@ -9,6 +9,7 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
`2.2.0-dev`_ (unreleased)
-------------------------
* Added support for ``--ciphers`` (`#870`_).
* Added support for ``$XDG_CONFIG_HOME`` (`#920`_).
@ -433,5 +434,6 @@ This project adheres to `Semantic Versioning <https://semver.org/>`_.
.. _#488: https://github.com/jakubroztocil/httpie/issues/488
.. _#840: https://github.com/jakubroztocil/httpie/issues/840
.. _#870: https://github.com/jakubroztocil/httpie/issues/870
.. _#895: https://github.com/jakubroztocil/httpie/issues/895
.. _#920: https://github.com/jakubroztocil/httpie/issues/920

View File

@ -1092,11 +1092,13 @@ path of the key file with ``--cert-key``:
SSL version
-----------
Use the ``--ssl=<PROTOCOL>`` 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``, ``tls1.3``. (The actually
available set of protocols may vary depending on your OpenSSL installation.)
Use the ``--ssl=<PROTOCOL>`` option 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``, ``tls1.3``.
(The actually available set of protocols may vary depending on your OpenSSL
installation.)
.. code-block:: bash
@ -1104,6 +1106,26 @@ available set of protocols may vary depending on your OpenSSL installation.)
$ http --ssl=ssl3 https://vulnerable.example.org
SSL ciphers
-----------
You can specify the available ciphers with ``--ciphers``.
It should be a string in the
`OpenSSL cipher list format <https://www.openssl.org/docs/man1.1.0/man1/ciphers.html>`_.
.. code-block:: bash
$ http --ciphers=ECDHE-RSA-AES128-GCM-SHA256 https://httpbin.org/get
Note: these cipher strings do not change the negotiated version of SSL or TLS,
they only affect the list of available cipher suites.
To see the default cipher string, run ``http --help`` and see
the ``--ciphers`` section under SSL.
Output options
==============

View File

@ -87,17 +87,3 @@ PRETTY_STDOUT_TTY_ONLY = object()
OUTPUT_OPTIONS_DEFAULT = OUT_RESP_HEAD + OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_STDOUT_REDIRECTED = OUT_RESP_BODY
OUTPUT_OPTIONS_DEFAULT_OFFLINE = OUT_REQ_HEAD + OUT_REQ_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',
'tls1.3': 'PROTOCOL_TLSv1_3',
}
SSL_VERSION_ARG_MAPPING = {
cli_arg: getattr(ssl, ssl_constant)
for cli_arg, ssl_constant in SSL_VERSION_ARG_MAPPING.items()
if hasattr(ssl, ssl_constant)
}

View File

@ -13,7 +13,7 @@ from httpie.cli.argtypes import (
from httpie.cli.constants import (
OUTPUT_OPTIONS, OUTPUT_OPTIONS_DEFAULT, OUT_REQ_BODY, OUT_REQ_HEAD,
OUT_RESP_BODY, OUT_RESP_HEAD, PRETTY_MAP, PRETTY_STDOUT_TTY_ONLY,
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY, SSL_VERSION_ARG_MAPPING,
SEPARATOR_GROUP_ALL_ITEMS, SEPARATOR_PROXY,
)
from httpie.output.formatters.colors import (
AUTO_STYLE, AVAILABLE_STYLES, DEFAULT_STYLE,
@ -21,6 +21,7 @@ from httpie.output.formatters.colors import (
from httpie.plugins import plugin_manager
from httpie.plugins.builtin import BuiltinAuthPlugin
from httpie.sessions import DEFAULT_SESSIONS_DIR
from httpie.ssl import DEFAULT_SSL_CIPHERS, AVAILABLE_SSL_VERSION_ARG_MAPPING
parser = HTTPieArgumentParser(
@ -580,7 +581,7 @@ ssl.add_argument(
ssl.add_argument(
'--ssl', # TODO: Maybe something more general, such as --secure-protocol?
dest='ssl_version',
choices=list(sorted(SSL_VERSION_ARG_MAPPING.keys())),
choices=list(sorted(AVAILABLE_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
@ -590,6 +591,17 @@ ssl.add_argument(
"""
)
ssl.add_argument(
'--ciphers',
help=f"""
A string in the OpenSSL cipher list format. By default, the following
is used:
{DEFAULT_SSL_CIPHERS}
"""
)
ssl.add_argument(
'--cert',
default=None,

View File

@ -9,13 +9,12 @@ from typing import Iterable, Union
from urllib.parse import urlparse, urlunparse
import requests
from requests.adapters import HTTPAdapter
from httpie import __version__
from httpie.cli.constants import SSL_VERSION_ARG_MAPPING
from httpie.cli.dicts import RequestHeadersDict
from httpie.plugins import plugin_manager
from httpie.sessions import get_httpie_session
from httpie.ssl import HTTPieHTTPSAdapter, AVAILABLE_SSL_VERSION_ARG_MAPPING
from httpie.utils import repr_dict
@ -57,6 +56,7 @@ def collect_messages(
send_kwargs_mergeable_from_env = make_send_kwargs_mergeable_from_env(args)
requests_session = build_requests_session(
ssl_version=args.ssl_version,
ciphers=args.ciphers,
)
if httpie_session:
@ -121,6 +121,7 @@ def collect_messages(
@contextmanager
def max_headers(limit):
# <https://github.com/jakubroztocil/httpie/issues/802>
# noinspection PyUnresolvedReferences
orig = http.client._MAXHEADERS
http.client._MAXHEADERS = limit or float('Inf')
try:
@ -145,26 +146,17 @@ def compress_body(request: requests.PreparedRequest, always: bool):
request.headers['Content-Length'] = str(len(deflated_data))
class HTTPieHTTPSAdapter(HTTPAdapter):
def __init__(self, ssl_version=None, **kwargs):
self._ssl_version = ssl_version
super().__init__(**kwargs)
def init_poolmanager(self, *args, **kwargs):
kwargs['ssl_version'] = self._ssl_version
super().init_poolmanager(*args, **kwargs)
def build_requests_session(
ssl_version: str = None,
ciphers: str = None,
) -> requests.Session:
requests_session = requests.Session()
# Install our adapter.
requests_session.mount('https://', HTTPieHTTPSAdapter(
ciphers=ciphers,
ssl_version=(
SSL_VERSION_ARG_MAPPING[ssl_version]
AVAILABLE_SSL_VERSION_ARG_MAPPING[ssl_version]
if ssl_version else None
)
))

38
httpie/ssl.py Normal file
View File

@ -0,0 +1,38 @@
import ssl
from requests.adapters import HTTPAdapter
# noinspection PyPackageRequirements
from urllib3.util.ssl_ import DEFAULT_CIPHERS, create_urllib3_context
DEFAULT_SSL_CIPHERS = DEFAULT_CIPHERS
SSL_VERSION_ARG_MAPPING = {
'ssl2.3': 'PROTOCOL_SSLv23',
'ssl3': 'PROTOCOL_SSLv3',
'tls1': 'PROTOCOL_TLSv1',
'tls1.1': 'PROTOCOL_TLSv1_1',
'tls1.2': 'PROTOCOL_TLSv1_2',
'tls1.3': 'PROTOCOL_TLSv1_3',
}
AVAILABLE_SSL_VERSION_ARG_MAPPING = {
arg: getattr(ssl, constant_name)
for arg, constant_name in SSL_VERSION_ARG_MAPPING.items()
if hasattr(ssl, constant_name)
}
class HTTPieHTTPSAdapter(HTTPAdapter):
def __init__(self, ssl_version: str = None, ciphers: str = None, **kwargs):
self._ssl_context: ssl.SSLContext = create_urllib3_context(
ciphers=ciphers,
ssl_version=ssl_version
)
super().__init__(**kwargs)
def init_poolmanager(self, *args, **kwargs):
kwargs['ssl_context'] = self._ssl_context
return super().init_poolmanager(*args, **kwargs)
def proxy_manager_for(self, *args, **kwargs):
kwargs['ssl_context'] = self._ssl_context
return super().proxy_manager_for(*args, **kwargs)

View File

@ -1,11 +1,9 @@
import os
import pytest
import pytest_httpbin.certs
import requests.exceptions
from httpie.ssl import AVAILABLE_SSL_VERSION_ARG_MAPPING, DEFAULT_SSL_CIPHERS
from httpie.status import ExitStatus
from httpie.cli.constants import SSL_VERSION_ARG_MAPPING
from utils import HTTP_OK, TESTS_ROOT, http
@ -23,10 +21,12 @@ except ImportError:
requests.exceptions.SSLError,
)
CERTS_ROOT = TESTS_ROOT / 'client_certs'
CLIENT_CERT = str(CERTS_ROOT / 'client.crt')
CLIENT_KEY = str(CERTS_ROOT / 'client.key')
CLIENT_PEM = str(CERTS_ROOT / 'client.pem')
CLIENT_CERT = os.path.join(TESTS_ROOT, 'client_certs', 'client.crt')
CLIENT_KEY = os.path.join(TESTS_ROOT, 'client_certs', 'client.key')
CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
# FIXME:
# We test against a local httpbin instance which uses a self-signed cert.
# Requests without --verify=<CA_BUNDLE> will fail with a verification error.
@ -34,7 +34,8 @@ CLIENT_PEM = os.path.join(TESTS_ROOT, 'client_certs', 'client.pem')
CA_BUNDLE = pytest_httpbin.certs.where()
@pytest.mark.parametrize('ssl_version', SSL_VERSION_ARG_MAPPING.keys())
@pytest.mark.parametrize('ssl_version',
AVAILABLE_SSL_VERSION_ARG_MAPPING.keys())
def test_ssl_version(httpbin_secure, ssl_version):
try:
r = http(
@ -113,3 +114,23 @@ class TestServerCert:
def test_verify_custom_ca_bundle_invalid_bundle(self, httpbin_secure):
with pytest.raises(ssl_errors):
http(httpbin_secure.url + '/get', '--verify', __file__)
def test_ciphers(httpbin_secure):
r = http(
httpbin_secure.url + '/get',
'--ciphers',
DEFAULT_SSL_CIPHERS,
)
assert HTTP_OK in r
def test_ciphers_none_can_be_selected(httpbin_secure):
r = http(
httpbin_secure.url + '/get',
'--ciphers',
'__FOO__',
tolerate_error_exit_status=True,
)
assert r.exit_status == ExitStatus.ERROR
assert 'No cipher can be selected.' in r.stderr