Проверка доменов по реестру РКН прямо в браузере с помощью Tampermonkey

Юрий Герук 2025-12-16 137
Проверка доменов по реестру РКН прямо в браузере с помощью Tampermonkey

Введение

Когда работаешь с доменами и сайтами, проверка на блокировки РКН быстро превращается в рутину. Один домен еще можно проверить вручную, но когда их десятки, начинается уныние в чистом виде.

Ни расширения, ни локальные базы не всегда удобны. Хочется простое решение, которое живет в браузере и работает по кнопке. В этой статье как раз такое. Скрипт устанавливается в Tampermonkey и позволяет проверять один или сразу несколько доменов через окно ввода.

Применение. Для чего, почему и как

Скрипт пригодится, если:

• нужно быстро проверить домен клиента перед переносом или настройками
• требуется массово прогнать список доменов
• надо подтвердить наличие домена в реестре для тикета, отчета или переписки
• важно сверить домен прямо сейчас, не гадая, насколько старая информация

Почему Tampermonkey:

• не надо собирать расширение и бороться с ограничениями браузеров
• работает в Chrome, Edge, Firefox и других
• легко правится под себя
• все находится в одном файле и обновляется в два клика

Смысл решения простой. Скрипт загружает список доменов из публичного API, хранит его в кэше и сравнивает введенные домены со списком. На каждый домен не делаются отдельные запросы, поэтому проверка происходит почти мгновенно.

Как установить и пользоваться

Установка Tampermonkey

  1. Установите расширение Tampermonkey для своего браузера.
  2. Убедитесь, что иконка Tampermonkey появилась в панели расширений.

Установка скрипта

  1. Нажмите на иконку Tampermonkey.
  2. Откройте пункт Создать новый скрипт.
  3. Удалите весь шаблонный код.
  4. Вставьте код ниже целиком.
  5. Сохраните скрипт.

Как пользоваться скриптом

  1. Откройте любую страницу.
  2. Внизу справа появится кнопка RKN check.
  3. Нажмите ее, откроется окно.
  4. Вставьте домены или ссылки списком. Можно через пробелы, запятые и переносы строк.
  5. Нажмите Проверить.
  6. Получите результат. При необходимости нажмите Копировать результат.

Если часть доменов не найдена, скрипт автоматически обновит список и перепроверит повторно. Если все найдено, блок Не найдено не будет выводиться, чтобы отчет выглядел логично.

Как работает скрипт

Скрипт берет список доменов из:

https://reestr.rublacklist.net/api/v3/domains/

Этот источник агрегирует данные из официального реестра РКН:

https://blocklist.rkn.gov.ru/

Дальше происходит следующее:

  1. Скрипт загружает список доменов и сохраняет его в кэш браузера на 10 минут.
  2. Домены нормализуются. Убираются протоколы, пути, www, порты.
  3. Проверка идет через быстрое сравнение с локальным списком.
  4. Если есть промахи, делается принудительное обновление списка и повторная перепроверка.
  5. В отчете выводятся только релевантные блоки, без лишних строк вроде Не найдено 0.

Скрипт для Tampermonkey

// ==UserScript==
// @name uCoz RKN Domain Checker (Bootstrap-like UI)
// @namespace ucoz-rkn-checker
// @version 1.2.0
// @description Проверка доменов по реестру reestr.rublacklist.net: окно ввода, кэш, умная перепроверка, аккуратный отчет
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect reestr.rublacklist.net
// ==/UserScript==

(function () {
 "use strict";

 const API_URL = "https://reestr.rublacklist.net/api/v3/domains/";

 // Данные в этом API, как правило, обновляются раз в несколько часов.
 // Поэтому кэш держим коротким, чтобы не жить на старом списке.
 const CACHE_TTL_MS = 10 * 60 * 1000; // 10 минут

 // Если есть не найденные домены, скрипт один раз принудительно обновит список и перепроверит.
 const AUTO_REFRESH_ON_MISS = true;

 const SOURCES_TEXT = [
 "Источник данных: reestr.rublacklist.net API v3 (список доменов).",
 "Официальная проверка РКН: blocklist.rkn.gov.ru."
 ].join(" ");

 const STORAGE_KEYS = {
 cacheData: "ucoz_rkn_cache_domains_v2",
 cacheTime: "ucoz_rkn_cache_time_v2"
 };

 function now() {
 return Date.now();
 }

 function safeText(s) {
 return String(s ?? "").replace(/[<>]/g, "");
 }

 function splitInputToCandidates(text) {
 return String(text || "")
 .replace(/\r/g, "\n")
 .split(/[\n,;|\t ]+/)
 .map(s => s.trim())
 .filter(Boolean);
 }

 function normalizeDomain(raw) {
 let s = String(raw || "").trim().toLowerCase();

 s = s.replace(/^"+|"+$/g, "");
 s = s.replace(/^'+|'+$/g, "");
 s = s.replace(/^\.+|\.+$/g, "");

 try {
 if (s.includes("://")) {
 const u = new URL(s);
 s = u.hostname || s;
 } else if (s.includes("/")) {
 const u = new URL("http://" + s);
 s = u.hostname || s;
 }
 } catch (e) {
 // ignore
 }

 s = s.replace(/:\d+$/g, "");
 s = s.replace(/^www\./, "");
 s = s.trim().replace(/^\.+|\.+$/g, "");

 return s;
 }

 function uniq(arr) {
 return [...new Set(arr)];
 }

 function gmRequestJson(url) {
 return new Promise((resolve, reject) => {
 GM_xmlhttpRequest({
 method: "GET",
 url,
 headers: { "Accept": "application/json" },
 onload: (resp) => {
 try {
 if (resp.status < 200 || resp.status >= 300) {
 reject(new Error("HTTP " + resp.status));
 return;
 }
 const data = JSON.parse(resp.responseText);
 resolve(data);
 } catch (e) {
 reject(e);
 }
 },
 onerror: (e) => reject(e),
 ontimeout: () => reject(new Error("timeout")),
 timeout: 45000
 });
 });
 }

 function listFromApiResponse(data) {
 const list = Array.isArray(data) ? data : (data && data.domains ? data.domains : []);
 return list
 .map(x => String(x).trim().toLowerCase())
 .filter(Boolean);
 }

 async function fetchRknDomainsSetNoCache() {
 const data = await gmRequestJson(API_URL);
 const normalized = listFromApiResponse(data);

 await GM_setValue(STORAGE_KEYS.cacheData, JSON.stringify(normalized));
 await GM_setValue(STORAGE_KEYS.cacheTime, String(now()));

 return new Set(normalized);
 }

 async function getRknDomainsSet() {
 const cachedTime = Number(await GM_getValue(STORAGE_KEYS.cacheTime, 0));
 const cachedRaw = await GM_getValue(STORAGE_KEYS.cacheData, "");

 if (cachedRaw && cachedTime && (now() - cachedTime) < CACHE_TTL_MS) {
 const list = JSON.parse(cachedRaw);
 return new Set(list.map(x => String(x).trim().toLowerCase()));
 }

 return await fetchRknDomainsSetNoCache();
 }

 function formatResultLines(title, items) {
 if (!items.length) return `${title}: 0`;
 return `${title}: ${items.length}\n\n` + items.join("\n");
 }

 function timeAgo(ms) {
 if (!ms || ms <= 0) return "нет данных";
 const sec = Math.floor(ms / 1000);
 if (sec < 60) return `${sec} сек назад`;
 const min = Math.floor(sec / 60);
 if (min < 60) return `${min} мин назад`;
 const h = Math.floor(min / 60);
 return `${h} ч назад`;
 }

 async function getCacheAgeText() {
 const cachedTime = Number(await GM_getValue(STORAGE_KEYS.cacheTime, 0));
 if (!cachedTime) return "кэш: нет";
 return `кэш: обновлялся ${timeAgo(now() - cachedTime)}`;
 }

 function createUi() {
 const style = document.createElement("style");
 style.textContent = `
 :root {
 --ucoz-bs-body: #212529;
 --ucoz-bs-muted: rgba(33,37,41,.65);
 --ucoz-bs-border: rgba(0,0,0,.175);
 --ucoz-bs-bg: #ffffff;
 --ucoz-bs-bg-soft: #f8f9fa;
 --ucoz-bs-primary: #0d6efd;
 --ucoz-bs-primary-hover: #0b5ed7;
 --ucoz-bs-radius: 14px;
 --ucoz-bs-shadow: 0 14px 40px rgba(0,0,0,.25);
 --ucoz-bs-font: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
 }

 #ucozRknBtn {
 position: fixed;
 right: 16px;
 bottom: 16px;
 z-index: 2147483647;
 padding: 10px 12px;
 border-radius: 999px;
 border: 1px solid var(--ucoz-bs-border);
 background: var(--ucoz-bs-bg);
 color: var(--ucoz-bs-body);
 font: 14px/1.2 var(--ucoz-bs-font);
 box-shadow: 0 6px 18px rgba(0,0,0,.12);
 cursor: pointer;
 user-select: none;
 }
 #ucozRknBtn:hover { transform: translateY(-1px); }

 #ucozRknModalBackdrop {
 position: fixed;
 inset: 0;
 z-index: 2147483646;
 background: rgba(0,0,0,.45);
 display: none;
 }

 #ucozRknModal {
 position: fixed;
 left: 50%;
 top: 50%;
 transform: translate(-50%, -50%);
 z-index: 2147483647;
 width: min(760px, calc(100vw - 24px));
 background: var(--ucoz-bs-bg);
 color: var(--ucoz-bs-body);
 border-radius: var(--ucoz-bs-radius);
 box-shadow: var(--ucoz-bs-shadow);
 border: 1px solid var(--ucoz-bs-border);
 display: none;
 overflow: hidden;
 font: 14px/1.4 var(--ucoz-bs-font);
 }

 #ucozRknHeader {
 padding: 14px 16px;
 background: var(--ucoz-bs-bg-soft);
 border-bottom: 1px solid var(--ucoz-bs-border);
 display: flex;
 align-items: center;
 justify-content: space-between;
 gap: 12px;
 }

 #ucozRknTitle {
 font-weight: 700;
 font-size: 16px;
 }

 #ucozRknSub {
 margin-top: 3px;
 color: var(--ucoz-bs-muted);
 font-size: 13px;
 }

 #ucozRknClose {
 border: 1px solid var(--ucoz-bs-border);
 background: var(--ucoz-bs-bg);
 cursor: pointer;
 font-size: 16px;
 line-height: 1;
 padding: 6px 10px;
 border-radius: 10px;
 }
 #ucozRknClose:hover { background: rgba(0,0,0,.04); }

 #ucozRknBody {
 padding: 14px 16px 16px;
 display: grid;
 gap: 10px;
 }

 #ucozRknInput {
 width: 100%;
 min-height: 120px;
 resize: vertical;
 padding: 12px;
 border-radius: 12px;
 border: 1px solid var(--ucoz-bs-border);
 outline: none;
 font: 14px/1.4 var(--ucoz-bs-font);
 }
 #ucozRknInput:focus {
 border-color: rgba(13,110,253,.55);
 box-shadow: 0 0 0 4px rgba(13,110,253,.15);
 }

 #ucozRknRow {
 display: flex;
 gap: 10px;
 flex-wrap: wrap;
 align-items: center;
 }

 .ucozBtn {
 border: 1px solid transparent;
 padding: 10px 12px;
 border-radius: 12px;
 cursor: pointer;
 font: 14px/1.2 var(--ucoz-bs-font);
 }

 .ucozBtn.primary {
 background: var(--ucoz-bs-primary);
 border-color: var(--ucoz-bs-primary);
 color: #fff;
 }
 .ucozBtn.primary:hover { background: var(--ucoz-bs-primary-hover); }

 .ucozBtn.outline {
 background: var(--ucoz-bs-bg);
 border-color: var(--ucoz-bs-border);
 color: var(--ucoz-bs-body);
 }
 .ucozBtn.outline:hover { background: rgba(0,0,0,.04); }

 .ucozBtn:disabled {
 opacity: .6;
 cursor: not-allowed;
 }

 #ucozRknStatus {
 margin-left: auto;
 color: var(--ucoz-bs-muted);
 font-size: 13px;
 white-space: nowrap;
 }

 #ucozRknOutput {
 width: 100%;
 min-height: 180px;
 resize: vertical;
 padding: 12px;
 border-radius: 12px;
 border: 1px solid var(--ucoz-bs-border);
 background: rgba(0,0,0,.03);
 font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
 font-size: 12px;
 }

 #ucozRknFooter {
 padding: 10px 16px 14px;
 border-top: 1px solid var(--ucoz-bs-border);
 background: var(--ucoz-bs-bg-soft);
 color: var(--ucoz-bs-muted);
 font-size: 12.5px;
 }

 #ucozRknFooter b {
 color: rgba(33,37,41,.85);
 }
 `;
 document.documentElement.appendChild(style);

 const btn = document.createElement("div");
 btn.id = "ucozRknBtn";
 btn.textContent = "RKN check";
 document.body.appendChild(btn);

 const backdrop = document.createElement("div");
 backdrop.id = "ucozRknModalBackdrop";
 document.body.appendChild(backdrop);

 const modal = document.createElement("div");
 modal.id = "ucozRknModal";
 modal.innerHTML = `
 <div id="ucozRknHeader">
 <div>
 <div id="ucozRknTitle">Проверка доменов по реестру</div>
 <div id="ucozRknSub">Вставь домены или ссылки списком. Поддерживаются пробелы, запятые и переносы строк.</div>
 </div>
 <button id="ucozRknClose" title="Закрыть">✕</button>
 </div>

 <div id="ucozRknBody">
 <textarea id="ucozRknInput" placeholder="пример: example.com&#10;https://sub.example.com/page&#10;foo.bar"></textarea>

 <div id="ucozRknRow">
 <button class="ucozBtn primary" id="ucozRknRun">Проверить</button>
 <button class="ucozBtn outline" id="ucozRknCopy">Копировать результат</button>
 <button class="ucozBtn outline" id="ucozRknClearCache">Сбросить кэш</button>
 <div id="ucozRknStatus"></div>
 </div>

 <textarea id="ucozRknOutput" readonly placeholder="Тут появится результат"></textarea>
 </div>

 <div id="ucozRknFooter">
 <b>Как это работает:</b> скрипт сверяет введенные домены со списком из API.
 <br>
 ${SOURCES_TEXT}
 </div>
 `;
 document.body.appendChild(modal);

 function open() {
 backdrop.style.display = "block";
 modal.style.display = "block";
 modal.querySelector("#ucozRknInput").focus();
 }

 function close() {
 backdrop.style.display = "none";
 modal.style.display = "none";
 }

 btn.addEventListener("click", open);
 backdrop.addEventListener("click", close);
 modal.querySelector("#ucozRknClose").addEventListener("click", close);

 GM_registerMenuCommand("uCoz RKN Checker: открыть окно", open);

 return {
 input: modal.querySelector("#ucozRknInput"),
 output: modal.querySelector("#ucozRknOutput"),
 runBtn: modal.querySelector("#ucozRknRun"),
 copyBtn: modal.querySelector("#ucozRknCopy"),
 clearCacheBtn: modal.querySelector("#ucozRknClearCache"),
 status: modal.querySelector("#ucozRknStatus")
 };
 }

 const ui = createUi();

 function buildReport(inRkn, notInRkn, note) {
 const parts = [];

 if (inRkn.length) parts.push(formatResultLines("В реестре", inRkn));
 if (notInRkn.length) parts.push(formatResultLines("Не найдено", notInRkn));

 if (!inRkn.length && !notInRkn.length) {
 parts.push("Похоже, нечего проверять. Проверь ввод.");
 }

 if (note) parts.push(note);

 return parts.join("\n\n");
 }

 function checkDomains(domains, set) {
 const inRkn = [];
 const notInRkn = [];

 for (const d of domains) {
 if (set.has(d)) inRkn.push(d);
 else notInRkn.push(d);
 }

 return { inRkn, notInRkn };
 }

 async function onRun() {
 const statusEl = ui.status;
 const outEl = ui.output;

 const raw = ui.input.value;
 const candidates = splitInputToCandidates(raw).map(normalizeDomain);
 const domains = uniq(candidates).filter(Boolean);

 if (!domains.length) {
 outEl.value = "Пусто. Вставь хотя бы один домен.";
 return;
 }

 ui.runBtn.disabled = true;
 ui.copyBtn.disabled = true;
 ui.clearCacheBtn.disabled = true;

 try {
 statusEl.textContent = "Загружаю список...";
 let set = await getRknDomainsSet();

 statusEl.textContent = "Сравниваю...";
 let res = checkDomains(domains, set);

 const cacheAge = await getCacheAgeText();
 let note = `Подсказка: ${cacheAge}. Кэш обновляется раз в ${Math.round(CACHE_TTL_MS / 60000)} мин.`;

 if (AUTO_REFRESH_ON_MISS && res.notInRkn.length) {
 statusEl.textContent = "Есть промахи. Обновляю список и перепроверяю...";
 set = await fetchRknDomainsSetNoCache();
 res = checkDomains(domains, set);

 note = "Подсказка: при наличии промахов скрипт сделает принудительное обновление списка и перепроверит повторно.";
 }

 outEl.value = buildReport(res.inRkn, res.notInRkn, note);
 statusEl.textContent = "Готово";
 } catch (e) {
 outEl.value = "Ошибка: " + safeText(e && e.message ? e.message : e);
 statusEl.textContent = "Не получилось";
 } finally {
 ui.runBtn.disabled = false;
 ui.copyBtn.disabled = false;
 ui.clearCacheBtn.disabled = false;
 }
 }

 async function onCopy() {
 try {
 await navigator.clipboard.writeText(ui.output.value || "");
 ui.status.textContent = "Скопировано";
 setTimeout(() => (ui.status.textContent = ""), 1200);
 } catch (e) {
 ui.status.textContent = "Не смог скопировать. Браузер может блокировать буфер обмена.";
 setTimeout(() => (ui.status.textContent = ""), 1600);
 }
 }

 async function onClearCache() {
 await GM_setValue(STORAGE_KEYS.cacheData, "");
 await GM_setValue(STORAGE_KEYS.cacheTime, "0");
 ui.status.textContent = "Кэш сброшен";
 setTimeout(() => (ui.status.textContent = ""), 1200);
 }

 ui.runBtn.addEventListener("click", onRun);
 ui.copyBtn.addEventListener("click", onCopy);
 ui.clearCacheBtn.addEventListener("click", onClearCache);
})();

Заключение

Этот userscript закрывает задачу быстро и без лишних сложностей. Проверка доменов становится вопросом одной кнопки, а отчет получается чистым и понятным.

Оцените полезность материала!

Лицензия: CC BY-SA 4.0

Автор: Юрий Герук

Комментарии