Skip to content
Open
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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,19 @@ in the `.github/workflows` folder.

## Usage

TBD
Download or reuse ChromeDriver with SSL verification enabled by default:

```bash
python -m html2pdf4doc.main get_driver
```

Disable SSL certificate verification only when needed, for example in a
restricted corporate environment with custom TLS interception:

```bash
python -m html2pdf4doc.main get_driver --disable-ssl-check
python -m html2pdf4doc.main print --disable-ssl-check input.html output.pdf
```

## Developer guide

Expand Down
78 changes: 67 additions & 11 deletions html2pdf4doc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import re
import subprocess
import sys
import warnings
import zipfile
from datetime import datetime
from enum import IntEnum
Expand All @@ -21,6 +22,7 @@
from selenium.common import SessionNotCreatedException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from urllib3.exceptions import InsecureRequestWarning
from webdriver_manager.core.os_manager import ChromeType, OperationSystemManager

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

SSL_CHECK_DISABLED_WARNING_PRINTED = False


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


def print_ssl_check_disabled_warning() -> None:
global SSL_CHECK_DISABLED_WARNING_PRINTED

if SSL_CHECK_DISABLED_WARNING_PRINTED:
return

print(
"warning: html2pdf4doc: SSL certificate verification is disabled "
"for HTTP downloads. This is insecure and should only be used "
"in trusted environments. Re-enable verification by removing "
"--disable-ssl-check.",
file=sys.stderr,
flush=True,
)
SSL_CHECK_DISABLED_WARNING_PRINTED = True


class HPDExitCode(IntEnum):
GENERAL_ERROR = 1
COULD_NOT_FIND_CHROME = 5
Expand Down Expand Up @@ -96,7 +117,9 @@ def __str__(self) -> str:


class ChromeDriverManager:
def get_chrome_driver(self, path_to_cache_dir: str) -> str:
def get_chrome_driver(
self, path_to_cache_dir: str, verify_ssl: bool = True
) -> str:
chrome_version: Optional[str] = self.get_chrome_version()

# If Web Driver Manager cannot detect Chrome, it returns None.
Expand Down Expand Up @@ -153,6 +176,7 @@ def get_chrome_driver(self, path_to_cache_dir: str) -> str:
os_type,
path_to_cached_chrome_driver_dir,
path_to_cached_chrome_driver,
verify_ssl,
)
assert os.path.isfile(path_to_downloaded_chrome_driver)
os.chmod(path_to_downloaded_chrome_driver, 0o755)
Expand All @@ -166,9 +190,10 @@ def _download_chromedriver(
os_type: str,
path_to_driver_cache_dir: str,
path_to_cached_chrome_driver: str,
verify_ssl: bool = True,
) -> str:
url = "https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json"
response = cls.send_http_get_request(url)
response = cls.send_http_get_request(url, verify_ssl=verify_ssl)
if response is None:
raise RuntimeError(
"Could not download known-good-versions-with-downloads.json"
Expand Down Expand Up @@ -210,7 +235,7 @@ def _download_chromedriver(
print( # noqa: T201
f"html2pdf4doc: downloading ChromeDriver from: {driver_url}"
)
response = cls.send_http_get_request(driver_url)
response = cls.send_http_get_request(driver_url, verify_ssl=verify_ssl)

if response is None:
raise RuntimeError(
Expand All @@ -234,22 +259,34 @@ def _download_chromedriver(
return path_to_cached_chrome_driver

@staticmethod
def send_http_get_request(url: str) -> Response:
def send_http_get_request(url: str, verify_ssl: bool = True) -> Response:
last_error: Optional[Exception] = None
for attempt in range(1, 4):
print( # noqa: T201
f"html2pdf4doc: sending GET request attempt {attempt}: {url}"
)
try:
return requests.get(url, timeout=(5, 5))
if verify_ssl:
return requests.get(url, timeout=(5, 5), verify=True)

print_ssl_check_disabled_warning()
with warnings.catch_warnings():
warnings.simplefilter("ignore", InsecureRequestWarning)
return requests.get(url, timeout=(5, 5), verify=False)
except requests.exceptions.SSLError as ssl_error_:
raise RuntimeError(
"SSL certificate verification failed for URL: "
f"{url}. If you trust the target and need to bypass "
"certificate verification, rerun the command with "
"--disable-ssl-check."
) from ssl_error_
except requests.exceptions.ConnectTimeout as connect_timeout_:
last_error = connect_timeout_
except requests.exceptions.ReadTimeout as read_timeout_:
last_error = read_timeout_
except Exception as exception_:
raise AssertionError(
"html2pdf4doc: unknown exception", exception_
) from None
except requests.exceptions.RequestException as request_error_:
last_error = request_error_
break
print( # noqa: T201
f"html2pdf4doc: "
f"failed to get response for URL: {url} with error: {last_error}"
Expand Down Expand Up @@ -423,14 +460,15 @@ def create_webdriver(
chromedriver_argument: Optional[str],
path_to_cache_dir: str,
page_load_timeout: int,
verify_ssl: bool = True,
debug: bool = False,
) -> webdriver.Chrome:
print("html2pdf4doc: Creating ChromeDriver service.", flush=True) # noqa: T201

path_to_chrome_driver: str
if chromedriver_argument is None:
path_to_chrome_driver = chrome_driver_manager.get_chrome_driver(
path_to_cache_dir
path_to_cache_dir, verify_ssl=verify_ssl
)
else:
path_to_chrome_driver = chromedriver_argument
Expand Down Expand Up @@ -545,6 +583,14 @@ def _main() -> None:
type=str,
help="Optional path to a cache directory whereto the ChromeDriver is downloaded.",
)
command_parser_get_driver.add_argument(
"--disable-ssl-check",
action="store_true",
help=(
"Disables SSL certificate verification for HTTP downloads. "
"By default SSL certificate verification is enabled."
),
)

#
# Print command.
Expand All @@ -564,6 +610,14 @@ def _main() -> None:
type=str,
help="Optional path to a cache directory whereto the ChromeDriver is downloaded.",
)
command_parser_print.add_argument(
"--disable-ssl-check",
action="store_true",
help=(
"Disables SSL certificate verification for HTTP downloads. "
"By default SSL certificate verification is enabled."
),
)
command_parser_print.add_argument(
"--page-load-timeout",
# 60 minutes should be enough to print even the largest documents.
Expand Down Expand Up @@ -620,7 +674,8 @@ def _main() -> None:
)

path_to_chrome = chrome_driver_manager.get_chrome_driver(
path_to_cache_dir
path_to_cache_dir,
verify_ssl=not args.disable_ssl_check,
)
print(f"html2pdf4doc: ChromeDriver available at path: {path_to_chrome}") # noqa: T201
sys.exit(0)
Expand All @@ -638,6 +693,7 @@ def _main() -> None:
args.chromedriver,
path_to_cache_dir,
page_load_timeout,
verify_ssl=not args.disable_ssl_check,
debug=args.debug,
)

Expand Down
98 changes: 97 additions & 1 deletion tests/unit/test_chrome_driver_manager.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import tempfile
from typing import Optional
from typing import Any, Dict, Optional

import html2pdf4doc.main as main_module
import pytest
import requests

from html2pdf4doc.main import ChromeDriverManager, HPDError, HPDExitCode

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

assert exc_info.type is HPDError
assert exc_info.value.exit_code == HPDExitCode.COULD_NOT_FIND_CHROME


def test_send_http_get_request_uses_ssl_verification_by_default(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured_kwargs: Dict[str, Any] = {}

def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
del args
captured_kwargs.update(kwargs)
return requests.Response()

monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)

ChromeDriverManager.send_http_get_request("https://example.com")

assert captured_kwargs["verify"] is True


def test_send_http_get_request_can_disable_ssl_verification(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured_kwargs: Dict[str, Any] = {}

def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
del args
captured_kwargs.update(kwargs)
return requests.Response()

monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)

ChromeDriverManager.send_http_get_request(
"https://example.com", verify_ssl=False
)

assert captured_kwargs["verify"] is False


def test_send_http_get_request_warns_once_when_ssl_check_disabled(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
del args, kwargs
return requests.Response()

monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)
monkeypatch.setattr(
main_module, "SSL_CHECK_DISABLED_WARNING_PRINTED", False
)

ChromeDriverManager.send_http_get_request(
"https://example.com", verify_ssl=False
)
ChromeDriverManager.send_http_get_request(
"https://example.com", verify_ssl=False
)

captured = capsys.readouterr()
assert captured.err.count("--disable-ssl-check") == 1


def test_send_http_get_request_does_not_warn_when_ssl_enabled(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
del args, kwargs
return requests.Response()

monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)
monkeypatch.setattr(
main_module, "SSL_CHECK_DISABLED_WARNING_PRINTED", False
)

ChromeDriverManager.send_http_get_request("https://example.com")

captured = capsys.readouterr()
assert captured.err == ""


def test_send_http_get_request_reports_ssl_hint(
monkeypatch: pytest.MonkeyPatch,
) -> None:
def fake_get(*args: Any, **kwargs: Any) -> requests.Response:
del args, kwargs
raise requests.exceptions.SSLError("certificate verify failed")

monkeypatch.setattr("html2pdf4doc.main.requests.get", fake_get)

with pytest.raises(RuntimeError) as exc_info:
ChromeDriverManager.send_http_get_request("https://example.com")

assert "--disable-ssl-check" in str(exc_info.value)