CCCPaste

start_gui

# start_gui.py
# Версия 4.1 (macOS): Исправлены пути, os.startfile и ошибка stop_proxy

import tkinter as tk
from tkinter import messagebox, ttk
import subprocess
import os
import sys
import threading
import asyncio
from pathlib import Path
import importlib.util
import socket

# --- Mitmproxy imports ---
try:
    from mitmproxy.options import Options
    from mitmproxy.tools.dump import DumpMaster
except ImportError:
    messagebox.showerror("Ошибка импорта",
                       "Не найден модуль mitmproxy.\n"
                       "Пожалуйста, установите его: pip3 install mitmproxy")
    sys.exit(1)

# --- Глобальные переменные ---
mitm_thread = None
mitm_master = None
mitm_loop = None # Для корректной остановки

# --- Функции ---
def resource_path(relative_path):
    if getattr(sys, 'frozen', False):
        base_dir = Path(sys.executable).parent.resolve()
    else:
        base_dir = Path(__file__).parent.resolve()
    return os.path.join(base_dir, relative_path)

def find_chrome_path():
    """Ищет Chrome в стандартных местах (macOS/Windows)."""
    if sys.platform == "darwin":  # 'darwin' - это macOS
        return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
    elif sys.platform == "win32":
        possible_paths = [
            r"C:\Program Files\Google\Chrome\Application\chrome.exe",
            r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
            os.path.expanduser(r"~\AppData\Local\Google\Chrome\Application\chrome.exe")
        ]
        for path in possible_paths:
            if os.path.exists(path):
                return path
        return "chrome.exe"
    else:
        return "google-chrome" # Для Linux

def get_lan_ip():
    s = None
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.connect(('8.8.8.8', 1))
        ip = s.getsockname()[0]
    except Exception:
        ip = '127.0.0.1'
    finally:
        if s:
            s.close()
    return ip

def show_lan_help():
    port = port_entry.get()
    ip = get_lan_ip()
    if ip == '127.0.0.1':
        messagebox.showwarning("LAN IP не найден", "Не удалось определить ваш LAN IP. Убедитесь, что вы подключены к сети.")
        return
    
    # НОВОЕ: Инструкция для macOS
    if sys.platform == "darwin":
        fw_help = (f"2. На этом Mac: Откройте 'Системные настройки' -> 'Сеть' -> 'Брандмауэр' (Firewall). "
                   f"Нажмите 'Параметры...' и добавьте (+) 'Python' (или это приложение), разрешив входящие подключения.")
    elif sys.platform == "win32":
        fw_help = (f"2. На этом ПК: Откройте Брандмауэр Windows и создайте 'Правило для входящих подключений', "
                   f"чтобы 'Разрешить' 'TCP' трафик на порт {port}.")
    else:
        fw_help = f"2. На этом ПК: Убедитесь, что брандмауэр разрешает входящие подключения на порт {port}."

    help_text = (
        f"Чтобы подключиться с другого устройства:\n\n"
        f"1. Устройство должно быть в той же Wi-Fi/LAN сети.\n\n"
        f"{fw_help}\n\n"
        f"3. На другом устройстве: В настройках Wi-Fi укажите прокси:\n"
        f"   - Хост/Сервер: {ip}\n"
        f"   - Порт: {port}\n\n"
        f"4. На другом устройстве: Установите сертификат mitmproxy. Самый простой способ - открыть в браузере http://mitm.it (когда прокси уже включен)."
    )
    messagebox.showinfo("Как подключиться по LAN", help_text)

async def run_mitmproxy(port, script_file, tk_root, host):
    global mitm_master
    conf_dir = os.path.join(os.path.abspath("."), ".mitmproxy")
    os.makedirs(conf_dir, exist_ok=True)

    opts = Options(
        listen_host=host,
        listen_port=int(port),
        ssl_insecure=True,
        confdir=conf_dir
    )

    try:
        mitm_master = DumpMaster(opts, with_termlog=False, with_dumper=False)

        # Динамическая загрузка скрипта
        spec = importlib.util.spec_from_file_location("sni_override", script_file)
        if spec is None:
            raise ImportError(f"Не удалось загрузить модуль из {script_file}")
        
        module = importlib.util.module_from_spec(spec)
        sys.modules["sni_override"] = module
        spec.loader.exec_module(module)

        if hasattr(module, "addons"):
            for addon in module.addons:
                mitm_master.addons.add(addon)
                print(f"Загружен аддон: {type(addon).__name__}")
        else:
            raise ValueError(f"В модуле {script_file} не найден список 'addons'")

        await mitm_master.run()
    except Exception as e:
        print(f"!!! Ошибка Mitmproxy: {e}")
        if "mitm_loop" in globals() and mitm_loop and mitm_loop.is_running():
             tk_root.after(0, lambda: messagebox.showerror("Ошибка Mitmproxy", str(e)))
             tk_root.after(0, stop_proxy)
    finally:
        print("--- Поток Mitmproxy завершен ---")


async def shutdown_mitmproxy():
    global mitm_master
    if mitm_master:
        try:
            mitm_master.shutdown()
            await asyncio.sleep(0.1)
        except Exception as e:
            print(f"!!! Ошибка при асинхронной остановке mitmproxy: {e}")

def run_proxy_server(port, script_file, tk_root, host):
    global mitm_master, mitm_loop
    mitm_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(mitm_loop)
    try:
        mitm_loop.run_until_complete(run_mitmproxy(port, script_file, tk_root, host))
    except Exception as e:
        print(f"!!! Ошибка в run_proxy_server: {e}")
    finally:
        try:
            if mitm_loop.is_running():
                mitm_loop.run_until_complete(mitm_loop.shutdown_asyncgens())
                mitm_loop.run_until_complete(mitm_loop.shutdown_default_executor())
        except Exception as e:
            print(f"!!! Ошибка при очистке цикла событий: {e}")
        finally:
            mitm_loop.close()
            print("--- Цикл событий закрыт ---")
            mitm_loop = None
            # mitm_master будет None после stop_proxy

def start_proxy():
    global mitm_thread
    if mitm_thread and mitm_thread.is_alive():
        messagebox.showwarning("Внимание", "Прокси уже запущен!")
        return

    port = port_entry.get()
    if not port.isdigit() or not (1024 < int(port) < 65535):
        messagebox.showerror("Ошибка", f"Введите корректный порт (например, 8087).")
        return
        
    try:
        script_path = resource_path("sni_override.py") # Используем твое имя файла
        
        if not os.path.exists(script_path):
             messagebox.showerror("Ошибка", f"Не найден файл аддона: {script_path}")
             return
        
        host = listen_mode_var.get()

        mitm_thread = threading.Thread(
            target=run_proxy_server,
            args=(port, script_path, root, host),
            daemon=True
        )
        mitm_thread.start()
        
        status_label.config(text=f"Прокси запущен на {host}:{port}", fg="green")
        start_button.config(state=tk.DISABLED)
        stop_button.config(state=tk.NORMAL)
        local_radio.config(state=tk.DISABLED)
        lan_radio.config(state=tk.DISABLED)
    except Exception as e:
        messagebox.showerror("Ошибка запуска", f"Не удалось запустить прокси-поток:\n{e}")

# ИСПРАВЛЕННАЯ ФУНКЦИЯ ОСТАНОВКИ
def stop_proxy():
    global mitm_master, mitm_thread, mitm_loop
    
    if mitm_master and mitm_loop and mitm_loop.is_running():
        print("--- Запрос на остановку прокси ---")
        try:
            # Потокобезопасно просим цикл событий запустить shutdown
            mitm_loop.call_soon_threadsafe(
                lambda: mitm_loop.create_task(shutdown_mitmproxy())
            )
            
            # Ждем, пока поток (в котором живет цикл) завершится сам
            mitm_thread.join(timeout=2.0)
            print("--- Поток прокси завершен ---")

        except Exception as e:
            print(f"!!! Ошибка при остановке прокси: {e}")
        finally:
            mitm_thread = None
            mitm_master = None
            mitm_loop = None
    
    # Обновляем GUI в любом случае
    status_label.config(text="Прокси остановлен", fg="red")
    start_button.config(state=tk.NORMAL)
    stop_button.config(state=tk.DISABLED)
    local_radio.config(state=tk.NORMAL)
    lan_radio.config(state=tk.NORMAL)


def launch_browser():
    port = port_entry.get()
    
    temp_dir = os.getenv("TEMP") or "/tmp" # НОВОЕ: /tmp для macOS/Linux
    if not temp_dir:
        messagebox.showerror("Ошибка", "Не удалось определить временную папку.")
        return
    profile_dir = os.path.join(temp_dir, "chrome-mitm-test")
    os.makedirs(profile_dir, exist_ok=True)
    
    chrome_path = find_chrome_path()
    if not os.path.exists(chrome_path):
        messagebox.showerror("Ошибка", f"Не удалось найти Google Chrome по пути: {chrome_path}\nУстановите его через Homebrew: brew install --cask google-chrome")
        return

    cmd = [
        chrome_path,
        f"--proxy-server=127.0.0.1:{port}",
        "--disable-quic",
        f"--user-data-dir={profile_dir}",
    ]
    
    try:
        subprocess.Popen(cmd)
    except Exception as e:
        messagebox.showerror("Ошибка запуска", f"Не удалось запустить Chrome:\n{e}")

def install_cert():
    cert_path_on_disk = os.path.join(os.path.abspath("."), ".mitmproxy", "mitmproxy-ca-cert.cer")

    if not os.path.exists(cert_path_on_disk):
        messagebox.showwarning("Внимание", 
            "Файл сертификата 'mitmproxy-ca-cert.cer' не найден.\n\n"
            "1. Сначала нажмите 'Start Proxy'.\n"
            "2. Прокси запустится и сгенерирует файл.\n"
            "3. Затем снова нажмите 'Установить сертификат'.")
        return
        
    try:
        # НОВОЕ: Кроссплатформенный запуск
        if sys.platform == "win32":
            os.startfile(cert_path_on_disk, 'open')
            messagebox.showinfo("Сертификат (Windows)", 
                "Открыто окно установки сертификата.\n\n"
                "1. Нажмите 'Установить сертификат...'\n"
                "2. Выберите 'Локальный компьютер'\n"
                "3. Выберите 'Поместить... в следующее хранилище' -> 'Обзор'\n"
                "4. Выберите 'Доверенные корневые центры сертификации'\n"
                "5. Нажмите 'Далее' и 'Готово'.")
        elif sys.platform == "darwin":
            subprocess.run(["open", cert_path_on_disk])
            messagebox.showinfo("Сертификат (macOS)", 
                "Открылась 'Связка ключей' (Keychain Access).\n\n"
                "1. Найдите сертификат 'mitmproxy' (он может быть во вкладке 'login' или 'Вход').\n"
                "2. Дважды кликните на него.\n"
                "3. Разверните секцию 'Доверие' (Trust).\n"
                "4. В 'При использовании...' выберите 'Всегда доверять' (Always Trust).\n"
                "5. Закройте окно (потребуется пароль).")
        else:
            subprocess.run(["open", cert_path_on_disk]) # Для Linux

    except Exception as e:
        messagebox.showerror("Ошибка", f"Не удалось открыть файл сертификата:\n{cert_path_on_disk}\n\n{e}")

def on_closing():
    """Вызывается при закрытии окна"""
    if mitm_master:
        stop_proxy()
    root.destroy()

# --- Создание GUI ---
root = tk.Tk()
root.title("SNI Proxy Manager (v4.0 macOS)")
root.geometry("430x250")
root.resizable(False, False)

frame = tk.Frame(root, padx=10, pady=10)
frame.pack(expand=True, fill=tk.BOTH)

# --- Строка Порта ---
port_frame = tk.Frame(frame)
port_frame.pack(fill=tk.X, pady=5)
port_label = tk.Label(port_frame, text="Порт прокси:", width=15, anchor="w")
port_label.pack(side=tk.LEFT)
port_entry = tk.Entry(port_frame, width=10)
port_entry.pack(side=tk.LEFT, padx=5)
port_entry.insert(0, "8087")

# --- Блок выбора режима ---
mode_frame = ttk.Labelframe(frame, text="Режим прослушивания", padding=5)
mode_frame.pack(fill=tk.X, pady=(5,0))

listen_mode_var = tk.StringVar(value="127.0.0.1")

local_radio = ttk.Radiobutton(mode_frame, text="Только локально (127.0.0.1)", variable=listen_mode_var, value="127.0.0.1")
local_radio.pack(side=tk.LEFT, padx=5)

local_ip = get_lan_ip()
lan_radio = ttk.Radiobutton(mode_frame, text=f"Для LAN ({local_ip})", variable=listen_mode_var, value="0.0.0.0")
lan_radio.pack(side=tk.LEFT, padx=5)

lan_help_button = ttk.Button(mode_frame, text="?", width=3, command=show_lan_help)
lan_help_button.pack(side=tk.RIGHT, padx=5)

# --- Кнопки управления ---
proxy_frame = tk.Frame(frame)
proxy_frame.pack(fill=tk.X, pady=10)
start_button = tk.Button(proxy_frame, text="Start Proxy", command=start_proxy, width=15)
start_button.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=2)
stop_button = tk.Button(proxy_frame, text="Stop Proxy", command=stop_proxy, state=tk.DISABLED, width=15)
stop_button.pack(side=tk.LEFT, expand=True, fill=tk.X, padx=2)

# --- Кнопки утилит ---
utils_frame = tk.Frame(frame)
utils_frame.pack(fill=tk.X, pady=5)
cert_button = tk.Button(utils_frame, text="Установить сертификат", command=install_cert)
cert_button.pack(expand=True, fill=tk.X, pady=2)
browser_button = tk.Button(utils_frame, text="Запустить Chrome (с прокси)", command=launch_browser)
browser_button.pack(expand=True, fill=tk.X, pady=2)

# --- Строка статуса ---
status_frame = tk.Frame(frame, relief=tk.SUNKEN, bd=1)
status_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(10,0))

status_label = tk.Label(status_frame, text="Прокси остановлен", fg="red", anchor="w")
status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)

root.protocol("WM_DELETE_WINDOW", on_closing)
root.mainloop()