Skip to content

Commit 19a64fa

Browse files
authored
improve(downloader): use a custom HTTP transport adapter instead of rough 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.
2 parents e845838 + 6294252 commit 19a64fa

File tree

2 files changed

+81
-5
lines changed

2 files changed

+81
-5
lines changed

qgis_deployment_toolbelt/utils/file_downloader.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77

88
# standard library
99
import logging
10+
import ssl
1011
import warnings
1112
from os import getenv
1213
from pathlib import Path
1314

1415
# 3rd party
1516
import truststore
1617
from requests import Response, Session
18+
from requests.adapters import HTTPAdapter
1719
from requests.exceptions import ConnectionError, HTTPError
1820
from requests.utils import requote_uri
1921
from urllib3.exceptions import InsecureRequestWarning
@@ -31,9 +33,6 @@
3133
# logs
3234
logger = logging.getLogger(__name__)
3335

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

45+
46+
# ############################################################################
47+
# ########## CLASSES #############
48+
# ################################
49+
50+
51+
class TruststoreAdapter(HTTPAdapter):
52+
"""Custom HTTP transport adapter made to use local trust store.
53+
54+
Source: <https://stackoverflow.com/a/78265028/2556577>
55+
Documentation: <https://requests.readthedocs.io/en/latest/user/advanced/#transport-adapters>
56+
"""
57+
58+
def init_poolmanager(
59+
self, connections: int, maxsize: int, block: bool = False
60+
) -> None:
61+
"""Initializes a urllib3 PoolManager.
62+
63+
Args:
64+
connections (int): number of urllib3 connection pools to cache.
65+
maxsize (int): maximum number of connections to save in the pool.
66+
block (bool, optional): Block when no free connections are available.. Defaults to False.
67+
68+
"""
69+
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
70+
return super().init_poolmanager(connections, maxsize, block, ssl_context=ctx)
71+
72+
4673
# ############################################################################
4774
# ########## FUNCTIONS ###########
4875
# ################################
@@ -68,8 +95,10 @@ def download_remote_file_to_local(
6895
content_type (str | None, optional): HTTP content-type. Defaults to None.
6996
chunk_size (int, optional): size of each chunk to read and write in bytes. \
7097
Defaults to 8192.
71-
timeout (tuple[int, int], optional): custom timeout (request, response). Defaults to (800, 800).
72-
use_stream (bool, optional): Option to enable/disable streaming download. Defaults to True.
98+
timeout (tuple[int, int], optional): custom timeout (request, response). \
99+
Defaults to (800, 800).
100+
use_stream (bool, optional): Option to enable/disable streaming download. \
101+
Defaults to True.
73102
74103
Returns:
75104
Path: path to the local file (should be the same as local_file_path)
@@ -93,6 +122,13 @@ def download_remote_file_to_local(
93122
dl_session.proxies.update(get_proxy_settings())
94123
dl_session.verify = str2bool(getenv("QDT_SSL_VERIFY", True))
95124

125+
# handle local system certificates store
126+
if str2bool(getenv("QDT_SSL_USE_SYSTEM_STORES", False)):
127+
logger.debug(
128+
"Option to use native system certificates stores is enabled."
129+
)
130+
dl_session.mount("https://", TruststoreAdapter())
131+
96132
with dl_session.get(
97133
url=requote_uri(remote_url_to_download), stream=True, timeout=timeout
98134
) as req:

tests/dev/dev_http_network_check.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import ssl
2+
from pathlib import Path
3+
4+
import truststore
5+
from requests import Session
6+
from requests.adapters import HTTPAdapter
7+
from requests.utils import requote_uri
8+
9+
# truststore.inject_into_ssl() # does not fit well package's usage
10+
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
11+
12+
remote_url_to_download: str = (
13+
"https://sigweb-rec.grandlyon.fr/qgis/plugins/dryade_n_tree_creator/version/0.1/download/dryade_n_tree_creator.zip"
14+
)
15+
16+
local_file_path: Path = Path("tests/fixtures/tmp/").joinpath(
17+
remote_url_to_download.split("/")[-1]
18+
)
19+
local_file_path.parent.mkdir(parents=True, exist_ok=True)
20+
21+
22+
class TruststoreAdapter(HTTPAdapter):
23+
"""_summary_
24+
25+
Source: https://stackoverflow.com/a/78265028/2556577
26+
27+
Args:
28+
HTTPAdapter (_type_): _description_
29+
"""
30+
31+
def init_poolmanager(self, connections, maxsize, block=False):
32+
ctx = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
33+
return super().init_poolmanager(connections, maxsize, block, ssl_context=ctx)
34+
35+
36+
with Session() as dl_session:
37+
dl_session.mount("https://", TruststoreAdapter())
38+
with dl_session.get(url=requote_uri(remote_url_to_download)) as req:
39+
req.raise_for_status()
40+
local_file_path.write_bytes(req.content)

0 commit comments

Comments
 (0)