Введение
Когда работаешь с доменами и сайтами, проверка на блокировки РКН быстро превращается в рутину. Один домен еще можно проверить вручную, но когда их десятки, начинается уныние в чистом виде.
Ни расширения, ни локальные базы не всегда удобны. Хочется простое решение, которое живет в браузере и работает по кнопке. В этой статье как раз такое. Скрипт устанавливается в Tampermonkey и позволяет проверять один или сразу несколько доменов через окно ввода.
Применение. Для чего, почему и как
Скрипт пригодится, если:
• нужно быстро проверить домен клиента перед переносом или настройками
• требуется массово прогнать список доменов
• надо подтвердить наличие домена в реестре для тикета, отчета или переписки
• важно сверить домен прямо сейчас, не гадая, насколько старая информация
Почему Tampermonkey:
• не надо собирать расширение и бороться с ограничениями браузеров
• работает в Chrome, Edge, Firefox и других
• легко правится под себя
• все находится в одном файле и обновляется в два клика
Смысл решения простой. Скрипт загружает список доменов из публичного API, хранит его в кэше и сравнивает введенные домены со списком. На каждый домен не делаются отдельные запросы, поэтому проверка происходит почти мгновенно.
Как установить и пользоваться
Установка Tampermonkey
- Установите расширение Tampermonkey для своего браузера.
- Убедитесь, что иконка Tampermonkey появилась в панели расширений.
Установка скрипта
- Нажмите на иконку Tampermonkey.
- Откройте пункт Создать новый скрипт.
- Удалите весь шаблонный код.
- Вставьте код ниже целиком.
- Сохраните скрипт.
Как пользоваться скриптом
- Откройте любую страницу.
- Внизу справа появится кнопка RKN check.
- Нажмите ее, откроется окно.
- Вставьте домены или ссылки списком. Можно через пробелы, запятые и переносы строк.
- Нажмите Проверить.
- Получите результат. При необходимости нажмите Копировать результат.
Если часть доменов не найдена, скрипт автоматически обновит список и перепроверит повторно. Если все найдено, блок Не найдено не будет выводиться, чтобы отчет выглядел логично.
Как работает скрипт
Скрипт берет список доменов из:
https://reestr.rublacklist.net/api/v3/domains/
Этот источник агрегирует данные из официального реестра РКН:
Дальше происходит следующее:
- Скрипт загружает список доменов и сохраняет его в кэш браузера на 10 минут.
- Домены нормализуются. Убираются протоколы, пути, www, порты.
- Проверка идет через быстрое сравнение с локальным списком.
- Если есть промахи, делается принудительное обновление списка и повторная перепроверка.
- В отчете выводятся только релевантные блоки, без лишних строк вроде Не найдено 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 https://sub.example.com/page 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
Автор: Юрий Герук
Комментарии