Repository: streamlink/streamlink

Issue:
TestSession.test_http_disable_dh fails with 5.5.0
### Checklist

- [X] This is a bug report and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed bug reports](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22bug%22)
- [X] [I have checked the commit log of the master branch](https://github.com/streamlink/streamlink/commits/master)

### Streamlink version

Latest stable release

### Description

Since 5.5.0 TestSession.test_http_disable_dh started to fail. The urllib3 version is 1.26.15, output below under debug log.


### Debug log

```text
================================================================================= FAILURES =================================================================================
_____________________________________________________________________ TestSession.test_http_disable_dh _____________________________________________________________________

self = <tests.test_session.TestSession testMethod=test_http_disable_dh>

    def test_http_disable_dh(self):
        session = self.subject(load_plugins=False)
        assert isinstance(session.http.adapters["https://"], HTTPAdapter)
        assert not isinstance(session.http.adapters["https://"], TLSNoDHAdapter)
    
>       session.set_option("http-disable-dh", True)

tests/test_session.py:404: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
pep517_tests_streamlink/usr/x86_64-pc-linux-gnu/lib/python3.10/site-packages/streamlink/session.py:493: in set_option
    self.options.set(key, value)
pep517_tests_streamlink/usr/x86_64-pc-linux-gnu/lib/python3.10/site-packages/streamlink/options.py:54: in set
    method(self, normalized, value)
pep517_tests_streamlink/usr/x86_64-pc-linux-gnu/lib/python3.10/site-packages/streamlink/session.py:119: in _set_http_disable_dh
    adapter = TLSNoDHAdapter()
/usr/lib/python3.10/site-packages/requests/adapters.py:155: in __init__
    self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <streamlink.plugin.api.http_session.TLSNoDHAdapter object at 0x7f04fbe22d10>, args = (10, 10), kwargs = {'block': False}

    def init_poolmanager(self, *args, **kwargs):
>       ctx = urllib3.util.create_urllib3_context()
E       AttributeError: module 'urllib3.util' has no attribute 'create_urllib3_context'

pep517_tests_streamlink/usr/x86_64-pc-linux-gnu/lib/python3.10/site-packages/streamlink/plugin/api/http_session.py:191: AttributeError
========================================================================= short test summary info ==========================================================================
FAILED tests/test_session.py::TestSession::test_http_disable_dh - AttributeError: module 'urllib3.util' has no attribute 'create_urllib3_context'
=============================================================== 1 failed, 5642 passed, 37 skipped in 22.51s ================================================================
```



Discussion:
Yep, my bad. Our CI tests don't run on urllib3 1.26.x, so this wasn't caught. I also force-pushed a couple of times in #5326 with changes to the SSLContext initialization and then apparently didn't locally run tests again with 1.26.15.

The `create_urllib3_context` method doesn't get re-exported in the `urllib3.util.__init__` module on <2.0.0. The docs don't mention it:
- https://urllib3.readthedocs.io/en/stable/advanced-usage.html#custom-ssl-contexts
- https://github.com/urllib3/urllib3/blob/1.26.0/src/urllib3/util/__init__.py
- https://github.com/urllib3/urllib3/blob/2.0.0/src/urllib3/util/__init__.py

Resolving patch:
diff --git a/src/streamlink/plugin/api/http_session.py b/src/streamlink/plugin/api/http_session.py
--- a/src/streamlink/plugin/api/http_session.py
+++ b/src/streamlink/plugin/api/http_session.py
@@ -13,6 +13,13 @@
 from streamlink.utils.parse import parse_json, parse_xml
 
 
+try:
+    from urllib3.util import create_urllib3_context  # type: ignore[attr-defined]
+except ImportError:  # pragma: no cover
+    # urllib3 <2.0.0 compat import
+    from urllib3.util.ssl_ import create_urllib3_context
+
+
 # urllib3>=2.0.0: enforce_content_length now defaults to True (keep the override for backwards compatibility)
 class _HTTPResponse(urllib3.response.HTTPResponse):
     def __init__(self, *args, **kwargs):
@@ -188,7 +195,7 @@ def request(self, method, url, *args, **kwargs):
 
 class TLSNoDHAdapter(HTTPAdapter):
     def init_poolmanager(self, *args, **kwargs):
-        ctx = urllib3.util.create_urllib3_context()
+        ctx = create_urllib3_context()
         ctx.load_default_certs()
         ciphers = ":".join(cipher.get("name") for cipher in ctx.get_ciphers())
         ciphers += ":!DH"
@@ -199,7 +206,7 @@ def init_poolmanager(self, *args, **kwargs):
 
 class TLSSecLevel1Adapter(HTTPAdapter):
     def init_poolmanager(self, *args, **kwargs):
-        ctx = urllib3.util.create_urllib3_context()
+        ctx = create_urllib3_context()
         ctx.load_default_certs()
         ctx.set_ciphers("DEFAULT:@SECLEVEL=1")
         kwargs["ssl_context"] = ctx


Test patch: