Skip to content

Commit

Permalink
improve(downloader): use a custom HTTP transport adapter instead of r…
Browse files Browse the repository at this point in the history
…ough injection (#549)

This PR follows up:

- #544
- #444

It improve how system's store certificates are used, preferring a custom
HTTP adapter to the SSL injection.

It's mainly inspired from https://stackoverflow.com/a/78265028/2556577.
  • Loading branch information
Guts authored Sep 4, 2024
2 parents e845838 + 6294252 commit 19a64fa
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 5 deletions.
46 changes: 41 additions & 5 deletions qgis_deployment_toolbelt/utils/file_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

# standard library
import logging
import ssl
import warnings
from os import getenv
from pathlib import Path

# 3rd party
import truststore
from requests import Response, Session
from requests.adapters import HTTPAdapter
from requests.exceptions import ConnectionError, HTTPError
from requests.utils import requote_uri
from urllib3.exceptions import InsecureRequestWarning
Expand All @@ -31,9 +33,6 @@
# logs
logger = logging.getLogger(__name__)

if str2bool(getenv("QDT_SSL_USE_SYSTEM_STORES", False)):
truststore.inject_into_ssl()
logger.debug("Option to use native system certificates stores is enabled.")
if not str2bool(getenv("QDT_SSL_VERIFY", True)):
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
logger.warning(
Expand All @@ -43,6 +42,34 @@
"See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#tls-warnings"
)


# ############################################################################
# ########## CLASSES #############
# ################################


class TruststoreAdapter(HTTPAdapter):
"""Custom HTTP transport adapter made to use local trust store.
Source: <https://stackoverflow.com/a/78265028/2556577>
Documentation: <https://requests.readthedocs.io/en/latest/user/advanced/#transport-adapters>
"""

def init_poolmanager(
self, connections: int, maxsize: int, block: bool = False
) -> None:
"""Initializes a urllib3 PoolManager.
Args:
connections (int): number of urllib3 connection pools to cache.
maxsize (int): maximum number of connections to save in the pool.
block (bool, optional): Block when no free connections are available.. Defaults to False.
"""
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
return super().init_poolmanager(connections, maxsize, block, ssl_context=ctx)


# ############################################################################
# ########## FUNCTIONS ###########
# ################################
Expand All @@ -68,8 +95,10 @@ def download_remote_file_to_local(
content_type (str | None, optional): HTTP content-type. Defaults to None.
chunk_size (int, optional): size of each chunk to read and write in bytes. \
Defaults to 8192.
timeout (tuple[int, int], optional): custom timeout (request, response). Defaults to (800, 800).
use_stream (bool, optional): Option to enable/disable streaming download. Defaults to True.
timeout (tuple[int, int], optional): custom timeout (request, response). \
Defaults to (800, 800).
use_stream (bool, optional): Option to enable/disable streaming download. \
Defaults to True.
Returns:
Path: path to the local file (should be the same as local_file_path)
Expand All @@ -93,6 +122,13 @@ def download_remote_file_to_local(
dl_session.proxies.update(get_proxy_settings())
dl_session.verify = str2bool(getenv("QDT_SSL_VERIFY", True))

# handle local system certificates store
if str2bool(getenv("QDT_SSL_USE_SYSTEM_STORES", False)):
logger.debug(
"Option to use native system certificates stores is enabled."
)
dl_session.mount("https://", TruststoreAdapter())

with dl_session.get(
url=requote_uri(remote_url_to_download), stream=True, timeout=timeout
) as req:
Expand Down
40 changes: 40 additions & 0 deletions tests/dev/dev_http_network_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import ssl
from pathlib import Path

import truststore
from requests import Session
from requests.adapters import HTTPAdapter
from requests.utils import requote_uri

# truststore.inject_into_ssl() # does not fit well package's usage
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

remote_url_to_download: str = (
"https://sigweb-rec.grandlyon.fr/qgis/plugins/dryade_n_tree_creator/version/0.1/download/dryade_n_tree_creator.zip"
)

local_file_path: Path = Path("tests/fixtures/tmp/").joinpath(
remote_url_to_download.split("/")[-1]
)
local_file_path.parent.mkdir(parents=True, exist_ok=True)


class TruststoreAdapter(HTTPAdapter):
"""_summary_
Source: https://stackoverflow.com/a/78265028/2556577
Args:
HTTPAdapter (_type_): _description_
"""

def init_poolmanager(self, connections, maxsize, block=False):
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
return super().init_poolmanager(connections, maxsize, block, ssl_context=ctx)


with Session() as dl_session:
dl_session.mount("https://", TruststoreAdapter())
with dl_session.get(url=requote_uri(remote_url_to_download)) as req:
req.raise_for_status()
local_file_path.write_bytes(req.content)

0 comments on commit 19a64fa

Please sign in to comment.