Загрузка данных


import os
import sys
import json
import urllib.parse
import urllib.request
import subprocess
import winreg
import time
import ctypes
import atexit
import signal
import socket
import threading
from concurrent.futures import ThreadPoolExecutor

# --- НАСТРОЙКИ ---
XRAY_EXE = "xray.exe"
KEY_FILE = "key.txt"
CONFIG_FILE = "config.json"
PROXY_HOST = "127.0.0.1"
HTTP_PORT = 10809
SOCKS_PORT = 10808

INTERNET_OPTION_SETTINGS_CHANGED = 39
INTERNET_OPTION_REFRESH = 37

global_process = None
process_lock = threading.Lock()
stop_ping_monitor = threading.Event()

_PROXY_HANDLER = urllib.request.ProxyHandler({
    'http': f'http://{PROXY_HOST}:{HTTP_PORT}',
    'https': f'http://{PROXY_HOST}:{HTTP_PORT}'
})
_PROXY_OPENER = urllib.request.build_opener(_PROXY_HANDLER)


def clear_console():
    """ Очистка экрана консоли """
    os.system('cls' if os.name == 'nt' else 'clear')


def refresh_wininet():
    """ Уведомление Windows об изменении параметров сети """
    try:
        wininet = ctypes.windll.Wininet
        wininet.InternetSetOptionW(0, INTERNET_OPTION_SETTINGS_CHANGED, 0, 0)
        wininet.InternetSetOptionW(0, INTERNET_OPTION_REFRESH, 0, 0)
    except Exception as e:
        print(f"[!] Ошибка WinAPI: {e}")


def measure_ping(host, port, timeout=1.5):
    """ Измерение задержки TCP-соединения (мс) c фильтрацией IPv4 """
    sock = None
    try:
        prev_timeout = socket.getdefaulttimeout()
        socket.setdefaulttimeout(timeout)
        try:
            addr_info = socket.getaddrinfo(host, port, socket.AF_INET, socket.SOCK_STREAM)
        finally:
            socket.setdefaulttimeout(prev_timeout)

        if not addr_info:
            return None
        target_ip = addr_info[0][4][0]

        start_time = time.perf_counter()
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        sock.connect((target_ip, port))
        return int((time.perf_counter() - start_time) * 1000)
    except Exception:
        return None
    finally:
        if sock:
            sock.close()


def check_proxy_health(timeout=3.0):
    """ Проверка реального прохождения HTTP-трафика через прокси """
    try:
        req = urllib.request.Request(
            "https://cp.cloudflare.com/generate_204", 
            headers={'User-Agent': 'Mozilla/5.0'}
        )
        with _PROXY_OPENER.open(req, timeout=timeout) as response:
            return response.status in (200, 204)
    except Exception:
        return False


def parse_vless_url(url_str, log_level="warning"):
    """ Парсинг VLESS ссылки и формирование конфигурации """
    parsed = urllib.parse.urlparse(url_str)
    if parsed.scheme != "vless":
        raise ValueError("Ссылка должна начинаться с vless://")

    uuid = parsed.username
    host = parsed.hostname
    port = parsed.port or 443

    params = dict(urllib.parse.parse_qsl(parsed.query))
    
    stream_type = params.get("type", "tcp")
    security = params.get("security", "none")
    sni = params.get("sni", host)
    fp = params.get("fp", "chrome")
    pbk = params.get("pbk", "")
    sid = params.get("sid", "")
    flow = params.get("flow", "")
    path = params.get("path", "/")
    service_name = params.get("serviceName", "")
    alpn_raw = params.get("alpn", "")
    spider_x = params.get("spiderx", "/")
    header_type = params.get("headerType", "none")

    stream_settings = {
        "network": stream_type,
        "security": security
    }

    if security == "reality":
        reality_settings = {
            "serverName": sni,
            "fingerprint": fp if fp else "chrome",
            "publicKey": pbk,
            "spiderX": spider_x
        }
        if sid:
            reality_settings["shortId"] = sid
        stream_settings["realitySettings"] = reality_settings
    elif security == "tls":
        tls_settings = {
            "serverName": sni,
            "allowInsecure": False,
            "fingerprint": fp if fp else "chrome"
        }
        if alpn_raw:
            tls_settings["alpn"] = [a.strip() for a in alpn_raw.split(",")]
        stream_settings["tlsSettings"] = tls_settings

    if stream_type == "grpc":
        stream_settings["grpcSettings"] = {"serviceName": service_name}
    elif stream_type in ("ws", "xhttp"):
        stream_settings[f"{stream_type}Settings"] = {"path": path}
    elif stream_type == "tcp" and header_type == "http":
        stream_settings["tcpSettings"] = {
            "header": {
                "type": "http",
                "request": {"path": [path]}
            }
        }

    user_data = {"id": uuid, "encryption": "none"}
    if flow:
        user_data["flow"] = flow

    config = {
        "log": {"loglevel": log_level},
        "inbounds": [
            {"port": HTTP_PORT, "listen": PROXY_HOST, "protocol": "http", "settings": {"timeout": 300}},
            {"port": SOCKS_PORT, "listen": PROXY_HOST, "protocol": "socks", "settings": {"udp": True}}
        ],
        "outbounds": [
            {
                "protocol": "vless",
                "settings": {
                    "vnext": [
                        {
                            "address": host,
                            "port": port,
                            "users": [user_data]
                        }
                    ]
                },
                "streamSettings": stream_settings
            }
        ]
    }
    return config, host, port


def set_windows_proxy(enable=True):
    """ Управление системным прокси Windows """
    reg_path = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings"
    try:
        with winreg.OpenKey(winreg.HKEY_CURRENT_USER, reg_path, 0, winreg.KEY_WRITE) as registry:
            if enable:
                winreg.SetValueEx(registry, "ProxyEnable", 0, winreg.REG_DWORD, 1)
                winreg.SetValueEx(registry, "ProxyServer", 0, winreg.REG_SZ, f"{PROXY_HOST}:{HTTP_PORT}")
                winreg.SetValueEx(registry, "ProxyOverride", 0, winreg.REG_SZ, "<local>;127.*;10.*;172.16.*;192.168.*")
                print(f"\n[+] Системный прокси Windows: ВКЛЮЧЕН ({PROXY_HOST}:{HTTP_PORT})")
            else:
                winreg.SetValueEx(registry, "ProxyEnable", 0, winreg.REG_DWORD, 0)
                winreg.SetValueEx(registry, "ProxyServer", 0, winreg.REG_SZ, "")
                
        refresh_wininet()
    except Exception as e:
        print(f"[!] Ошибка реестра Windows: {e}")


def cleanup():
    """ Завершение процессов и сброс параметров """
    global global_process
    stop_ping_monitor.set()
    set_windows_proxy(False)
    
    with process_lock:
        if global_process and global_process.poll() is None:
            global_process.terminate()
            try:
                global_process.wait(timeout=2)
            except subprocess.TimeoutExpired:
                global_process.kill()
            global_process = None

    if os.path.exists(CONFIG_FILE):
        for _ in range(5):
            try:
                os.remove(CONFIG_FILE)
                break
            except OSError:
                time.sleep(0.1)


def _win_ctrl_handler(ctrl_type):
    cleanup()
    os._exit(0)


def setup_os_handlers():
    atexit.register(cleanup)
    signal.signal(signal.SIGINT, lambda s, f: sys.exit(0))
    signal.signal(signal.SIGTERM, lambda s, f: sys.exit(0))

    if sys.platform == "win32":
        PHANDLER_ROUTINE = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_ulong)
        global _handler_ref
        _handler_ref = PHANDLER_ROUTINE(_win_ctrl_handler)
        ctypes.windll.kernel32.SetConsoleCtrlHandler(_handler_ref, True)


def get_server_metadata(url_str):
    try:
        parsed = urllib.parse.urlparse(url_str)
        host = parsed.hostname
        port = parsed.port or 443
        if not host:
            return None
            
        name = urllib.parse.unquote(parsed.fragment) if parsed.fragment else f"{host}:{port}"
        
        ping = measure_ping(host, port)
        return {
            "name": name,
            "host": host,
            "port": port,
            "ping": ping,
            "url": url_str
        }
    except Exception:
        return None


def select_server(vless_urls):
    while True:
        print("\n[*] Измерение задержки серверов...")
        servers = []
        with ThreadPoolExecutor(max_workers=min(len(vless_urls), 20)) as executor:
            results = executor.map(get_server_metadata, vless_urls)
            servers = [res for res in results if res]

        if not servers:
            print("[!] Нет доступных серверов.")
            return None

        servers.sort(key=lambda x: (x['ping'] is None, x['ping']))

        print("\nСписок серверов:")
        print("-" * 60)
        for idx, srv in enumerate(servers, start=1):
            ping_str = f"{srv['ping']} ms" if srv['ping'] is not None else "TIMEOUT"
            print(f" [{idx}] {srv['name']:<30} | Пинг: {ping_str}")
        print(" [R] Обновить пинг")
        print("-" * 60)

        choice = input(f"Выберите номер сервера (1-{len(servers)}) или 'R' для обновления: ").strip()
        if choice.lower() in ("r", "refresh", "к"):
            continue
        try:
            idx = int(choice) - 1
            if 0 <= idx < len(servers):
                return servers[idx]['url']
            print("[!] Неверный номер.")
        except ValueError:
            print("[!] Неверный ввод.")
        except (KeyboardInterrupt, EOFError):
            return None


def live_ping_monitor(host, port, show_logs):
    """ Фоновое динамическое обновление пинга активного сервера """
    while not stop_ping_monitor.is_set():
        ping = measure_ping(host, port)
        ping_str = f"{ping} ms" if ping is not None else "TIMEOUT"
        if not show_logs:
            msg = f"\r[STATUS] {host}:{port} | Пинг: {ping_str:<8} | Enter — выход"
            sys.stdout.write(msg.ljust(75))
            sys.stdout.flush()
        stop_ping_monitor.wait(3.0)

    if not show_logs:
        sys.stdout.write("\r" + " " * 75 + "\r")
        sys.stdout.flush()


def main():
    global global_process
    setup_os_handlers()

    set_windows_proxy(False)

    print("=" * 60)
    print(" МИКРО VPN-КЛИЕНТ (XRAY CORE)")
    print("=" * 60)

    if not os.path.exists(XRAY_EXE):
        print(f"\n[!] ОШИБКА: Файл '{XRAY_EXE}' не найден!")
        input("\nНажмите Enter для выхода...")
        return

    if not os.path.exists(KEY_FILE):
        print(f"\n[!] ОШИБКА: Файл '{KEY_FILE}' не найден!")
        input("\nНажмите Enter для выхода...")
        return

    with open(KEY_FILE, "r", encoding="utf-8") as f:
        vless_urls = [line.strip() for line in f if line.strip().startswith("vless://")]

    if not vless_urls:
        print(f"\n[!] В '{KEY_FILE}' не найдено VLESS-ссылок!")
        input("\nНажмите Enter для выхода...")
        return

    selected_url = select_server(vless_urls)
    if not selected_url:
        return

    show_logs = input("\nПоказывать логи Xray в консоли? (y/n): ").strip().lower() in ("y", "yes", "д", "да")
    log_level = "info" if show_logs else "warning"

    # Очищаем консоль от списка серверов и вопросов перед запуском
    clear_console()

    try:
        config_data, target_host, target_port = parse_vless_url(selected_url, log_level=log_level)
        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
            json.dump(config_data, f, indent=2)
        print("[+] Конфигурация сформирована.")
    except Exception as e:
        print(f"\n[!] Ошибка парсинга ссылки: {e}")
        input("\nНажмите Enter для выхода...")
        return

    try:
        print("[*] Запуск Xray-core...")
        stdout_dest = None if show_logs else subprocess.DEVNULL
        stderr_dest = None if show_logs else subprocess.DEVNULL

        with process_lock:
            global_process = subprocess.Popen(
                [XRAY_EXE, "run", "-c", CONFIG_FILE],
                stdout=stdout_dest,
                stderr=stderr_dest
            )

        time.sleep(1.0)

        if global_process.poll() is not None:
            print("\n[!] ОШИБКА: Xray-core завершил работу сразу после запуска.")
            print("[!] Проверьте занятость портов 10808 / 10809.")
            input("\nНажмите Enter для выхода...")
            return

        print("[*] Проверка проходимости трафика через прокси...")
        if not check_proxy_health(timeout=3.5):
            print("\n[!] ОШИБКА: Трафик не проходит через прокси.")
            print("[!] Ключ недействителен или соединение блокируется.")
            cleanup()
            input("\nНажмите Enter для выхода...")
            return

        set_windows_proxy(True)

        stop_ping_monitor.clear()
        ping_thread = threading.Thread(
            target=live_ping_monitor, 
            args=(target_host, target_port, show_logs), 
            daemon=True
        )
        ping_thread.start()

        if show_logs:
            print("\nНажмите Enter для отключения...")

        input()
    except KeyboardInterrupt:
        pass
    except Exception as e:
        print(f"[!] Ошибка исполнения: {e}")
    finally:
        print("\n[*] Завершение работы и сброс прокси...")
        cleanup()
        print("[+] Готово.")


if __name__ == "__main__":
    main()