Обработать защитную проверку#

Guard нужен, когда сайт вместо обычного ответа возвращает защитную проверку. Он распознаёт помеху, вызывает типизированный solver, применяет решение и заново готовит исходный запрос в той же транспортной сессии.

Выбрать готовую защиту#

Установите пакет с поддерживаемыми пресетами:

uv add "eazy-sdk-core[presets]"

Готовые страницы описывают настройку Cloudflare Challenge Pages, Turnstile и reCAPTCHA. Пресет устанавливается в корне SDK через ClientConfig(security=Security.of(guard)).

Объявить свой guard#

Пример сайта требует captcha после отправки пароля. HTTP-клиент решает её cookie, а браузер нажимает кнопку внутри той же операции.

detect(...) возвращает LoginCaptcha только для знакомого ответа 403. solve(...) получает SolveContext и создаёт решение с cookie через self.solution(...).

# examples/mail/http/captcha_probe.py
@dataclass(frozen=True, slots=True)
class LoginCaptcha:
    site_key: str


class LoginCaptchaGuard(Guard[LoginCaptcha]):
    """Apply a teaching clearance cookie, then repeat the rejected login step."""

    scope = host("mail.example")
    replay = rejected_before_execution(max_replays=1)

    def __init__(self) -> None:
        self.solved: list[str] = []

    def detect(self, response: ResponseContext[object]) -> LoginCaptcha | None:
        if response.response.status_code != 403:
            return None
        document = response.json.value
        if not isinstance(document, dict) or document.get("kind") != "captcha":
            return None
        site_key = document.get("site_key")
        return LoginCaptcha(site_key) if isinstance(site_key, str) else None

    def solve(self, challenge: LoginCaptcha, context: SolveContext) -> GuardSolution:
        self.solved.append(challenge.site_key)
        return self.solution(cookies={"login_clearance": "solved"})

Guard устанавливается один раз в ClientConfig. После решения runtime повторяет отклонённый password-запрос в пределах бюджета max_replays=1.

# examples/mail/http/captcha.py
def login_after_captcha() -> tuple[str, int, str | None, str]:
    site = MailSite()
    guard = LoginCaptchaGuard()
    config = ClientConfig(security=Security.of(guard))
    with configured_mail_client(site, config) as client:
        login = LoginApi(client)
        password = login.identify(email="captcha@mail.example")
        otp = login.password(login_id=password.login_id, password=VALID_INPUT)
    return (
        guard.solved[0],
        len(site.state.password_cookies),
        site.state.password_cookies[1],
        type(otp).__name__,
    )
# examples/docs/protection.py
def main() -> None:
    challenge, requests, cookie, next_step = login_after_captcha()
    print(f"challenge: {challenge}")
    print(f"password requests: {requests}")
    print(f"clearance cookie: {cookie}")
    print(f"result: {next_step}")

Браузер видит настоящую страницу проверки:

<!-- examples/mail/site/app.py -->
<main data-page="captcha" data-login-id="{login_id}">
  <div class="captcha" data-site-key="mail-login">Подтвердите вход</div>
  <form method="post" action="/login/captcha">
    <input name="login_id" type="hidden" value="{login_id}">
    <button data-action="solve" type="submit">Я не робот</button>
  </form>
</main>

Handle следит за элементом captcha, нажимает кнопку и возвращает управление исходной BrowserOperation. Для вызывающего переход к OtpStep остаётся одним методом.

# examples/mail/browser/captcha.py
CAPTCHA = Handle(
    when=visible(css('[data-page="captcha"]')),
    do=click(css('[data-action="solve"]')),
)


@dataclass(frozen=True, slots=True, kw_only=True)
class SubmitPasswordWithCaptcha(BrowserOperation[PasswordPage, OtpStep]):
    __browser__ = Browser.act(
        PasswordPage,
        at=css('input[name="password"]'),
        handlers=(CAPTCHA,),
        errors=PASSWORD_FAILURES,
        outcomes=outcomes(
            when(visible(css('input[name="code"]')), to=OtpStep),
            otherwise=_password_stalled,
        ),
    )

    password: str

    async def act(self, content: PasswordPage) -> None:
        await content.password.fill(self.password)
        await content.submit.click()


class CaptchaLoginPortal(AsyncBrowserApi):
    errors = ACCOUNT_FAILURES

    identify = op(Identify)
    password = op(SubmitPasswordWithCaptcha)
    otp = op(SubmitOtp)

Ответ сайта

Что делает правило

Что получает вызывающий

Обычный ответ

detect(...) возвращает None

Результат операции

Знакомая помеха

Solver создаёт GuardSolution

Результат повторённой операции

Повреждённая помеха

Detector сообщает ошибку разбора

Типизированную ошибку стадии

Бюджет исчерпан

Повтор запрещён

ReplayDeniedError

Запускаемый файл целиком
"""Solve one response challenge and replay the rejected request."""

from examples.mail.http.captcha import login_after_captcha


# region docs: protection-run
# examples/docs/protection.py
def main() -> None:
    challenge, requests, cookie, next_step = login_after_captcha()
    print(f"challenge: {challenge}")
    print(f"password requests: {requests}")
    print(f"clearance cookie: {cookie}")
    print(f"result: {next_step}")
# endregion docs: protection-run


if __name__ == "__main__":
    main()
challenge: mail-login
password requests: 2
clearance cookie: login_clearance=solved
result: OtpStep

Настроить область, хранение и повтор#

Атрибут

Назначение

scope

Ограничивает правило хостом, операцией или другим RequestScope

cache="none"

Решение не покидает текущую попытку

cache="call"

Решение повторно используется только внутри одного вызова

cache="session"

Решение разделяется вызовами одной транспортной сессии

replay

Задаёт доказательство безопасности и число повторов

revision

Отделяет новое правило от ранее сохранённых решений

headers, query и body объявляют назначения, которые разрешено менять решению. Для cookie отдельное разрешение не требуется. Кэш можно очистить методом client.invalidate_protection(...) для конкретного guard, имени политики или всей сессии.

Решать в той же транспортной сессии#

context.fetch(...) загружает вспомогательный ресурс через тот же handler, proxy и cookie jar. Такой запрос обходит guards, middleware, rate limit и повтор, поэтому solver не вызывает сам себя. Каждый полученный Set-Cookie остаётся в сессии защищённого вызова.

context.identity описывает User-Agent, объявленный proxy и метку impersonation. Сохранённое решение привязано к этому отпечатку и сбрасывается при смене сетевой личности.

Предупреждение

Если solver использует отдельный браузер или HTTP-клиент вместо context.fetch(...), он сам должен сохранить тот же proxy, User-Agent, TLS-отпечаток и cookies.

Разобрать ошибку стадии#

Ошибка

Стадия

ProtectionConfigurationError

Некорректная схема до транспортного вызова

ChallengeDetectionError

Неожиданная ошибка detector

ChallengeParseError

Помеха распознана, но её данные повреждены

ChallengeSolveError

Solver не получил решение

ChallengeApplicationError

Решение нельзя безопасно применить

ReplayDeniedError

Нет доказанного или доступного повтора

Ошибки сохраняют исходное исключение в __cause__, но не включают тело ответа, cookies или credentials в собственное сообщение. Низкоуровневые контракты генераторов находятся в eazy_sdk.protection.advanced; прикладному SDK обычно достаточно Guard или готового пресета.

Дальше#