Проблема: Когда Авито притворяется Роскомнадзором В белом списке (контрольные сайты, которые должны работать всегда: Госуслуги, Яндекс и тд.) внезапно загорелся красным Авито:
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
Буду рад любому фидбеку!
ссылка на оригинал статьи https://habr.com/ru/articles/1081618/