# 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()