806 lines
27 KiB
JavaScript
806 lines
27 KiB
JavaScript
(function (root) {
|
|
'use strict';
|
|
|
|
var DEFAULT_MARKERS = [
|
|
'доступ ограничен',
|
|
'доступ к сайту ограничен',
|
|
'сайт заблокирован',
|
|
'заблокировано',
|
|
'ограничение доступа',
|
|
'ресурс заблокирован',
|
|
'по решению органов государственной власти',
|
|
'роскомнадзор',
|
|
'forbidden',
|
|
'403 forbidden',
|
|
'404 not found',
|
|
'bad gateway',
|
|
'service unavailable',
|
|
'cloudflare host error',
|
|
'host error',
|
|
'cloudflare ray id',
|
|
'ray id',
|
|
'cloudflare-ray',
|
|
'cf-ray',
|
|
'идентификатор луча cloudflare',
|
|
'идентификатор ray cloudflare',
|
|
'идентификатор cloudflare ray',
|
|
'идентификатор луча',
|
|
'web server is down',
|
|
'origin is unreachable',
|
|
'connection timed out',
|
|
'время ожидания соединения истекло',
|
|
'истекло время ожидания соединения',
|
|
'код ошибки 520',
|
|
'код ошибки 521',
|
|
'код ошибки 522',
|
|
'код ошибки 523',
|
|
'код ошибки 524',
|
|
'код ошибки 525',
|
|
'код ошибки 526',
|
|
'код ошибки 530',
|
|
'ошибка 520',
|
|
'ошибка 521',
|
|
'ошибка 522',
|
|
'ошибка 523',
|
|
'ошибка 524',
|
|
'ошибка 525',
|
|
'ошибка 526',
|
|
'ошибка 530',
|
|
'первоначальное соединение между сетью cloudflare',
|
|
'между сетью cloudflare и исходным веб-сервером',
|
|
'исходным веб-сервером истекло',
|
|
'веб-страница не может быть отображена',
|
|
'a timeout occurred',
|
|
'gateway timeout',
|
|
'error 520',
|
|
'error 521',
|
|
'error 522',
|
|
'error 523',
|
|
'error 524',
|
|
'error 525',
|
|
'error 526',
|
|
'error 530',
|
|
'the web server is down',
|
|
'the origin web server is not reachable',
|
|
'the initial connection between cloudflare',
|
|
'cloudflare was unable to establish a connection',
|
|
'browser working',
|
|
'cloudflare working'
|
|
];
|
|
|
|
var DEFAULT_MARKERS_TEXT = DEFAULT_MARKERS.join('\n');
|
|
var DEFAULT_MARKERS_MAP = DEFAULT_MARKERS.reduce(function (result, marker) {
|
|
result[String(marker || '').toLowerCase()] = true;
|
|
return result;
|
|
}, {});
|
|
|
|
function uid() {
|
|
return 'rule-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
|
}
|
|
|
|
function createMultclubRule(id) {
|
|
return {
|
|
id: id || uid(),
|
|
name: 'Multclub numeric mirrors',
|
|
enabled: true,
|
|
hostRegex: '^multclub\\d*\\.lol$',
|
|
protocol: 'https',
|
|
mode: 'probe',
|
|
strategy: 'number',
|
|
template: 'multclub{n}.lol',
|
|
includeEmpty: true,
|
|
numberStart: 1,
|
|
numberEnd: 200,
|
|
numberStep: 1,
|
|
padLength: 0,
|
|
letterCharset: 'abcdefghijklmnopqrstuvwxyz',
|
|
letterMinLen: 1,
|
|
letterMaxLen: 2,
|
|
maxCandidates: 300,
|
|
customHosts: '',
|
|
timeoutMs: 3000,
|
|
parallelChecks: 8,
|
|
maxRedirects: 220,
|
|
cacheTtlHours: 0,
|
|
preferCachedHost: true,
|
|
blockedMarkers: DEFAULT_MARKERS_TEXT
|
|
};
|
|
}
|
|
|
|
function createEmptyRule(id) {
|
|
return {
|
|
id: id || uid(),
|
|
name: 'New mirror rule',
|
|
enabled: true,
|
|
hostRegex: '^example\\d*\\.com$',
|
|
protocol: 'https',
|
|
mode: 'probe',
|
|
strategy: 'number',
|
|
template: 'example{n}.com',
|
|
includeEmpty: false,
|
|
numberStart: 1,
|
|
numberEnd: 100,
|
|
numberStep: 1,
|
|
padLength: 0,
|
|
letterCharset: 'abcdefghijklmnopqrstuvwxyz',
|
|
letterMinLen: 1,
|
|
letterMaxLen: 2,
|
|
maxCandidates: 300,
|
|
customHosts: '',
|
|
timeoutMs: 3000,
|
|
parallelChecks: 8,
|
|
maxRedirects: 220,
|
|
cacheTtlHours: 0,
|
|
preferCachedHost: true,
|
|
blockedMarkers: DEFAULT_MARKERS_TEXT
|
|
};
|
|
}
|
|
|
|
function getDefaultRules() {
|
|
return [createMultclubRule('default-multclub')];
|
|
}
|
|
|
|
function normalizeRule(rule) {
|
|
var result = Object.assign(createEmptyRule(), rule || {});
|
|
|
|
if (!result.id) result.id = uid();
|
|
if (!result.strategy) result.strategy = 'number';
|
|
if (!result.mode) result.mode = 'step';
|
|
if (!result.protocol) result.protocol = 'https';
|
|
if (!result.blockedMarkers) result.blockedMarkers = DEFAULT_MARKERS_TEXT;
|
|
if (typeof result.preferCachedHost !== 'boolean') result.preferCachedHost = true;
|
|
|
|
return result;
|
|
}
|
|
|
|
function validateRuleCode(rule) {
|
|
if (rule.strategy === 'list') {
|
|
if (!String(rule.customHosts || '').trim()) {
|
|
return {
|
|
code: 'listHostsRequired',
|
|
message: 'Для режима списка нужно указать хотя бы один host.'
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (!String(rule.hostRegex || '').trim()) {
|
|
return {
|
|
code: 'hostRegexRequired',
|
|
message: 'Поле hostRegex не должно быть пустым.'
|
|
};
|
|
}
|
|
|
|
try {
|
|
new RegExp(rule.hostRegex, 'i');
|
|
} catch (e) {
|
|
return {
|
|
code: 'hostRegexInvalid',
|
|
detail: e.message,
|
|
message: 'Неверный RegExp в поле hostRegex: ' + e.message
|
|
};
|
|
}
|
|
|
|
if (rule.strategy === 'number' && String(rule.template || '').indexOf('{n}') === -1) {
|
|
return {
|
|
code: 'numberTemplateMissing',
|
|
message: 'Для цифрового перебора шаблон host должен содержать {n}.'
|
|
};
|
|
}
|
|
|
|
if (rule.strategy === 'letters' && String(rule.template || '').indexOf('{s}') === -1) {
|
|
return {
|
|
code: 'lettersTemplateMissing',
|
|
message: 'Для буквенного перебора шаблон host должен содержать {s}.'
|
|
};
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function validateRule(rule) {
|
|
var error = validateRuleCode(rule);
|
|
return error ? error.message : null;
|
|
}
|
|
|
|
function safeUrl(rawUrl) {
|
|
try {
|
|
return new URL(rawUrl);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function normalizeHost(host) {
|
|
return String(host || '').toLowerCase().trim();
|
|
}
|
|
|
|
function escapeRegex(text) {
|
|
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
function splitHosts(text) {
|
|
return String(text || '')
|
|
.split(/[\n,;\s]+/)
|
|
.map(normalizeHost)
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function uniquePush(result, seen, host) {
|
|
host = normalizeHost(host);
|
|
if (!host || seen[host]) return;
|
|
seen[host] = true;
|
|
result.push(host);
|
|
}
|
|
|
|
function getPath(urlObj) {
|
|
return urlObj.pathname + urlObj.search + urlObj.hash;
|
|
}
|
|
|
|
function getProtocol(rule, sourceUrlObj) {
|
|
var protocol = String(rule.protocol || 'https').replace(':', '').toLowerCase();
|
|
|
|
if (protocol === 'same') {
|
|
return sourceUrlObj.protocol.replace(':', '') || 'https';
|
|
}
|
|
|
|
if (protocol !== 'http' && protocol !== 'https') return 'https';
|
|
return protocol;
|
|
}
|
|
|
|
function buildTargetUrl(rule, host, sourceUrlObj) {
|
|
return getProtocol(rule, sourceUrlObj) + '://' + host + getPath(sourceUrlObj);
|
|
}
|
|
|
|
function urlsReferToSameNavigation(a, b) {
|
|
var au = safeUrl(a);
|
|
var bu = safeUrl(b);
|
|
|
|
if (!au || !bu) return false;
|
|
|
|
return (
|
|
au.protocol === bu.protocol &&
|
|
normalizeHost(au.hostname) === normalizeHost(bu.hostname) &&
|
|
getPath(au) === getPath(bu)
|
|
);
|
|
}
|
|
|
|
function padNumber(number, padLength) {
|
|
var text = String(number);
|
|
padLength = Number(padLength || 0);
|
|
|
|
while (text.length < padLength) text = '0' + text;
|
|
return text;
|
|
}
|
|
|
|
function hostFromNumber(rule, number) {
|
|
var template = String(rule.template || 'example{n}.com');
|
|
var value = number === null ? '' : padNumber(number, rule.padLength);
|
|
return normalizeHost(template.replace(/\{n\}/g, value));
|
|
}
|
|
|
|
function inferNumberFromTemplate(rule, host) {
|
|
var template = String(rule && rule.template || '');
|
|
if (template.indexOf('{n}') === -1) return null;
|
|
|
|
var escaped = escapeRegex(template).replace(/\\\{n\\\}/g, '(\\d*)');
|
|
var re = new RegExp('^' + escaped + '$', 'i');
|
|
var match = normalizeHost(host).match(re);
|
|
|
|
if (!match) return null;
|
|
if (match[1] === '') return '';
|
|
|
|
var n = Number(match[1]);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
|
|
function numberOrDefault(value, fallback) {
|
|
if (value === null || value === undefined || value === '') return fallback;
|
|
var n = Number(value);
|
|
return Number.isFinite(n) ? n : fallback;
|
|
}
|
|
|
|
function getNumberRange(rule) {
|
|
var start = numberOrDefault(rule.numberStart, 1);
|
|
var end = numberOrDefault(rule.numberEnd, 100);
|
|
|
|
if (end < start) {
|
|
var tmp = start;
|
|
start = end;
|
|
end = tmp;
|
|
}
|
|
|
|
return { start: start, end: end };
|
|
}
|
|
|
|
function hostPassesStrategyGuard(rule, hostname) {
|
|
if (!rule || rule.strategy !== 'number') return true;
|
|
if (String(rule.template || '').indexOf('{n}') === -1) return false;
|
|
|
|
var n = inferNumberFromTemplate(rule, hostname);
|
|
if (n === null) return false;
|
|
if (n === '') return rule.includeEmpty === true;
|
|
|
|
var range = getNumberRange(rule);
|
|
return n >= range.start && n <= range.end;
|
|
}
|
|
|
|
function matchesRule(rule, hostname, onError) {
|
|
if (!rule || rule.enabled === false) return false;
|
|
|
|
if (rule.strategy === 'list') {
|
|
return splitHosts(rule.customHosts).indexOf(normalizeHost(hostname)) !== -1;
|
|
}
|
|
|
|
if (!rule.hostRegex) return false;
|
|
|
|
try {
|
|
if (!new RegExp(rule.hostRegex, 'i').test(hostname)) return false;
|
|
return hostPassesStrategyGuard(rule, hostname);
|
|
} catch (e) {
|
|
if (typeof onError === 'function') onError(e, rule);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function findRuleForHost(rules, hostname, onError) {
|
|
hostname = normalizeHost(hostname);
|
|
|
|
for (var i = 0; i < rules.length; i += 1) {
|
|
if (matchesRule(rules[i], hostname, onError)) return rules[i];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function generateNumberHosts(rule, currentHost, result, seen) {
|
|
var range = getNumberRange(rule);
|
|
var start = range.start;
|
|
var end = range.end;
|
|
var step = Math.max(1, Number(rule.numberStep || 1));
|
|
var currentNumber = inferNumberFromTemplate(rule, currentHost);
|
|
|
|
function addNumber(n) {
|
|
uniquePush(result, seen, hostFromNumber(rule, n));
|
|
}
|
|
|
|
if (currentNumber !== null && currentNumber !== '') {
|
|
for (var up = currentNumber + step; up <= end; up += step) addNumber(up);
|
|
|
|
if (rule.includeEmpty === true) {
|
|
uniquePush(result, seen, hostFromNumber(rule, null));
|
|
}
|
|
|
|
for (var down = start; down < currentNumber; down += step) addNumber(down);
|
|
return;
|
|
}
|
|
|
|
if (currentNumber === '' || rule.includeEmpty === true) {
|
|
uniquePush(result, seen, hostFromNumber(rule, null));
|
|
}
|
|
|
|
for (var n = start; n <= end; n += step) addNumber(n);
|
|
}
|
|
|
|
function hostFromLetters(rule, token) {
|
|
var template = String(rule.template || 'example{s}.com');
|
|
return normalizeHost(template.replace(/\{s\}/g, token));
|
|
}
|
|
|
|
function inferLettersFromTemplate(rule, host) {
|
|
var template = String(rule.template || '');
|
|
if (template.indexOf('{s}') === -1) return null;
|
|
|
|
var escaped = escapeRegex(template).replace(/\\\{s\\\}/g, '([a-z0-9_-]+)');
|
|
var re = new RegExp('^' + escaped + '$', 'i');
|
|
var match = normalizeHost(host).match(re);
|
|
|
|
return match ? match[1] : null;
|
|
}
|
|
|
|
function generateTokens(charset, minLen, maxLen, limit) {
|
|
charset = String(charset || 'abcdefghijklmnopqrstuvwxyz');
|
|
minLen = Math.max(1, Number(minLen || 1));
|
|
maxLen = Math.max(minLen, Number(maxLen || minLen));
|
|
limit = Math.max(1, Number(limit || 300));
|
|
|
|
var tokens = [];
|
|
|
|
function walk(prefix, targetLen) {
|
|
if (tokens.length >= limit) return;
|
|
|
|
if (prefix.length === targetLen) {
|
|
tokens.push(prefix);
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < charset.length; i += 1) {
|
|
walk(prefix + charset[i], targetLen);
|
|
if (tokens.length >= limit) return;
|
|
}
|
|
}
|
|
|
|
for (var len = minLen; len <= maxLen; len += 1) {
|
|
walk('', len);
|
|
if (tokens.length >= limit) break;
|
|
}
|
|
|
|
return tokens;
|
|
}
|
|
|
|
function generateLetterHosts(rule, currentHost, result, seen) {
|
|
var tokens = generateTokens(rule.letterCharset, rule.letterMinLen, rule.letterMaxLen, rule.maxCandidates);
|
|
var currentToken = inferLettersFromTemplate(rule, currentHost);
|
|
var index = currentToken === null ? -1 : tokens.indexOf(currentToken);
|
|
var ordered = index >= 0 ? tokens.slice(index + 1).concat(tokens.slice(0, index)) : tokens;
|
|
|
|
for (var i = 0; i < ordered.length; i += 1) {
|
|
uniquePush(result, seen, hostFromLetters(rule, ordered[i]));
|
|
}
|
|
}
|
|
|
|
function estimateCandidateCount(rule) {
|
|
if (!rule) return 0;
|
|
|
|
if (rule.strategy === 'list') {
|
|
return splitHosts(rule.customHosts).length;
|
|
}
|
|
|
|
if (rule.strategy === 'letters') {
|
|
var charset = String(rule.letterCharset || 'abcdefghijklmnopqrstuvwxyz');
|
|
var minLen = Math.max(1, Number(rule.letterMinLen || 1));
|
|
var maxLen = Math.max(minLen, Number(rule.letterMaxLen || minLen));
|
|
var limit = Math.max(1, Number(rule.maxCandidates || 300));
|
|
var count = 0;
|
|
|
|
for (var len = minLen; len <= maxLen; len += 1) {
|
|
count += Math.pow(charset.length, len);
|
|
if (count >= limit) return limit;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
var range = getNumberRange(rule);
|
|
var step = Math.max(1, Number(rule.numberStep || 1));
|
|
var numericCount = Math.floor((range.end - range.start) / step) + 1;
|
|
return numericCount + (rule.includeEmpty === true ? 1 : 0) + 1;
|
|
}
|
|
|
|
function getEffectiveMaxRedirects(rule) {
|
|
var configured = Math.max(1, Number(rule && rule.maxRedirects || 50));
|
|
var neededForRange = Math.max(1, estimateCandidateCount(rule) + 5);
|
|
return Math.max(configured, neededForRange);
|
|
}
|
|
|
|
function buildCandidates(rule, currentHost, cachedHost) {
|
|
var result = [];
|
|
var seen = Object.create(null);
|
|
|
|
if (cachedHost && matchesRule(rule, cachedHost)) uniquePush(result, seen, cachedHost);
|
|
|
|
if (rule.strategy === 'list') {
|
|
splitHosts(rule.customHosts).forEach(function (host) {
|
|
uniquePush(result, seen, host);
|
|
});
|
|
} else if (rule.strategy === 'letters') {
|
|
generateLetterHosts(rule, currentHost, result, seen);
|
|
} else {
|
|
generateNumberHosts(rule, currentHost, result, seen);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getCacheKey(rule) {
|
|
return 'lastWorkingHost:' + (rule.id || rule.name || rule.hostRegex || 'unknown');
|
|
}
|
|
|
|
function getMarkers(rule) {
|
|
var source = DEFAULT_MARKERS.slice();
|
|
var custom = [];
|
|
var seen = Object.create(null);
|
|
|
|
if (rule && Array.isArray(rule.blockedMarkers)) {
|
|
custom = rule.blockedMarkers;
|
|
} else if (rule && typeof rule.blockedMarkers === 'string') {
|
|
custom = rule.blockedMarkers.split(/\n+/);
|
|
}
|
|
|
|
custom.forEach(function (item) {
|
|
source.push(item);
|
|
});
|
|
|
|
return source
|
|
.map(function (item) { return String(item || '').trim().toLowerCase(); })
|
|
.filter(function (item) {
|
|
if (!item || seen[item]) return false;
|
|
seen[item] = true;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function findCloudflareRayMarker(normalizedText) {
|
|
var text = String(normalizedText || '').toLowerCase();
|
|
var hasCloudflare = text.indexOf('cloudflare') !== -1;
|
|
|
|
if (text.indexOf('cf-ray') !== -1 || text.indexOf('cloudflare-ray') !== -1) {
|
|
return 'cloudflare-ray-id';
|
|
}
|
|
|
|
if (/\bcf[\s_-]*ray(?:[\s_-]*id)?\b/.test(text)) return 'cloudflare-ray-id';
|
|
if (/\bray[\s_-]*id\b/.test(text) && hasCloudflare) return 'cloudflare-ray-id';
|
|
if (/\bray[\s_-]*identifier\b/.test(text) && hasCloudflare) return 'cloudflare-ray-id';
|
|
|
|
var hasIdentifier = text.indexOf('идентификатор') !== -1;
|
|
var hasRayRu = text.indexOf('луча') !== -1 || text.indexOf('луч') !== -1 || text.indexOf('ray') !== -1;
|
|
|
|
if (hasCloudflare && hasIdentifier && hasRayRu) return 'cloudflare-ray-id-ru';
|
|
|
|
if (text.indexOf('идентификатор луча cloudflare') !== -1 ||
|
|
text.indexOf('идентификатор cloudflare ray') !== -1 ||
|
|
text.indexOf('идентификатор ray cloudflare') !== -1) {
|
|
return 'cloudflare-ray-id-ru';
|
|
}
|
|
|
|
if (hasCloudflare && /\bray\b/.test(text) && /\b[a-f0-9]{12,64}\b/i.test(text)) {
|
|
return 'cloudflare-ray-id-token';
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function findBlockedMarkerInText(rule, text) {
|
|
var normalized = String(text || '').toLowerCase();
|
|
var rayMarker = findCloudflareRayMarker(normalized);
|
|
if (rayMarker) return rayMarker;
|
|
|
|
var markers = getMarkers(rule);
|
|
for (var i = 0; i < markers.length; i += 1) {
|
|
if (normalized.indexOf(markers[i]) !== -1) return markers[i];
|
|
}
|
|
|
|
var cloudflareCode = normalized.match(/\b(520|521|522|523|524|525|526|530)\b/);
|
|
if (cloudflareCode && normalized.indexOf('cloudflare') !== -1) {
|
|
return 'cloudflare-status-' + cloudflareCode[1];
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function collapseText(text) {
|
|
return String(text || '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function normalizeMarker(marker) {
|
|
return String(marker || '').toLowerCase().trim();
|
|
}
|
|
|
|
function isBuiltInMarker(marker) {
|
|
marker = normalizeMarker(marker);
|
|
return Boolean(DEFAULT_MARKERS_MAP[marker]) ||
|
|
marker.indexOf('cloudflare-status-') === 0 ||
|
|
marker.indexOf('cloudflare-ray-id') === 0;
|
|
}
|
|
|
|
function isCloudflareLikeMarker(marker) {
|
|
marker = normalizeMarker(marker);
|
|
return marker.indexOf('cloudflare') !== -1 ||
|
|
marker.indexOf('cf-ray') !== -1 ||
|
|
marker.indexOf('ray id') !== -1 ||
|
|
marker.indexOf('ray-id') !== -1 ||
|
|
marker.indexOf('host error') !== -1 ||
|
|
marker.indexOf('origin is unreachable') !== -1 ||
|
|
marker.indexOf('connection timed out') !== -1 ||
|
|
marker.indexOf('web server is down') !== -1 ||
|
|
marker.indexOf('gateway timeout') !== -1 ||
|
|
/\berror 52[0-6]\b/.test(marker) ||
|
|
/\berror 530\b/.test(marker);
|
|
}
|
|
|
|
function isGenericHttpErrorMarker(marker) {
|
|
marker = normalizeMarker(marker);
|
|
return marker === 'forbidden' ||
|
|
marker === '403 forbidden' ||
|
|
marker === '404 not found' ||
|
|
marker === 'bad gateway' ||
|
|
marker === 'service unavailable';
|
|
}
|
|
|
|
function pageLooksFocusedOnGenericHttpError(visibleText, marker) {
|
|
var text = collapseText(visibleText).toLowerCase();
|
|
|
|
if (!text) return false;
|
|
if (text.length > 1800) return false;
|
|
|
|
marker = normalizeMarker(marker);
|
|
|
|
if (marker === 'forbidden') {
|
|
return text === 'forbidden' ||
|
|
/\b403\b/.test(text) ||
|
|
/\baccess denied\b/.test(text) ||
|
|
/\byou do not have permission\b/.test(text) ||
|
|
/\byou don't have permission\b/.test(text) ||
|
|
/\brequest forbidden\b/.test(text);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function countBuiltInErrorSignals(text) {
|
|
text = String(text || '').toLowerCase();
|
|
|
|
var signals = [
|
|
'доступ ограничен',
|
|
'сайт заблокирован',
|
|
'заблокировано',
|
|
'ограничение доступа',
|
|
'ресурс заблокирован',
|
|
'роскомнадзор',
|
|
'по решению органов государственной власти',
|
|
'403',
|
|
'404',
|
|
'forbidden',
|
|
'access denied',
|
|
'not found',
|
|
'bad gateway',
|
|
'service unavailable',
|
|
'host error',
|
|
'origin is unreachable',
|
|
'connection timed out',
|
|
'web server is down',
|
|
'gateway timeout',
|
|
'cloudflare',
|
|
'ray id',
|
|
'cf-ray',
|
|
'error 520',
|
|
'error 521',
|
|
'error 522',
|
|
'error 523',
|
|
'error 524',
|
|
'error 525',
|
|
'error 526',
|
|
'error 530',
|
|
'код ошибки 520',
|
|
'код ошибки 521',
|
|
'код ошибки 522',
|
|
'код ошибки 523',
|
|
'код ошибки 524',
|
|
'код ошибки 525',
|
|
'код ошибки 526',
|
|
'код ошибки 530',
|
|
'ошибка 520',
|
|
'ошибка 521',
|
|
'ошибка 522',
|
|
'ошибка 523',
|
|
'ошибка 524',
|
|
'ошибка 525',
|
|
'ошибка 526',
|
|
'ошибка 530'
|
|
];
|
|
var count = 0;
|
|
|
|
signals.forEach(function (signal) {
|
|
if (text.indexOf(signal) !== -1) count += 1;
|
|
});
|
|
|
|
return count;
|
|
}
|
|
|
|
function pageLooksFocusedOnBuiltInMarker(visibleText, marker) {
|
|
var text = collapseText(visibleText).toLowerCase();
|
|
|
|
if (!text) return false;
|
|
if (isGenericHttpErrorMarker(marker)) return pageLooksFocusedOnGenericHttpError(visibleText, marker);
|
|
if (isCloudflareLikeMarker(marker)) return text.length <= 4500 && countBuiltInErrorSignals(text.slice(0, 4500)) >= 2;
|
|
if (text.length <= 1800) return true;
|
|
if (text.length > 4500) return false;
|
|
return countBuiltInErrorSignals(text.slice(0, 4500)) >= 2;
|
|
}
|
|
|
|
function pageLooksFocusedOnBlockedError(title, visibleText, marker) {
|
|
var titleMarker = findBlockedMarkerInText({}, title);
|
|
|
|
if (titleMarker) return true;
|
|
if (isBuiltInMarker(marker)) return pageLooksFocusedOnBuiltInMarker(visibleText, marker);
|
|
return true;
|
|
}
|
|
|
|
function findBlockedMarkerInPage(rule, title, visibleText) {
|
|
var titleMarker = findBlockedMarkerInText(rule, title);
|
|
var bodyMarker;
|
|
|
|
if (titleMarker) return titleMarker;
|
|
|
|
bodyMarker = findBlockedMarkerInText(rule, visibleText);
|
|
if (!bodyMarker) return '';
|
|
|
|
return pageLooksFocusedOnBlockedError(title, visibleText, bodyMarker) ? bodyMarker : '';
|
|
}
|
|
|
|
function stripHtmlForMarkerScan(text) {
|
|
var source = String(text || '');
|
|
|
|
if (source.indexOf('<') === -1 || !/<(?:html|head|body|title|script|style|template|div|span|p|h[1-6]|main|section|article|pre|code|br|meta|link)\b/i.test(source)) {
|
|
return source;
|
|
}
|
|
|
|
return source
|
|
.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
|
|
.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
|
|
.replace(/<template\b[^>]*>[\s\S]*?<\/template>/gi, ' ')
|
|
.replace(/<([a-z][\w:-]*)\b[^>]*(?:\shidden\b|style\s*=\s*["'][^"']*(?:display\s*:\s*none|visibility\s*:\s*hidden)[^"']*["'])[^>]*>[\s\S]*?<\/\1>/gi, ' ')
|
|
.replace(/<!--[\s\S]*?-->/g, ' ')
|
|
.replace(/<[^>]+>/g, ' ')
|
|
.replace(/ | /gi, ' ')
|
|
.replace(/&/gi, '&')
|
|
.replace(/</gi, '<')
|
|
.replace(/>/gi, '>')
|
|
.replace(/"/gi, '"')
|
|
.replace(/'|'/gi, "'");
|
|
}
|
|
|
|
function extractHtmlTitle(text) {
|
|
var match = String(text || '').match(/<title\b[^>]*>([\s\S]*?)<\/title>/i);
|
|
return match ? stripHtmlForMarkerScan(match[1]) : '';
|
|
}
|
|
|
|
function responseLooksBlocked(rule, text) {
|
|
var title = extractHtmlTitle(text);
|
|
var visibleText = stripHtmlForMarkerScan(text);
|
|
return Boolean(findBlockedMarkerInPage(rule, title, visibleText));
|
|
}
|
|
|
|
function preferCachedHostEnabled(rule) {
|
|
return rule.preferCachedHost !== false;
|
|
}
|
|
|
|
var api = {
|
|
DEFAULT_MARKERS: DEFAULT_MARKERS.slice(),
|
|
DEFAULT_MARKERS_TEXT: DEFAULT_MARKERS_TEXT,
|
|
uid: uid,
|
|
createMultclubRule: createMultclubRule,
|
|
createEmptyRule: createEmptyRule,
|
|
getDefaultRules: getDefaultRules,
|
|
normalizeRule: normalizeRule,
|
|
validateRuleCode: validateRuleCode,
|
|
validateRule: validateRule,
|
|
safeUrl: safeUrl,
|
|
normalizeHost: normalizeHost,
|
|
escapeRegex: escapeRegex,
|
|
splitHosts: splitHosts,
|
|
uniquePush: uniquePush,
|
|
getPath: getPath,
|
|
getProtocol: getProtocol,
|
|
buildTargetUrl: buildTargetUrl,
|
|
urlsReferToSameNavigation: urlsReferToSameNavigation,
|
|
padNumber: padNumber,
|
|
hostFromNumber: hostFromNumber,
|
|
inferNumberFromTemplate: inferNumberFromTemplate,
|
|
getNumberRange: getNumberRange,
|
|
hostPassesStrategyGuard: hostPassesStrategyGuard,
|
|
matchesRule: matchesRule,
|
|
findRuleForHost: findRuleForHost,
|
|
generateNumberHosts: generateNumberHosts,
|
|
hostFromLetters: hostFromLetters,
|
|
inferLettersFromTemplate: inferLettersFromTemplate,
|
|
generateTokens: generateTokens,
|
|
generateLetterHosts: generateLetterHosts,
|
|
estimateCandidateCount: estimateCandidateCount,
|
|
getEffectiveMaxRedirects: getEffectiveMaxRedirects,
|
|
buildCandidates: buildCandidates,
|
|
getCacheKey: getCacheKey,
|
|
getMarkers: getMarkers,
|
|
findCloudflareRayMarker: findCloudflareRayMarker,
|
|
findBlockedMarkerInText: findBlockedMarkerInText,
|
|
findBlockedMarkerInPage: findBlockedMarkerInPage,
|
|
stripHtmlForMarkerScan: stripHtmlForMarkerScan,
|
|
responseLooksBlocked: responseLooksBlocked,
|
|
preferCachedHostEnabled: preferCachedHostEnabled
|
|
};
|
|
|
|
root.AutoMirrorShared = api;
|
|
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this);
|