Приветствую, Хабр!
Я не претендую на срывание покров или какой-то революционный способ, но данный метод позволит как минимум сохранить ту часть трафика, так преданного вашему проекту/сайту/блогу, и немного вернуть справедливость со всеми этими перипетиями с массовыми блокировками.
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": 1, "2": "<!-- RKN-BLOCK-URANUS-PLS -->", "3": "https://notblocked.ru"}
, где 1 — это параметр «enabled», которым мы указываем редиректить или нет в случае недоступности искомого контента на странице, 2 — собственно, сам искомый текст, 3 — домен, на который будем перенаправлять пользователя в случае отсутствия текста.Осталось дело за малым — подключить наш воркер на всех страницах нашего сайта:
<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);
});
}());
}
});