Приветствую, Хабр!
Я не претендую на срывание покров или какой-то революционный способ, но данный метод позволит как минимум сохранить ту часть трафика, так преданного вашему проекту/сайту/блогу, и немного вернуть справедливость со всеми этими перипетиями с массовыми блокировками.
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 подключен.
Мы в полном обмундировании готовы встречать блокировку.
И, конечно же, выкладываю полный вариант моего воркера:
// 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);
});
}());
}
});
Комментарии (33)
Bonio
16.09.2021 20:24+2У меня эта идея давно в голове вертелась. У нее есть минус, это будет работать только для тех, кто уже посещал сайт до блокировки.
Sabin
16.09.2021 20:43+1Думаю, что при блокировке хоть раз да проверят, сработала ли она. А тут прямо на тестовом оборудовании сразу редирект и произойдет. +1 адрес в список и смысл этого воркера теряется
xjukebox Автор
16.09.2021 20:48+1Это уже нюансы, с которыми нужно работать отдельно.
Как показывает практика - боты РКН при посещении сайта не подгружают ничего, кроме исходного кода страницы. Соответственно о посещениях таких сайтов с полноценным WebDriver'ом, который ещё и с сервис-воркерами полноценно работает не может идти и речи.
JerleShannara
17.09.2021 02:04+1Интересно, а с каким user-agent они по сайтам гуляют, друг очень интересуется…
Saiv46
17.09.2021 05:29Мы не знаем, но сайт Умного Голосования при использовании wget перенаправляет на сюрприз. Вероятно это было для РКН или провайдеров что таки доступность сайтов перед блокировкой.
xjukebox Автор
17.09.2021 10:36+1Иногда без явно указанного агента, но в частоте случаев такие варианты:
Mozilla/5.0 (Windows NT 5.1; rv:35.0) Gecko/20100101 Firefox/35.0
Mozilla/5.0 (Windows NT 5.1; rv:26.0) Gecko/20100101 Firefox/26.0
Извините, больше расписать не могу по понятным причинам.
staticmain
17.09.2021 00:07У всено вышеописанного есть проблемы. У части провайдеров часть сайтов заблокирована по https, из-за чего браузер не показывает заглушку, он говорит, что домен Х, а сертификат от домена провайдера поэтому туда мы не пойдем. И только если разрешить переход на сайт - то будет уже заглушка, но при этом на домене сайта.
Другие провайдеры делают редирект, поэтому при тексте заглушки сразу будет домен провайдера, домена сайта не будет, поэтому воркер не будет знать куда редиректить.
xjukebox Автор
17.09.2021 08:11Редирект провайдера происходит с заблокированного сайта. Поэтому прежде чем провайдер отправит клиента на свою страницу - на вашей заблокированной будет 307 редирект с пустым ответом. А это значит, что воркер и в этом случае не сможет найти искомый текст и отправит клиента не на страницу с ошибкой и не на страницу с заглушкой, а на тот домен, который будет указан в settings.
Vilgelm
17.09.2021 03:39Может быть тогда не редирект делать, а просто подгружать контент с другого домена в фрейме?
dartraiden
17.09.2021 04:02+2Таким образом вместо заглушки провайдера о том, что сайт заблокирован пользователь переходит на незаблокированный домен.
Зачастую вместо заглушки пользователь получает тупо PR CONNECT RESET ERROR.xjukebox Автор
17.09.2021 08:06Даже если не будет заглушки - воркер не сможет найти искомый контент на сайте и переадресует клиента на новый домен. Попробуйте :)
Furriest
Может я не въезжаю, сорри. Но не понимаю логику работы. Service worker подключен к заблокированному сайту example.com. Пользователь открыл у себя новое окно браузера, набрал example.com, ТСПУ перехватило запрос в DNS и HTTP-реквест и заблокировало их, а в клиента плюнула HTTP 302 на картинку блокировки. Откуда у клиента в его браузере возьмется service worker, чтобы определить, что блокировка произошла, и переадресовать клиента на новый сайт?
Или все клиенты сначала должны зайти на сайт до блокировки и закэшировать у себя воркер?
xjukebox Автор
Да, клиенты должны посетить хотя бы раз сайт до блокировки.
Anrikigai
Все-таки можно поподробнее?
Вот я посетил сайт, закрыл браузер со всеми вкладками, открыл заново, набрал руками адрес...
Вместо, чтобы полезть на запрошенный сайт с заглушкой, браузер запустит скрипт из кеша?
xjukebox Автор
Посетили сайт
В кэш подгружается воркер, который при каждом последующем посещении будет проверять есть ли на странице текст "Х" и если его нет - перенаправлять на другой домен, которого нет в РКН
Если при следующем посещении сайта ваш провайдер будет пытаться вас перенаправить или показать заглушку о том, что сайт заблокирован - сработает сценарий из предыдущего пункта
hw_store
"на другой домен, которого нет в РКН" - на какой такой другой домен?
Что вообще в данном контексте подразумевается под словом "домен"?
Если вообще не использовать в браузере доменные имена, а набирать IP-адрес цифрами, то... ?
...например, когда я набираю адрес своего сайта, то там открывается сайт
...а когда набираю адрес сайта одной организации, не так давно признанной самизнаетечем, то там выскакивает заглушка, видимо, хостера - "direct IP access not allowed"
xjukebox Автор
Допустим у вас есть site1.com, который пока доступен в РФ.
Рассмотрим несколько вариантов блокировки и пути их решения с помощью редиректа воркером:
РКН может заблокировать отдельную страницу на сайте https://site1.com/index.html (редиректим посетителя на https://site1.com/index_not_blocked.html)
Домен/поддомен без блокировки по маске site1.com или sub.site1.com (редиректим на любое другое "зеркало", подойдёт и поддомен notblocked.site1.com)
Домен и все поддомены заблокированы *.site1.com (редиректим на site2.com, поддомены site1.com не подойдут)
hw_store
Я вот чего не понимаю. Блокировка, если я правильно понимаю, происходит на этапе запроса к DNS. Если скрипт, анализируя возвращаемый ответ, обнаруживает блокировку, то он подменяет доменное имя в запросе, так? Однако заблокированным может оказаться любой из доменов перечисленной в Вашем ответе структуры, и даже все домены - ведь они статически прописываются как переменные в скрипте при его первоначальной загрузке с незаблокированного сайта.
grumbler66rus
Блокировка, если я правильно понимаю, происходит на этапе запроса к DNS.Вы неправильно понимаете.
Подмена DNS на сервере DNS провайдера — это только один из многих, причём устаревший, метод блокировки, который обходится тривиально, использованием публичного или своего собственного сервера DNS. (Да, я знаю про DNS spoofing, это выявляется стандартным образом, браузеры уже умеют с ним бороться.)
Все без исключения крупные операторы интернета используют DPI для выявления запросов к запрещённым цензурой ресурсам.
Yuribtr
На данный момент РКН не может блокировать отдельную страницу на сайте, который работает через https. Поэтому РКН блокирует доступ ко всему сайту.
aamonster
А зачем проверять наличие текста? Сервис воркеры ж работают только на https, так что в случае перенаправления/заглушки – сайт будет просто недоступен.
Осталось от отладки на localhost или готовитесь к MiTM-сертификату от РКН?
xjukebox Автор
Воркер «подселяется» в кэш только с HTTPS, но содержание может проверять с любой, подконтрольно ему, страницы.
Использовал этот метод для своей сети сайтов со спорным содержанием. Фича спасла мне много нервов и времени, поэтому решил поделиться. :)
aamonster
Ему разве подконтрольны не только страницы с того же хоста и протокола? Хотите сказать, что если подселился с https://example.com, то и для http://example.com будет работать?
xjukebox Автор
Извиняюсь, да, вы правы. При запросе HTTP воркер не работает. Только вот провайдеру переадресацию в случае блокировки нужно будет делать именно с HTTPS, а значит переадресация произойдёт.
aamonster
Погодите. Как провайдер умудрится сделать переадресацию https? Он же без сертификата не может подменить ответ сервера, будет показываться лишь ошибка в браузере.
xjukebox Автор
Всё так. Если не установлен сервис воркер провайдер попытается сделать переадресацию и в браузере клиент увидит ошибку "PR CONNECT RESET ERROR", если же воркер установлен - произойдёт редирект на незаблокированный сайт.
ctpayc
А что если сделать расширение для браузера? Вести базу с заблокированными + альтернативными доменами:
перехватываем запрос
ищем альтернативный адрес
перенаправляем
профит