mirror of
https://github.com/VOID-Anonymity/V.O.I.D-VPN_Bypass.git
synced 2026-09-22 23:08:10 +03:00
Парсер
This commit is contained in:
@@ -0,0 +1,982 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import aiofiles
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
import hashlib
|
||||||
|
import socket
|
||||||
|
import random
|
||||||
|
import urllib.parse
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
|
||||||
|
# ========= ДИАГНОСТИКА =========
|
||||||
|
print(f"🚀 Запуск парсера...")
|
||||||
|
print(f"📂 Текущая директория: {os.getcwd()}")
|
||||||
|
print(f"🐍 Python версия: {sys.version}")
|
||||||
|
|
||||||
|
# ========= ФАЙЛЫ =========
|
||||||
|
SOURCES_FILE = "sources.txt"
|
||||||
|
OUTPUT_FILE = "url.txt"
|
||||||
|
CLEAN_FILE = "url_clean.txt"
|
||||||
|
FILTERED_FILE = "url_filtered.txt"
|
||||||
|
NAMED_FILE = "url_named.txt"
|
||||||
|
ENCODED_FILE = "url_encoded.txt"
|
||||||
|
WORK_FILE = "url_work.txt"
|
||||||
|
LOG_FILE = "log.txt"
|
||||||
|
PROCESSED_FILE = "processed.json"
|
||||||
|
CACHE_FILE = "cache_results.json"
|
||||||
|
DEBUG_FILE = "debug_failed.txt"
|
||||||
|
XRAY_LOG_FILE = "xray_errors.log"
|
||||||
|
|
||||||
|
# ========= НАСТРОЙКИ =========
|
||||||
|
THREADS_DOWNLOAD = 50
|
||||||
|
CYCLE_DELAY = 3600
|
||||||
|
LOG_CLEAN_INTERVAL = 86400
|
||||||
|
CYCLES_BEFORE_DEBUG_CLEAN = 5
|
||||||
|
|
||||||
|
XRAY_MAX_WORKERS = 30
|
||||||
|
XRAY_TEST_URL = "https://www.gstatic.com/generate_204"
|
||||||
|
XRAY_TIMEOUT = 3
|
||||||
|
MAX_RETRIES = 2
|
||||||
|
RETRY_DELAY = 1
|
||||||
|
|
||||||
|
print(f"⚡ Настройки: XRAY_MAX_WORKERS={XRAY_MAX_WORKERS}, TIMEOUT={XRAY_TIMEOUT}")
|
||||||
|
|
||||||
|
cycle_counter = 0
|
||||||
|
|
||||||
|
# ========= РЕГУЛЯРКИ =========
|
||||||
|
VLESS_REGEX = re.compile(r"vless://[0-9a-fA-F\-]{36}@[^\s\"'<]+", re.IGNORECASE)
|
||||||
|
UUID_REGEX = re.compile(
|
||||||
|
r"[0-9a-fA-F]{8}-"
|
||||||
|
r"[0-9a-fA-F]{4}-"
|
||||||
|
r"[0-9a-fA-F]{4}-"
|
||||||
|
r"[0-9a-fA-F]{4}-"
|
||||||
|
r"[0-9a-fA-F]{12}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ========= СЛОВАРЬ ДОМЕНОВ =========
|
||||||
|
DOMAIN_NAMES = {
|
||||||
|
'x5.ru': 'Пятёрочка', '5ka.ru': 'Пятёрочка', '5ka-cdn.ru': 'Пятёрочка',
|
||||||
|
'5ka.static.ru': 'Пятёрочка', 'ads.x5.ru': 'Пятёрочка', 'perekrestok.ru': 'Перекрёсток',
|
||||||
|
'vprok.ru': 'Перекрёсток', 'dixy.ru': 'Дикси', 'fasssst.ru': 'Fasssst',
|
||||||
|
'rontgen.fasssst.ru': 'Fasssst', 'res.fasssst.ru': 'Fasssst', 'yt.fasssst.ru': 'Fasssst',
|
||||||
|
'fast.strelkavpn.ru': 'StrelkaVPN', 'strelkavpn.ru': 'StrelkaVPN', 'maviks.ru': 'Maviks',
|
||||||
|
'ru.maviks.ru': 'Maviks', 'a.ru.maviks.ru': 'Maviks', 'tree-top.cc': 'TreeTop',
|
||||||
|
'a.ru.tree-top.cc': 'TreeTop', 'connect-iskra.ru': 'Iskra', '212-wl.connect-iskra.ru': 'Iskra',
|
||||||
|
'vk.com': 'VK', 'vk.ru': 'VK', 'vkontakte.ru': 'VK', 'userapi.com': 'VK',
|
||||||
|
'cdn.vk.com': 'VK', 'cdn.vk.ru': 'VK', 'id.vk.com': 'VK', 'id.vk.ru': 'VK',
|
||||||
|
'login.vk.com': 'VK', 'login.vk.ru': 'VK', 'api.vk.com': 'VK', 'api.vk.ru': 'VK',
|
||||||
|
'im.vk.com': 'VK', 'm.vk.com': 'VK', 'm.vk.ru': 'VK', 'sun6-22.userapi.com': 'VK',
|
||||||
|
'sun6-21.userapi.com': 'VK', 'sun6-20.userapi.com': 'VK', 'sun9-38.userapi.com': 'VK',
|
||||||
|
'sun9-101.userapi.com': 'VK', 'pptest.userapi.com': 'VK', 'vk-portal.net': 'VK',
|
||||||
|
'stats.vk-portal.net': 'VK', 'akashi.vk-portal.net': 'VK', 'vkvideo.ru': 'VK Видео',
|
||||||
|
'm.vkvideo.ru': 'VK Видео', 'queuev4.vk.com': 'VK', 'eh.vk.com': 'VK', 'cloud.vk.com': 'VK',
|
||||||
|
'cloud.vk.ru': 'VK', 'admin.cs7777.vk.ru': 'VK', 'admin.tau.vk.ru': 'VK',
|
||||||
|
'analytics.vk.ru': 'VK', 'api.cs7777.vk.ru': 'VK', 'api.tau.vk.ru': 'VK',
|
||||||
|
'away.cs7777.vk.ru': 'VK', 'away.tau.vk.ru': 'VK', 'business.vk.ru': 'VK',
|
||||||
|
'connect.cs7777.vk.ru': 'VK', 'cs7777.vk.ru': 'VK', 'dev.cs7777.vk.ru': 'VK',
|
||||||
|
'dev.tau.vk.ru': 'VK', 'expert.vk.ru': 'VK', 'id.cs7777.vk.ru': 'VK', 'id.tau.vk.ru': 'VK',
|
||||||
|
'login.cs7777.vk.ru': 'VK', 'login.tau.vk.ru': 'VK', 'm.cs7777.vk.ru': 'VK',
|
||||||
|
'm.tau.vk.ru': 'VK', 'm.vkvideo.cs7777.vk.ru': 'VK Видео', 'me.cs7777.vk.ru': 'VK',
|
||||||
|
'ms.cs7777.vk.ru': 'VK', 'music.vk.ru': 'VK Музыка', 'oauth.cs7777.vk.ru': 'VK',
|
||||||
|
'oauth.tau.vk.ru': 'VK', 'oauth2.cs7777.vk.ru': 'VK', 'ord.vk.ru': 'VK', 'push.vk.ru': 'VK',
|
||||||
|
'r.vk.ru': 'VK', 'target.vk.ru': 'VK', 'tech.vk.ru': 'VK', 'ui.cs7777.vk.ru': 'VK',
|
||||||
|
'ui.tau.vk.ru': 'VK', 'vkvideo.cs7777.vk.ru': 'VK Видео', 'speedload.ru': 'Speedload',
|
||||||
|
'api.speedload.ru': 'Speedload', 'chat.speedload.ru': 'Speedload', 'serverstats.ru': 'ServerStats',
|
||||||
|
'cdnfive.serverstats.ru': 'ServerStats', 'cdncloudtwo.serverstats.ru': 'ServerStats',
|
||||||
|
'furypay.ru': 'FuryPay', 'api.furypay.ru': 'FuryPay', 'jojack.ru': 'JoJack',
|
||||||
|
'spb.jojack.ru': 'JoJack', 'at.jojack.ru': 'JoJack', 'tcp-reset-club.net': 'TCP Reset',
|
||||||
|
'est01-ss01.tcp-reset-club.net': 'TCP Reset', 'nl01-ss01.tcp-reset-club.net': 'TCP Reset',
|
||||||
|
'ru01-blh01.tcp-reset-club.net': 'TCP Reset', 'gov.ru': 'Госуслуги', 'kremlin.ru': 'Кремль',
|
||||||
|
'government.ru': 'Правительство', 'duma.gov.ru': 'Госдума', 'genproc.gov.ru': 'Генпрокуратура',
|
||||||
|
'epp.genproc.gov.ru': 'Генпрокуратура', 'cikrf.ru': 'ЦИК', 'izbirkom.ru': 'Избирком',
|
||||||
|
'gosuslugi.ru': 'Госуслуги', 'sfd.gosuslugi.ru': 'Госуслуги', 'esia.gosuslugi.ru': 'Госуслуги',
|
||||||
|
'bot.gosuslugi.ru': 'Госуслуги', 'contract.gosuslugi.ru': 'Госуслуги',
|
||||||
|
'novorossiya.gosuslugi.ru': 'Госуслуги', 'pos.gosuslugi.ru': 'Госуслуги',
|
||||||
|
'lk.gosuslugi.ru': 'Госуслуги', 'map.gosuslugi.ru': 'Госуслуги',
|
||||||
|
'partners.gosuslugi.ru': 'Госуслуги', 'gosweb.gosuslugi.ru': 'Госуслуги',
|
||||||
|
'voter.gosuslugi.ru': 'Госуслуги', 'gu-st.ru': 'Госуслуги', 'nalog.ru': 'ФНС',
|
||||||
|
'pfr.gov.ru': 'ПФР', 'digital.gov.ru': 'Минцифры', 'adm.digital.gov.ru': 'Минцифры',
|
||||||
|
'xn--80ajghhoc2aj1c8b.xn--p1ai': 'Минцифры', 'a.res-nsdi.ru': 'NSDI',
|
||||||
|
'b.res-nsdi.ru': 'NSDI', 'a.auth-nsdi.ru': 'NSDI', 'b.auth-nsdi.ru': 'NSDI',
|
||||||
|
'ok.ru': 'Одноклассники', 'odnoklassniki.ru': 'Одноклассники', 'cdn.ok.ru': 'Одноклассники',
|
||||||
|
'st.okcdn.ru': 'Одноклассники', 'st.ok.ru': 'Одноклассники', 'apiok.ru': 'Одноклассники',
|
||||||
|
'jira.apiok.ru': 'Одноклассники', 'api.ok.ru': 'Одноклассники', 'm.ok.ru': 'Одноклассники',
|
||||||
|
'live.ok.ru': 'Одноклассники', 'multitest.ok.ru': 'Одноклассники', 'dating.ok.ru': 'Одноклассники',
|
||||||
|
'tamtam.ok.ru': 'Одноклассники', '742231.ms.ok.ru': 'Одноклассники', 'ozon.ru': 'Ozon',
|
||||||
|
'www.ozon.ru': 'Ozon', 'seller.ozon.ru': 'Ozon', 'bank.ozon.ru': 'Ozon', 'pay.ozon.ru': 'Ozon',
|
||||||
|
'securepay.ozon.ru': 'Ozon', 'adv.ozon.ru': 'Ozon', 'invest.ozon.ru': 'Ozon',
|
||||||
|
'ord.ozon.ru': 'Ozon', 'autodiscover.ord.ozon.ru': 'Ozon', 'st.ozone.ru': 'Ozon',
|
||||||
|
'ir.ozone.ru': 'Ozon', 'vt-1.ozone.ru': 'Ozon', 'ir-2.ozone.ru': 'Ozon', 'xapi.ozon.ru': 'Ozon',
|
||||||
|
'owa.ozon.ru': 'Ozon', 'learning.ozon.ru': 'Ozon', 'mapi.learning.ozon.ru': 'Ozon',
|
||||||
|
'ws.seller.ozon.ru': 'Ozon', 'wildberries.ru': 'Wildberries', 'wb.ru': 'Wildberries',
|
||||||
|
'static.wb.ru': 'Wildberries', 'seller.wildberries.ru': 'Wildberries',
|
||||||
|
'banners.wildberries.ru': 'Wildberries', 'fw.wb.ru': 'Wildberries', 'finance.wb.ru': 'Wildberries',
|
||||||
|
'jitsi.wb.ru': 'Wildberries', 'dnd.wb.ru': 'Wildberries', 'user-geo-data.wildberries.ru': 'Wildberries',
|
||||||
|
'banners-website.wildberries.ru': 'Wildberries', 'chat-prod.wildberries.ru': 'Wildberries',
|
||||||
|
'a.wb.ru': 'Wildberries', 'avito.ru': 'Avito', 'm.avito.ru': 'Avito', 'api.avito.ru': 'Avito',
|
||||||
|
'avito.st': 'Avito', 'img.avito.st': 'Avito', 'sntr.avito.ru': 'Avito', 'stats.avito.ru': 'Avito',
|
||||||
|
'cs.avito.ru': 'Avito', 'www.avito.st': 'Avito', 'st.avito.ru': 'Avito', 'www.avito.ru': 'Avito',
|
||||||
|
**{f'{i:02d}.img.avito.st': 'Avito' for i in range(100)},
|
||||||
|
'sberbank.ru': 'Сбербанк', 'online.sberbank.ru': 'Сбербанк', 'sber.ru': 'Сбербанк',
|
||||||
|
'id.sber.ru': 'Сбербанк', 'bfds.sberbank.ru': 'Сбербанк', 'cms-res-web.online.sberbank.ru': 'Сбербанк',
|
||||||
|
'esa-res.online.sberbank.ru': 'Сбербанк', 'pl-res.online.sberbank.ru': 'Сбербанк',
|
||||||
|
'www.sberbank.ru': 'Сбербанк', 'vtb.ru': 'ВТБ', 'www.vtb.ru': 'ВТБ', 'online.vtb.ru': 'ВТБ',
|
||||||
|
'chat3.vtb.ru': 'ВТБ', 's.vtb.ru': 'ВТБ', 'sso-app4.vtb.ru': 'ВТБ', 'sso-app5.vtb.ru': 'ВТБ',
|
||||||
|
'gazprombank.ru': 'Газпромбанк', 'alfabank.ru': 'Альфа-Банк', 'metrics.alfabank.ru': 'Альфа-Банк',
|
||||||
|
'tinkoff.ru': 'Тинькофф', 'tbank.ru': 'Тинькофф', 'cdn.tbank.ru': 'Тинькофф', 'hrc.tbank.ru': 'Тинькофф',
|
||||||
|
'cobrowsing.tbank.ru': 'Тинькофф', 'le.tbank.ru': 'Тинькофф', 'id.tbank.ru': 'Тинькофф',
|
||||||
|
'imgproxy.cdn-tinkoff.ru': 'Тинькофф', 'banki.ru': 'Банки.ру', 'yandex.ru': 'Яндекс',
|
||||||
|
'ya.ru': 'Яндекс', 'dzen.ru': 'Дзен', 'kinopoisk.ru': 'Кинопоиск', 'yastatic.net': 'Яндекс',
|
||||||
|
'yandex.net': 'Яндекс', 'mail.yandex.ru': 'Яндекс Почта', 'disk.yandex.ru': 'Яндекс Диск',
|
||||||
|
'maps.yandex.ru': 'Яндекс Карты', 'api-maps.yandex.ru': 'Яндекс Карты',
|
||||||
|
'enterprise.api-maps.yandex.ru': 'Яндекс Карты', 'music.yandex.ru': 'Яндекс Музыка',
|
||||||
|
'yandex.by': 'Яндекс', 'yandex.com': 'Яндекс', 'travel.yandex.ru': 'Яндекс Путешествия',
|
||||||
|
'informer.yandex.ru': 'Яндекс', 'mediafeeds.yandex.ru': 'Яндекс', 'mediafeeds.yandex.com': 'Яндекс',
|
||||||
|
'uslugi.yandex.ru': 'Яндекс Услуги', 'kiks.yandex.ru': 'Яндекс', 'kiks.yandex.com': 'Яндекс',
|
||||||
|
'frontend.vh.yandex.ru': 'Яндекс', 'favicon.yandex.ru': 'Яндекс', 'favicon.yandex.com': 'Яндекс',
|
||||||
|
'favicon.yandex.net': 'Яндекс', 'browser.yandex.ru': 'Яндекс Браузер',
|
||||||
|
'browser.yandex.com': 'Яндекс Браузер', 'api.browser.yandex.ru': 'Яндекс Браузер',
|
||||||
|
'api.browser.yandex.com': 'Яндекс Браузер', 'wap.yandex.ru': 'Яндекс', 'wap.yandex.com': 'Яндекс',
|
||||||
|
'300.ya.ru': 'Яндекс', 'brontp-pre.yandex.ru': 'Яндекс', 'suggest.dzen.ru': 'Дзен',
|
||||||
|
'suggest.sso.dzen.ru': 'Дзен', 'sso.dzen.ru': 'Дзен', 'mail.yandex.com': 'Яндекс Почта',
|
||||||
|
'yabs.yandex.ru': 'Яндекс', 'neuro.translate.yandex.ru': 'Яндекс Перевод', 'cdn.yandex.ru': 'Яндекс',
|
||||||
|
'zen.yandex.ru': 'Дзен', 'zen.yandex.com': 'Дзен', 'zen.yandex.net': 'Дзен',
|
||||||
|
'collections.yandex.ru': 'Яндекс Коллекции', 'collections.yandex.com': 'Яндекс Коллекции',
|
||||||
|
'an.yandex.ru': 'Яндекс', 'sba.yandex.ru': 'Яндекс', 'sba.yandex.com': 'Яндекс',
|
||||||
|
'sba.yandex.net': 'Яндекс', 'surveys.yandex.ru': 'Яндекс Опросы',
|
||||||
|
'yabro-wbplugin.edadeal.yandex.ru': 'Яндекс', 'api.events.plus.yandex.net': 'Яндекс Плюс',
|
||||||
|
'speller.yandex.net': 'Яндекс Спеллер', 'avatars.mds.yandex.net': 'Яндекс',
|
||||||
|
'avatars.mds.yandex.com': 'Яндекс', 'mc.yandex.ru': 'Яндекс', 'mc.yandex.com': 'Яндекс',
|
||||||
|
'3475482542.mc.yandex.ru': 'Яндекс', 'zen-yabro-morda.mediascope.mc.yandex.ru': 'Яндекс',
|
||||||
|
'travel.yastatic.net': 'Яндекс', 'api.uxfeedback.yandex.net': 'Яндекс',
|
||||||
|
'api.s3.yandex.net': 'Яндекс', 'cdn.s3.yandex.net': 'Яндекс',
|
||||||
|
'uxfeedback-cdn.s3.yandex.net': 'Яндекс', 'uxfeedback.yandex.ru': 'Яндекс',
|
||||||
|
'cloudcdn-m9-15.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-14.cdn.yandex.net': 'Яндекс',
|
||||||
|
'cloudcdn-m9-13.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-12.cdn.yandex.net': 'Яндекс',
|
||||||
|
'cloudcdn-m9-10.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-9.cdn.yandex.net': 'Яндекс',
|
||||||
|
'cloudcdn-m9-7.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-6.cdn.yandex.net': 'Яндекс',
|
||||||
|
'cloudcdn-m9-5.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-4.cdn.yandex.net': 'Яндекс',
|
||||||
|
'cloudcdn-m9-3.cdn.yandex.net': 'Яндекс', 'cloudcdn-m9-2.cdn.yandex.net': 'Яндекс',
|
||||||
|
'cloudcdn-ams19.cdn.yandex.net': 'Яндекс', 'http-check-headers.yandex.ru': 'Яндекс',
|
||||||
|
'cloud.cdn.yandex.net': 'Яндекс', 'cloud.cdn.yandex.com': 'Яндекс',
|
||||||
|
'cloud.cdn.yandex.ru': 'Яндекс', 'dr2.yandex.net': 'Яндекс', 'dr.yandex.net': 'Яндекс',
|
||||||
|
's3.yandex.net': 'Яндекс', 'static-mon.yandex.net': 'Яндекс', 'sync.browser.yandex.net': 'Яндекс',
|
||||||
|
'storage.ape.yandex.net': 'Яндекс', 'strm-rad-23.strm.yandex.net': 'Яндекс',
|
||||||
|
'strm.yandex.net': 'Яндекс', 'strm.yandex.ru': 'Яндекс', 'log.strm.yandex.ru': 'Яндекс',
|
||||||
|
'egress.yandex.net': 'Яндекс', 'cdnrhkgfkkpupuotntfj.svc.cdn.yandex.net': 'Яндекс',
|
||||||
|
'csp.yandex.net': 'Яндекс', 'mail.ru': 'Mail.ru', 'e.mail.ru': 'Mail.ru', 'my.mail.ru': 'Mail.ru',
|
||||||
|
'cloud.mail.ru': 'Mail.ru', 'inbox.ru': 'Mail.ru', 'list.ru': 'Mail.ru', 'bk.ru': 'Mail.ru',
|
||||||
|
'myteam.mail.ru': 'Mail.ru', 'trk.mail.ru': 'Mail.ru', '1l-api.mail.ru': 'Mail.ru',
|
||||||
|
'rutube.ru': 'Rutube', 'static.rutube.ru': 'Rutube', 'rutubelist.ru': 'Rutube',
|
||||||
|
'pic.rutubelist.ru': 'Rutube', 'ssp.rutube.ru': 'Rutube', 'preview.rutube.ru': 'Rutube',
|
||||||
|
'goya.rutube.ru': 'Rutube', 'smotrim.ru': 'Смотрим', 'ivi.ru': 'Ivi', 'cdn.ivi.ru': 'Ivi',
|
||||||
|
'okko.tv': 'Okko', 'start.ru': 'Start', 'wink.ru': 'Wink', 'kion.ru': 'Kion',
|
||||||
|
'premier.one': 'Premier', 'more.tv': 'More.tv', 'm.47news.ru': '47 News',
|
||||||
|
'lenta.ru': 'Лента.ру', 'gazeta.ru': 'Газета.ру', 'kp.ru': 'Комсомольская Правда',
|
||||||
|
'rambler.ru': 'Рамблер', 'ria.ru': 'РИА Новости', 'tass.ru': 'ТАСС',
|
||||||
|
'interfax.ru': 'Интерфакс', 'kommersant.ru': 'Коммерсант', 'vedomosti.ru': 'Ведомости',
|
||||||
|
'rbc.ru': 'РБК', 'russian.rt.com': 'RT', 'iz.ru': 'Известия', 'mk.ru': 'Московский Комсомолец',
|
||||||
|
'rg.ru': 'Российская Газета', 'www.kinopoisk.ru': 'Кинопоиск', 'widgets.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'payment-widget.plus.kinopoisk.ru': 'Кинопоиск', 'external-api.mediabilling.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'external-api.plus.kinopoisk.ru': 'Кинопоиск', 'graphql-web.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'graphql.kinopoisk.ru': 'Кинопоиск', 'tickets.widget.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'st.kinopoisk.ru': 'Кинопоиск', 'quiz.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'payment-widget.kinopoisk.ru': 'Кинопоиск', 'payment-widget-smarttv.plus.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'oneclick-payment.kinopoisk.ru': 'Кинопоиск', 'microapps.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'ma.kinopoisk.ru': 'Кинопоиск', 'hd.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'crowdtest.payment-widget-smarttv.plus.tst.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'crowdtest.payment-widget.plus.tst.kinopoisk.ru': 'Кинопоиск', 'api.plus.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'st-im.kinopoisk.ru': 'Кинопоиск', 'sso.kinopoisk.ru': 'Кинопоиск', 'touch.kinopoisk.ru': 'Кинопоиск',
|
||||||
|
'2gis.ru': '2ГИС', '2gis.com': '2ГИС', 'api.2gis.ru': '2ГИС', 'keys.api.2gis.com': '2ГИС',
|
||||||
|
'tutu.ru': 'Туту.ру', 'img.tutu.ru': 'Туту.ру', 'rzd.ru': 'РЖД', 'ticket.rzd.ru': 'РЖД',
|
||||||
|
'cdek.ru': 'СДЭК', 'cdek.market': 'СДЭК', 'calc.cdek.ru': 'СДЭК', 'pochta.ru': 'Почта России',
|
||||||
|
'rostelecom.ru': 'Ростелеком', 'rt.ru': 'Ростелеком', 'mts.ru': 'МТС', 'megafon.ru': 'Мегафон',
|
||||||
|
'beeline.ru': 'Билайн', 'tele2.ru': 'Tele2', 't2.ru': 'Tele2', 'www.t2.ru': 'Tele2',
|
||||||
|
'msk.t2.ru': 'Tele2', 's3.t2.ru': 'Tele2', 'yota.ru': 'Yota', 'domru.ru': 'Дом.ру',
|
||||||
|
'ertelecom.ru': 'ЭР-Телеком', 'selectel.ru': 'Selectel', 'timeweb.ru': 'Timeweb',
|
||||||
|
'gismeteo.ru': 'Гисметео', 'meteoinfo.ru': 'Метео', 'rp5.ru': 'RП5', 'hh.ru': 'HeadHunter',
|
||||||
|
'superjob.ru': 'SuperJob', 'rabota.ru': 'Работа.ру', 'auto.ru': 'Auto.ru',
|
||||||
|
'sso.auto.ru': 'Auto.ru', 'drom.ru': 'Drom', 'avto.ru': 'Avto.ru', 'eda.ru': 'Eda.ru',
|
||||||
|
'food.ru': 'Food.ru', 'edadeal.ru': 'Edadeal', 'delivery-club.ru': 'Delivery Club',
|
||||||
|
'leroymerlin.ru': 'Леруа Мерлен', 'lemanapro.ru': 'Лемана Про', 'cdn.lemanapro.ru': 'Лемана Про',
|
||||||
|
'static.lemanapro.ru': 'Лемана Про', 'dmp.dmpkit.lemanapro.ru': 'Лемана Про',
|
||||||
|
'receive-sentry.lmru.tech': 'Лемана Про', 'partners.lemanapro.ru': 'Лемана Про',
|
||||||
|
'petrovich.ru': 'Петрович', 'maxidom.ru': 'Максидом', 'vseinstrumenti.ru': 'ВсеИнструменты',
|
||||||
|
'220-volt.ru': '220 Вольт', 'max.ru': 'Max', 'dev.max.ru': 'Max', 'web.max.ru': 'Max',
|
||||||
|
'api.max.ru': 'Max', 'legal.max.ru': 'Max', 'st.max.ru': 'Max', 'botapi.max.ru': 'Max',
|
||||||
|
'link.max.ru': 'Max', 'download.max.ru': 'Max', 'i.max.ru': 'Max', 'help.max.ru': 'Max',
|
||||||
|
'mos.ru': 'Мос.ру', 'taximaxim.ru': 'Такси Максим', 'moskva.taximaxim.ru': 'Такси Максим'
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"📋 Загружено {len(DOMAIN_NAMES)} доменов в словарь")
|
||||||
|
|
||||||
|
# ========= ЛОГ =========
|
||||||
|
async def log(message: str):
|
||||||
|
try:
|
||||||
|
now = datetime.now()
|
||||||
|
if os.path.exists(LOG_FILE):
|
||||||
|
mtime = datetime.fromtimestamp(os.path.getmtime(LOG_FILE))
|
||||||
|
if now - mtime > timedelta(seconds=LOG_CLEAN_INTERVAL):
|
||||||
|
open(LOG_FILE, "w").close()
|
||||||
|
|
||||||
|
async with aiofiles.open(LOG_FILE, "a", encoding="utf-8") as f:
|
||||||
|
await f.write(f"[{now}] {message}\n")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def log_xray_error(message: str):
|
||||||
|
try:
|
||||||
|
with open(XRAY_LOG_FILE, "a", encoding="utf-8") as f:
|
||||||
|
f.write(f"[{datetime.now().strftime('%H:%M:%S')}] {message}\n")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ========= ВАЛИДАЦИЯ =========
|
||||||
|
def validate_vless(url: str) -> bool:
|
||||||
|
if not url.startswith("vless://"): return False
|
||||||
|
if not UUID_REGEX.search(url): return False
|
||||||
|
if "@" not in url or ":" not in url: return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# ========= WHITELIST =========
|
||||||
|
def load_whitelist_domains():
|
||||||
|
domains = set()
|
||||||
|
suffixes = []
|
||||||
|
if os.path.exists("whitelist.txt"):
|
||||||
|
try:
|
||||||
|
with open("whitelist.txt", "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
for line in f:
|
||||||
|
d = line.strip().lower()
|
||||||
|
if d:
|
||||||
|
domains.add(d)
|
||||||
|
suffixes.append("." + d)
|
||||||
|
print(f"📋 Загружено {len(domains)} доменов из whitelist.txt")
|
||||||
|
except:
|
||||||
|
print("⚠️ Ошибка загрузки whitelist.txt")
|
||||||
|
return domains, suffixes
|
||||||
|
|
||||||
|
# ========= ПРОТОКОЛ / SNI / НАЗВАНИЕ =========
|
||||||
|
def detect_protocol(vless_url: str) -> str:
|
||||||
|
try:
|
||||||
|
no_scheme = vless_url[len("vless://"):]
|
||||||
|
after_at = no_scheme.split("@", 1)[1]
|
||||||
|
query = after_at.split("?", 1)[1] if "?" in after_at else ""
|
||||||
|
params = dict(urllib.parse.parse_qsl(query))
|
||||||
|
|
||||||
|
transport = params.get("type", "").lower()
|
||||||
|
security = params.get("security", "").lower()
|
||||||
|
|
||||||
|
if transport in ("ws", "websocket"): return "WS"
|
||||||
|
if transport in ("grpc", "gun"): return "gRPC"
|
||||||
|
if transport in ("xhttp", "httpupgrade"): return "XHTTP"
|
||||||
|
if transport in ("h2", "http2"): return "H2"
|
||||||
|
if transport == "tcp": return "TCP"
|
||||||
|
if security == "reality": return "Reality"
|
||||||
|
if security in ("tls", "xtls"): return "TLS"
|
||||||
|
return "TCP"
|
||||||
|
except:
|
||||||
|
return "Неизвестно"
|
||||||
|
|
||||||
|
def extract_all_possible_domains(vless_url: str) -> list:
|
||||||
|
domains = set()
|
||||||
|
try:
|
||||||
|
if not vless_url.startswith("vless://"): return []
|
||||||
|
content = vless_url[8:]
|
||||||
|
at_pos = content.find('@')
|
||||||
|
if at_pos == -1: return []
|
||||||
|
|
||||||
|
after_at = content[at_pos+1:]
|
||||||
|
q_pos = after_at.find('?')
|
||||||
|
|
||||||
|
if q_pos != -1:
|
||||||
|
host_part = after_at[:q_pos]
|
||||||
|
query_part = after_at[q_pos+1:]
|
||||||
|
else:
|
||||||
|
host_part = after_at
|
||||||
|
query_part = ""
|
||||||
|
|
||||||
|
host = host_part.split(':', 1)[0]
|
||||||
|
if host and '.' in host:
|
||||||
|
domains.add(host.lower())
|
||||||
|
|
||||||
|
if query_part:
|
||||||
|
if '#' in query_part: query_part = query_part.split('#', 1)[0]
|
||||||
|
params = dict(urllib.parse.parse_qsl(query_part))
|
||||||
|
|
||||||
|
for k in ['sni', 'host']:
|
||||||
|
if k in params and '.' in params[k]:
|
||||||
|
domains.add(params[k].lower())
|
||||||
|
|
||||||
|
if 'path' in params:
|
||||||
|
path_parts = re.findall(r'[a-zA-Z0-9][a-zA-Z0-9\-\.]+[a-zA-Z0-9]\.[a-zA-Z]{2,}', params['path'])
|
||||||
|
domains.update(d.lower() for d in path_parts)
|
||||||
|
|
||||||
|
return list(domains)
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_human_name(domain: str) -> str:
|
||||||
|
if not domain: return "Неизвестно"
|
||||||
|
d = domain.lower()
|
||||||
|
|
||||||
|
if d in DOMAIN_NAMES: return DOMAIN_NAMES[d]
|
||||||
|
|
||||||
|
parts = d.split('.')
|
||||||
|
for i in range(len(parts) - 1):
|
||||||
|
if ".".join(parts[i:]) in DOMAIN_NAMES:
|
||||||
|
return DOMAIN_NAMES[".".join(parts[i:])]
|
||||||
|
|
||||||
|
if len(parts) >= 2 and ".".join(parts[-2:]) in DOMAIN_NAMES:
|
||||||
|
return DOMAIN_NAMES[".".join(parts[-2:])]
|
||||||
|
|
||||||
|
return "Неизвестно"
|
||||||
|
|
||||||
|
def filter_by_sni(vless_url: str, whitelist_domains: set, whitelist_suffixes: list) -> bool:
|
||||||
|
domains = extract_all_possible_domains(vless_url)
|
||||||
|
|
||||||
|
if whitelist_domains:
|
||||||
|
for domain in domains:
|
||||||
|
if domain in whitelist_domains or any(domain.endswith(s) for s in whitelist_suffixes): return True
|
||||||
|
if len(domain.split('.')) >= 2 and '.'.join(domain.split('.')[-2:]) in whitelist_domains: return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
for domain in domains:
|
||||||
|
if domain in DOMAIN_NAMES: return True
|
||||||
|
parts = domain.split('.')
|
||||||
|
for i in range(len(parts) - 1):
|
||||||
|
if ".".join(parts[i:]) in DOMAIN_NAMES: return True
|
||||||
|
if len(parts) >= 2 and ".".join(parts[-2:]) in DOMAIN_NAMES: return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ========= СКАЧИВАНИЕ =========
|
||||||
|
async def fetch(session, url, sem):
|
||||||
|
async with sem:
|
||||||
|
try:
|
||||||
|
print(f"Скачиваю: {url}")
|
||||||
|
async with session.get(url, timeout=15) as resp:
|
||||||
|
if resp.status == 200:
|
||||||
|
return await resp.text()
|
||||||
|
except Exception as e:
|
||||||
|
await log(f"Ошибка при скачивании {url}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def process_url(session, url, sem, output_queue, stats_lock, stats):
|
||||||
|
content = await fetch(session, url, sem)
|
||||||
|
async with stats_lock:
|
||||||
|
stats["processed"] += 1
|
||||||
|
|
||||||
|
if content:
|
||||||
|
matches = VLESS_REGEX.findall(content)
|
||||||
|
if matches:
|
||||||
|
async with stats_lock:
|
||||||
|
stats["found"] += len(matches)
|
||||||
|
for match in matches:
|
||||||
|
await output_queue.put(match)
|
||||||
|
|
||||||
|
async with stats_lock:
|
||||||
|
print(f"Обработано: {stats['processed']} | Найдено VLESS: {stats['found']}", end="\r")
|
||||||
|
|
||||||
|
async def writer_task(output_queue, output_file):
|
||||||
|
"""Асинхронно пишет строки из очереди в файл."""
|
||||||
|
async with aiofiles.open(output_file, "w", encoding="utf-8") as f:
|
||||||
|
while True:
|
||||||
|
line = await output_queue.get()
|
||||||
|
if line is None: # сигнал завершения
|
||||||
|
break
|
||||||
|
await f.write(line + "\n")
|
||||||
|
output_queue.task_done()
|
||||||
|
|
||||||
|
# ========= ОЧИСТКА =========
|
||||||
|
async def clean_vless():
|
||||||
|
print("\nОчищаю дубликаты и проверяю валидность...")
|
||||||
|
if not os.path.exists(OUTPUT_FILE): return
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(OUTPUT_FILE, "r", encoding="utf-8") as f:
|
||||||
|
lines = await f.readlines()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
unique_valid = {url.strip() for url in lines if url.strip() and validate_vless(url.strip())}
|
||||||
|
|
||||||
|
async with aiofiles.open(CLEAN_FILE, "w", encoding="utf-8") as f:
|
||||||
|
await f.write("\n".join(unique_valid) + "\n")
|
||||||
|
|
||||||
|
print(f"Очистка завершена. Итоговых конфигов: {len(unique_valid)}")
|
||||||
|
|
||||||
|
# ========= ФИЛЬТРАЦИЯ ПО WHITELIST =========
|
||||||
|
async def filter_vless():
|
||||||
|
print("\n=== Фильтрация по whitelist ===")
|
||||||
|
if not os.path.exists(CLEAN_FILE): return
|
||||||
|
|
||||||
|
domains, suffixes = load_whitelist_domains()
|
||||||
|
try:
|
||||||
|
with open(CLEAN_FILE, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
total = sum(1 for _ in f)
|
||||||
|
except:
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
passed, processed = 0, 0
|
||||||
|
|
||||||
|
async with aiofiles.open(CLEAN_FILE, "r", encoding="utf-8") as f_in, \
|
||||||
|
aiofiles.open(FILTERED_FILE, "w", encoding="utf-8") as f_out:
|
||||||
|
async for line in f_in:
|
||||||
|
processed += 1
|
||||||
|
url = line.strip()
|
||||||
|
if not url: continue
|
||||||
|
|
||||||
|
if filter_by_sni(url, domains, suffixes):
|
||||||
|
await f_out.write(url + "\n")
|
||||||
|
passed += 1
|
||||||
|
|
||||||
|
if processed % 100 == 0 and total > 0:
|
||||||
|
print(f"Фильтрация: {processed}/{total} | Подошло: {passed}", end="\r")
|
||||||
|
|
||||||
|
print(f"\nФильтрация завершена. Итог: {passed} конфигов.")
|
||||||
|
|
||||||
|
# ========= ПЕРЕИМЕНОВАНИЕ =========
|
||||||
|
async def rename_configs():
|
||||||
|
print("\n=== Переименование конфигов по протоколу и SNI ===")
|
||||||
|
if not os.path.exists(FILTERED_FILE): return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(FILTERED_FILE, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
total = sum(1 for _ in f)
|
||||||
|
except:
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
processed = 0
|
||||||
|
|
||||||
|
async with aiofiles.open(FILTERED_FILE, "r", encoding="utf-8") as f_in, \
|
||||||
|
aiofiles.open(NAMED_FILE, "w", encoding="utf-8") as f_out:
|
||||||
|
async for line in f_in:
|
||||||
|
processed += 1
|
||||||
|
url = line.strip()
|
||||||
|
if not url: continue
|
||||||
|
|
||||||
|
protocol = detect_protocol(url)
|
||||||
|
domains = extract_all_possible_domains(url)
|
||||||
|
human_name = "Неизвестно"
|
||||||
|
|
||||||
|
if domains:
|
||||||
|
for domain in domains:
|
||||||
|
name = get_human_name(domain)
|
||||||
|
if name != "Неизвестно":
|
||||||
|
human_name = name
|
||||||
|
break
|
||||||
|
if human_name == "Неизвестно" and domains:
|
||||||
|
human_name = domains[0].split('.')[-2].capitalize() if len(domains[0].split('.')) >= 2 else domains[0]
|
||||||
|
|
||||||
|
title = f"{protocol}, {human_name} [V.O.I.D]"
|
||||||
|
base = url.split("#", 1)[0]
|
||||||
|
new_url = f"{base}#{title}"
|
||||||
|
|
||||||
|
await f_out.write(new_url + "\n")
|
||||||
|
|
||||||
|
if processed % 500 == 0 and total > 0:
|
||||||
|
print(f"Переименовано: {processed}/{total}", end="\r")
|
||||||
|
|
||||||
|
print(f"\nПереименование завершено. Итог: {processed} конфигов.")
|
||||||
|
|
||||||
|
# ========= НОРМАЛИЗАЦИЯ И КОДИРОВАНИЕ URL =========
|
||||||
|
def encode_vless_url(url: str) -> str:
|
||||||
|
"""Корректное кодирование VLESS URL, сохраняя уже закодированные символы."""
|
||||||
|
try:
|
||||||
|
if not url.startswith("vless://"): return url
|
||||||
|
|
||||||
|
content = url[8:]
|
||||||
|
at_pos = content.find('@')
|
||||||
|
if at_pos == -1: return url
|
||||||
|
|
||||||
|
uuid = content[:at_pos]
|
||||||
|
after_at = content[at_pos+1:]
|
||||||
|
|
||||||
|
q_pos = after_at.find('?')
|
||||||
|
if q_pos != -1:
|
||||||
|
host_part = after_at[:q_pos]
|
||||||
|
params_part = after_at[q_pos+1:]
|
||||||
|
else:
|
||||||
|
host_part = after_at
|
||||||
|
params_part = ""
|
||||||
|
|
||||||
|
hash_pos = host_part.find('#')
|
||||||
|
if hash_pos != -1:
|
||||||
|
host_only = host_part[:hash_pos]
|
||||||
|
fragment = host_part[hash_pos+1:]
|
||||||
|
else:
|
||||||
|
host_only = host_part
|
||||||
|
fragment = ""
|
||||||
|
|
||||||
|
if not fragment and params_part:
|
||||||
|
hash_pos = params_part.find('#')
|
||||||
|
if hash_pos != -1:
|
||||||
|
params_only = params_part[:hash_pos]
|
||||||
|
fragment = params_part[hash_pos+1:]
|
||||||
|
params_part = params_only
|
||||||
|
|
||||||
|
# Парсим параметры с сохранением оригинального кодирования
|
||||||
|
params = dict(urllib.parse.parse_qsl(params_part, keep_blank_values=True))
|
||||||
|
encoded_params = []
|
||||||
|
for k, v in params.items():
|
||||||
|
if k in ['security', 'type', 'fp', 'pbk', 'sid', 'flow']:
|
||||||
|
encoded_params.append(f"{k}={v}")
|
||||||
|
else:
|
||||||
|
# Кодируем значение, но оставляем уже закодированные %XX
|
||||||
|
# Для простоты используем quote с safe='%'
|
||||||
|
encoded_v = urllib.parse.quote(v, safe='%')
|
||||||
|
encoded_params.append(f"{k}={encoded_v}")
|
||||||
|
|
||||||
|
new_params = "&".join(encoded_params)
|
||||||
|
|
||||||
|
# Фрагмент кодируем только если есть не-ASCII символы
|
||||||
|
try:
|
||||||
|
fragment.encode('ascii')
|
||||||
|
encoded_fragment = fragment
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
encoded_fragment = urllib.parse.quote(fragment, safe='')
|
||||||
|
|
||||||
|
base = f"vless://{uuid}@{host_only}"
|
||||||
|
if new_params: base += f"?{new_params}"
|
||||||
|
if encoded_fragment: base += f"#{encoded_fragment}"
|
||||||
|
|
||||||
|
return base
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return url
|
||||||
|
|
||||||
|
async def encode_all_configs():
|
||||||
|
print("\n=== Кодирование конфигов для Xray ===")
|
||||||
|
if not os.path.exists(NAMED_FILE): return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(NAMED_FILE, 'r', encoding='utf-8') as f:
|
||||||
|
configs = [line.strip() for line in f if line.strip()]
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
total = len(configs)
|
||||||
|
changed = 0
|
||||||
|
|
||||||
|
async with aiofiles.open(ENCODED_FILE, "w", encoding="utf-8") as f_out:
|
||||||
|
for i, url in enumerate(configs, 1):
|
||||||
|
encoded_url = encode_vless_url(url)
|
||||||
|
await f_out.write(encoded_url + "\n")
|
||||||
|
if encoded_url != url: changed += 1
|
||||||
|
if i % 500 == 0:
|
||||||
|
print(f"Закодировано: {i}/{total} | Изменено: {changed}", end="\r")
|
||||||
|
|
||||||
|
print(f"\nКодирование завершено. Всего: {total}, изменено: {changed}")
|
||||||
|
|
||||||
|
# ========= АЛЬТЕРНАТИВНЫЕ МЕТОДЫ ПРОВЕРКИ =========
|
||||||
|
def check_tcp_connection(host: str, port: int, timeout: int = 2) -> bool:
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
result = sock.connect_ex((host, port))
|
||||||
|
sock.close()
|
||||||
|
return result == 0
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_tls_handshake(host: str, port: int = 443, sni: str = None, timeout: int = 2) -> tuple:
|
||||||
|
try:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
context.check_hostname = False
|
||||||
|
context.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
sock.connect((host, port))
|
||||||
|
|
||||||
|
ssl_sock = context.wrap_socket(sock, server_hostname=sni or host)
|
||||||
|
ssl_sock.do_handshake()
|
||||||
|
version = ssl_sock.version()
|
||||||
|
ssl_sock.close()
|
||||||
|
sock.close()
|
||||||
|
return (True, version, None)
|
||||||
|
except Exception as e:
|
||||||
|
return (False, None, str(e))
|
||||||
|
|
||||||
|
# ========= XRAY-ТЕСТЕР =========
|
||||||
|
class SimpleProgress:
|
||||||
|
def __init__(self, total):
|
||||||
|
self.total = total
|
||||||
|
self.current = 0
|
||||||
|
self.start_time = time.time()
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.working_count = 0
|
||||||
|
self.retry_count = 0
|
||||||
|
|
||||||
|
def update(self, status='', working=False, retry=False):
|
||||||
|
with self.lock:
|
||||||
|
self.current += 1
|
||||||
|
if working: self.working_count += 1
|
||||||
|
if retry: self.retry_count += 1
|
||||||
|
if self.current % 10 == 0 or self.current == self.total:
|
||||||
|
elapsed = time.time() - self.start_time
|
||||||
|
speed = self.current / elapsed if elapsed > 0 else 0
|
||||||
|
print(f"\r📊 [{self.current}/{self.total}] ✅:{self.working_count} 🔄:{self.retry_count} {speed:.1f} к/с {status}", end='', flush=True)
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
elapsed = time.time() - self.start_time
|
||||||
|
print(f"\r✅ Готово! {self.current} конфигов за {elapsed:.1f}с ({self.current/elapsed:.1f} к/с), рабочих: {self.working_count}, повторов: {self.retry_count}")
|
||||||
|
|
||||||
|
class PortManager:
|
||||||
|
def __init__(self, start=20000, end=25000):
|
||||||
|
self.ports = list(range(start, end + 1))
|
||||||
|
self.used = set()
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
|
def get_port(self):
|
||||||
|
with self.lock:
|
||||||
|
available = [p for p in self.ports if p not in self.used]
|
||||||
|
if not available: return None
|
||||||
|
port = random.choice(available)
|
||||||
|
self.used.add(port)
|
||||||
|
return port
|
||||||
|
|
||||||
|
def release_port(self, port):
|
||||||
|
with self.lock:
|
||||||
|
self.used.discard(port)
|
||||||
|
|
||||||
|
class XrayTester:
|
||||||
|
def __init__(self, input_file='url_encoded.txt', output_file='url_work.txt', max_workers=30):
|
||||||
|
self.input_file = input_file
|
||||||
|
self.output_file = output_file
|
||||||
|
self.max_workers = max_workers
|
||||||
|
self.test_url = XRAY_TEST_URL
|
||||||
|
self.timeout = XRAY_TIMEOUT
|
||||||
|
self.max_retries = MAX_RETRIES
|
||||||
|
self.retry_delay = RETRY_DELAY
|
||||||
|
self.xray_dir = Path('./xray_bin')
|
||||||
|
self.xray_path = self.xray_dir / ('xray.exe' if os.name == 'nt' else 'xray')
|
||||||
|
self.port_manager = PortManager()
|
||||||
|
self.debug_file = DEBUG_FILE
|
||||||
|
self.xray_log_file = XRAY_LOG_FILE
|
||||||
|
|
||||||
|
print(f"🔍 XrayTester инициализирован")
|
||||||
|
# Синхронная проверка наличия Xray при инициализации (вызывается из executor)
|
||||||
|
self._ensure_xray()
|
||||||
|
|
||||||
|
def _ensure_xray(self):
|
||||||
|
"""Проверяет наличие Xray и скачивает при необходимости."""
|
||||||
|
if not self.xray_path.exists():
|
||||||
|
print("⬇️ Скачиваю Xray...")
|
||||||
|
self._download_xray()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
result = subprocess.run([str(self.xray_path), '-version'], capture_output=True, text=True, timeout=5)
|
||||||
|
version = result.stdout.split('\n')[0] if result.stdout else 'Unknown'
|
||||||
|
print(f"✅ Xray готов: {version}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Ошибка версии Xray: {e}, пробую переустановить")
|
||||||
|
self._download_xray()
|
||||||
|
|
||||||
|
def _download_xray(self):
|
||||||
|
"""Загружает Xray с официального репозитория."""
|
||||||
|
import urllib.request, zipfile, platform
|
||||||
|
self.xray_dir.mkdir(exist_ok=True)
|
||||||
|
system = platform.system().lower()
|
||||||
|
if system == 'windows':
|
||||||
|
url = "https://github.com/XTLS/Xray-core/releases/latest/download/Xray-windows-64.zip"
|
||||||
|
elif system == 'linux':
|
||||||
|
url = "https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip"
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"Unsupported OS: {system}")
|
||||||
|
|
||||||
|
zip_path = self.xray_dir / "xray.zip"
|
||||||
|
try:
|
||||||
|
urllib.request.urlretrieve(url, zip_path)
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(self.xray_dir)
|
||||||
|
zip_path.unlink()
|
||||||
|
# Устанавливаем права на исполнение для Linux/Mac
|
||||||
|
if os.name != 'nt':
|
||||||
|
os.chmod(self.xray_path, 0o755)
|
||||||
|
print("✅ Xray загружен")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Ошибка загрузки Xray: {e}")
|
||||||
|
|
||||||
|
def parse_vless_url(self, url):
|
||||||
|
try:
|
||||||
|
if not url.startswith('vless://'): return None
|
||||||
|
content = url[8:]
|
||||||
|
at_pos = content.find('@')
|
||||||
|
if at_pos == -1: return None
|
||||||
|
|
||||||
|
uuid = content[:at_pos]
|
||||||
|
after_at = content[at_pos+1:]
|
||||||
|
|
||||||
|
q_pos = after_at.find('?')
|
||||||
|
if q_pos != -1:
|
||||||
|
host_part = after_at[:q_pos]
|
||||||
|
query = after_at[q_pos+1:]
|
||||||
|
else:
|
||||||
|
host_part = after_at
|
||||||
|
query = ""
|
||||||
|
|
||||||
|
host_part = host_part.split('#')[0]
|
||||||
|
host, port = (host_part.split(':', 1) + [443])[:2]
|
||||||
|
|
||||||
|
params = dict(urllib.parse.parse_qsl(query))
|
||||||
|
return {'uuid': uuid, 'host': host, 'port': int(port), 'params': params, 'url': url}
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_xray_config(self, parsed, port):
|
||||||
|
try:
|
||||||
|
params = parsed['params']
|
||||||
|
flow = params.get('flow', '')
|
||||||
|
security = params.get('security', '')
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"log": {"loglevel": "error"},
|
||||||
|
"inbounds": [{"port": port, "protocol": "socks", "settings": {"auth": "noauth", "udp": False}, "tag": "socks-in"}],
|
||||||
|
"outbounds": [{
|
||||||
|
"protocol": "vless",
|
||||||
|
"settings": {"vnext": [{"address": parsed['host'], "port": parsed['port'], "users": [{"id": parsed['uuid'], "encryption": "none", "flow": flow}]}]},
|
||||||
|
"streamSettings": {"network": params.get('type', 'tcp'), "security": security},
|
||||||
|
"tag": "proxy"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
if security == 'reality':
|
||||||
|
config["outbounds"][0]["streamSettings"]["realitySettings"] = {
|
||||||
|
"serverName": params.get('sni', parsed['host']), "fingerprint": params.get('fp', 'chrome'),
|
||||||
|
"publicKey": params.get('pbk', ''), "shortId": params.get('sid', ''), "spiderX": params.get('spx', '/')
|
||||||
|
}
|
||||||
|
elif security == 'tls':
|
||||||
|
config["outbounds"][0]["streamSettings"]["tlsSettings"] = {"serverName": params.get('sni', parsed['host']), "allowInsecure": True}
|
||||||
|
|
||||||
|
if params.get('type') in ('ws', 'websocket'):
|
||||||
|
config["outbounds"][0]["streamSettings"]["wsSettings"] = {"path": params.get('path', '/'), "headers": {"Host": params.get('host', params.get('sni', parsed['host']))}}
|
||||||
|
|
||||||
|
if params.get('type') in ('grpc', 'gun'):
|
||||||
|
config["outbounds"][0]["streamSettings"]["grpcSettings"] = {"serviceName": params.get('servicename', params.get('service', '')), "multiMode": True}
|
||||||
|
|
||||||
|
return config
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _read_stderr(self, process):
|
||||||
|
"""Читает stderr процесса, чтобы избежать блокировки."""
|
||||||
|
try:
|
||||||
|
for line in process.stderr:
|
||||||
|
pass # игнорируем
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_with_xray(self, parsed, port, attempt=1):
|
||||||
|
config_file, process = None, None
|
||||||
|
try:
|
||||||
|
config = self.create_xray_config(parsed, port)
|
||||||
|
if not config: return None
|
||||||
|
|
||||||
|
fd, config_file = tempfile.mkstemp(suffix='.json')
|
||||||
|
os.close(fd)
|
||||||
|
with open(config_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f)
|
||||||
|
|
||||||
|
creationflags = subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[str(self.xray_path), '-c', config_file],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
creationflags=creationflags
|
||||||
|
)
|
||||||
|
# Запускаем поток для чтения stderr
|
||||||
|
stderr_thread = threading.Thread(target=self._read_stderr, args=(process,), daemon=True)
|
||||||
|
stderr_thread.start()
|
||||||
|
|
||||||
|
time.sleep(1.0)
|
||||||
|
if process.poll() is not None:
|
||||||
|
return "CRASH"
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
session = requests.Session()
|
||||||
|
session.proxies = {'https': f'socks5://127.0.0.1:{port}'}
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = session.get(self.test_url, timeout=self.timeout)
|
||||||
|
if r.status_code in [200, 204]:
|
||||||
|
return {'working': True, 'ping': (time.time() - start) * 1000, 'method': 'xray'}
|
||||||
|
return "FAIL"
|
||||||
|
except requests.exceptions.Timeout: return "TIMEOUT"
|
||||||
|
except requests.exceptions.ConnectionError: return "CONN_ERROR"
|
||||||
|
except Exception: return "ERROR"
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return "EXCEPTION"
|
||||||
|
finally:
|
||||||
|
if process:
|
||||||
|
try:
|
||||||
|
process.terminate()
|
||||||
|
process.wait(timeout=2.0)
|
||||||
|
except:
|
||||||
|
process.kill()
|
||||||
|
if config_file and os.path.exists(config_file):
|
||||||
|
try: os.remove(config_file)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
def check_alternative_methods(self, parsed, url):
|
||||||
|
host, port = parsed['host'], parsed['port']
|
||||||
|
params = parsed['params']
|
||||||
|
if not check_tcp_connection(host, port, timeout=2): return None
|
||||||
|
|
||||||
|
security = params.get('security', '')
|
||||||
|
if security in ['reality', 'tls']:
|
||||||
|
tls_ok, _, _ = check_tls_handshake(host, port, params.get('sni', host), timeout=2)
|
||||||
|
if tls_ok:
|
||||||
|
return {'url': url, 'ping': 100, 'method': 'tls_check', 'security': security}
|
||||||
|
return None
|
||||||
|
|
||||||
|
def test_one(self, url):
|
||||||
|
parsed = self.parse_vless_url(url)
|
||||||
|
if not parsed: return None
|
||||||
|
|
||||||
|
port = self.port_manager.get_port()
|
||||||
|
if port:
|
||||||
|
try:
|
||||||
|
for attempt in range(1, self.max_retries + 1):
|
||||||
|
result = self.test_with_xray(parsed, port, attempt)
|
||||||
|
if isinstance(result, dict) and result.get('working'):
|
||||||
|
self.port_manager.release_port(port)
|
||||||
|
return {'url': url, 'ping': result['ping'], 'method': 'xray'}
|
||||||
|
if result in ["TIMEOUT", "FAIL", "CONN_ERROR", "CRASH"] and attempt < self.max_retries:
|
||||||
|
time.sleep(self.retry_delay)
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
self.port_manager.release_port(port)
|
||||||
|
|
||||||
|
return self.check_alternative_methods(parsed, url)
|
||||||
|
|
||||||
|
def test_all(self):
|
||||||
|
if not os.path.exists(self.input_file): return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.input_file, 'r', encoding='utf-8') as f:
|
||||||
|
all_urls = [line.strip() for line in f if line.strip()]
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not all_urls: return
|
||||||
|
|
||||||
|
print(f"\n🔍 Тестирование {len(all_urls)} конфигов")
|
||||||
|
if os.path.exists(self.debug_file): os.remove(self.debug_file)
|
||||||
|
|
||||||
|
working = []
|
||||||
|
progress = SimpleProgress(len(all_urls))
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||||
|
futures = {executor.submit(self.test_one, url): url for url in all_urls}
|
||||||
|
for future in as_completed(futures):
|
||||||
|
try:
|
||||||
|
result = future.result(timeout=self.timeout + 5)
|
||||||
|
if result:
|
||||||
|
working.append(result)
|
||||||
|
progress.update('✅', working=True)
|
||||||
|
else:
|
||||||
|
progress.update('❌', working=False)
|
||||||
|
except Exception:
|
||||||
|
progress.update('⚠️', working=False)
|
||||||
|
|
||||||
|
progress.finish()
|
||||||
|
working.sort(key=lambda x: x['ping'])
|
||||||
|
|
||||||
|
with open(self.output_file, 'w', encoding='utf-8') as f:
|
||||||
|
for w in working:
|
||||||
|
f.write(w['url'] + '\n')
|
||||||
|
|
||||||
|
print(f"\n📊 Результаты: ✅ Рабочих: {len(working)} | ❌ Не рабочих: {len(all_urls)-len(working)}\n")
|
||||||
|
return working
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.test_all()
|
||||||
|
|
||||||
|
# ========= ОСНОВНОЙ ЦИКЛ =========
|
||||||
|
async def main_cycle():
|
||||||
|
global cycle_counter
|
||||||
|
cycle_counter += 1
|
||||||
|
print(f"\n=== Новый цикл #{cycle_counter} ===")
|
||||||
|
|
||||||
|
if cycle_counter % CYCLES_BEFORE_DEBUG_CLEAN == 0 and os.path.exists(DEBUG_FILE):
|
||||||
|
os.remove(DEBUG_FILE)
|
||||||
|
|
||||||
|
if os.path.exists(OUTPUT_FILE): os.remove(OUTPUT_FILE)
|
||||||
|
if not os.path.exists(SOURCES_FILE): return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(SOURCES_FILE, "r", encoding="utf-8", errors="ignore") as f:
|
||||||
|
urls = [line.strip() for line in f if line.strip()]
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not urls: return
|
||||||
|
print(f"📥 Загружаю {len(urls)} источников...")
|
||||||
|
|
||||||
|
sem = asyncio.Semaphore(THREADS_DOWNLOAD)
|
||||||
|
output_queue = asyncio.Queue()
|
||||||
|
stats = {"processed": 0, "found": 0}
|
||||||
|
stats_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
# Запускаем писатель в фоне
|
||||||
|
writer = asyncio.create_task(writer_task(output_queue, OUTPUT_FILE))
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
tasks = [process_url(session, url, sem, output_queue, stats_lock, stats) for url in urls]
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
# Сигнализируем писателю о завершении
|
||||||
|
await output_queue.put(None)
|
||||||
|
await writer
|
||||||
|
|
||||||
|
print(f"\n✅ Скачивание завершено. Найдено VLESS: {stats['found']}")
|
||||||
|
await log(f"Скачивание завершено. Найдено VLESS: {stats['found']}")
|
||||||
|
|
||||||
|
if stats['found'] > 0:
|
||||||
|
await clean_vless()
|
||||||
|
await filter_vless()
|
||||||
|
await rename_configs()
|
||||||
|
await encode_all_configs()
|
||||||
|
|
||||||
|
print("\n=== Запуск Xray-проверки ===")
|
||||||
|
tester = XrayTester(input_file=ENCODED_FILE, output_file=WORK_FILE, max_workers=XRAY_MAX_WORKERS)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
await loop.run_in_executor(None, tester.run)
|
||||||
|
else:
|
||||||
|
print("⏭️ Нет новых конфигов для обработки")
|
||||||
|
|
||||||
|
async def run_forever():
|
||||||
|
print("\n🔄 Запуск бесконечного цикла...")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
cycle_start = time.time()
|
||||||
|
await main_cycle()
|
||||||
|
print(f"✅ Цикл завершен за {time.time() - cycle_start:.1f}с")
|
||||||
|
await asyncio.sleep(CYCLE_DELAY)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ Критическая ошибка в цикле: {e}")
|
||||||
|
await log(f"Критическая ошибка: {e}")
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(main_cycle())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n👋 Программа остановлена")
|
||||||
Reference in New Issue
Block a user