Skip to content

Commit a720175

Browse files
authored
Merge pull request #98 from pmo73/disable-ssl-verify
feat(html2pdf4doc): Add option to disable SSL certificate check
2 parents 48caca7 + 46c1361 commit a720175

3 files changed

Lines changed: 180 additions & 13 deletions

File tree

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,22 @@ in the `.github/workflows` folder.
2424

2525
## Usage
2626

27-
TBD
27+
Download or reuse ChromeDriver with SSL verification enabled by default:
28+
29+
```bash
30+
python -m html2pdf4doc.main get_driver
31+
python -m html2pdf4doc.main print input.html output.pdf
32+
```
33+
34+
### Disable SSL (corporate environments)
35+
36+
Disable SSL certificate verification only when needed, for example in a
37+
restricted corporate environment with custom TLS interception:
38+
39+
```bash
40+
python -m html2pdf4doc.main get_driver --disable-ssl-check
41+
python -m html2pdf4doc.main print --disable-ssl-check input.html output.pdf
42+
```
2843

2944
## Developer guide
3045

html2pdf4doc/main.py

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import re
88
import subprocess
99
import sys
10+
import warnings
1011
import zipfile
1112
from datetime import datetime
1213
from enum import IntEnum
@@ -21,6 +22,7 @@
2122
from selenium.common import SessionNotCreatedException
2223
from selenium.webdriver.chrome.options import Options
2324
from selenium.webdriver.chrome.service import Service
25+
from urllib3.exceptions import InsecureRequestWarning
2426
from webdriver_manager.core.os_manager import ChromeType, OperationSystemManager
2527

2628
from . import (
@@ -37,6 +39,8 @@
3739
# https://stackoverflow.com/questions/3597480/how-to-make-python-3-print-utf8
3840
sys.stdout = open(sys.stdout.fileno(), mode="w", encoding="utf8", closefd=False)
3941

42+
SSL_CHECK_DISABLED_WARNING_PRINTED = False
43+
4044

4145
@contextlib.contextmanager
4246
def measure_performance(title: str) -> Iterator[None]:
@@ -60,6 +64,23 @@ def extract_page_count(logs: List[Dict[str, str]]) -> int:
6064
raise ValueError("No page count found in logs.")
6165

6266

67+
def print_ssl_check_disabled_warning() -> None:
68+
global SSL_CHECK_DISABLED_WARNING_PRINTED # noqa: PLW0603
69+
70+
if SSL_CHECK_DISABLED_WARNING_PRINTED:
71+
return
72+
73+
print( # noqa: T201
74+
"warning: html2pdf4doc: SSL certificate verification is disabled "
75+
"for HTTP downloads. This is insecure and should only be used "
76+
"in trusted environments. Re-enable verification by removing "
77+
"--disable-ssl-check.",
78+
file=sys.stderr,
79+
flush=True,
80+
)
81+
SSL_CHECK_DISABLED_WARNING_PRINTED = True
82+
83+
6384
class HPDExitCode(IntEnum):
6485
GENERAL_ERROR = 1
6586
COULD_NOT_FIND_CHROME = 5
@@ -96,7 +117,9 @@ def __str__(self) -> str:
96117

97118

98119
class ChromeDriverManager:
99-
def get_chrome_driver(self, path_to_cache_dir: str) -> str:
120+
def get_chrome_driver(
121+
self, path_to_cache_dir: str, verify_ssl: bool = True
122+
) -> str:
100123
chrome_version: Optional[str] = self.get_chrome_version()
101124

102125
# If Web Driver Manager cannot detect Chrome, it returns None.
@@ -153,6 +176,7 @@ def get_chrome_driver(self, path_to_cache_dir: str) -> str:
153176
os_type,
154177
path_to_cached_chrome_driver_dir,
155178
path_to_cached_chrome_driver,
179+
verify_ssl,
156180
)
157181
assert os.path.isfile(path_to_downloaded_chrome_driver)
158182
os.chmod(path_to_downloaded_chrome_driver, 0o755)
@@ -166,9 +190,10 @@ def _download_chromedriver(
166190
os_type: str,
167191
path_to_driver_cache_dir: str,
168192
path_to_cached_chrome_driver: str,
193+
verify_ssl: bool = True,
169194
) -> str:
170195
url = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"
171-
response = cls.send_http_get_request(url)
196+
response = cls.send_http_get_request(url, verify_ssl=verify_ssl)
172197
if response is None:
173198
raise RuntimeError(
174199
"Could not download known-good-versions-with-downloads.json"
@@ -210,7 +235,7 @@ def _download_chromedriver(
210235
print( # noqa: T201
211236
f"html2pdf4doc: downloading ChromeDriver from: {driver_url}"
212237
)
213-
response = cls.send_http_get_request(driver_url)
238+
response = cls.send_http_get_request(driver_url, verify_ssl=verify_ssl)
214239

215240
if response is None:
216241
raise RuntimeError(
@@ -234,22 +259,34 @@ def _download_chromedriver(
234259
return path_to_cached_chrome_driver
235260

236261
@staticmethod
237-
def send_http_get_request(url: str) -> Response:
262+
def send_http_get_request(url: str, verify_ssl: bool = True) -> Response:
238263
last_error: Optional[Exception] = None
239264
for attempt in range(1, 4):
240265
print( # noqa: T201
241266
f"html2pdf4doc: sending GET request attempt {attempt}: {url}"
242267
)
243268
try:
244-
return requests.get(url, timeout=(5, 5))
269+
if verify_ssl:
270+
return requests.get(url, timeout=(5, 5), verify=True)
271+
272+
print_ssl_check_disabled_warning()
273+
with warnings.catch_warnings():
274+
warnings.simplefilter("ignore", InsecureRequestWarning)
275+
return requests.get(url, timeout=(5, 5), verify=False)
276+
except requests.exceptions.SSLError as ssl_error_:
277+
raise RuntimeError(
278+
"SSL certificate verification failed for URL: "
279+
f"{url}. If you trust the target and need to bypass "
280+
"certificate verification, rerun the command with "
281+
"--disable-ssl-check."
282+
) from ssl_error_
245283
except requests.exceptions.ConnectTimeout as connect_timeout_:
246284
last_error = connect_timeout_
247285
except requests.exceptions.ReadTimeout as read_timeout_:
248286
last_error = read_timeout_
249-
except Exception as exception_:
250-
raise AssertionError(
251-
"html2pdf4doc: unknown exception", exception_
252-
) from None
287+
except requests.exceptions.RequestException as request_error_:
288+
last_error = request_error_
289+
break
253290
print( # noqa: T201
254291
f"html2pdf4doc: "
255292
f"failed to get response for URL: {url} with error: {last_error}"
@@ -423,14 +460,15 @@ def create_webdriver(
423460
chromedriver_argument: Optional[str],
424461
path_to_cache_dir: str,
425462
page_load_timeout: int,
463+
verify_ssl: bool = True,
426464
debug: bool = False,
427465
) -> webdriver.Chrome:
428466
print("html2pdf4doc: Creating ChromeDriver service.", flush=True) # noqa: T201
429467

430468
path_to_chrome_driver: str
431469
if chromedriver_argument is None:
432470
path_to_chrome_driver = chrome_driver_manager.get_chrome_driver(
433-
path_to_cache_dir
471+
path_to_cache_dir, verify_ssl=verify_ssl
434472
)
435473
else:
436474
path_to_chrome_driver = chromedriver_argument
@@ -545,6 +583,14 @@ def _main() -> None:
545583
type=str,
546584
help="Optional path to a cache directory whereto the ChromeDriver is downloaded.",
547585
)
586+
command_parser_get_driver.add_argument(
587+
"--disable-ssl-check",
588+
action="store_true",
589+
help=(
590+
"Disables SSL certificate verification for HTTP downloads. "
591+
"By default SSL certificate verification is enabled."
592+
),
593+
)
548594

549595
#
550596
# Print command.
@@ -564,6 +610,14 @@ def _main() -> None:
564610
type=str,
565611
help="Optional path to a cache directory whereto the ChromeDriver is downloaded.",
566612
)
613+
command_parser_print.add_argument(
614+
"--disable-ssl-check",
615+
action="store_true",
616+
help=(
617+
"Disables SSL certificate verification for HTTP downloads. "
618+
"By default SSL certificate verification is enabled."
619+
),
620+
)
567621
command_parser_print.add_argument(
568622
"--page-load-timeout",
569623
# 60 minutes should be enough to print even the largest documents.
@@ -620,7 +674,8 @@ def _main() -> None:
620674
)
621675

622676
path_to_chrome = chrome_driver_manager.get_chrome_driver(
623-
path_to_cache_dir
677+
path_to_cache_dir,
678+
verify_ssl=not args.disable_ssl_check,
624679
)
625680
print(f"html2pdf4doc: ChromeDriver available at path: {path_to_chrome}") # noqa: T201
626681
sys.exit(0)
@@ -638,6 +693,7 @@ def _main() -> None:
638693
args.chromedriver,
639694
path_to_cache_dir,
640695
page_load_timeout,
696+
verify_ssl=not args.disable_ssl_check,
641697
debug=args.debug,
642698
)
643699

tests/unit/test_chrome_driver_manager.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import tempfile
2-
from typing import Optional
2+
from typing import Any, Dict, Optional
33

44
import pytest
5+
import requests
56

7+
import html2pdf4doc.main as main_module
68
from html2pdf4doc.main import ChromeDriverManager, HPDError, HPDExitCode
79

810

@@ -25,3 +27,97 @@ def test_raises_error_when_cannot_detect_chrome() -> None:
2527

2628
assert exc_info.type is HPDError
2729
assert exc_info.value.exit_code == HPDExitCode.COULD_NOT_FIND_CHROME
30+
31+
32+
def test_send_http_get_request_uses_ssl_verification_by_default(
33+
monkeypatch: pytest.MonkeyPatch,
34+
) -> None:
35+
captured_kwargs: Dict[str, Any] = {}
36+
37+
def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
38+
del args
39+
captured_kwargs.update(kwargs)
40+
return requests.Response()
41+
42+
monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)
43+
44+
ChromeDriverManager.send_http_get_request("https://example.com")
45+
46+
assert captured_kwargs["verify"] is True
47+
48+
49+
def test_send_http_get_request_can_disable_ssl_verification(
50+
monkeypatch: pytest.MonkeyPatch,
51+
) -> None:
52+
captured_kwargs: Dict[str, Any] = {}
53+
54+
def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
55+
del args
56+
captured_kwargs.update(kwargs)
57+
return requests.Response()
58+
59+
monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)
60+
61+
ChromeDriverManager.send_http_get_request(
62+
"https://example.com", verify_ssl=False
63+
)
64+
65+
assert captured_kwargs["verify"] is False
66+
67+
68+
def test_send_http_get_request_warns_once_when_ssl_check_disabled(
69+
monkeypatch: pytest.MonkeyPatch,
70+
capsys: pytest.CaptureFixture[str],
71+
) -> None:
72+
def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
73+
del args, kwargs
74+
return requests.Response()
75+
76+
monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)
77+
monkeypatch.setattr(
78+
main_module, "SSL_CHECK_DISABLED_WARNING_PRINTED", False
79+
)
80+
81+
ChromeDriverManager.send_http_get_request(
82+
"https://example.com", verify_ssl=False
83+
)
84+
ChromeDriverManager.send_http_get_request(
85+
"https://example.com", verify_ssl=False
86+
)
87+
88+
captured = capsys.readouterr()
89+
assert captured.err.count("--disable-ssl-check") == 1
90+
91+
92+
def test_send_http_get_request_does_not_warn_when_ssl_enabled(
93+
monkeypatch: pytest.MonkeyPatch,
94+
capsys: pytest.CaptureFixture[str],
95+
) -> None:
96+
def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
97+
del args, kwargs
98+
return requests.Response()
99+
100+
monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)
101+
monkeypatch.setattr(
102+
main_module, "SSL_CHECK_DISABLED_WARNING_PRINTED", False
103+
)
104+
105+
ChromeDriverManager.send_http_get_request("https://example.com")
106+
107+
captured = capsys.readouterr()
108+
assert captured.err == ""
109+
110+
111+
def test_send_http_get_request_reports_ssl_hint(
112+
monkeypatch: pytest.MonkeyPatch,
113+
) -> None:
114+
def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
115+
del args, kwargs
116+
raise requests.exceptions.SSLError("certificate verify failed")
117+
118+
monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)
119+
120+
with pytest.raises(RuntimeError) as exc_info:
121+
ChromeDriverManager.send_http_get_request("https://example.com")
122+
123+
assert "--disable-ssl-check" in str(exc_info.value)

0 commit comments

Comments
 (0)