From 68a21929a3a459ff5091b9f7957f10aa6919d262 Mon Sep 17 00:00:00 2001 From: Gerard Weatherby Date: Mon, 18 Aug 2025 15:55:06 -0400 Subject: [PATCH] Add HTTPAdapter to allow connection to old servers with archaic SSL handshakes Signed-off-by: Gerard Weatherby --- examples/http_adapter.py | 68 ++++++++++++++++++++++++++++++++++++++++ src/redfish/rest/v1.py | 29 +++++++++++------ 2 files changed, 87 insertions(+), 10 deletions(-) create mode 100755 examples/http_adapter.py diff --git a/examples/http_adapter.py b/examples/http_adapter.py new file mode 100755 index 0000000..c07e018 --- /dev/null +++ b/examples/http_adapter.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +import argparse +from pathlib import Path +from typing import Optional +import ssl +from requests.adapters import HTTPAdapter +from urllib3.poolmanager import PoolManager + +from redfish import redfish_client + + +def make_dhe_compatible_context( + cafile: Optional[Path] = None, + *, + seclevel: int = 1, + verify: bool = True, + tls12_only: bool = True, +) -> ssl.SSLContext: + """ + Build an SSLContext that accepts legacy DHE handshakes (small DH groups). + - seclevel=1 usually permits 1024-bit DH. Use 0 only as a last resort. + - If verify=True and the server is self-signed, pass its PEM as `cafile`. + - DHE is TLS<=1.2; set tls12_only=True to pin TLS 1.2. + """ + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.set_ciphers(f"DEFAULT:@SECLEVEL={seclevel}:DHE") + ctx.options |= ssl.OP_NO_COMPRESSION + if tls12_only: + ctx.minimum_version = ssl.TLSVersion.TLSv1_2 + ctx.maximum_version = ssl.TLSVersion.TLSv1_2 + + if verify: + if cafile: + ctx.load_verify_locations(str(cafile)) + else: + ctx.load_default_certs() + else: + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + return ctx + +class SSLContextAdapter(HTTPAdapter): + """requests adapter that injects a custom ssl_context into urllib3.""" + def __init__(self, ssl_context: ssl.SSLContext, **kwargs): + self._ssl_context = ssl_context + super().__init__(**kwargs) + + def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs): + pool_kwargs["ssl_context"] = self._ssl_context + self.poolmanager = PoolManager( + num_pools=connections, maxsize=maxsize, block=block, **pool_kwargs + ) + + def proxy_manager_for(self, proxy, **proxy_kwargs): + proxy_kwargs["ssl_context"] = self._ssl_context + return super().proxy_manager_for(proxy, **proxy_kwargs) + + +# Test dhe adapter +ctx = make_dhe_compatible_context(seclevel=0, verify=False) +adapter = SSLContextAdapter(ctx) +parser = argparse.ArgumentParser( ) +parser.add_argument('url',help="Server with DHE encryption") +args = parser.parse_args() + +client = redfish_client(args.url,https_adapter=adapter) + + diff --git a/src/redfish/rest/v1.py b/src/redfish/rest/v1.py index 698db02..4969926 100644 --- a/src/redfish/rest/v1.py +++ b/src/redfish/rest/v1.py @@ -468,7 +468,8 @@ class RestClientBase(object): def __init__(self, base_url, username=None, password=None, default_prefix='/redfish/v1/', sessionkey=None, capath=None, cafile=None, timeout=None, - max_retry=None, proxies=None, check_connectivity=True): + max_retry=None, proxies=None, check_connectivity=True, + https_adapter = None): """Initialization of the base class RestClientBase :param base_url: The URL of the remote system @@ -494,7 +495,7 @@ def __init__(self, base_url, username=None, password=None, :param check_connectivity: A boolean to determine whether the client immediately checks for connectivity to the base_url or not. :type check_connectivity: bool - + :type https_adapter: requests.adpaters.HTTPAdapter """ self.__base_url = base_url.rstrip('/') @@ -507,6 +508,8 @@ def __init__(self, base_url, username=None, password=None, self._session = requests_unixsocket.Session() else: self._session = requests.Session() + if https_adapter: + self._session.mount('https://',https_adapter) self._timeout = timeout self._max_retry = max_retry if max_retry is not None else 10 self._proxies = proxies @@ -1079,7 +1082,8 @@ def __init__(self, base_url, username=None, password=None, default_prefix='/redfish/v1/', sessionkey=None, capath=None, cafile=None, timeout=None, - max_retry=None, proxies=None, check_connectivity=True): + max_retry=None, proxies=None, check_connectivity=True, + https_adapter=None): """Initialize HttpClient :param base_url: The url of the remote system @@ -1105,14 +1109,15 @@ def __init__(self, base_url, username=None, password=None, :param check_connectivity: A boolean to determine whether the client immediately checks for connectivity to the base_url or not. :type check_connectivity: bool - + :param https_adapter session adapter for HTTPS + :type https_adapter: requests.adpaters.HTTPAdapter """ super(HttpClient, self).__init__(base_url, username=username, password=password, default_prefix=default_prefix, sessionkey=sessionkey, capath=capath, cafile=cafile, timeout=timeout, max_retry=max_retry, proxies=proxies, - check_connectivity=check_connectivity) + check_connectivity=check_connectivity,https_adapter=https_adapter) try: self.login_url = self.root.Links.Sessions['@odata.id'] @@ -1169,7 +1174,8 @@ def redfish_client(base_url=None, username=None, password=None, default_prefix='/redfish/v1/', sessionkey=None, capath=None, cafile=None, timeout=None, - max_retry=None, proxies=None, check_connectivity=True): + max_retry=None, proxies=None, check_connectivity=True, + https_adapter=None): """Create and return appropriate REDFISH client instance.""" """ Instantiates appropriate Redfish object based on existing""" """ configuration. Use this to retrieve a pre-configured Redfish object @@ -1196,14 +1202,17 @@ def redfish_client(base_url=None, username=None, password=None, :type proxies: dict :param check_connectivity: A boolean to determine whether the client immediately checks for connectivity to the base_url or not. - :type check_connectivity: bool + :type check_connectivity: bo#ol + :param https_adapter session adapter for HTTPS + :type https_adapter: requests.adpaters.HTTPAdapter :returns: a client object. """ if "://" not in base_url: - warnings.warn("Scheme not specified for '{}'; adding 'https://'".format(base_url)) - base_url = "https://" + base_url + warnings.warn("Scheme not specified for '{}'; adding 'https://'".format(base_url)) + base_url = "https://" + base_url return HttpClient(base_url=base_url, username=username, password=password, default_prefix=default_prefix, sessionkey=sessionkey, capath=capath, cafile=cafile, timeout=timeout, - max_retry=max_retry, proxies=proxies, check_connectivity=check_connectivity) + max_retry=max_retry, proxies=proxies, check_connectivity=check_connectivity, + https_adapter=https_adapter)