mtproto recode
This commit is contained in:
parent
95f99be26b
commit
6766db9812
|
|
@ -303,7 +303,7 @@ jobs:
|
||||||
Maintainer: Flowseal
|
Maintainer: Flowseal
|
||||||
Depends: libgtk-3-0, libayatana-appindicator3-1, python3-tk
|
Depends: libgtk-3-0, libayatana-appindicator3-1, python3-tk
|
||||||
Description: Telegram Desktop WebSocket Bridge Proxy
|
Description: Telegram Desktop WebSocket Bridge Proxy
|
||||||
SOCKS5/WebSocket bridge proxy for Telegram Desktop with tray UI.
|
MTProto/WebSocket bridge proxy for Telegram Desktop with tray UI.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
dpkg-deb --build --root-owner-group \
|
dpkg-deb --build --root-owner-group \
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PATH=/opt/venv/bin:$PATH \
|
PATH=/opt/venv/bin:$PATH \
|
||||||
TG_WS_PROXY_HOST=0.0.0.0 \
|
TG_WS_PROXY_HOST=0.0.0.0 \
|
||||||
TG_WS_PROXY_PORT=1080 \
|
TG_WS_PROXY_PORT=1443 \
|
||||||
TG_WS_PROXY_DC_IPS="2:149.154.167.220 4:149.154.167.220"
|
TG_WS_PROXY_DC_IPS="2:149.154.167.220 4:149.154.167.220"
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
|
|
@ -39,7 +39,7 @@ COPY README.md LICENSE ./
|
||||||
|
|
||||||
USER app
|
USER app
|
||||||
|
|
||||||
EXPOSE 1080/tcp
|
EXPOSE 1443/tcp
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/tini", "--", "/bin/sh", "-lc", "set -eu; args=\"--host ${TG_WS_PROXY_HOST} --port ${TG_WS_PROXY_PORT}\"; for dc in ${TG_WS_PROXY_DC_IPS}; do args=\"$args --dc-ip $dc\"; done; exec python -u proxy/tg_ws_proxy.py $args \"$@\"", "--"]
|
ENTRYPOINT ["/usr/bin/tini", "--", "/bin/sh", "-lc", "set -eu; args=\"--host ${TG_WS_PROXY_HOST} --port ${TG_WS_PROXY_PORT}\"; for dc in ${TG_WS_PROXY_DC_IPS}; do args=\"$args --dc-ip $dc\"; done; exec python -u proxy/tg_ws_proxy.py $args \"$@\"", "--"]
|
||||||
CMD []
|
CMD []
|
||||||
|
|
|
||||||
31
README.md
31
README.md
|
|
@ -12,17 +12,17 @@
|
||||||
|
|
||||||
# TG WS Proxy
|
# TG WS Proxy
|
||||||
|
|
||||||
**Локальный SOCKS5-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние сервера.
|
**Локальный MTProto-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние сервера.
|
||||||
|
|
||||||
<img width="529" height="487" alt="image" src="https://github.com/user-attachments/assets/6a4cf683-0df8-43af-86c1-0e8f08682b62" />
|
<img width="529" height="487" alt="image" src="https://github.com/user-attachments/assets/6a4cf683-0df8-43af-86c1-0e8f08682b62" />
|
||||||
|
|
||||||
## Как это работает
|
## Как это работает
|
||||||
|
|
||||||
```
|
```
|
||||||
Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegram DC
|
Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram DC
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Приложение поднимает локальный SOCKS5-прокси на `127.0.0.1:1080`
|
1. Приложение поднимает MTProto прокси на `127.0.0.1:1443`
|
||||||
2. Перехватывает подключения к IP-адресам Telegram
|
2. Перехватывает подключения к IP-адресам Telegram
|
||||||
3. Извлекает DC ID из MTProto obfuscation init-пакета
|
3. Извлекает DC ID из MTProto obfuscation init-пакета
|
||||||
4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены Telegram
|
4. Устанавливает WebSocket (TLS) соединение к соответствующему DC через домены Telegram
|
||||||
|
|
@ -38,7 +38,7 @@ Telegram Desktop → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS → Telegra
|
||||||
|
|
||||||
**Меню трея:**
|
**Меню трея:**
|
||||||
|
|
||||||
- **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку
|
- **Открыть в Telegram** — автоматически настроить прокси через `tg://proxy` ссылку
|
||||||
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
||||||
- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub)
|
- **Настройки...** — GUI-редактор конфигурации (в т.ч. версия приложения, опциональная проверка обновлений с GitHub)
|
||||||
- **Открыть логи** — открыть файл логов
|
- **Открыть логи** — открыть файл логов
|
||||||
|
|
@ -86,7 +86,7 @@ chmod +x TgWsProxy_linux_amd64
|
||||||
|
|
||||||
### Консольный proxy
|
### Консольный proxy
|
||||||
|
|
||||||
Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки:
|
Для запуска только proxy без tray-интерфейса достаточно базовой установки:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -e .
|
pip install -e .
|
||||||
|
|
@ -124,9 +124,15 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
|
||||||
|
|
||||||
| Аргумент | По умолчанию | Описание |
|
| Аргумент | По умолчанию | Описание |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `--port` | `1080` | Порт SOCKS5-прокси |
|
| `--port` | `1443` | Порт прокси |
|
||||||
| `--host` | `127.0.0.1` | Хост SOCKS5-прокси |
|
| `--host` | `127.0.0.1` | Хост прокси |
|
||||||
|
| `--secret` | `random` | 32 hex chars secret для авторизации клиентов |
|
||||||
| `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (можно указать несколько раз) |
|
| `--dc-ip` | `2:149.154.167.220`, `4:149.154.167.220` | Целевой IP для DC (можно указать несколько раз) |
|
||||||
|
| `--buf-kb` | `256` | Размер буфера в КБ
|
||||||
|
| `--pool-size` | `4` | Количество заготовленных соединений на каждый DC
|
||||||
|
| `--log-file` | выкл. | Путь до файла, в который сохранять логи
|
||||||
|
| `--log-max-mb` | `5` | Максимальный размер файла логов в МБ (после идёт перезапись)
|
||||||
|
| `--log-backups` | `0` | Количество сохранений логов после перезаписи
|
||||||
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
|
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
|
||||||
|
|
||||||
**Примеры:**
|
**Примеры:**
|
||||||
|
|
@ -166,10 +172,10 @@ tg-ws-proxy-tray-linux = "linux:main"
|
||||||
|
|
||||||
1. Telegram → **Настройки** → **Продвинутые настройки** → **Тип подключения** → **Прокси**
|
1. Telegram → **Настройки** → **Продвинутые настройки** → **Тип подключения** → **Прокси**
|
||||||
2. Добавить прокси:
|
2. Добавить прокси:
|
||||||
- **Тип:** SOCKS5
|
- **Тип:** MTProto
|
||||||
- **Сервер:** `127.0.0.1`
|
- **Сервер:** `127.0.0.1` (или переопределенный вами)
|
||||||
- **Порт:** `1080`
|
- **Порт:** `1443` (или переопределенный вами)
|
||||||
- **Логин/Пароль:** оставить пустыми
|
- **Secret:** из настроек или логов
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
|
|
@ -182,7 +188,8 @@ Tray-приложение хранит данные в:
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 1080,
|
"port": 1443,
|
||||||
|
"secret": "...",
|
||||||
"dc_ip": [
|
"dc_ip": [
|
||||||
"2:149.154.167.220",
|
"2:149.154.167.220",
|
||||||
"4:149.154.167.220"
|
"4:149.154.167.220"
|
||||||
|
|
|
||||||
158
linux.py
158
linux.py
|
|
@ -20,7 +20,9 @@ import pystray
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
|
from proxy.tg_ws_proxy import proxy_config
|
||||||
from proxy import __version__
|
from proxy import __version__
|
||||||
|
|
||||||
from utils.default_config import default_tray_config
|
from utils.default_config import default_tray_config
|
||||||
from ui.ctk_tray_ui import (
|
from ui.ctk_tray_ui import (
|
||||||
install_tray_config_buttons,
|
install_tray_config_buttons,
|
||||||
|
|
@ -33,7 +35,7 @@ from ui.ctk_theme import (
|
||||||
CONFIG_DIALOG_FRAME_PAD,
|
CONFIG_DIALOG_FRAME_PAD,
|
||||||
CONFIG_DIALOG_SIZE,
|
CONFIG_DIALOG_SIZE,
|
||||||
FIRST_RUN_SIZE,
|
FIRST_RUN_SIZE,
|
||||||
create_ctk_root,
|
create_ctk_toplevel,
|
||||||
ctk_theme_for_platform,
|
ctk_theme_for_platform,
|
||||||
main_content_frame,
|
main_content_frame,
|
||||||
)
|
)
|
||||||
|
|
@ -56,6 +58,9 @@ _config: dict = {}
|
||||||
_exiting: bool = False
|
_exiting: bool = False
|
||||||
_lock_file_path: Optional[Path] = None
|
_lock_file_path: Optional[Path] = None
|
||||||
|
|
||||||
|
_ctk_root = None
|
||||||
|
_ctk_root_ready = threading.Event()
|
||||||
|
|
||||||
log = logging.getLogger("tg-ws-tray")
|
log = logging.getLogger("tg-ws-tray")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -246,18 +251,59 @@ def _apply_linux_ctk_window_icon(root) -> None:
|
||||||
root.iconphoto(False, root._ctk_icon_photo)
|
root.iconphoto(False, root._ctk_icon_photo)
|
||||||
|
|
||||||
|
|
||||||
def _run_proxy_thread(
|
def _ensure_ctk_thread() -> bool:
|
||||||
port: int, dc_opt: Dict[int, str], verbose: bool, host: str = "127.0.0.1"
|
"""Start the persistent hidden CTk root in its own thread (once)."""
|
||||||
):
|
global _ctk_root
|
||||||
|
if _ctk_root_ready.is_set():
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
global _ctk_root
|
||||||
|
from ui.ctk_theme import (
|
||||||
|
apply_ctk_appearance,
|
||||||
|
_install_tkinter_variable_del_guard,
|
||||||
|
)
|
||||||
|
_install_tkinter_variable_del_guard()
|
||||||
|
apply_ctk_appearance(ctk)
|
||||||
|
_ctk_root = ctk.CTk()
|
||||||
|
_ctk_root.withdraw()
|
||||||
|
_ctk_root_ready.set()
|
||||||
|
_ctk_root.mainloop()
|
||||||
|
|
||||||
|
threading.Thread(target=_run, daemon=True, name="ctk-root").start()
|
||||||
|
_ctk_root_ready.wait(timeout=5.0)
|
||||||
|
return _ctk_root is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _ctk_run_dialog(build_fn) -> None:
|
||||||
|
"""Schedule build_fn(done_event) on the CTk thread and block until done_event is set."""
|
||||||
|
if _ctk_root is None:
|
||||||
|
return
|
||||||
|
done = threading.Event()
|
||||||
|
|
||||||
|
def _invoke():
|
||||||
|
try:
|
||||||
|
build_fn(done)
|
||||||
|
except Exception:
|
||||||
|
log.exception("CTk dialog failed")
|
||||||
|
done.set()
|
||||||
|
|
||||||
|
_ctk_root.after(0, _invoke)
|
||||||
|
done.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def _run_proxy_thread():
|
||||||
global _async_stop
|
global _async_stop
|
||||||
|
|
||||||
loop = _asyncio.new_event_loop()
|
loop = _asyncio.new_event_loop()
|
||||||
_asyncio.set_event_loop(loop)
|
_asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
stop_ev = _asyncio.Event()
|
stop_ev = _asyncio.Event()
|
||||||
_async_stop = (loop, stop_ev)
|
_async_stop = (loop, stop_ev)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(
|
loop.run_until_complete(
|
||||||
tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host)
|
tg_ws_proxy._run(stop_event=stop_ev)
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.error("Proxy thread crashed: %s", exc)
|
log.error("Proxy thread crashed: %s", exc)
|
||||||
|
|
@ -279,27 +325,29 @@ def start_proxy():
|
||||||
cfg = _config
|
cfg = _config
|
||||||
port = cfg.get("port", DEFAULT_CONFIG["port"])
|
port = cfg.get("port", DEFAULT_CONFIG["port"])
|
||||||
host = cfg.get("host", DEFAULT_CONFIG["host"])
|
host = cfg.get("host", DEFAULT_CONFIG["host"])
|
||||||
|
secret = cfg.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])
|
dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])
|
||||||
verbose = cfg.get("verbose", False)
|
buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])
|
||||||
|
pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dc_opt = tg_ws_proxy.parse_dc_ip_list(dc_ip_list)
|
dc_redirects = tg_ws_proxy.parse_dc_ip_list(dc_ip_list)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
log.error("Bad config dc_ip: %s", e)
|
log.error("Bad config dc_ip: %s", e)
|
||||||
_show_error(f"Ошибка конфигурации:\n{e}")
|
_show_error(f"Ошибка конфигурации:\n{e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
log.info("Starting proxy on %s:%d ...", host, port)
|
proxy_config.port = port
|
||||||
|
proxy_config.host = host
|
||||||
|
proxy_config.secret = secret
|
||||||
|
proxy_config.dc_redirects = dc_redirects
|
||||||
|
proxy_config.buffer_size = max(4, buf_kb) * 1024
|
||||||
|
proxy_config.pool_size = max(0, pool_size)
|
||||||
|
|
||||||
buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])
|
log.info("Starting proxy on %s:%d ...", host, port)
|
||||||
pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])
|
|
||||||
tg_ws_proxy._RECV_BUF = max(4, buf_kb) * 1024
|
|
||||||
tg_ws_proxy._SEND_BUF = tg_ws_proxy._RECV_BUF
|
|
||||||
tg_ws_proxy._WS_POOL_SIZE = max(0, pool_size)
|
|
||||||
|
|
||||||
_proxy_thread = threading.Thread(
|
_proxy_thread = threading.Thread(
|
||||||
target=_run_proxy_thread,
|
target=_run_proxy_thread,
|
||||||
args=(port, dc_opt, verbose, host),
|
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name="proxy",
|
name="proxy",
|
||||||
)
|
)
|
||||||
|
|
@ -389,7 +437,9 @@ def _maybe_notify_update_async():
|
||||||
def _on_open_in_telegram(icon=None, item=None):
|
def _on_open_in_telegram(icon=None, item=None):
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
url = f"tg://socks?server={host}&port={port}"
|
secret = _config.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
|
|
||||||
|
url = f"tg://proxy?server={host}&port={port}&secret=dd{secret}"
|
||||||
log.info("Copying %s", url)
|
log.info("Copying %s", url)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -412,16 +462,16 @@ def _on_edit_config(icon=None, item=None):
|
||||||
|
|
||||||
|
|
||||||
def _edit_config_dialog():
|
def _edit_config_dialog():
|
||||||
if ctk is None:
|
if not _ensure_ctk_thread():
|
||||||
_show_error("customtkinter не установлен.")
|
_show_error("customtkinter не установлен.")
|
||||||
return
|
return
|
||||||
|
|
||||||
cfg = dict(_config)
|
cfg = dict(_config)
|
||||||
|
|
||||||
|
def _build(done: threading.Event):
|
||||||
theme = ctk_theme_for_platform()
|
theme = ctk_theme_for_platform()
|
||||||
w, h = CONFIG_DIALOG_SIZE
|
w, h = CONFIG_DIALOG_SIZE
|
||||||
|
root = create_ctk_toplevel(
|
||||||
root = create_ctk_root(
|
|
||||||
ctk,
|
ctk,
|
||||||
title="TG WS Proxy — Настройки",
|
title="TG WS Proxy — Настройки",
|
||||||
width=w,
|
width=w,
|
||||||
|
|
@ -432,14 +482,16 @@ def _edit_config_dialog():
|
||||||
|
|
||||||
fpx, fpy = CONFIG_DIALOG_FRAME_PAD
|
fpx, fpy = CONFIG_DIALOG_FRAME_PAD
|
||||||
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
|
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
|
||||||
|
|
||||||
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
|
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
|
||||||
|
|
||||||
widgets = install_tray_config_form(
|
widgets = install_tray_config_form(
|
||||||
ctk, scroll, theme, cfg, DEFAULT_CONFIG,
|
ctk, scroll, theme, cfg, DEFAULT_CONFIG,
|
||||||
show_autostart=False,
|
show_autostart=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _finish():
|
||||||
|
root.destroy()
|
||||||
|
done.set()
|
||||||
|
|
||||||
def on_save():
|
def on_save():
|
||||||
merged = validate_config_form(
|
merged = validate_config_form(
|
||||||
widgets, DEFAULT_CONFIG, include_autostart=False)
|
widgets, DEFAULT_CONFIG, include_autostart=False)
|
||||||
|
|
@ -451,36 +503,26 @@ def _edit_config_dialog():
|
||||||
save_config(new_cfg)
|
save_config(new_cfg)
|
||||||
_config.update(new_cfg)
|
_config.update(new_cfg)
|
||||||
log.info("Config saved: %s", new_cfg)
|
log.info("Config saved: %s", new_cfg)
|
||||||
|
|
||||||
_tray_icon.menu = _build_menu()
|
_tray_icon.menu = _build_menu()
|
||||||
|
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
|
do_restart = messagebox.askyesno(
|
||||||
if messagebox.askyesno(
|
|
||||||
"Перезапустить?",
|
"Перезапустить?",
|
||||||
"Настройки сохранены.\n\nПерезапустить прокси сейчас?",
|
"Настройки сохранены.\n\nПерезапустить прокси сейчас?",
|
||||||
parent=root,
|
parent=root)
|
||||||
):
|
_finish()
|
||||||
root.destroy()
|
if do_restart:
|
||||||
restart_proxy()
|
threading.Thread(
|
||||||
else:
|
target=restart_proxy, daemon=True).start()
|
||||||
root.destroy()
|
|
||||||
|
|
||||||
def on_cancel():
|
def on_cancel():
|
||||||
root.destroy()
|
_finish()
|
||||||
|
|
||||||
|
root.protocol("WM_DELETE_WINDOW", on_cancel)
|
||||||
install_tray_config_buttons(
|
install_tray_config_buttons(
|
||||||
ctk, footer, theme, on_save=on_save, on_cancel=on_cancel)
|
ctk, footer, theme, on_save=on_save, on_cancel=on_cancel)
|
||||||
|
|
||||||
try:
|
_ctk_run_dialog(_build)
|
||||||
root.mainloop()
|
|
||||||
finally:
|
|
||||||
import tkinter as tk
|
|
||||||
try:
|
|
||||||
if root.winfo_exists():
|
|
||||||
root.destroy()
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _on_open_logs(icon=None, item=None):
|
def _on_open_logs(icon=None, item=None):
|
||||||
|
|
@ -511,6 +553,12 @@ def _on_exit(icon=None, item=None):
|
||||||
_exiting = True
|
_exiting = True
|
||||||
log.info("User requested exit")
|
log.info("User requested exit")
|
||||||
|
|
||||||
|
if _ctk_root is not None:
|
||||||
|
try:
|
||||||
|
_ctk_root.after(0, _ctk_root.quit)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _force_exit():
|
def _force_exit():
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
@ -525,18 +573,18 @@ def _show_first_run():
|
||||||
_ensure_dirs()
|
_ensure_dirs()
|
||||||
if FIRST_RUN_MARKER.exists():
|
if FIRST_RUN_MARKER.exists():
|
||||||
return
|
return
|
||||||
|
if not _ensure_ctk_thread():
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
|
||||||
|
|
||||||
if ctk is None:
|
|
||||||
FIRST_RUN_MARKER.touch()
|
FIRST_RUN_MARKER.touch()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
|
secret = _config.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
|
|
||||||
|
def _build(done: threading.Event):
|
||||||
theme = ctk_theme_for_platform()
|
theme = ctk_theme_for_platform()
|
||||||
w, h = FIRST_RUN_SIZE
|
w, h = FIRST_RUN_SIZE
|
||||||
|
root = create_ctk_toplevel(
|
||||||
root = create_ctk_root(
|
|
||||||
ctk,
|
ctk,
|
||||||
title="TG WS Proxy",
|
title="TG WS Proxy",
|
||||||
width=w,
|
width=w,
|
||||||
|
|
@ -548,21 +596,15 @@ def _show_first_run():
|
||||||
def on_done(open_tg: bool):
|
def on_done(open_tg: bool):
|
||||||
FIRST_RUN_MARKER.touch()
|
FIRST_RUN_MARKER.touch()
|
||||||
root.destroy()
|
root.destroy()
|
||||||
|
done.set()
|
||||||
if open_tg:
|
if open_tg:
|
||||||
_on_open_in_telegram()
|
_on_open_in_telegram()
|
||||||
|
|
||||||
populate_first_run_window(
|
populate_first_run_window(
|
||||||
ctk, root, theme, host=host, port=port, on_done=on_done)
|
ctk, root, theme, host=host, port=port, secret=secret,
|
||||||
|
on_done=on_done)
|
||||||
|
|
||||||
try:
|
_ctk_run_dialog(_build)
|
||||||
root.mainloop()
|
|
||||||
finally:
|
|
||||||
import tkinter as tk
|
|
||||||
try:
|
|
||||||
if root.winfo_exists():
|
|
||||||
root.destroy()
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _has_ipv6_enabled() -> bool:
|
def _has_ipv6_enabled() -> bool:
|
||||||
|
|
@ -617,9 +659,11 @@ def _build_menu():
|
||||||
return None
|
return None
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
|
link_host = tg_ws_proxy.get_link_host(host)
|
||||||
|
|
||||||
return pystray.Menu(
|
return pystray.Menu(
|
||||||
pystray.MenuItem(
|
pystray.MenuItem(
|
||||||
f"Открыть в Telegram ({host}:{port})", _on_open_in_telegram, default=True
|
f"Открыть в Telegram ({link_host}:{port})", _on_open_in_telegram, default=True
|
||||||
),
|
),
|
||||||
pystray.Menu.SEPARATOR,
|
pystray.Menu.SEPARATOR,
|
||||||
pystray.MenuItem("Перезапустить прокси", _on_restart),
|
pystray.MenuItem("Перезапустить прокси", _on_restart),
|
||||||
|
|
|
||||||
59
macos.py
59
macos.py
|
|
@ -30,7 +30,9 @@ except ImportError:
|
||||||
pyperclip = None
|
pyperclip = None
|
||||||
|
|
||||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
|
from proxy.tg_ws_proxy import proxy_config
|
||||||
from proxy import __version__
|
from proxy import __version__
|
||||||
|
|
||||||
from utils.default_config import default_tray_config
|
from utils.default_config import default_tray_config
|
||||||
|
|
||||||
APP_NAME = "TgWsProxy"
|
APP_NAME = "TgWsProxy"
|
||||||
|
|
@ -271,17 +273,18 @@ def _ask_yes_no_close(text: str,
|
||||||
|
|
||||||
# Proxy lifecycle
|
# Proxy lifecycle
|
||||||
|
|
||||||
def _run_proxy_thread(port: int, dc_opt: Dict[int, str], verbose: bool,
|
def _run_proxy_thread():
|
||||||
host: str = '127.0.0.1'):
|
|
||||||
global _async_stop
|
global _async_stop
|
||||||
|
|
||||||
loop = _asyncio.new_event_loop()
|
loop = _asyncio.new_event_loop()
|
||||||
_asyncio.set_event_loop(loop)
|
_asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
stop_ev = _asyncio.Event()
|
stop_ev = _asyncio.Event()
|
||||||
_async_stop = (loop, stop_ev)
|
_async_stop = (loop, stop_ev)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(
|
loop.run_until_complete(
|
||||||
tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host))
|
tg_ws_proxy._run(stop_event=stop_ev))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.error("Proxy thread crashed: %s", exc)
|
log.error("Proxy thread crashed: %s", exc)
|
||||||
if "Address already in use" in str(exc):
|
if "Address already in use" in str(exc):
|
||||||
|
|
@ -304,27 +307,29 @@ def start_proxy():
|
||||||
cfg = _config
|
cfg = _config
|
||||||
port = cfg.get("port", DEFAULT_CONFIG["port"])
|
port = cfg.get("port", DEFAULT_CONFIG["port"])
|
||||||
host = cfg.get("host", DEFAULT_CONFIG["host"])
|
host = cfg.get("host", DEFAULT_CONFIG["host"])
|
||||||
|
secret = cfg.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])
|
dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])
|
||||||
verbose = cfg.get("verbose", False)
|
buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])
|
||||||
|
pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dc_opt = tg_ws_proxy.parse_dc_ip_list(dc_ip_list)
|
dc_redirects = tg_ws_proxy.parse_dc_ip_list(dc_ip_list)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
log.error("Bad config dc_ip: %s", e)
|
log.error("Bad config dc_ip: %s", e)
|
||||||
_show_error(f"Ошибка конфигурации:\n{e}")
|
_show_error(f"Ошибка конфигурации:\n{e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
log.info("Starting proxy on %s:%d ...", host, port)
|
proxy_config.port = port
|
||||||
|
proxy_config.host = host
|
||||||
|
proxy_config.secret = secret
|
||||||
|
proxy_config.dc_redirects = dc_redirects
|
||||||
|
proxy_config.buffer_size = max(4, buf_kb) * 1024
|
||||||
|
proxy_config.pool_size = max(0, pool_size)
|
||||||
|
|
||||||
buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])
|
log.info("Starting proxy on %s:%d ...", host, port)
|
||||||
pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])
|
|
||||||
tg_ws_proxy._RECV_BUF = max(4, buf_kb) * 1024
|
|
||||||
tg_ws_proxy._SEND_BUF = tg_ws_proxy._RECV_BUF
|
|
||||||
tg_ws_proxy._WS_POOL_SIZE = max(0, pool_size)
|
|
||||||
|
|
||||||
_proxy_thread = threading.Thread(
|
_proxy_thread = threading.Thread(
|
||||||
target=_run_proxy_thread,
|
target=_run_proxy_thread,
|
||||||
args=(port, dc_opt, verbose, host),
|
|
||||||
daemon=True, name="proxy")
|
daemon=True, name="proxy")
|
||||||
_proxy_thread.start()
|
_proxy_thread.start()
|
||||||
|
|
||||||
|
|
@ -352,7 +357,9 @@ def restart_proxy():
|
||||||
def _on_open_in_telegram(_=None):
|
def _on_open_in_telegram(_=None):
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
url = f"tg://socks?server={host}&port={port}"
|
secret = _config.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
|
|
||||||
|
url = f"tg://proxy?server={host}&port={port}&secret=dd{secret}"
|
||||||
log.info("Opening %s", url)
|
log.info("Opening %s", url)
|
||||||
try:
|
try:
|
||||||
result = subprocess.call(['open', url])
|
result = subprocess.call(['open', url])
|
||||||
|
|
@ -502,6 +509,17 @@ def _edit_config_dialog():
|
||||||
_show_error("Порт должен быть числом 1-65535")
|
_show_error("Порт должен быть числом 1-65535")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Secret
|
||||||
|
secret_str = _osascript_input(
|
||||||
|
"MTProto Secret (32 hex символа):",
|
||||||
|
cfg.get("secret", DEFAULT_CONFIG["secret"]))
|
||||||
|
if secret_str is None:
|
||||||
|
return
|
||||||
|
secret_str = secret_str.strip().lower()
|
||||||
|
if len(secret_str) != 32 or not all(c in "0123456789abcdef" for c in secret_str):
|
||||||
|
_show_error("Secret должен быть строкой из 32 шестнадцатеричных символов.")
|
||||||
|
return
|
||||||
|
|
||||||
# DC-IP mappings
|
# DC-IP mappings
|
||||||
dc_default = ", ".join(cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"]))
|
dc_default = ", ".join(cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"]))
|
||||||
dc_str = _osascript_input(
|
dc_str = _osascript_input(
|
||||||
|
|
@ -548,6 +566,7 @@ def _edit_config_dialog():
|
||||||
new_cfg = {
|
new_cfg = {
|
||||||
"host": host,
|
"host": host,
|
||||||
"port": port,
|
"port": port,
|
||||||
|
"secret": secret_str,
|
||||||
"dc_ip": dc_lines,
|
"dc_ip": dc_lines,
|
||||||
"verbose": verbose,
|
"verbose": verbose,
|
||||||
"buf_kb": adv.get("buf_kb", cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])),
|
"buf_kb": adv.get("buf_kb", cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])),
|
||||||
|
|
@ -576,7 +595,9 @@ def _show_first_run():
|
||||||
|
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
tg_url = f"tg://socks?server={host}&port={port}"
|
secret = _config.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
|
|
||||||
|
tg_url = f"tg://proxy?server={host}&port={port}&secret=dd{secret}"
|
||||||
|
|
||||||
text = (
|
text = (
|
||||||
f"Прокси запущен и работает в строке меню.\n\n"
|
f"Прокси запущен и работает в строке меню.\n\n"
|
||||||
|
|
@ -586,7 +607,8 @@ def _show_first_run():
|
||||||
f" Или ссылка: {tg_url}\n\n"
|
f" Или ссылка: {tg_url}\n\n"
|
||||||
f"Вручную:\n"
|
f"Вручную:\n"
|
||||||
f" Настройки → Продвинутые → Тип подключения → Прокси\n"
|
f" Настройки → Продвинутые → Тип подключения → Прокси\n"
|
||||||
f" SOCKS5 → {host} : {port} (без логина/пароля)\n\n"
|
f" MTProto → {host} : {port} \n"
|
||||||
|
f" Secret: dd{secret} \n\n"
|
||||||
f"Открыть прокси в Telegram сейчас?"
|
f"Открыть прокси в Telegram сейчас?"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -646,9 +668,10 @@ class TgWsProxyApp(_TgWsProxyAppBase):
|
||||||
|
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
|
link_host = tg_ws_proxy.get_link_host(host)
|
||||||
|
|
||||||
self._open_tg_item = rumps.MenuItem(
|
self._open_tg_item = rumps.MenuItem(
|
||||||
f"Открыть в Telegram ({host}:{port})",
|
f"Открыть в Telegram ({link_host}:{port})",
|
||||||
callback=_on_open_in_telegram)
|
callback=_on_open_in_telegram)
|
||||||
self._restart_item = rumps.MenuItem(
|
self._restart_item = rumps.MenuItem(
|
||||||
"Перезапустить прокси",
|
"Перезапустить прокси",
|
||||||
|
|
@ -690,8 +713,10 @@ class TgWsProxyApp(_TgWsProxyAppBase):
|
||||||
def update_menu_title(self):
|
def update_menu_title(self):
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
|
link_host = tg_ws_proxy.get_link_host(host)
|
||||||
|
|
||||||
self._open_tg_item.title = (
|
self._open_tg_item.title = (
|
||||||
f"Открыть в Telegram ({host}:{port})")
|
f"Открыть в Telegram ({link_host}:{port})")
|
||||||
|
|
||||||
|
|
||||||
def run_menubar():
|
def run_menubar():
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -22,7 +22,7 @@ keywords = [
|
||||||
"proxy",
|
"proxy",
|
||||||
"bypass",
|
"bypass",
|
||||||
"websocket",
|
"websocket",
|
||||||
"socks5",
|
"mtproto",
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,30 @@ def create_ctk_root(
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
def create_ctk_toplevel(
|
||||||
|
ctk: Any,
|
||||||
|
*,
|
||||||
|
title: str,
|
||||||
|
width: int,
|
||||||
|
height: int,
|
||||||
|
theme: CtkTheme,
|
||||||
|
topmost: bool = True,
|
||||||
|
after_create: Optional[Callable[[Any], None]] = None,
|
||||||
|
) -> Any:
|
||||||
|
root = ctk.CTkToplevel()
|
||||||
|
root.title(title)
|
||||||
|
root.resizable(False, False)
|
||||||
|
center_ctk_geometry(root, width, height)
|
||||||
|
root.configure(fg_color=theme.bg)
|
||||||
|
if topmost:
|
||||||
|
root.attributes("-topmost", True)
|
||||||
|
root.lift()
|
||||||
|
root.focus_force()
|
||||||
|
if after_create:
|
||||||
|
after_create(root)
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
def main_content_frame(
|
def main_content_frame(
|
||||||
ctk: Any,
|
ctk: Any,
|
||||||
root: Any,
|
root: Any,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||||
|
|
@ -22,13 +23,16 @@ from ui.ctk_tooltip import attach_ctk_tooltip, attach_tooltip_to_widgets
|
||||||
|
|
||||||
# Подсказки для формы настроек (новые пользователи)
|
# Подсказки для формы настроек (новые пользователи)
|
||||||
_TIP_HOST = (
|
_TIP_HOST = (
|
||||||
"Адрес, на котором прокси принимает SOCKS5-подключения.\n"
|
"Адрес, на котором прокси принимает подключения.\n"
|
||||||
"Обычно 127.0.0.1 — локальная сеть, 0.0.0.0 - все интерфейсы"
|
"Обычно 127.0.0.1 — локальная сеть, 0.0.0.0 - все интерфейсы"
|
||||||
)
|
)
|
||||||
_TIP_PORT = (
|
_TIP_PORT = (
|
||||||
"Порт SOCKS5. В Telegram Desktop в настройках прокси должен быть "
|
"Порт прокси. В Telegram Desktop в настройках прокси должен быть "
|
||||||
"указан тот же порт"
|
"указан тот же порт"
|
||||||
)
|
)
|
||||||
|
_TIP_SECRET = (
|
||||||
|
"Секретный ключ для авторизации клиентов\n"
|
||||||
|
)
|
||||||
_TIP_DC = (
|
_TIP_DC = (
|
||||||
"Соответствие номера датацентра Telegram (DC) и IP-адреса сервера.\n"
|
"Соответствие номера датацентра Telegram (DC) и IP-адреса сервера.\n"
|
||||||
"Каждая строка: «номер:IP», например 2:149.154.167.220. "
|
"Каждая строка: «номер:IP», например 2:149.154.167.220. "
|
||||||
|
|
@ -120,6 +124,7 @@ def _config_section(
|
||||||
class TrayConfigFormWidgets:
|
class TrayConfigFormWidgets:
|
||||||
host_var: Any
|
host_var: Any
|
||||||
port_var: Any
|
port_var: Any
|
||||||
|
secret_var: Any
|
||||||
dc_textbox: Any
|
dc_textbox: Any
|
||||||
verbose_var: Any
|
verbose_var: Any
|
||||||
adv_entries: List[Any]
|
adv_entries: List[Any]
|
||||||
|
|
@ -158,7 +163,7 @@ def install_tray_config_form(
|
||||||
|
|
||||||
inner_w = _CONFIG_FORM_INNER_WIDTH
|
inner_w = _CONFIG_FORM_INNER_WIDTH
|
||||||
|
|
||||||
conn = _config_section(ctk, frame, theme, "Подключение SOCKS5")
|
conn = _config_section(ctk, frame, theme, "Подключение MTProto")
|
||||||
|
|
||||||
host_row = ctk.CTkFrame(conn, fg_color="transparent")
|
host_row = ctk.CTkFrame(conn, fg_color="transparent")
|
||||||
host_row.pack(fill="x")
|
host_row.pack(fill="x")
|
||||||
|
|
@ -215,6 +220,57 @@ def install_tray_config_form(
|
||||||
port_entry.pack(anchor="w")
|
port_entry.pack(anchor="w")
|
||||||
attach_tooltip_to_widgets([port_lbl, port_entry, port_col], _TIP_PORT)
|
attach_tooltip_to_widgets([port_lbl, port_entry, port_col], _TIP_PORT)
|
||||||
|
|
||||||
|
secret_row = ctk.CTkFrame(conn, fg_color="transparent")
|
||||||
|
secret_row.pack(fill="x")
|
||||||
|
|
||||||
|
secret_col = ctk.CTkFrame(secret_row, fg_color="transparent")
|
||||||
|
secret_col.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
||||||
|
secret_lbl = ctk.CTkLabel(
|
||||||
|
secret_col,
|
||||||
|
text="Secret",
|
||||||
|
font=(theme.ui_font_family, 12),
|
||||||
|
text_color=theme.text_secondary,
|
||||||
|
anchor="w",
|
||||||
|
)
|
||||||
|
secret_lbl.pack(anchor="w", pady=(0, 2))
|
||||||
|
secret_var = ctk.StringVar(value=cfg.get("secret", default_config["secret"]))
|
||||||
|
secret_entry = ctk.CTkEntry(
|
||||||
|
secret_col,
|
||||||
|
textvariable=secret_var,
|
||||||
|
width=160,
|
||||||
|
height=36,
|
||||||
|
font=(theme.ui_font_family, 13),
|
||||||
|
corner_radius=10,
|
||||||
|
fg_color=theme.bg,
|
||||||
|
border_color=theme.field_border,
|
||||||
|
border_width=1,
|
||||||
|
text_color=theme.text_primary,
|
||||||
|
)
|
||||||
|
secret_entry.pack(fill="x", pady=(0, 0))
|
||||||
|
attach_tooltip_to_widgets([secret_lbl, secret_entry, secret_col], _TIP_SECRET)
|
||||||
|
|
||||||
|
regen_col = ctk.CTkFrame(secret_row, fg_color="transparent")
|
||||||
|
regen_col.pack(side="left", anchor="s")
|
||||||
|
ctk.CTkLabel(
|
||||||
|
regen_col,
|
||||||
|
text="",
|
||||||
|
font=(theme.ui_font_family, 12),
|
||||||
|
).pack(pady=(0, 2))
|
||||||
|
ctk.CTkButton(
|
||||||
|
regen_col,
|
||||||
|
text="↺",
|
||||||
|
width=36,
|
||||||
|
height=36,
|
||||||
|
font=(theme.ui_font_family, 18),
|
||||||
|
corner_radius=10,
|
||||||
|
fg_color=theme.tg_blue,
|
||||||
|
hover_color=theme.tg_blue_hover,
|
||||||
|
text_color="#ffffff",
|
||||||
|
border_width=1,
|
||||||
|
border_color=theme.field_border,
|
||||||
|
command=lambda: secret_var.set(os.urandom(16).hex()),
|
||||||
|
).pack()
|
||||||
|
|
||||||
dc_inner = _config_section(ctk, frame, theme, "Датацентры Telegram (DC → IP)")
|
dc_inner = _config_section(ctk, frame, theme, "Датацентры Telegram (DC → IP)")
|
||||||
dc_lbl = ctk.CTkLabel(
|
dc_lbl = ctk.CTkLabel(
|
||||||
dc_inner,
|
dc_inner,
|
||||||
|
|
@ -395,6 +451,7 @@ def install_tray_config_form(
|
||||||
return TrayConfigFormWidgets(
|
return TrayConfigFormWidgets(
|
||||||
host_var=host_var,
|
host_var=host_var,
|
||||||
port_var=port_var,
|
port_var=port_var,
|
||||||
|
secret_var=secret_var,
|
||||||
dc_textbox=dc_textbox,
|
dc_textbox=dc_textbox,
|
||||||
verbose_var=verbose_var,
|
verbose_var=verbose_var,
|
||||||
adv_entries=adv_entries,
|
adv_entries=adv_entries,
|
||||||
|
|
@ -459,6 +516,7 @@ def validate_config_form(
|
||||||
new_cfg: Dict[str, Any] = {
|
new_cfg: Dict[str, Any] = {
|
||||||
"host": host_val,
|
"host": host_val,
|
||||||
"port": port_val,
|
"port": port_val,
|
||||||
|
"secret": widgets.secret_var.get().strip(),
|
||||||
"dc_ip": lines,
|
"dc_ip": lines,
|
||||||
"verbose": widgets.verbose_var.get(),
|
"verbose": widgets.verbose_var.get(),
|
||||||
}
|
}
|
||||||
|
|
@ -517,12 +575,13 @@ def populate_first_run_window(
|
||||||
*,
|
*,
|
||||||
host: str,
|
host: str,
|
||||||
port: int,
|
port: int,
|
||||||
|
secret: str,
|
||||||
on_done: Callable[[bool], None],
|
on_done: Callable[[bool], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Содержимое окна первого запуска. on_done(open_in_telegram) — по «Начать» и по закрытию окна.
|
Содержимое окна первого запуска. on_done(open_in_telegram) — по «Начать» и по закрытию окна.
|
||||||
"""
|
"""
|
||||||
tg_url = f"tg://socks?server={host}&port={port}"
|
tg_url = f"tg://proxy?server={host}&port={port}&secret=dd{secret}"
|
||||||
fpx, fpy = FIRST_RUN_FRAME_PAD
|
fpx, fpy = FIRST_RUN_FRAME_PAD
|
||||||
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
|
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
|
||||||
|
|
||||||
|
|
@ -544,7 +603,8 @@ def populate_first_run_window(
|
||||||
(f" Или ссылка: {tg_url}", False),
|
(f" Или ссылка: {tg_url}", False),
|
||||||
("\n Вручную:", True),
|
("\n Вручную:", True),
|
||||||
(" Настройки → Продвинутые → Тип подключения → Прокси", False),
|
(" Настройки → Продвинутые → Тип подключения → Прокси", False),
|
||||||
(f" SOCKS5 → {host} : {port} (без логина/пароля)", False),
|
(f" MTProto → {host} : {port}", False),
|
||||||
|
(f" Secret: dd{secret}", False),
|
||||||
]
|
]
|
||||||
|
|
||||||
for text, bold in sections:
|
for text, bold in sections:
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
_TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
_TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
||||||
"port": 1080,
|
"port": 1443,
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
"dc_ip": ["2:149.154.167.220", "4:149.154.167.220"],
|
||||||
"verbose": False,
|
"verbose": False,
|
||||||
|
|
@ -22,6 +23,9 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
||||||
def default_tray_config() -> Dict[str, Any]:
|
def default_tray_config() -> Dict[str, Any]:
|
||||||
"""Новая копия конфига по умолчанию для текущей ОС."""
|
"""Новая копия конфига по умолчанию для текущей ОС."""
|
||||||
cfg = dict(_TRAY_DEFAULTS_COMMON)
|
cfg = dict(_TRAY_DEFAULTS_COMMON)
|
||||||
|
cfg["secret"] = os.urandom(16).hex()
|
||||||
|
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
cfg["autostart"] = False
|
cfg["autostart"] = False
|
||||||
|
|
||||||
return cfg
|
return cfg
|
||||||
|
|
|
||||||
167
windows.py
167
windows.py
|
|
@ -37,7 +37,9 @@ except ImportError:
|
||||||
Image = ImageDraw = ImageFont = None
|
Image = ImageDraw = ImageFont = None
|
||||||
|
|
||||||
import proxy.tg_ws_proxy as tg_ws_proxy
|
import proxy.tg_ws_proxy as tg_ws_proxy
|
||||||
|
from proxy.tg_ws_proxy import proxy_config
|
||||||
from proxy import __version__
|
from proxy import __version__
|
||||||
|
|
||||||
from utils.default_config import default_tray_config
|
from utils.default_config import default_tray_config
|
||||||
from ui.ctk_tray_ui import (
|
from ui.ctk_tray_ui import (
|
||||||
install_tray_config_buttons,
|
install_tray_config_buttons,
|
||||||
|
|
@ -50,7 +52,7 @@ from ui.ctk_theme import (
|
||||||
CONFIG_DIALOG_FRAME_PAD,
|
CONFIG_DIALOG_FRAME_PAD,
|
||||||
CONFIG_DIALOG_SIZE,
|
CONFIG_DIALOG_SIZE,
|
||||||
FIRST_RUN_SIZE,
|
FIRST_RUN_SIZE,
|
||||||
create_ctk_root,
|
create_ctk_toplevel,
|
||||||
ctk_theme_for_platform,
|
ctk_theme_for_platform,
|
||||||
main_content_frame,
|
main_content_frame,
|
||||||
)
|
)
|
||||||
|
|
@ -76,6 +78,9 @@ _config: dict = {}
|
||||||
_exiting: bool = False
|
_exiting: bool = False
|
||||||
_lock_file_path: Optional[Path] = None
|
_lock_file_path: Optional[Path] = None
|
||||||
|
|
||||||
|
_ctk_root = None
|
||||||
|
_ctk_root_ready = threading.Event()
|
||||||
|
|
||||||
log = logging.getLogger("tg-ws-tray")
|
log = logging.getLogger("tg-ws-tray")
|
||||||
|
|
||||||
_user32 = ctypes.windll.user32
|
_user32 = ctypes.windll.user32
|
||||||
|
|
@ -312,17 +317,18 @@ def _load_icon():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _run_proxy_thread(port: int, dc_opt: Dict[int, str], verbose: bool,
|
def _run_proxy_thread():
|
||||||
host: str = '127.0.0.1'):
|
|
||||||
global _async_stop
|
global _async_stop
|
||||||
|
|
||||||
loop = _asyncio.new_event_loop()
|
loop = _asyncio.new_event_loop()
|
||||||
_asyncio.set_event_loop(loop)
|
_asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
stop_ev = _asyncio.Event()
|
stop_ev = _asyncio.Event()
|
||||||
_async_stop = (loop, stop_ev)
|
_async_stop = (loop, stop_ev)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(
|
loop.run_until_complete(
|
||||||
tg_ws_proxy._run(port, dc_opt, stop_event=stop_ev, host=host))
|
tg_ws_proxy._run(stop_event=stop_ev))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.error("Proxy thread crashed: %s", exc)
|
log.error("Proxy thread crashed: %s", exc)
|
||||||
if "10048" in str(exc) or "Address already in use" in str(exc):
|
if "10048" in str(exc) or "Address already in use" in str(exc):
|
||||||
|
|
@ -341,27 +347,29 @@ def start_proxy():
|
||||||
cfg = _config
|
cfg = _config
|
||||||
port = cfg.get("port", DEFAULT_CONFIG["port"])
|
port = cfg.get("port", DEFAULT_CONFIG["port"])
|
||||||
host = cfg.get("host", DEFAULT_CONFIG["host"])
|
host = cfg.get("host", DEFAULT_CONFIG["host"])
|
||||||
|
secret = cfg.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])
|
dc_ip_list = cfg.get("dc_ip", DEFAULT_CONFIG["dc_ip"])
|
||||||
verbose = cfg.get("verbose", False)
|
buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])
|
||||||
|
pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dc_opt = tg_ws_proxy.parse_dc_ip_list(dc_ip_list)
|
dc_redirects = tg_ws_proxy.parse_dc_ip_list(dc_ip_list)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
log.error("Bad config dc_ip: %s", e)
|
log.error("Bad config dc_ip: %s", e)
|
||||||
_show_error(f"Ошибка конфигурации:\n{e}")
|
_show_error(f"Ошибка конфигурации:\n{e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
log.info("Starting proxy on %s:%d ...", host, port)
|
proxy_config.port = port
|
||||||
|
proxy_config.host = host
|
||||||
|
proxy_config.secret = secret
|
||||||
|
proxy_config.dc_redirects = dc_redirects
|
||||||
|
proxy_config.buffer_size = max(4, buf_kb) * 1024
|
||||||
|
proxy_config.pool_size = max(0, pool_size)
|
||||||
|
|
||||||
buf_kb = cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])
|
log.info("Starting proxy on %s:%d ...", host, port)
|
||||||
pool_size = cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])
|
|
||||||
tg_ws_proxy._RECV_BUF = max(4, buf_kb) * 1024
|
|
||||||
tg_ws_proxy._SEND_BUF = tg_ws_proxy._RECV_BUF
|
|
||||||
tg_ws_proxy._WS_POOL_SIZE = max(0, pool_size)
|
|
||||||
|
|
||||||
_proxy_thread = threading.Thread(
|
_proxy_thread = threading.Thread(
|
||||||
target=_run_proxy_thread,
|
target=_run_proxy_thread,
|
||||||
args=(port, dc_opt, verbose, host),
|
|
||||||
daemon=True, name="proxy")
|
daemon=True, name="proxy")
|
||||||
_proxy_thread.start()
|
_proxy_thread.start()
|
||||||
|
|
||||||
|
|
@ -440,10 +448,55 @@ def _maybe_notify_update_async():
|
||||||
threading.Thread(target=_work, daemon=True, name="update-check").start()
|
threading.Thread(target=_work, daemon=True, name="update-check").start()
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_ctk_thread() -> bool:
|
||||||
|
"""Start the persistent hidden CTk root in its own thread (once)."""
|
||||||
|
global _ctk_root
|
||||||
|
if ctk is None:
|
||||||
|
return False
|
||||||
|
if _ctk_root_ready.is_set():
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _run():
|
||||||
|
global _ctk_root
|
||||||
|
from ui.ctk_theme import (
|
||||||
|
apply_ctk_appearance,
|
||||||
|
_install_tkinter_variable_del_guard,
|
||||||
|
)
|
||||||
|
_install_tkinter_variable_del_guard()
|
||||||
|
apply_ctk_appearance(ctk)
|
||||||
|
_ctk_root = ctk.CTk()
|
||||||
|
_ctk_root.withdraw()
|
||||||
|
_ctk_root_ready.set()
|
||||||
|
_ctk_root.mainloop()
|
||||||
|
|
||||||
|
threading.Thread(target=_run, daemon=True, name="ctk-root").start()
|
||||||
|
_ctk_root_ready.wait(timeout=5.0)
|
||||||
|
return _ctk_root is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _ctk_run_dialog(build_fn) -> None:
|
||||||
|
"""Schedule build_fn(done_event) on the CTk thread and block until done_event is set."""
|
||||||
|
if _ctk_root is None:
|
||||||
|
return
|
||||||
|
done = threading.Event()
|
||||||
|
|
||||||
|
def _invoke():
|
||||||
|
try:
|
||||||
|
build_fn(done)
|
||||||
|
except Exception:
|
||||||
|
log.exception("CTk dialog failed")
|
||||||
|
done.set()
|
||||||
|
|
||||||
|
_ctk_root.after(0, _invoke)
|
||||||
|
done.wait()
|
||||||
|
|
||||||
|
|
||||||
def _on_open_in_telegram(icon=None, item=None):
|
def _on_open_in_telegram(icon=None, item=None):
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
url = f"tg://socks?server={host}&port={port}"
|
secret = _config.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
|
|
||||||
|
url = f"tg://proxy?server={host}&port={port}&secret=dd{secret}"
|
||||||
log.info("Opening %s", url)
|
log.info("Opening %s", url)
|
||||||
try:
|
try:
|
||||||
result = webbrowser.open(url)
|
result = webbrowser.open(url)
|
||||||
|
|
@ -476,26 +529,23 @@ def _on_edit_config(icon=None, item=None):
|
||||||
|
|
||||||
|
|
||||||
def _edit_config_dialog():
|
def _edit_config_dialog():
|
||||||
if ctk is None:
|
if not _ensure_ctk_thread():
|
||||||
_show_error("customtkinter не установлен.")
|
_show_error("customtkinter не установлен.")
|
||||||
return
|
return
|
||||||
|
|
||||||
cfg = dict(_config)
|
cfg = dict(_config)
|
||||||
cfg["autostart"] = is_autostart_enabled()
|
cfg["autostart"] = is_autostart_enabled()
|
||||||
|
|
||||||
# Make sure that the autostart key is removed if autostart
|
|
||||||
# is disabled, even if the executable file is moved.
|
|
||||||
if _supports_autostart() and not cfg["autostart"]:
|
if _supports_autostart() and not cfg["autostart"]:
|
||||||
set_autostart_enabled(False)
|
set_autostart_enabled(False)
|
||||||
|
|
||||||
|
def _build(done: threading.Event):
|
||||||
theme = ctk_theme_for_platform()
|
theme = ctk_theme_for_platform()
|
||||||
w, h = CONFIG_DIALOG_SIZE
|
w, h = CONFIG_DIALOG_SIZE
|
||||||
if _supports_autostart():
|
if _supports_autostart():
|
||||||
h += 100
|
h += 100
|
||||||
|
|
||||||
icon_path = str(Path(__file__).parent / "icon.ico")
|
icon_path = str(Path(__file__).parent / "icon.ico")
|
||||||
|
root = create_ctk_toplevel(
|
||||||
root = create_ctk_root(
|
|
||||||
ctk,
|
ctk,
|
||||||
title="TG WS Proxy — Настройки",
|
title="TG WS Proxy — Настройки",
|
||||||
width=w,
|
width=w,
|
||||||
|
|
@ -506,9 +556,7 @@ def _edit_config_dialog():
|
||||||
|
|
||||||
fpx, fpy = CONFIG_DIALOG_FRAME_PAD
|
fpx, fpy = CONFIG_DIALOG_FRAME_PAD
|
||||||
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
|
frame = main_content_frame(ctk, root, theme, padx=fpx, pady=fpy)
|
||||||
|
|
||||||
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
|
scroll, footer = tray_settings_scroll_and_footer(ctk, frame, theme)
|
||||||
|
|
||||||
widgets = install_tray_config_form(
|
widgets = install_tray_config_form(
|
||||||
ctk,
|
ctk,
|
||||||
scroll,
|
scroll,
|
||||||
|
|
@ -519,6 +567,10 @@ def _edit_config_dialog():
|
||||||
autostart_value=cfg.get("autostart", False),
|
autostart_value=cfg.get("autostart", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _finish():
|
||||||
|
root.destroy()
|
||||||
|
done.set()
|
||||||
|
|
||||||
def on_save():
|
def on_save():
|
||||||
merged = validate_config_form(
|
merged = validate_config_form(
|
||||||
widgets,
|
widgets,
|
||||||
|
|
@ -533,39 +585,28 @@ def _edit_config_dialog():
|
||||||
save_config(new_cfg)
|
save_config(new_cfg)
|
||||||
_config.update(new_cfg)
|
_config.update(new_cfg)
|
||||||
log.info("Config saved: %s", new_cfg)
|
log.info("Config saved: %s", new_cfg)
|
||||||
|
|
||||||
if _supports_autostart():
|
if _supports_autostart():
|
||||||
set_autostart_enabled(bool(new_cfg.get("autostart", False)))
|
set_autostart_enabled(bool(new_cfg.get("autostart", False)))
|
||||||
|
|
||||||
_tray_icon.menu = _build_menu()
|
_tray_icon.menu = _build_menu()
|
||||||
|
|
||||||
# Win32 MessageBox из того же потока, что и mainloop CTk, блокирует обработку Tcl/Tk
|
|
||||||
# и даёт зависание; tkinter.messagebox согласован с циклом окна.
|
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
if messagebox.askyesno("Перезапустить?",
|
do_restart = messagebox.askyesno(
|
||||||
"Настройки сохранены.\n\n"
|
"Перезапустить?",
|
||||||
"Перезапустить прокси сейчас?",
|
"Настройки сохранены.\n\nПерезапустить прокси сейчас?",
|
||||||
parent=root):
|
parent=root)
|
||||||
root.destroy()
|
_finish()
|
||||||
restart_proxy()
|
if do_restart:
|
||||||
else:
|
threading.Thread(
|
||||||
root.destroy()
|
target=restart_proxy, daemon=True).start()
|
||||||
|
|
||||||
def on_cancel():
|
def on_cancel():
|
||||||
root.destroy()
|
_finish()
|
||||||
|
|
||||||
|
root.protocol("WM_DELETE_WINDOW", on_cancel)
|
||||||
install_tray_config_buttons(
|
install_tray_config_buttons(
|
||||||
ctk, footer, theme, on_save=on_save, on_cancel=on_cancel)
|
ctk, footer, theme, on_save=on_save, on_cancel=on_cancel)
|
||||||
|
|
||||||
try:
|
_ctk_run_dialog(_build)
|
||||||
root.mainloop()
|
|
||||||
finally:
|
|
||||||
import tkinter as tk
|
|
||||||
try:
|
|
||||||
if root.winfo_exists():
|
|
||||||
root.destroy()
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _on_open_logs(icon=None, item=None):
|
def _on_open_logs(icon=None, item=None):
|
||||||
|
|
@ -584,6 +625,12 @@ def _on_exit(icon=None, item=None):
|
||||||
_exiting = True
|
_exiting = True
|
||||||
log.info("User requested exit")
|
log.info("User requested exit")
|
||||||
|
|
||||||
|
if _ctk_root is not None:
|
||||||
|
try:
|
||||||
|
_ctk_root.after(0, _ctk_root.quit)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _force_exit():
|
def _force_exit():
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
@ -593,23 +640,23 @@ def _on_exit(icon=None, item=None):
|
||||||
icon.stop()
|
icon.stop()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _show_first_run():
|
def _show_first_run():
|
||||||
_ensure_dirs()
|
_ensure_dirs()
|
||||||
if FIRST_RUN_MARKER.exists():
|
if FIRST_RUN_MARKER.exists():
|
||||||
return
|
return
|
||||||
|
if not _ensure_ctk_thread():
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
|
||||||
|
|
||||||
if ctk is None:
|
|
||||||
FIRST_RUN_MARKER.touch()
|
FIRST_RUN_MARKER.touch()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
|
secret = _config.get("secret", DEFAULT_CONFIG["secret"])
|
||||||
|
|
||||||
|
def _build(done: threading.Event):
|
||||||
theme = ctk_theme_for_platform()
|
theme = ctk_theme_for_platform()
|
||||||
icon_path = str(Path(__file__).parent / "icon.ico")
|
icon_path = str(Path(__file__).parent / "icon.ico")
|
||||||
w, h = FIRST_RUN_SIZE
|
w, h = FIRST_RUN_SIZE
|
||||||
root = create_ctk_root(
|
root = create_ctk_toplevel(
|
||||||
ctk,
|
ctk,
|
||||||
title="TG WS Proxy",
|
title="TG WS Proxy",
|
||||||
width=w,
|
width=w,
|
||||||
|
|
@ -621,21 +668,15 @@ def _show_first_run():
|
||||||
def on_done(open_tg: bool):
|
def on_done(open_tg: bool):
|
||||||
FIRST_RUN_MARKER.touch()
|
FIRST_RUN_MARKER.touch()
|
||||||
root.destroy()
|
root.destroy()
|
||||||
|
done.set()
|
||||||
if open_tg:
|
if open_tg:
|
||||||
_on_open_in_telegram()
|
_on_open_in_telegram()
|
||||||
|
|
||||||
populate_first_run_window(
|
populate_first_run_window(
|
||||||
ctk, root, theme, host=host, port=port, on_done=on_done)
|
ctk, root, theme, host=host, port=port, secret=secret,
|
||||||
|
on_done=on_done)
|
||||||
|
|
||||||
try:
|
_ctk_run_dialog(_build)
|
||||||
root.mainloop()
|
|
||||||
finally:
|
|
||||||
import tkinter as tk
|
|
||||||
try:
|
|
||||||
if root.winfo_exists():
|
|
||||||
root.destroy()
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _has_ipv6_enabled() -> bool:
|
def _has_ipv6_enabled() -> bool:
|
||||||
|
|
@ -695,9 +736,11 @@ def _build_menu():
|
||||||
return None
|
return None
|
||||||
host = _config.get("host", DEFAULT_CONFIG["host"])
|
host = _config.get("host", DEFAULT_CONFIG["host"])
|
||||||
port = _config.get("port", DEFAULT_CONFIG["port"])
|
port = _config.get("port", DEFAULT_CONFIG["port"])
|
||||||
|
link_host = tg_ws_proxy.get_link_host(host)
|
||||||
|
|
||||||
return pystray.Menu(
|
return pystray.Menu(
|
||||||
pystray.MenuItem(
|
pystray.MenuItem(
|
||||||
f"Открыть в Telegram ({host}:{port})",
|
f"Открыть в Telegram ({link_host}:{port})",
|
||||||
_on_open_in_telegram,
|
_on_open_in_telegram,
|
||||||
default=True),
|
default=True),
|
||||||
pystray.Menu.SEPARATOR,
|
pystray.Menu.SEPARATOR,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue