Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve(downloader): use a custom HTTP transport adapter instead of rough injection #549

Merged
merged 1 commit into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)