zapretgui/tests/test_chrome_https.py
loop-uh 0a3978de8a
All checks were successful
Zapret source guards / validate (push) Successful in 10s
Windows-сборка / Dev Dev 21.1.7.189 опубликован
YouTube проверяется целиком и по настоящему видеосерверу: подбор по трём именам, чтение страницы распаковывается, общий googlevideo.com больше не проверяется
Причина того, что видеосервер почти никогда не находился на странице YouTube: страница приходила сжатой, а libcurl её не распаковывал, поэтому в ней нельзя было найти ни одного имени rr…googlevideo.com. Теперь страница запрашивается с gzip и deflate и распаковывается в самом запросе. Без настоящего узла видеосервер не проверяется и отчёт так и говорит: общий адрес googlevideo.com не видеосервер и всегда отвечает ошибкой. Карточка YouTube в подборе ведёт на страницу, а страница, видеосервер вашей сети и превью проверяются вместе, профиль пишется на все три имени. Шаг «Передача данных» засчитывает страницу, которая больше 16 КБ и пришла целиком.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 11:41:05 -04:00

206 lines
9.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Запрос «как Chrome»: исход по кодам libcurl без сети (настоящий curl подменён).
Настоящие сокеты (молчащий сервер, закрытый порт, сброс при рукопожатии) — в
tests/test_diagnostics_verdict.py::TlsProbeFailureTests и tests/test_run_stop.py.
"""
from __future__ import annotations
import errno
import unittest
from unittest.mock import patch
from diagnostics import tls_probe
from utils import chrome_https
from utils.socket_cancel import SocketCancel
WRITE_ERROR = 0xFFFFFFFF
class _Opt:
URL = "url"
RESOLVE = "resolve"
CAINFO = "cainfo"
HTTP_CONTENT_DECODING = "decoding"
CONNECTTIMEOUT_MS = "connect_ms"
LOW_SPEED_LIMIT = "low_limit"
LOW_SPEED_TIME = "low_time"
TIMEOUT_MS = "timeout_ms"
HEADERFUNCTION = "header_fn"
WRITEFUNCTION = "write_fn"
VERBOSE = "verbose"
DEBUGFUNCTION = "debug_fn"
NOSIGNAL = "nosignal"
NOBODY = "nobody"
HTTPHEADER = "headers"
class _Info:
OS_ERRNO = "os_errno"
RESPONSE_CODE = "response_code"
NUM_CONNECTS = "num_connects"
class _CurlError(Exception):
def __init__(self, code: int, message: str = "") -> None:
super().__init__(message or f"curl: ({code})")
self.code = code
class _Script:
"""Что «пришло по сети»: события по порядку и чем закончилось."""
def __init__(self, *steps, error: int = 0, os_errno: int = 0, message: str = "") -> None:
self.steps = steps
self.error = error
self.message = message
self.os_errno = os_errno
self.options: dict = {}
self.impersonated = ""
def _api(script: _Script):
class _Curl:
def impersonate(self, target, default_headers=True):
script.impersonated = target
def setopt(self, option, value):
script.options[option] = value
def perform(self, clear_headers=True, clear_resolve=True):
script.cleared = (clear_headers, clear_resolve)
for kind, value in script.steps:
if kind == "debug":
script.options[_Opt.DEBUGFUNCTION](*value)
elif kind == "header":
script.options[_Opt.HEADERFUNCTION](value)
elif kind == "body":
if script.options[_Opt.WRITEFUNCTION](value) == WRITE_ERROR:
raise _CurlError(23)
if script.error:
raise _CurlError(script.error, script.message)
def getinfo(self, info):
return script.os_errno if info == _Info.OS_ERRNO else 0
def close(self):
pass
return chrome_https._CurlApi(_Curl, _CurlError, _Opt, _Info, WRITE_ERROR)
HELLO = ("debug", (6, b"\x16\x03\x01"))
SECURED = ("debug", (0, b"SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256"))
REQUEST = ("debug", (2, b"GET / HTTP/2"))
OK_HEADER = ("header", b"HTTP/2 200 \r\n")
def _get(script: _Script, **kwargs):
with patch.object(chrome_https, "_curl_api", return_value=_api(script)):
return chrome_https.chrome_get("example.com", kwargs.pop("ip", "203.0.113.5"), timeout=1.0, **kwargs)
class ChromeHttpsTests(unittest.TestCase):
def test_request_is_pinned_to_the_address_and_looks_like_chrome(self) -> None:
script = _Script(HELLO, SECURED, REQUEST, OK_HEADER, ("body", b"x" * 10))
result = _get(script, ip="2001:db8::5", read_limit=100)
self.assertEqual(script.impersonated, chrome_https.CHROME_TARGET)
# Chrome на Windows с русским языком, а не профиль с Mac.
self.assertIn(b'sec-ch-ua-platform: "Windows"', script.options[_Opt.HTTPHEADER])
self.assertEqual(script.options[_Opt.RESOLVE], [b"example.com:443:[2001:db8::5]"])
# Тело не распаковывается: размер — байты на проводе.
self.assertEqual(script.options[_Opt.HTTP_CONTENT_DECODING], 0)
self.assertEqual((result.kind, result.status, result.body_size, result.tls_version), ("ok", 200, 10, "TLSv1.3"))
# Списки заголовков и адреса живут до закрытия соединения, а не до конца запроса.
self.assertEqual(script.cleared, (False, False))
def test_page_content_is_unpacked_here_not_by_libcurl_in_pieces(self) -> None:
import gzip
page = b'"url":"https://rr1---sn-abc.googlevideo.com/videoplayback"' + b"x" * 5000
packed = gzip.compress(page)
cut = len(packed) // 2
script = _Script(
HELLO, SECURED, ("header", b"HTTP/2 200 \r\n"), ("header", b"content-encoding: gzip\r\n"),
("body", packed[:cut]), ("body", packed[cut:]),
)
result = _get(script, decode_body=True, read_limit=100_000)
# libcurl не распаковывает ни в какой сборке; просим только gzip и deflate и распаковываем сами.
self.assertEqual(script.options[_Opt.HTTP_CONTENT_DECODING], 0)
self.assertIn(b"Accept-Encoding: gzip, deflate", script.options[_Opt.HTTPHEADER])
self.assertNotIn(b"Accept-Encoding: gzip, deflate, br, zstd", script.options[_Opt.HTTPHEADER])
self.assertEqual(bytes(result.body), page)
def test_a_page_read_in_pieces_can_stop_as_soon_as_the_wanted_text_is_decoded(self) -> None:
import gzip
packed = gzip.compress(b"head " + b"googlevideo " + b"x" * 100_000)
script = _Script(
HELLO, SECURED, OK_HEADER, ("header", b"Content-Encoding: gzip\r\n"),
("body", packed[:60]), ("body", packed[60:]),
)
result = _get(script, decode_body=True, read_limit=100_000, body_done=lambda body: b"googlevideo" in body)
self.assertIn(b"googlevideo", bytes(result.body))
def test_without_decoding_the_browser_encodings_and_the_wire_size_stay(self) -> None:
script = _Script(HELLO, SECURED, OK_HEADER, ("header", b"content-encoding: gzip\r\n"), ("body", b"\x1f\x8b" + b"z" * 20))
result = _get(script, read_limit=100)
self.assertIn(b"Accept-Encoding: gzip, deflate, br, zstd", script.options[_Opt.HTTPHEADER])
self.assertEqual(result.body_size, 22)
def test_reading_stops_at_the_limit_without_calling_it_a_failure(self) -> None:
result = _get(_Script(HELLO, SECURED, OK_HEADER, ("body", b"x" * 500), ("body", b"y" * 500)), read_limit=400)
self.assertEqual((result.kind, result.body_cut, result.body_size), ("ok", False, 500))
def test_reset_in_the_middle_of_the_body_is_a_cut(self) -> None:
result = _get(_Script(HELLO, SECURED, OK_HEADER, ("body", b"x" * 16_000), error=56), read_limit=40_000)
self.assertEqual((result.kind, result.body_cut, result.body_size), ("ok", True, 16_000))
def test_stop_in_the_middle_of_the_body_is_not_a_freeze(self) -> None:
"""Лимит времени снял медленную загрузку после 16 КБ — это не обрыв ТСПУ."""
cancel = SocketCancel()
script = _Script(HELLO, SECURED, OK_HEADER, ("body", b"x" * 16_000), error=28)
original = script.steps
def steps():
yield from original[:-1]
cancel.cancel()
yield original[-1]
script.steps = steps()
result = _get(script, read_limit=64_000, cancel=cancel)
self.assertEqual(result.kind, tls_probe.KIND_CANCELLED)
self.assertFalse(result.body_cut)
def test_stages_of_failure(self) -> None:
# Сброс посреди рукопожатия: код системы говорит «сброс».
reset = _get(_Script(HELLO, error=35, os_errno=errno.ECONNRESET))
self.assertEqual((reset.kind, reset.stage), (tls_probe.KIND_RESET, tls_probe.STAGE_TLS))
# Рукопожатие не сошлось без сброса — ошибка шифрования.
tls = _get(_Script(HELLO, error=35))
self.assertEqual(tls.kind, tls_probe.KIND_TLS)
# Пустой ответ после рукопожатия — сброс.
empty = _get(_Script(HELLO, SECURED, REQUEST, error=52))
self.assertEqual((empty.kind, empty.stage), (tls_probe.KIND_RESET, tls_probe.STAGE_READ))
# Молчание после приветствия — таймаут шифрования, а не «не подключились».
silent = _get(_Script(HELLO, error=28))
self.assertEqual((silent.kind, silent.stage), (tls_probe.KIND_TIMEOUT, tls_probe.STAGE_TLS))
def test_connect_failures_are_told_apart(self) -> None:
refused = _get(_Script(error=7, os_errno=errno.ECONNREFUSED))
self.assertEqual((refused.kind, refused.connect_fail), (tls_probe.KIND_CONNECT, "refused"))
silent = _get(_Script(error=28))
self.assertEqual((silent.kind, silent.connect_fail), (tls_probe.KIND_CONNECT, "timeout"))
def test_wrong_certificate_is_named(self) -> None:
script = _Script(HELLO, error=60, message="SSL: no alternative certificate subject name matches target hostname")
result = _get(script)
self.assertEqual((result.kind, result.cert_problem), (tls_probe.KIND_CERT, "сертификат выдан другому сайту"))
if __name__ == "__main__":
unittest.main()