feat: implement universal runner for standard and GOST TLS

- Add smart two-phase TLS detection with caching in tests/utils/tls.py

- Add auto-discovery for Chromium-GOST and Yandex.Browser

- Add --tls-mode and --ignore-https-errors CLI options

- Update browser_page context with ignore_https_errors and unified viewport

- Add unit tests in tests/utils/test_tls.py

- Update README.md and .env.example
This commit is contained in:
VolandSZ
2026-09-03 00:02:36 +03:00
parent f511102289
commit cdca9d6003
8 changed files with 515 additions and 30 deletions

View File

@ -17,3 +17,31 @@ PREFECTURE_PASSWORD=
# Базовый URL портала
BASE_URL=https://vet.goznak.ru
# ---------------------------------------------------------------------------
# Режим TLS и браузер (проверяется автоматически при старте тестов)
# ---------------------------------------------------------------------------
# Режим TLS: auto (по умолчанию), gost (принудительно ГОСТ TLS), standard (обычный TLS)
# Также можно передать флаг в pytest: pytest --tls-mode=gost
TLS_MODE=auto
# Вариант 1 (автозапуск): путь к исполняемому файлу Chromium GOST / Яндекс.Браузер
# ОПЦИОНАЛЬНО: если браузер установлен в стандартный каталог (Program Files, AppData),
# он будет найден автоматически без заполнения этой строки.
# Скачать Chromium GOST: https://github.com/deemru/chromium-gost/releases
CHROMIUM_GOST_PATH=
# Вариант 2: порт CDP для подключения к предзапущенному Chromium GOST
# Запуск: chrome.exe --remote-debugging-port=9222 --no-first-run
CHROMIUM_GOST_CDP_PORT=9222
# ---------------------------------------------------------------------------
# Кастомизация отчёта
# ---------------------------------------------------------------------------
# Название среды тестирования (отображается в блоке Environment отчёта)
TEST_ENV=Production
# Имя тестировщика (отображается в блоке Environment отчёта)
TESTER_NAME=QA Team

View File

@ -10,9 +10,11 @@
- [Структура проекта](#структура-проекта)
- [Быстрый старт](#быстрый-старт)
- [Запуск тестов](#запуск-тестов)
- [Поддержка Стандартного и ГОСТ TLS](#поддержка-стандартного-и-гост-tls)
- [Создание нового теста](#создание-нового-теста)
- [Добавление генератора данных (ФЛК)](#добавление-генератора-данных-флк)
- [Добавление статического артефакта](#добавление-статического-артефакта)
- [Создание теста через визуальный рекордер (Codegen)](#создание-теста-через-визуальный-рекордер-codegen)
---
@ -20,15 +22,19 @@
```
e2e_Pets/
├── .env # Учётные данные (не в git)
├── .env # Учётные данные и настройки среды (не в git)
├── .env.example # Шаблон .env
├── conftest.py # Корневой conftest (CLI-флаги, HTML-отчёт и метаданные)
├── pytest.ini # Конфигурация pytest
├── requirements.txt # Зависимости
└── tests/
├── conftest.py # Глобальные фикстуры (browser_page, bp_state)
├── conftest.py # Глобальные фикстуры (browser_page, bp_state, _auto_browser)
├── testdata/
│ └── generators.py # Генераторы случайных данных по ФЛК
├── utils/
│ ├── tls.py # Детектор TLS и адаптивный менеджер браузеров
│ └── test_tls.py # Модульные тесты механизмов TLS
├── 01_vetdept/ # ЛК: Управление ветеринарии
│ ├── conftest.py # fixtures_dir, shared_state
@ -134,6 +140,8 @@ Copy-Item .env.example .env
| `--slowmo 500` | Замедление действий на 500мс (для отладки) |
| `-k "BP01"` | Запустить только тесты содержащие "BP01" в имени |
| `--co -q` | Только показать список тестов без запуска |
| `--tls-mode=auto` | Режим TLS: `auto` (по умолчанию — автоопределение), `gost` или `standard` |
| `--ignore-https-errors` | Игнорировать ошибки SSL сертификатов (по умолчанию включено) |
```powershell
# Показать список всех тестов (без запуска)
@ -144,10 +152,64 @@ Copy-Item .env.example .env
# Запустить в headless-режиме только vetdept
.\venv\Scripts\pytest.exe tests/01_vetdept/ --headless --slowmo 0
# Принудительный запуск в режиме ГОСТ TLS (через Chromium-GOST / Яндекс.Браузер)
.\venv\Scripts\pytest.exe --tls-mode=gost
# Принудительный запуск в режиме стандартного TLS (встроенный Chromium Playwright)
.\venv\Scripts\pytest.exe --tls-mode=standard
```
---
## Поддержка Стандартного и ГОСТ TLS
В проект встроен универсальный механизм запуска, который автоматически определяет, какой протокол шифрования используется на целевом портале (`BASE_URL`), и выбирает подходящий браузер.
### Как работает автоопределение:
1. **Двухфазная детекция**:
- Выполняется тестовое SSL-рукопожатие сокета.
- Если сайт использует стандартные алгоритмы (TLS 1.2 / TLS 1.3, AES, CHACHA20 и др.), даже при наличии самоподписанного или тестового корпоративного сертификата, система распознаёт его как **Стандартный TLS** и запускает встроенный **Chromium Playwright**.
- Если сервер требует российские криптографические алгоритмы ГОСТ (ГОСТ Р 34.12-2015 «Кузнечик»/«Магма», ГОСТ Р 34.10-2012, 28147-89), которые не поддерживаются штатным OpenSSL в Python, система идентифицирует **ГОСТ TLS**.
2. **Автопоиск браузера для ГОСТ**:
- Система автоматически сканирует стандартные каталоги установки:
- `%LOCALAPPDATA%\Chromium-Gost\Application\chrome.exe`
- `%PROGRAMFILES%\Chromium-Gost\chrome.exe` (и в `Program Files (x86)`)
- **Яндекс.Браузер** (при установленном КриптоПро CSP): `%LOCALAPPDATA%\Yandex\...`, `%PROGRAMFILES%\Yandex\...`
- Системный `PATH` (`chromium-gost`, `yandex-browser`).
- Если браузер найден в системе, тесты стартуют автоматически без необходимости вручную прописывать путь в `.env`.
3. **Варианты запуска под ГОСТ**:
- **Автозапуск (рекомендуется)**: Установить [Chromium-GOST](https://github.com/deemru/chromium-gost/releases) или Яндекс.Браузер с КриптоПро CSP. При нестандартном расположении указать в `.env`:
```ini
CHROMIUM_GOST_PATH=C:\CustomPath\chrome.exe
```
- **Подключение по CDP (если браузер уже запущен вручную)**:
```powershell
chrome.exe --remote-debugging-port=9222 --no-first-run
```
Тесты автоматически подключатся к открытому браузеру на порту `9222`.
4. **Управление режимом TLS**:
- Через аргумент CLI:
```powershell
.\venv\Scripts\pytest.exe --tls-mode=auto # Автоопределение (по умолчанию)
.\venv\Scripts\pytest.exe --tls-mode=gost # Принудительно ГОСТ
.\venv\Scripts\pytest.exe --tls-mode=standard # Принудительно Стандартный
```
- Либо через `.env`:
```ini
TLS_MODE=auto # auto | gost | standard
```
5. **Отображение в HTML-отчёте**:
- В блоке **Environment** сгенерированного отчёта `html-report/report.html` фиксируются:
- Реальный режим TLS и использованный шифр (например, `Стандартный TLS (TLSv1.3, TLS_AES_256_GCM_SHA384)`).
- Тип и путь фактически запущенного браузера (`Chromium (Playwright)` / `Chromium GOST` / `Яндекс.Браузер`).
---
## Создание нового теста
### Нужен новый бизнес-процесс?

View File

@ -1,15 +1,40 @@
"""
Корневой conftest.py — конфигурация на уровне всей сессии.
Здесь размещены хуки которые должны работать глобально.
Здесь размещены хуки, которые должны работать глобально.
"""
import os
# pyrefly: ignore [missing-import]
import pytest
# pyrefly: ignore [missing-import]
from dotenv import load_dotenv
from tests.utils.tls import detect_tls_mode, tls_info_key, find_gost_browser_executable
load_dotenv()
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
# ---------------------------------------------------------------------------
# Регистрация CLI аргументов запуска
# ---------------------------------------------------------------------------
def pytest_addoption(parser):
"""Регистрирует дополнительные флаги командной строки для pytest."""
parser.addoption(
"--tls-mode",
action="store",
default="auto",
choices=["auto", "gost", "standard"],
help="Режим TLS: auto (по умолчанию — автоопределение), gost (принудительно ГОСТ TLS), standard (обычный TLS).",
)
parser.addoption(
"--ignore-https-errors",
action="store_true",
default=True,
help="Игнорировать ошибки SSL сертификатов в браузере (по умолчанию: True).",
)
# ---------------------------------------------------------------------------
# Настройка блока Environment и заголовка HTML-отчёта
# ---------------------------------------------------------------------------
@ -20,20 +45,30 @@ def pytest_sessionstart(session):
pytest-html 4.x читает данные через config.stash[metadata_key]
(API pytest-metadata 3.x). Наш хук изменяет именно этот stash.
Чтобы изменить содержимое — редактируй словарь ниже.
Чтобы убрать строку — удали соответствующий ключ.
Чтобы добавить строку — добавь новую пару ключ/значение.
Порядок записей соответствует порядку в отчёте.
"""
config = session.config
# Получаем режим TLS из CLI аргумента или .env
cli_tls_mode = config.getoption("--tls-mode", default="auto")
force_mode = None if cli_tls_mode == "auto" else cli_tls_mode
# Определяем режим TLS с кэшированием и сохраняем в stash сессии
is_gost, tls_desc = detect_tls_mode(BASE_URL, force_mode=force_mode)
config.stash[tls_info_key] = (is_gost, tls_desc)
if is_gost:
gost_exec = find_gost_browser_executable()
if gost_exec and "yandex" in gost_exec.lower():
browser_name = "Яндекс.Браузер (ГОСТ)"
else:
browser_name = "Chromium GOST"
else:
browser_name = "Chromium (Playwright)"
try:
# pytest-metadata 3.x — используется pytest-html 4.x
from pytest_metadata.plugin import metadata_key
metadata = config.stash[metadata_key]
except (ImportError, KeyError):
# Fallback на старый API (pytest-metadata < 3.0)
metadata = getattr(config, "_metadata", None)
if metadata is None:
return
@ -42,12 +77,12 @@ def pytest_sessionstart(session):
metadata.clear()
# Заполняем значимыми данными для отчёта руководителю
# ↓ Редактируй эти значения под свой проект ↓
metadata["Проект"] = "e2e_Pets — Учёт домашних животных"
metadata["Портал"] = BASE_URL
metadata["Окружение"] = os.getenv("TEST_ENV", "Production")
metadata["Браузер"] = "Chromium (Playwright)"
metadata["Тестировщик"] = os.getenv("TESTER_NAME", "QA Team")
metadata["Проект"] = "e2e_Pets — Учёт домашних животных"
metadata["Портал"] = BASE_URL
metadata["TLS Режим"] = tls_desc
metadata["Браузер"] = browser_name
metadata["Окружение"] = os.getenv("TEST_ENV", "Production")
metadata["Тестировщик"] = os.getenv("TESTER_NAME", "QA Team")
def pytest_html_report_title(report):

View File

@ -1,4 +1,5 @@
import os
# pyrefly: ignore [missing-import]
import pytest
# pyrefly: ignore [missing-import]
from playwright.sync_api import Page, expect

View File

@ -4,16 +4,16 @@ import pytest
# pyrefly: ignore [missing-import]
from dotenv import load_dotenv
# pyrefly: ignore [missing-import]
from playwright.sync_api import Page
from playwright.sync_api import Page, Playwright
# Загружаем переменные из .env (файл находится в корне проекта)
from tests.utils.tls import detect_tls_mode, launch_browser_for_tls, tls_info_key
# Загружаем переменные из .env
load_dotenv()
# Базовый URL портала — берётся из .env или дефолт
BASE_URL = os.getenv("BASE_URL", "https://vet.goznak.ru")
# Учётные данные для разных ролей — берутся из .env
CREDENTIALS = {
"vetdept": {
@ -47,29 +47,69 @@ def _login(page: Page, role: str) -> None:
f"ОШИБКА: Переменные окружения для роли '{role}' не заданы. "
f"Скопируй .env.example в .env и заполни данные."
)
print(f"\n[АВТОЛОГИН] Вход под ролью: {role}")
page.goto(BASE_URL)
print(f"\n[АВТОЛОГИН] Роль: {role} | URL: {BASE_URL}")
page.goto(f"{BASE_URL}/auth")
page.wait_for_load_state("networkidle")
page.get_by_role("textbox", name="Email").fill(creds["login"])
page.get_by_role("textbox", name="Введите пароль").fill(creds["password"])
page.get_by_role("button", name="Войти в аккаунт").click()
page.wait_for_selector("#app", timeout=10000)
# Ожидаем успешного входа: переход с /auth в личный кабинет (/dashboard и т.д.)
page.wait_for_url(lambda url: "/auth" not in url, timeout=15000)
page.wait_for_load_state("networkidle")
# ---------------------------------------------------------------------------
# Фикстура: браузер с авто-адаптацией под тип TLS (session scope)
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def _auto_browser(playwright: Playwright, browser_type_launch_args: dict, pytestconfig):
"""
Сессионный браузер, адаптированный под тип TLS целевого портала.
Применяет аргументы запуска из pytest.ini и CLI (--headed, --slowmo, --tls-mode).
"""
# Получаем результат детекции из stash или вычисляем
tls_info = pytestconfig.stash.get(tls_info_key, None)
if tls_info is None:
cli_tls_mode = pytestconfig.getoption("--tls-mode", default="auto")
force_mode = None if cli_tls_mode == "auto" else cli_tls_mode
tls_info = detect_tls_mode(BASE_URL, force_mode=force_mode)
is_gost, tls_desc = tls_info
browser, browser_desc = launch_browser_for_tls(
playwright=playwright,
launch_args=browser_type_launch_args,
is_gost=is_gost,
base_url=BASE_URL,
)
print(f"\n[БРАУЗЕР] {browser_desc} | {tls_desc}")
yield browser
browser.close()
# ---------------------------------------------------------------------------
# Фикстура для бизнес-процессов (scope=class)
# Один браузер на весь класс — авторизация выполняется один раз.
# Создаёт собственный context и page, чтобы избежать ScopeMismatch
# с function-scoped page фикстурой pytest-playwright.
# ---------------------------------------------------------------------------
@pytest.fixture(scope="class")
def browser_page(browser, request):
def browser_page(_auto_browser, request, pytestconfig):
"""
Авторизованная страница с жизненным циклом на весь класс (бизнес-процесс).
Все шаги внутри класса работают в одной вкладке браузера.
Браузер выбирается автоматически:
- Стандартный TLS -> встроенный Chromium
- ГОСТ TLS -> Chromium GOST (exe или CDP)
"""
role = _get_role(request)
context = browser.new_context()
ignore_https = pytestconfig.getoption("--ignore-https-errors", default=True)
context = _auto_browser.new_context(
ignore_https_errors=ignore_https,
viewport={"width": 1920, "height": 1080},
)
page = context.new_page()
_login(page, role)
yield page
@ -98,19 +138,24 @@ def bp_state() -> dict:
# ---------------------------------------------------------------------------
# Устаревшая фикстура — оставлена для обратной совместимости
# Используй browser_page для новых тестов
# ---------------------------------------------------------------------------
@pytest.fixture(scope="function")
def role_page(page: Page, request):
def role_page(_auto_browser, request, pytestconfig):
"""
[DEPRECATED] Авторизованная страница с жизненным циклом на один тест.
Для новых тестов используй browser_page (scope=class).
"""
role = _get_role(request)
ignore_https = pytestconfig.getoption("--ignore-https-errors", default=True)
context = _auto_browser.new_context(
ignore_https_errors=ignore_https,
viewport={"width": 1920, "height": 1080},
)
page = context.new_page()
_login(page, role)
yield page
page.context.clear_cookies()
context.close()
# ---------------------------------------------------------------------------

3
tests/utils/__init__.py Normal file
View File

@ -0,0 +1,3 @@
from .tls import detect_tls_mode, launch_browser_for_tls
__all__ = ["detect_tls_mode", "launch_browser_for_tls"]

82
tests/utils/test_tls.py Normal file
View File

@ -0,0 +1,82 @@
"""
Тесты модуля tests/utils/tls.py:
Проверка двухфазной детекции TLS, кэширования и автопоиска ГОСТ-браузера.
"""
import ssl
from unittest.mock import patch, MagicMock
import pytest
from tests.utils.tls import detect_tls_mode, find_gost_browser_executable, _probe_tls_socket
def test_detect_tls_mode_http():
is_gost, desc = detect_tls_mode("http://example.com")
assert is_gost is False
assert "HTTP" in desc
def test_detect_tls_mode_forced_gost():
is_gost, desc = detect_tls_mode("https://any-site.com", force_mode="gost")
assert is_gost is True
assert "ГОСТ" in desc
def test_detect_tls_mode_forced_standard():
is_gost, desc = detect_tls_mode("https://any-site.com", force_mode="standard")
assert is_gost is False
assert "Стандартный" in desc
def test_detect_tls_mode_real_goznak():
# Реальный вызов к тестовому порталу
is_gost, desc = detect_tls_mode("https://vet.goznak.ru")
assert is_gost is False
assert "TLS" in desc
def test_probe_tls_untrusted_standard_cert():
"""
Проверяет, что если сайт на стандартном TLS имеет недоверенный/самоподписанный сертификат,
он НЕ определяется ошибочно как ГОСТ.
"""
with patch("ssl.create_default_context") as mock_def_ctx, \
patch("ssl._create_unverified_context") as mock_unv_ctx, \
patch("socket.socket") as mock_socket:
# Первая попытка с проверкой сертификата падает с SSLCertVerificationError
mock_sock_inst = MagicMock()
mock_def_ctx.return_value.wrap_socket.side_effect = ssl.SSLCertVerificationError("certificate verify failed")
# Вторая попытка с CERT_NONE успешна
mock_unv_sock = MagicMock()
mock_unv_sock.version.return_value = "TLSv1.3"
mock_unv_sock.cipher.return_value = ("TLS_AES_256_GCM_SHA384", "TLSv1.3", 256)
mock_unv_ctx.return_value.wrap_socket.return_value.__enter__.return_value = mock_unv_sock
is_gost, desc = _probe_tls_socket("self-signed.example.com", 443)
assert is_gost is False
assert "недоверенный сертификат" in desc
def test_probe_tls_real_gost_cipher_error():
"""
Проверяет, что если handshake падает с ошибкой шифров (несовместимый протокол/шифр ГОСТ),
он определяется как ГОСТ TLS.
"""
with patch("ssl.create_default_context") as mock_def_ctx, \
patch("ssl._create_unverified_context") as mock_unv_ctx, \
patch("socket.socket") as mock_socket:
mock_def_ctx.return_value.wrap_socket.side_effect = ssl.SSLError(ssl.SSL_ERROR_SSL, "UNSUPPORTED_PROTOCOL")
mock_unv_ctx.return_value.wrap_socket.side_effect = ssl.SSLError(ssl.SSL_ERROR_SSL, "NO_CIPHERS_AVAILABLE")
is_gost, desc = _probe_tls_socket("gost.example.com", 443)
assert is_gost is True
assert "ГОСТ TLS" in desc
def test_find_gost_browser_executable():
"""Проверяет поиск ГОСТ-браузера."""
path = find_gost_browser_executable()
# На данной системе должен найти установленный браузер или вернуть None
if path:
assert ("chrome.exe" in path.lower()) or ("browser.exe" in path.lower())

229
tests/utils/tls.py Normal file
View File

@ -0,0 +1,229 @@
"""
Утилита для определения типа TLS (стандартный vs ГОСТ TLS)
и управления запуском соответствующего браузера.
"""
import functools
import os
import shutil
import socket
import ssl
from typing import Any, Dict, Optional, Tuple
from urllib.parse import urlparse
# pyrefly: ignore [missing-import]
import pytest
# pyrefly: ignore [missing-import]
from playwright.sync_api import Browser, Playwright
# Ключ для сохранения результатов детекции TLS в pytest stash
tls_info_key = pytest.StashKey[Tuple[bool, str]]()
def find_gost_browser_executable() -> Optional[str]:
"""
Выполняет автопоиск исполняемого файла Chromium-GOST или Яндекс.Браузера,
поддерживающего ГОСТ TLS через системный криптопровайдер (КриптоПро CSP).
Возвращает:
Абсолютный путь к найденному браузеру или None.
"""
# 1. Приоритет — путь, явно заданный в .env
custom_path = os.getenv("CHROMIUM_GOST_PATH", "").strip()
if custom_path and os.path.exists(custom_path):
return os.path.abspath(custom_path)
# 2. Список типовых путей установки в ОС Windows
local_app_data = os.environ.get("LOCALAPPDATA", "")
prog_files = os.environ.get("ProgramFiles", "")
prog_files_x86 = os.environ.get("ProgramFiles(x86)", "")
candidates = [
# Chromium-Gost (AppData / Program Files)
os.path.join(local_app_data, "Chromium-Gost", "Application", "chrome.exe"),
os.path.join(local_app_data, "Programs", "Chromium-Gost", "Application", "chrome.exe"),
os.path.join(prog_files, "Chromium-Gost", "chrome.exe"),
os.path.join(prog_files, "Chromium-Gost", "Application", "chrome.exe"),
os.path.join(prog_files_x86, "Chromium-Gost", "chrome.exe"),
os.path.join(prog_files_x86, "Chromium-Gost", "Application", "chrome.exe"),
# Яндекс.Браузер (поддерживает ГОСТ TLS при наличии КриптоПро CSP)
os.path.join(local_app_data, "Yandex", "YandexBrowser", "Application", "browser.exe"),
os.path.join(prog_files, "Yandex", "YandexBrowser", "Application", "browser.exe"),
os.path.join(prog_files_x86, "Yandex", "YandexBrowser", "Application", "browser.exe"),
# Linux / macOS пути
"/opt/chromium-gost/chrome",
"/usr/bin/chromium-gost",
"/Applications/Chromium-Gost.app/Contents/MacOS/Chromium-Gost",
"/Applications/Yandex.app/Contents/MacOS/Yandex",
]
for path in candidates:
if path and os.path.exists(path):
return os.path.abspath(path)
# 3. Поиск в системном PATH
for name in ("chromium-gost", "chrome-gost", "yandex-browser"):
found = shutil.which(name)
if found:
return os.path.abspath(found)
return None
def _probe_tls_socket(host: str, port: int) -> Tuple[bool, str]:
"""
Выполняет двухфазную проверку TLS сокета:
Фаза 1: стандартная проверка с валидацией цепочки сертификатов.
Фаза 2: если ошибка связана с недоверенным/тестовым сертификатом,
проверяется handshake без валидации сертификата (CERT_NONE).
Если он успешен — это стандартный TLS (не ГОСТ!).
Если же handshake не может договориться по ciphers/protocols — это ГОСТ TLS.
"""
# Фаза 1: Обычный контекст с валидацией
ctx = ssl.create_default_context()
try:
with ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
s.settimeout(5)
s.connect((host, port))
cipher = s.cipher()
cipher_name = cipher[0] if cipher else "Unknown"
return False, f"Стандартный TLS ({s.version()}, {cipher_name})"
except ssl.SSLCertVerificationError as e:
# Сертификат недоверен / самоподписан, но шифрование может быть стандартным!
# Проверяем Фазу 2 с CERT_NONE
try:
unverified_ctx = ssl._create_unverified_context()
with unverified_ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
s.settimeout(5)
s.connect((host, port))
cipher = s.cipher()
cipher_name = cipher[0] if cipher else "Unknown"
return False, f"Стандартный TLS (недоверенный сертификат: {s.version()}, {cipher_name})"
except ssl.SSLError:
pass
return True, f"ГОСТ TLS (ошибка верификации и шифра: {e})"
except ssl.SSLError as e:
reason = getattr(e, "reason", str(e))
# Пробуем без проверки сертификата на случай, если причина в самоподписанном сертификате
try:
unverified_ctx = ssl._create_unverified_context()
with unverified_ctx.wrap_socket(socket.socket(), server_hostname=host) as s:
s.settimeout(5)
s.connect((host, port))
cipher = s.cipher()
cipher_name = cipher[0] if cipher else "Unknown"
return False, f"Стандартный TLS (без валидации: {s.version()}, {cipher_name})"
except Exception:
# Стандартный OpenSSL не понимает ГОСТ ciphers / протокол
return True, f"ГОСТ TLS (OpenSSL handshake: {reason})"
except Exception as e:
# Эвристика: если сервис paws.* недоступен по таймауту/сети, но является целевым ГОСТ-контуром
if "paws" in host.lower():
return True, f"ГОСТ TLS (домен {host}, fallback: {e})"
return False, f"Стандартный TLS (fallback: {e})"
@functools.lru_cache(maxsize=32)
def detect_tls_mode(url: str, force_mode: Optional[str] = None) -> Tuple[bool, str]:
"""
Определяет тип TLS соединения для целевого URL с кэшированием результатов.
Возвращает:
(is_gost: bool, description: str)
Приоритет:
1. Аргумент force_mode (например, переданный из CLI pytest --tls-mode)
2. Переменная USE_GOST или TLS_MODE в .env
3. Двухфазная проверка через сокет (handshake):
- Успешное подключение стандартным OpenSSL -> Стандартный TLS (False)
- Ошибка шифрования (UNSUPPORTED_PROTOCOL, NO_CIPHERS_AVAILABLE и т.д.) -> ГОСТ TLS (True)
- Домен содержит "paws" при таймауте -> ГОСТ TLS (True)
"""
# 1. Приоритет — явный force_mode
mode = (force_mode or os.getenv("TLS_MODE") or os.getenv("USE_GOST") or "").strip().lower()
if mode in ("gost", "true", "1", "yes", "on"):
return True, "Принудительно (ГОСТ TLS)"
if mode in ("standard", "regular", "false", "0", "no", "off"):
return False, "Принудительно (Стандартный TLS)"
# 2. Проверка протокола
parsed = urlparse(url)
if parsed.scheme == "http":
return False, "HTTP (без TLS)"
host = parsed.hostname or url
port = parsed.port or 443
# 3. Проверка сокета
return _probe_tls_socket(host, port)
def launch_browser_for_tls(
playwright: Playwright,
launch_args: Dict[str, Any],
is_gost: bool,
base_url: str
) -> Tuple[Browser, str]:
"""
Запускает подходящий браузер в зависимости от типа TLS.
Возвращает:
(browser_instance, description_string)
Логика:
- Стандартный TLS: запускает встроенный Chromium Playwright с аргументами launch_args.
- ГОСТ TLS:
1. Ищет путь к Chromium-GOST / Яндекс.Браузеру через find_gost_browser_executable().
2. Если найден — запускает с заданными launch_args.
3. Если не найден — пробует подключиться к запущенному браузеру по CDP (CHROMIUM_GOST_CDP_PORT).
4. Если оба варианта недоступны — выдаёт информативную ошибку с подробными шагами решения.
"""
cdp_port = os.getenv("CHROMIUM_GOST_CDP_PORT", "9222").strip()
if not is_gost:
# Стандартный TLS
browser = playwright.chromium.launch(**launch_args)
return browser, "Chromium (Playwright)"
# ГОСТ TLS: Вариант 1 — автопоиск и запуск по executable_path
gost_path = find_gost_browser_executable()
if gost_path and os.path.exists(gost_path):
try:
browser = playwright.chromium.launch(
executable_path=gost_path,
**launch_args
)
browser_name = "Яндекс.Браузер" if "yandex" in gost_path.lower() else "Chromium GOST"
return browser, f"{browser_name} ({gost_path})"
except Exception as e:
print(f"\n[GOST] Предупреждение: ошибка прямого запуска ({gost_path}): {e}")
print("[GOST] Пробуем подключение по CDP...")
# ГОСТ TLS: Вариант 2 — подключение по CDP
cdp_url = f"http://localhost:{cdp_port}"
try:
connect_args: Dict[str, Any] = {"timeout": 4000}
if "slow_mo" in launch_args:
connect_args["slow_mo"] = launch_args["slow_mo"]
browser = playwright.chromium.connect_over_cdp(cdp_url, **connect_args)
return browser, f"Chromium GOST (CDP порт {cdp_port})"
except Exception:
pass
# Ничего не подошло — информативное сообщение об ошибке
pytest.fail(
f"\n{'=' * 70}\n"
f"ОШИБКА: Обнаружен ГОСТ TLS для адреса: {base_url}\n"
f"Стандартный встроенный Chromium не поддерживает криптоалгоритмы ГОСТ.\n\n"
f"Для запуска тестов выберите один из вариантов:\n\n"
f"Вариант 1 (Автозапуск — рекомендуемый):\n"
f" 1. Скачайте Chromium GOST:\n"
f" https://github.com/deemru/chromium-gost/releases\n"
f" 2. Распакуйте/установите его (он будет найден автоматически), либо укажите в .env:\n"
f" CHROMIUM_GOST_PATH=C:\\Path\\To\\chromium-gost\\chrome.exe\n"
f" * Примечание: также поддерживается установленный Яндекс.Браузер с КриптоПро CSP.\n\n"
f"Вариант 2 (Подключение по CDP):\n"
f" 1. Запустите Chromium GOST вручную с флагом отладки:\n"
f" chrome.exe --remote-debugging-port={cdp_port} --no-first-run\n"
f" 2. Запустите pytest повторно\n"
f"{'=' * 70}"
)