mirror of
https://github.com/VOID-Anonymity/V.O.I.D-VPN_Bypass.git
synced 2026-09-22 23:08:10 +03:00
925 lines
43 KiB
Python
925 lines
43 KiB
Python
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"🚀 Запуск мультиканального парсера [V.O.I.D]...")
|
||
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 = "configs/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, TROJAN, HY2) =========
|
||
# Используем не захватывающую группу для протоколов
|
||
CONFIG_REGEX = re.compile(r"(?:vless|trojan|hysteria2|hy2)://[^\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}"
|
||
)
|
||
|
||
# ========= СЛОВАРЬ ДОМЕНОВ (RU-SEGMENT) =========
|
||
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_config(url: str) -> bool:
|
||
"""Проверяет валидность конфига для VLESS, Trojan, Hy2."""
|
||
try:
|
||
if url.startswith("vless://"):
|
||
return bool(UUID_REGEX.search(url)) and "@" in url and ":" in url
|
||
if url.startswith(("trojan://", "hysteria2://", "hy2://")):
|
||
# Для трояна и гистерии формат может отличаться от UUID, проверяем наличие структуры
|
||
return "@" in url and ":" in url
|
||
return False
|
||
except:
|
||
return False
|
||
|
||
# ========= 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(url: str) -> str:
|
||
try:
|
||
if url.startswith("hysteria2://") or url.startswith("hy2://"):
|
||
return "Hy2"
|
||
if url.startswith("trojan://"):
|
||
return "Trojan"
|
||
|
||
no_scheme = url.split("://", 1)[1]
|
||
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 "VLESS"
|
||
except:
|
||
return "VPN"
|
||
|
||
def extract_all_possible_domains(url: str) -> list:
|
||
domains = set()
|
||
try:
|
||
parts = url.split("://", 1)
|
||
if len(parts) < 2: return []
|
||
|
||
content = parts[1]
|
||
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))
|
||
|
||
# Стандартные ключи для SNI и Host
|
||
for k in ['sni', 'host', 'peer']:
|
||
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(url: str, whitelist_domains: set, whitelist_suffixes: list) -> bool:
|
||
domains = extract_all_possible_domains(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 = CONFIG_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']} | Найдено конфигов: {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_configs():
|
||
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_config(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_configs():
|
||
print("\n=== Фильтрация по whitelist (V.O.I.D) ===")
|
||
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_config_url(url: str) -> str:
|
||
"""Корректное кодирование URL для всех типов протоколов."""
|
||
try:
|
||
schemes = ("vless://", "trojan://", "hysteria2://", "hy2://")
|
||
current_scheme = next((s for s in schemes if url.startswith(s)), None)
|
||
if not current_scheme: return url
|
||
|
||
content = url[len(current_scheme):]
|
||
at_pos = content.find('@')
|
||
if at_pos == -1: return url
|
||
|
||
auth = 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', 'mode']:
|
||
encoded_params.append(f"{k}={v}")
|
||
else:
|
||
encoded_v = urllib.parse.quote(v, safe='%')
|
||
encoded_params.append(f"{k}={encoded_v}")
|
||
|
||
new_params = "&".join(encoded_params)
|
||
|
||
try:
|
||
fragment.encode('ascii')
|
||
encoded_fragment = fragment
|
||
except UnicodeEncodeError:
|
||
encoded_fragment = urllib.parse.quote(fragment, safe='')
|
||
|
||
base = f"{current_scheme}{auth}@{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_config_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📊 [V.O.I.D][{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}")
|
||
|
||
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
|
||
self._ensure_xray()
|
||
|
||
def _ensure_xray(self):
|
||
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)
|
||
print(f"✅ Xray ядро готово")
|
||
except:
|
||
self._download_xray()
|
||
|
||
def _download_xray(self):
|
||
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"
|
||
else:
|
||
url = "https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip"
|
||
|
||
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()
|
||
if os.name != 'nt': os.chmod(self.xray_path, 0o755)
|
||
except Exception as e:
|
||
print(f"❌ Ошибка загрузки Xray: {e}")
|
||
|
||
def parse_url(self, url):
|
||
try:
|
||
schemes = ("vless://", "trojan://", "hysteria2://", "hy2://")
|
||
scheme = next((s for s in schemes if url.startswith(s)), None)
|
||
if not scheme: return None
|
||
|
||
content = url[len(scheme):]
|
||
at_pos = content.find('@')
|
||
if at_pos == -1: return None
|
||
|
||
auth = content[:at_pos]
|
||
after_at = content[at_pos+1:]
|
||
|
||
q_pos = after_at.find('?')
|
||
host_part = after_at[:q_pos] if q_pos != -1 else after_at
|
||
query = after_at[q_pos+1:] if q_pos != -1 else ""
|
||
|
||
host_part = host_part.split('#')[0]
|
||
host, port = (host_part.split(':', 1) + [443])[:2]
|
||
|
||
return {
|
||
'scheme': scheme.replace("://", ""),
|
||
'auth': auth, 'host': host,
|
||
'port': int(port),
|
||
'params': dict(urllib.parse.parse_qsl(query)),
|
||
'url': url
|
||
}
|
||
except: return None
|
||
|
||
def create_xray_config(self, parsed, port):
|
||
try:
|
||
p = parsed['params']
|
||
protocol = parsed['scheme']
|
||
if protocol == "hy2": protocol = "hysteria2"
|
||
|
||
outbound = {
|
||
"protocol": protocol,
|
||
"settings": {},
|
||
"streamSettings": {
|
||
"network": p.get('type', 'tcp'),
|
||
"security": p.get('security', 'none')
|
||
},
|
||
"tag": "proxy"
|
||
}
|
||
|
||
if protocol == "vless":
|
||
outbound["settings"] = {"vnext": [{"address": parsed['host'], "port": parsed['port'], "users": [{"id": parsed['auth'], "encryption": "none", "flow": p.get('flow', '')}]}]}
|
||
elif protocol == "trojan":
|
||
outbound["settings"] = {"servers": [{"address": parsed['host'], "port": parsed['port'], "password": parsed['auth']}]}
|
||
elif protocol == "hysteria2":
|
||
outbound["settings"] = {"servers": [{"address": parsed['host'], "port": parsed['port'], "password": parsed['auth']}]}
|
||
outbound["streamSettings"]["network"] = "udp"
|
||
|
||
# Reality / TLS
|
||
if p.get('security') == 'reality':
|
||
outbound["streamSettings"]["realitySettings"] = {
|
||
"serverName": p.get('sni', parsed['host']), "fingerprint": p.get('fp', 'chrome'),
|
||
"publicKey": p.get('pbk', ''), "shortId": p.get('sid', ''), "spiderX": p.get('spx', '/')
|
||
}
|
||
elif p.get('security') == 'tls':
|
||
outbound["streamSettings"]["tlsSettings"] = {"serverName": p.get('sni', parsed['host']), "allowInsecure": True}
|
||
|
||
config = {
|
||
"log": {"loglevel": "error"},
|
||
"inbounds": [{"port": port, "protocol": "socks", "settings": {"auth": "noauth", "udp": False}}],
|
||
"outbounds": [outbound]
|
||
}
|
||
return config
|
||
except: return None
|
||
|
||
def test_with_xray(self, parsed, port):
|
||
config_file, process = None, None
|
||
try:
|
||
config = self.create_xray_config(parsed, port)
|
||
if not config: return "FAIL"
|
||
|
||
fd, config_file = tempfile.mkstemp(suffix='.json')
|
||
os.close(fd)
|
||
with open(config_file, 'w', encoding='utf-8') as f:
|
||
json.dump(config, f)
|
||
|
||
flags = 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.DEVNULL, creationflags=flags)
|
||
|
||
time.sleep(1.2)
|
||
if process.poll() is not None: return "CRASH"
|
||
|
||
start = time.time()
|
||
try:
|
||
r = requests.get(self.test_url, proxies={'https': f'socks5://127.0.0.1:{port}'}, timeout=self.timeout)
|
||
if r.status_code in [200, 204]:
|
||
return {'working': True, 'ping': (time.time() - start) * 1000}
|
||
except: pass
|
||
return "FAIL"
|
||
finally:
|
||
if process:
|
||
try:
|
||
process.terminate()
|
||
process.wait(timeout=1)
|
||
except: pass
|
||
if config_file and os.path.exists(config_file):
|
||
try: os.remove(config_file)
|
||
except: pass
|
||
|
||
def test_one(self, url):
|
||
parsed = self.parse_url(url)
|
||
if not parsed: return None
|
||
|
||
port = self.port_manager.get_port()
|
||
if not port: return None
|
||
|
||
try:
|
||
for _ in range(self.max_retries):
|
||
res = self.test_with_xray(parsed, port)
|
||
if isinstance(res, dict): return {'url': url, 'ping': res['ping']}
|
||
time.sleep(self.retry_delay)
|
||
finally:
|
||
self.port_manager.release_port(port)
|
||
return None
|
||
|
||
def test_all(self):
|
||
if not os.path.exists(self.input_file): return
|
||
with open(self.input_file, 'r', encoding='utf-8') as f:
|
||
all_urls = [line.strip() for line in f if line.strip()]
|
||
|
||
if not all_urls: return
|
||
print(f"\n🔍 Тестирование {len(all_urls)} конфигов (VLESS/Trojan/Hy2)")
|
||
|
||
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):
|
||
res = future.result()
|
||
if res:
|
||
working.append(res)
|
||
progress.update('✅', working=True)
|
||
else:
|
||
progress.update('❌')
|
||
|
||
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"📊 Результаты: ✅ Рабочих: {len(working)}\n")
|
||
|
||
def run(self):
|
||
self.test_all()
|
||
|
||
# ========= ОСНОВНОЙ ЦИКЛ =========
|
||
async def main_cycle():
|
||
global cycle_counter
|
||
cycle_counter += 1
|
||
print(f"\n=== Новый цикл #{cycle_counter} [V.O.I.D Project] ===")
|
||
|
||
if os.path.exists(OUTPUT_FILE): os.remove(OUTPUT_FILE)
|
||
if not os.path.exists(SOURCES_FILE):
|
||
print(f"❌ Файл {SOURCES_FILE} не найден!")
|
||
return
|
||
|
||
try:
|
||
with open(SOURCES_FILE, "r", encoding="utf-8") as f:
|
||
urls = [line.strip() for line in f if line.strip()]
|
||
except: return
|
||
|
||
if not urls: return
|
||
|
||
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
|
||
|
||
if stats['found'] > 0:
|
||
await clean_configs()
|
||
await filter_configs()
|
||
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)
|
||
await asyncio.get_event_loop().run_in_executor(None, tester.run)
|
||
else:
|
||
print("⏭️ Новых конфигов не обнаружено")
|
||
|
||
async def run_forever():
|
||
while True:
|
||
try:
|
||
await main_cycle()
|
||
print(f"📡 Ожидание следующего цикла ({CYCLE_DELAY}с)...")
|
||
await asyncio.sleep(CYCLE_DELAY)
|
||
except KeyboardInterrupt: break
|
||
except Exception as e:
|
||
print(f"⚠️ Ошибка: {e}")
|
||
await asyncio.sleep(60)
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
# Для первого запуска выполняем один цикл, либо run_forever() для сервера
|
||
asyncio.run(main_cycle())
|
||
except KeyboardInterrupt:
|
||
print("\n👋 Парсер остановлен пользователем")
|