Обработать защитную проверку#
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)
Ответ сайта |
Что делает правило |
Что получает вызывающий |
|---|---|---|
Обычный ответ |
|
Результат операции |
Знакомая помеха |
Solver создаёт |
Результат повторённой операции |
Повреждённая помеха |
Detector сообщает ошибку разбора |
Типизированную ошибку стадии |
Бюджет исчерпан |
Повтор запрещён |
|
Запускаемый файл целиком
"""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
Настроить область, хранение и повтор#
Атрибут |
Назначение |
|---|---|
|
Ограничивает правило хостом, операцией или другим |
|
Решение не покидает текущую попытку |
|
Решение повторно используется только внутри одного вызова |
|
Решение разделяется вызовами одной транспортной сессии |
|
Задаёт доказательство безопасности и число повторов |
|
Отделяет новое правило от ранее сохранённых решений |
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.
Разобрать ошибку стадии#
Ошибка |
Стадия |
|---|---|
|
Некорректная схема до транспортного вызова |
|
Неожиданная ошибка detector |
|
Помеха распознана, но её данные повреждены |
|
Solver не получил решение |
|
Решение нельзя безопасно применить |
|
Нет доказанного или доступного повтора |
Ошибки сохраняют исходное исключение в __cause__, но не включают тело ответа, cookies или
credentials в собственное сообщение. Низкоуровневые контракты генераторов находятся в
eazy_sdk.protection.advanced; прикладному SDK обычно достаточно Guard или готового пресета.