From ff6ae776b9509f3cdaa2bf2832e9438897373a79 Mon Sep 17 00:00:00 2001 From: GeoJulien Date: Fri, 23 Oct 2026 06:38:06 +0200 Subject: [PATCH] improve(network): always check HTTPS certificates when it comes to retrieve remote images length --- docs/configuration.md | 14 ++++++++++++++ mkdocs_rss_plugin/util.py | 27 +++++++++++++++------------ tests/test_rss_util.py | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c95b99d..4871896 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -127,6 +127,20 @@ Output: ``` + +!!! note "Remote images served over HTTPS" + Since version 1.20, TLS certificates are always verified when retrieving a remote image length. If verification fails, the build goes on but the image length cannot be determined, just as for an unreachable image. + + To trust a custom certificate authority (corporate proxy, self-signed internal server...), point `REQUESTS_CA_BUNDLE` to a bundle that includes it. This variable **replaces** the default bundle, so append your certificate to it rather than using it alone: + + ```sh + cat "$(python -c 'import certifi; print(certifi.where())')" my-ca.pem > ca-bundle.pem + REQUESTS_CA_BUNDLE="$PWD/ca-bundle.pem" mkdocs build + ``` + + Responses are cached in [`cache_dir`](#cache_dir): clear it to force new requests. + + ---- ## Plugin options diff --git a/mkdocs_rss_plugin/util.py b/mkdocs_rss_plugin/util.py index 0f6d852..65ca6e2 100644 --- a/mkdocs_rss_plugin/util.py +++ b/mkdocs_rss_plugin/util.py @@ -15,7 +15,6 @@ # 3rd party import markdown -import urllib3 from cachecontrol import CacheControl from cachecontrol.caches.file_cache import SeparateBodyFileCache from git import ( @@ -30,7 +29,8 @@ from mkdocs.structure.pages import Page from mkdocs.utils import get_build_datetime from requests import Session -from requests.exceptions import ConnectionError, HTTPError +from requests.exceptions import ConnectionError as RequestsConnectionError +from requests.exceptions import HTTPError, SSLError # package from mkdocs_rss_plugin.constants import ( @@ -53,7 +53,6 @@ # ################################ logger = get_plugin_logger(MKDOCS_LOGGER_NAME) -urllib3.disable_warnings() # disable warnings for unverified requests # ############################################################################ # ########## Classes ############# @@ -690,12 +689,12 @@ def get_remote_image_length( http_method: str = "HEAD", attempt: int = 0, req_timeout: tuple[float, float] = (5, 30), - ssl_verify: bool = True, ) -> Optional[int]: """Retrieve length for remote images (starting with 'http'). Firstly, it tries to perform a HEAD request and get the length from the headers. \ - If it fails, it tries again with a GET and disabling SSL verification. + If it fails, it tries again with a GET. TLS certificate verification is always \ + enforced: a TLS error is never retried. Args: image_url (str): image URL @@ -704,8 +703,6 @@ def get_remote_image_length( attempt (int, optional): request tries counter. Defaults to 0. req_timeout (tuple[float, float], optional): (connect, read) timeout in \ secondes. Defaults to (5, 30). - ssl_verify (bool, optional): option to perform SSL verification or not. - Defaults to True. Returns: int | None: image length as int or None @@ -724,19 +721,25 @@ def get_remote_image_length( method=http_method, timeout=req_timeout, url=image_url, - verify=ssl_verify, ) req_response.raise_for_status() img_length = req_response.headers.get("content-length") - except (ConnectionError, HTTPError) as err: + except SSLError as err: + # SSLError subclasses RequestsConnectionError: catch it first and never + # retry, a failed certificate check must not be worked around. + logger.info( + f"Remote image skipped: TLS verification failed for {image_url}. " + f"Trace: {err}" + ) + return None + except (RequestsConnectionError, HTTPError) as err: logger.debug( f"Remote image could not been reached: {image_url}. " - f"Trying again with {http_method} and disabling SSL verification. " - f"Attempt: {attempt}/2. Trace: {err}" + f"Trying again with GET. Attempt: {attempt}/2. Trace: {err}" ) if attempt < 2: return self.get_remote_image_length( - image_url, http_method="GET", attempt=attempt, ssl_verify=False + image_url, http_method="GET", attempt=attempt ) else: logger.info( diff --git a/tests/test_rss_util.py b/tests/test_rss_util.py index 30b6188..3c45686 100644 --- a/tests/test_rss_util.py +++ b/tests/test_rss_util.py @@ -16,8 +16,10 @@ # Standard library import unittest from pathlib import Path +from unittest.mock import patch # 3rd party +from requests.exceptions import SSLError from validator_collection import checkers # plugin target @@ -95,6 +97,19 @@ def test_remote_image_none(self): ) self.assertIsNone(img_length) + def test_remote_image_ssl_error_not_retried(self): + """TLS failures must not be retried, especially without verification.""" + with patch.object( + self.plg_utils.req_session, "request", side_effect=SSLError("bad cert") + ) as mock_request: + img_length = self.plg_utils.get_remote_image_length( + image_url="https://self-signed.example.org/image.png" + ) + + self.assertIsNone(img_length) + mock_request.assert_called_once() + self.assertNotIn("verify", mock_request.call_args.kwargs) + def test_get_value_from_dot_key(self): param_list = [ {