Когда антибот притворяется РКН: ложные заглушки и локальный Web UI в rkn-block-checker 0.6.0
Проблема: Когда Авито притворяется Роскомнадзором В белом списке (контрольные сайты, которые должны работать всегда: Госуслуги, Яндекс и тд.) внезапно загорелся красным Авито:
avito ✗ HTTP_STUB HTTP 429 (37ms)
└ response body matches a known ISP stub-page marker
Скрипт на полном серьезе утверждал: “На Авито висит заглушка провайдера о блокировке РКН, уверенность: HIGH” Откуда на Авито взялась заглушка РКН? Всё дело в наивном поиске подстрок. В коде был массив маркеров STUB_MARKERS, характерных для страниц провайдеров:
STUB_MARKERS = (
"доступ ограничен",
"решению роскомнадзора",
"заблокирован",
"единый реестр",
# ...
)
Авито известен защитой от авто парса. Увидев сетевой запрос без накопленных кук от стандартной библиотеки (даже несмотря на имитацию Chrome User-Agent), WAF/антибот выдал стандартный статус 429 Too Many Requests.
А на самой HTML-странице ошибки 429 крупным шрифтом было написано: «Доступ с вашего IP временно ограничен». Скрипт скачал тело ответа, нашел вхождение подстроки “доступ ограничен” и радостно сообщил пользователю, что ресурс заблокирован цензурой на магистрали
Решение: Провайдерские заглушки в России имеют характерную особенность: они почти всегда маскируются под статус 200 OK (перехват с подменой тела) либо возвращают специализированный статус 451 Unavailable For Legal Reasons. HTTP 429 (Rate Limit) и 403 с капчей - это реакция самого целевого сервера, а не фильтра ТСПУ на магистрали. В core.py пришлось явно развести понятия цензуры и антибот-защиты:
if http_mod.looks_like_stub(probe.body_snippet):
if res.status_code == 429:
res.verdict = Verdict.DOWN
res.confidence = Confidence.LOW
res.notes.append("http 429 (antibot rate limit) contained words similar to a block stub")
return res
res.verdict = Verdict.HTTP_STUB
res.confidence = Confidence.HIGH
res.notes.append(
"response body matches a known ISP stub-page marker"
)
return res
Web UI: Зачем консольной утилите браузер?
Изначально проект задумывался строго как CLI. Но когда начинаешь разбирать спорные кейсы (“почему конкретный сервис то работает, то нет”), сухого текста в консоли становится мало. Хочется:
Кликнуть по сайту и увидеть детальный таймлайн: на сколько миллисекунд задержался TCP, как ответил TLS, что именно вернул DNS.
Дать инструмент тем, кто не привык работать в терминале.
Добавлять кастомные домены прямо на лету и перепроверять их в один клик.
Тащить ради этого тяжелые зависимости вроде FastAPI, Flask или Uvicorn не хотелось категорически. Инструмент должен ставиться за секунду через pip install в чистый системный Python.
Поэтому веб-интерфейс был написан максимально легковесно:
Бэкенд: встроенный http.server.ThreadingHTTPServer из стандартной библиотеки Python.
Фронтенд: один файл index.html на ванильном JS и CSS без сборщиков, node_modules и внешних CDN.
Запускается одной командой:
rkn-check startweb
или
rkn-check startweb --port 8080
Сервер по умолчанию слушает 127.0.0.1:7777. Как устроен реалтайм без WebSocket Чтобы интерфейс не висел мертвым во время проверки нескольких десятков сайтов, результаты должны прилетать по мере готовности (как в CLI через as_completed()). Вместо того чтобы городить WebSocket или постоянный polling, бэкенд отдает результаты обычным HTTP-стримингом в формате NDJSON (Newline Delimited JSON):
self.send_response(200)
self.send_header("Content-Type", "application/x-ndjson; charset=utf-8")
self.end_headers()
for res in iter_check_urls(targets, max_workers=workers, timeout=timeout):
line = json.dumps(res.to_dict()) + "\n"
self.wfile.write(line.encode("utf-8"))
self.wfile.flush() # Выталкиваем чанк в сокет сразу
А в браузере стандартный fetch читает поток чанков через ReadableStreamDefaultReader:
const response = await fetch('/api/scan', { method: 'POST', body: JSON.stringify(payload) });
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
let lines = buffer.split('\n');
buffer = lines.pop(); // Неполную строку оставляем в буфере
for (let line of lines) {
if (!line.trim()) continue;
const result = JSON.parse(line);
updateTableRow(result); // Обновляем конкретную строчку в таблице на лету
}
}
Что в итоге (релиз 0.6.0)
В версию 0.6.0 вошли:
Полноценный локальный Web UI (rkn-check startweb) с графиками, мультиязычностью (RU/EN) и ручным добавлением произвольных URL
Фильтрация ложных срабатываний на антибот-заглушки (HTTP 429)
Возможность экспорта полного отчета в JSON прямо из браузера в один клик
Код по-прежнему полностью открыт под MIT:
PyPI: pip install rkn-block-checker --upgrade
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.