
Приветствую, Хабр!
Я не претендую на срывание покров или какой-то революционный способ, но данный метод позволит как минимум сохранить ту часть трафика, так преданного вашему проекту/сайту/блогу, и немного вернуть справедливость со всеми этими перипетиями с массовыми блокировками.
TL;DR
Суть способа в обыгрывании возможности Service Worker'ов проверять контент на подконтрольных ему страницам. Если воркер не находит определённого текста на странице — происходит редирект. Таким образом вместо заглушки провайдера о том, что сайт заблокирован пользователь переходит на незаблокированный домен.
Этап 1
Итак, для приготовления нам понадобится всего ничего:
- сайт, который (пока ещё) не заблокирован;
- источник, который при запросе на него будет выдавать URL на новый, незаблокированный ресурс (о них немного позже);
- JS файл — сервис-воркер, который мы будем использовать по прямому назначению, а именно, если руководствоваться статьёй:
Одной из важнейших проблем, от которой страдали пользователи веб-приложений, была работа в условиях потери связи
Начнём, пожалуй, с основы нашего воркера — переменных и констант:
// DEBUG_MODE - при true будет выводить в console log некоторые результаты выполнения наших функций const DEBUG_MODE = false; const DNS_RESOLVER_URL = "https://dns.google.com/resolve?type=TXT&name="; var settings = { enabled: 1, block_id: "<!-- RKN-BLOCK-URANUS-PLS -->", // Часть контента, при отсутствии которого наш воркер будет считать, что страница заблокирована redirect_url: "//google.com", // Fallback URL, если не нашли настроек для текущего домена dns_domains: ["subdomain.somesite.com", "subdomain.somesite.ru"] // Наши домены, в DNS ТХТ-записях у которых хранятся наши настройки. Если что-то случится с одним - воркер проверит на другом и далее по списку }; var redirect_params = { utm_term: self.location.hostname+'_swredir' // Исключительно для удобства добавляем ко всем редиректам utm_term, чтобы было понятно откуда и сколько мы спасли людей };
Установим event'ы fetch и install. Очевидно, это та «база» которая будет выполнять необходимые действия при установке воркера и каждом отдельном запросе к подконтрольным сервис воркеру ресурсам:
self.addEventListener("install", function () { self.skipWaiting(); checkSettings(); log("Install event"); }); self.addEventListener("fetch", function (event) { if (event.request.redirect === "manual" && navigator.onLine === true) { event.respondWith(async function() { await checkSettings(); return fetch(event.request) .then(function (response) { return process(response, event.request.url); }) .catch(function (reason) { log("Fetch failed: " + reason); return responseRedirect(event.request.url); }); }()); } });
Как вы заметили, в этой части мы используем функцию checkSettings(), с помощью которой мы и получаем набор настроек для домена, которые мы будем хранить в DNS TXT-записи того же или любого другого домена.
Конкретно в моём варианте используется текстовая версия DNS-резолвера от Google, но, возможно, вы сможете придумать что-то лучше. Пишите в комментарии.
function checkSettings(i = 0) { return fetch(DNS_RESOLVER_URL + settings.dns_domains[i], {cache: 'no-cache'}) .then(function (response) { return response.clone().json(); }) .then(function (data) { return JSON.parse(data['Answer'][0]['data']); }) .then(function (data) { settings.enabled = data[1]; settings.block_id = (data[2]) ? data[2] : settings.block_id; settings.redirect_url = (data[3]) ? data[3] : settings.redirect_url; settings.last_update = Date.now(); log("Settings updated: " + JSON.stringify(settings)); return true; }) .catch(function (reason) { if (settings.dns_domains.length - 1 > i) { log("Check settings on other domains DNS TXT: " + reason); return checkSettings(++i); } else { settings.enabled = 0; log("Settings error: " + reason); return false; } }); }
Как видно из функции checkSettings — мы обращаемся непосредственно к API DNS-резолвера гугла, дабы получить наш набор настроек. Что же наш воркер ожидает увидеть?
Набор параметров в виде JSON:
, где 1 — это параметр «enabled», которым мы указываем редиректить или нет в случае недоступности искомого контента на странице, 2 — собственно, сам искомый текст, 3 — домен, на который будем перенаправлять пользователя в случае отсутствия текста.{"1": 1, "2": "<!-- RKN-BLOCK-URANUS-PLS -->", "3": "https://notblocked.ru"}
Осталось дело за малым — подключить наш воркер на всех страницах нашего сайта:
<script>navigator.serviceWorker.register('/rp-sw.js');</script>
Эпилог
Итак, наш сайт пока не заблокирован, DNS-записи готовы, SW подключен.
Мы в полном обмундировании готовы встречать блокировку.
И, конечно же, выкладываю полный вариант моего воркера:
rp-sw.js
// DEBUG_MODE - при true будет выводить в console log некоторые результаты выполнения наших функций const DEBUG_MODE = false; const DNS_RESOLVER_URL = "https://dns.google.com/resolve?type=TXT&name="; var settings = { enabled: 1, block_id: "<!-- RKN-BLOCK-URANUS-PLS -->", // Часть контента, при отсутствии которого наш воркер будет считать, что страница заблокирована redirect_url: "//google.com", // Fallback URL, если не нашли настроек для текущего домена, то куда будем редиректить если enabled: 1 dns_domains: ["subdomain.somesite.com", "subdomain.somesite.ru"] // Наши домены, в DNS ТХТ-записях у которых хранятся наши настройки }; var redirect_params = { utm_term: self.location.hostname+'_swredir' }; function getUrlParams(url, prop) { var params = {}; url = url || ''; var searchIndex = url.indexOf('?'); if (-1 === searchIndex || url.length === searchIndex + 1) { return {}; } var search = decodeURIComponent( url.slice( searchIndex + 1 ) ); var definitions = search.split( '&' ); definitions.forEach( function( val, key ) { var parts = val.split( '=', 2 ); params[ parts[ 0 ] ] = parts[ 1 ]; } ); return ( prop && params.hasOwnProperty(prop) ) ? params[ prop ] : params; } function process(response, requestUrl) { log("Process started"); if (settings.enabled === 1) { return response.clone().text() .then(function(body) { if (checkBody(body)) { log("Check body success"); return true; } }) .then(function (result) { if (result) { return response; } else { log("Check failed. Send redirect to: " + getRedirectUrl(settings.redirect_url)); return responseRedirect(requestUrl); } }); } else { return response; } } function checkBody(body) { return (body.indexOf(settings.block_id) >= 0); } function checkSettings(i = 0) { return fetch(DNS_RESOLVER_URL + settings.dns_domains[i], {cache: 'no-cache'}) .then(function (response) { return response.clone().json(); }) .then(function (data) { return JSON.parse(data['Answer'][0]['data']); }) .then(function (data) { settings.enabled = data[1]; settings.block_id = (data[2]) ? data[2] : settings.block_id; settings.redirect_url = (data[3]) ? data[3] : settings.redirect_url; settings.last_update = Date.now(); log("Settings updated: " + JSON.stringify(settings)); return true; }) .catch(function (reason) { if (settings.dns_domains.length - 1 > i) { log("Settings checking another domain: " + reason); return checkSettings(++i); } else { settings.enabled = 0; log("Settings error: " + reason); return false; } }); } function responseRedirect(requestUrl) { redirect_params = getUrlParams(requestUrl); redirect_params.utm_term = self.location.hostname+'_swredir'; var redirect = { status: 302, statusText: "Found", headers: { Location: getRedirectUrl(settings.redirect_url) } }; return new Response('', redirect); } function getRedirectUrl(url) { url += (url.indexOf('?') === -1 ? '?' : '&') + queryParams(redirect_params); return url; } function queryParams(params) { return Object.keys(params).map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])).join('&'); } function log(text) { if (DEBUG_MODE) { console.log(text); } } self.addEventListener("install", function () { self.skipWaiting(); checkSettings(); log("Install event"); }); self.addEventListener("fetch", function (event) { if (event.request.redirect === "manual" && navigator.onLine === true) { event.respondWith(async function() { await checkSettings(); return fetch(event.request) .then(function (response) { return process(response, event.request.url); }) .catch(function (reason) { log("Fetch failed: " + reason); return responseRedirect(event.request.url); }); }()); } });
