15 Commits
v1.7.1 ... main

Author SHA1 Message Date
Flowseal
2afd80825b docs update 2026-06-17 11:33:20 +03:00
Flowseal
12fafbc8f4 Version bump 2026-06-17 11:05:17 +03:00
Flowseal
5839ca2564 Portable mode 2026-06-17 11:02:04 +03:00
Flowseal
e40c571009 #646 fixes 2026-06-17 10:25:45 +03:00
Konukhov Yaroslav
96e5b4b639 fix: add WebSocket keepalive pings to prevent idle disconnects (#646) (#925) 2026-06-17 10:13:55 +03:00
Flowseal
13d2b1db6d #943 fixes 2026-06-17 09:54:53 +03:00
Yan
a29a1a8610 Добавлена сборка Windows ARM64 в Build & Release workflow (#943) 2026-06-17 09:50:32 +03:00
partoftheworlD
94010f1481 Added support for cf worker for docker container (#996) 2026-06-17 09:45:25 +03:00
Kenyka Kenykovich
42172235c7 Исправлен fallback список CF proxy доменов (добавлена запятая) (#958) 2026-06-17 09:44:00 +03:00
Flowseal
b0010af130 #924 improvements 2026-06-17 09:43:06 +03:00
Konukhov Yaroslav
784a7f659b fix: diagnose permission and bad-address bind failures on startup (#924) 2026-06-17 09:24:44 +03:00
Konukhov Yaroslav
21fe672963 fix: rotate log files instead of growing without bound (#885) (#932) 2026-06-17 09:13:32 +03:00
Flowseal
ed46ecce5a version bump 2026-06-03 17:14:12 +03:00
Flowseal
9562b11101 docs 2026-06-03 17:13:47 +03:00
Flowseal
dfdb993da5 shuffle cfworker domains 2026-06-03 17:09:16 +03:00
19 changed files with 348 additions and 62 deletions

View File

@@ -8,3 +8,8 @@ clngqrflngqin.com
tjacxbqtj.com
bxaxtxmrw.com
dmohrsgmohcrwb.com
vwbmtmoi.com
khgrre.com
ulihssf.com
tmhqsdqmfpmk.com
xwuwoqbm.com

View File

@@ -17,7 +17,7 @@ permissions:
contents: write
jobs:
build-windows:
build-windows-x64:
runs-on: windows-latest
steps:
- name: Checkout
@@ -73,9 +73,85 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: TgWsProxy
name: TgWsProxy-windows-x64
path: dist/TgWsProxy_windows.exe
build-windows-arm64:
runs-on: windows-11-arm
env:
CRYPTOGRAPHY_VERSION: "46.0.5"
ARM64_WHEELHOUSE: wheelhouse-arm64
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
architecture: arm64
cache: "pip"
- name: Restore ARM64 cryptography wheel
id: cryptography-wheel-cache
uses: actions/cache@v4
with:
path: ${{ env.ARM64_WHEELHOUSE }}
key: windows-arm64-py311-cryptography-${{ env.CRYPTOGRAPHY_VERSION }}-${{ hashFiles('pyproject.toml', '.github/workflows/build.yml') }}
- name: Install ARM64 OpenSSL
if: steps.cryptography-wheel-cache.outputs.cache-hit != 'true'
shell: pwsh
run: |
vcpkg install openssl:arm64-windows-static
$opensslDir = "$env:VCPKG_INSTALLATION_ROOT\installed\arm64-windows-static"
"OPENSSL_DIR=$opensslDir" >> $env:GITHUB_ENV
"OPENSSL_STATIC=1" >> $env:GITHUB_ENV
"VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" >> $env:GITHUB_ENV
- name: Build ARM64 cryptography wheel
if: steps.cryptography-wheel-cache.outputs.cache-hit != 'true'
run: |
mkdir $env:ARM64_WHEELHOUSE
pip wheel --no-deps --wheel-dir $env:ARM64_WHEELHOUSE "cryptography==$env:CRYPTOGRAPHY_VERSION"
- name: Install dependencies & pyinstaller
run: pip install --find-links $env:ARM64_WHEELHOUSE . "pyinstaller==6.13.0"
- name: Build EXE with PyInstaller
run: pyinstaller packaging/windows.spec --noconfirm
- name: Strip Rich PE header
shell: bash
run: |
python -c "
import struct, pathlib
exe = pathlib.Path('dist/TgWsProxy.exe')
data = bytearray(exe.read_bytes())
rich = data.find(b'Rich')
if rich == -1:
print('Rich header not found, skipping')
raise SystemExit(0)
ck = struct.unpack_from('<I', data, rich + 4)[0]
dans = struct.pack('<I', 0x536E6144 ^ ck)
ds = data.find(dans)
if ds == -1:
print('DanS marker not found, skipping')
raise SystemExit(0)
data[ds:rich + 8] = b'\x00' * (rich + 8 - ds)
exe.write_bytes(data)
print(f'Stripped Rich header: offset {ds}..{rich+8}')
"
- name: Rename artifact
run: mv dist/TgWsProxy.exe dist/TgWsProxy_windows_arm64.exe
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: TgWsProxy-windows-arm64
path: dist/TgWsProxy_windows_arm64.exe
build-win7:
runs-on: windows-latest
strategy:
@@ -439,7 +515,7 @@ jobs:
dist/TgWsProxy_linux_amd64.rpm
release:
needs: [build-windows, build-win7, build-macos, build-linux]
needs: [build-windows-x64, build-windows-arm64, build-win7, build-macos, build-linux]
runs-on: ubuntu-latest
if: ${{ github.event.inputs.make_release == 'true' }}
steps:
@@ -463,6 +539,7 @@ jobs:
> Добавьте `185.199.109.133 release-assets.githubusercontent.com` в hosts или воспользуйтесь зеркалом: https://sourceforge.net/projects/tg-ws-proxy.mirror/files/
files: |
dist/TgWsProxy_windows.exe
dist/TgWsProxy_windows_arm64.exe
dist/TgWsProxy_windows_7_64bit.exe
dist/TgWsProxy_windows_7_32bit.exe
dist/TgWsProxy_macos_universal.dmg

View File

@@ -25,7 +25,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
TG_WS_PROXY_HOST=0.0.0.0 \
TG_WS_PROXY_PORT=1443 \
TG_WS_PROXY_SECRET="" \
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" \
TG_WS_PROXY_CF_WORKER=""
RUN apt-get update \
&& apt-get install -y --no-install-recommends tini ca-certificates \
@@ -42,5 +43,5 @@ USER app
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; if [ -n \"${TG_WS_PROXY_SECRET}\" ]; then args=\"$args --secret ${TG_WS_PROXY_SECRET}\"; fi; exec /opt/venv/bin/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; if [ -n \"${TG_WS_PROXY_SECRET}\" ]; then args=\"$args --secret ${TG_WS_PROXY_SECRET}\"; fi; if [ -n \"${TG_WS_PROXY_CF_WORKER}\" ]; then args=\"$args --cfproxy-worker-domain ${TG_WS_PROXY_CF_WORKER}\"; fi; exec /opt/venv/bin/python -u proxy/tg_ws_proxy.py $args \"$@\"", "--"]
CMD []

View File

@@ -35,12 +35,13 @@ tg://proxy?server=172.17.0.2&port=1443&secret=dd68f127db1d...
Все настройки задаются переменными окружения при запуске контейнера:
| Переменная | Описание | По умолчанию |
|-----------------------|------------------------------------------------|--------------------------------------|
| `TG_WS_PROXY_HOST` | Адрес для приёма подключений | `0.0.0.0` |
| `TG_WS_PROXY_PORT` | Порт внутри контейнера | `1443` |
| `TG_WS_PROXY_SECRET` | Секретный ключ | `random` |
| `TG_WS_PROXY_DC_IPS` | Пары «номер DC:IP» через пробел | `2:149.154.167.220 4:149.154.167.220`|
| Переменная | Описание | По умолчанию |
| ----------------------- | --------------------------------- | ------------------------------------- |
| `TG_WS_PROXY_HOST` | `Адрес для приёма подключений` | `0.0.0.0` |
| `TG_WS_PROXY_PORT` | `Порт внутри контейнера` | `1443` |
| `TG_WS_PROXY_SECRET` | `Секретный ключ` | `random` |
| `TG_WS_PROXY_DC_IPS` | `Пары «номер DC:IP» через пробел` | `2:149.154.167.220 4:149.154.167.220` |
| `TG_WS_PROXY_CF_WORKER` | `Домен Cloudflare Worker` | `None` |
Пример с ручным указанием секрета:

View File

@@ -49,13 +49,14 @@
- [Fake TLS + upstream в Nginx](./FakeTlsNginx.md)
- [Файлы конфигурации Tray-приложения](./TrayConfig.md)
- [Установка из исходников](./BuildFromSource.md)
- [Руководство для контрибьюторов](../CONTRIBUTING.md)
- [Руководство для контрибьюторов](./CONTRIBUTING.md)
## Windows: быстрый вход
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте:
- `TgWsProxy_windows.exe` (Windows 10+)
- `TgWsProxy_windows.exe` (Windows 10+ x64)
- `TgWsProxy_windows_arm64.exe` (Windows 10+ ARM64)
- `TgWsProxy_windows_7_64bit.exe` (Windows 7 x64)
- `TgWsProxy_windows_7_32bit.exe` (Windows 7 x32)
@@ -116,7 +117,8 @@ Telegram Desktop → MTProto Proxy (127.0.0.1:1443) → WebSocket → Telegram D
Минимально поддерживаемые версии ОС для текущих бинарных сборок:
- Windows 10+ для `TgWsProxy_windows.exe`
- Windows 10+ x64 для `TgWsProxy_windows.exe`
- Windows 10+ ARM64 для `TgWsProxy_windows_arm64.exe`
- Windows 7 (x64) для `TgWsProxy_windows_7_64bit.exe`
- Windows 7 (x32) для `TgWsProxy_windows_7_32bit.exe`
- Intel macOS 10.15+

View File

@@ -2,7 +2,8 @@
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте:
- `TgWsProxy_windows.exe` (Windows 10+)
- `TgWsProxy_windows.exe` (Windows 10+ x64)
- `TgWsProxy_windows_arm64.exe` (Windows 10+ ARM64)
- `TgWsProxy_windows_7_64bit.exe` (Windows 7 x64)
- `TgWsProxy_windows_7_32bit.exe` (Windows 7 x32)
@@ -42,6 +43,10 @@
- **Порт:** `1443` (или переопределенный вами)
- **Secret:** из настроек или логов
## Портативный режим
Портативный режим автоматически включается, если рядом с исполняемым файлом есть папка с названием `TgWsProxy_data`.
Либо можно принудительно включить портативный режим (который сам создаст папку), запустив исполняемый файл с параметром `--portable`.
## Установка из исходников
Подробная инструкция: [BuildFromSource.md](./BuildFromSource.md)

View File

@@ -32,6 +32,7 @@ from utils.tray_common import (
LOG_FILE, acquire_lock, apply_proxy_config, ensure_dirs, load_config,
log, release_lock, save_config, setup_logging, stop_proxy, tg_proxy_url,
)
from utils.diagnostics import diagnose_listen_error
MENUBAR_ICON_PATH = APP_DIR / "menubar_icon.png"
@@ -184,13 +185,9 @@ def _run_proxy_thread() -> None:
loop.run_until_complete(_run(stop_event=stop_ev))
except Exception as exc:
log.error("Proxy thread crashed: %s", exc)
if "Address already in use" in str(exc):
_show_error(
"Не удалось запустить прокси:\n"
"Порт уже используется другим приложением.\n\n"
"Закройте приложение, использующее этот порт, "
"или измените порт в настройках прокси и перезапустите."
)
msg, _ = diagnose_listen_error(exc)
if msg:
_show_error(msg)
finally:
loop.close()
_async_stop = None

View File

@@ -4,8 +4,8 @@
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
filevers=(1, 7, 1, 0),
prodvers=(1, 7, 1, 0),
filevers=(1, 7, 3, 0),
prodvers=(1, 7, 3, 0),
mask=0x3f,
flags=0x0,
OS=0x40004,
@@ -21,12 +21,12 @@ VSVersionInfo(
[
StringStruct(u'CompanyName', u'Flowseal'),
StringStruct(u'FileDescription', u'Telegram Desktop WebSocket Bridge Proxy'),
StringStruct(u'FileVersion', u'1.7.1.0'),
StringStruct(u'FileVersion', u'1.7.3.0'),
StringStruct(u'InternalName', u'TgWsProxy'),
StringStruct(u'LegalCopyright', u'Copyright (c) Flowseal. MIT License.'),
StringStruct(u'OriginalFilename', u'TgWsProxy.exe'),
StringStruct(u'ProductName', u'TG WS Proxy'),
StringStruct(u'ProductVersion', u'1.7.1.0'),
StringStruct(u'ProductVersion', u'1.7.3.0'),
]
)
]

View File

@@ -1,6 +1,6 @@
from .config import parse_dc_ip_list, proxy_config, coerce_domain_list
from .utils import get_link_host, build_github_opener
__version__ = "1.7.1"
__version__ = "1.7.3"
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list", "build_github_opener", "coerce_domain_list"]

View File

@@ -1,6 +1,7 @@
import asyncio
import logging
import struct
import random
from typing import List, Optional
from urllib.parse import urlencode
@@ -180,6 +181,8 @@ async def _cfproxy_worker_fallback(reader, writer, relay_init, label,
if not worker_domains:
return False
random.shuffle(worker_domains)
for worker_domain in worker_domains:
ws = await cf_worker_pool.get(dc, worker_domain, fallback_dst)
if ws:
@@ -263,6 +266,26 @@ async def _tcp_fallback(reader, writer, dst, port, relay_init, label, ctx: Crypt
return True
async def _ws_keepalive(ws, interval: float):
"""Send periodic WS PING frames to keep the upstream flow warm.
A non-positive interval disables keepalive. The loop exits on send
failure so a dead upstream is detected promptly instead of lingering
until the next client packet (see issue #646).
"""
if interval <= 0:
return
interval = max(1.0, interval) # reasonable minimum
try:
while True:
await asyncio.sleep(interval)
await ws.send_ping()
except (asyncio.CancelledError, ConnectionError, OSError):
return
async def bridge_ws_reencrypt(reader, writer, ws: RawWebSocket, label,
ctx: CryptoCtx,
dc=None, is_media=False,
@@ -334,12 +357,15 @@ async def bridge_ws_reencrypt(reader, writer, ws: RawWebSocket, label,
tasks = [asyncio.create_task(tcp_to_ws()),
asyncio.create_task(ws_to_tcp())]
keepalive = asyncio.create_task(
_ws_keepalive(ws, proxy_config.ws_keepalive_interval))
try:
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
finally:
keepalive.cancel()
for t in tasks:
t.cancel()
for t in tasks:
for t in (*tasks, keepalive):
try:
await t
except BaseException:

View File

@@ -29,7 +29,12 @@ _CFPROXY_ENC: List[str] = [
'clngqrflngqin.com',
'tjacxbqtj.com',
'bxaxtxmrw.com',
'dmohrsgmohcrwb.com'
'dmohrsgmohcrwb.com',
'vwbmtmoi.com',
'khgrre.com',
'ulihssf.com',
'tmhqsdqmfpmk.com',
'xwuwoqbm.com'
]
_S = ''.join(chr(c) for c in (46, 99, 111, 46, 117, 107))
@@ -62,6 +67,7 @@ class ProxyConfig:
cfproxy_worker_domains: List[str] = field(default_factory=list)
fake_tls_domain: str = ''
proxy_protocol: bool = False
ws_keepalive_interval: float = 30.0
proxy_config = ProxyConfig()

View File

@@ -154,6 +154,13 @@ class RawWebSocket:
self._build_frame(self.OP_BINARY, part, mask=True))
await self.writer.drain()
async def send_ping(self, payload: bytes = b''):
if self._closed:
raise ConnectionError("WebSocket closed")
frame = self._build_frame(self.OP_PING, payload, mask=True)
self.writer.write(frame)
await self.writer.drain()
async def recv(self) -> Optional[bytes]:
while not self._closed:
opcode, payload = await self._read_frame()

View File

@@ -568,8 +568,9 @@ def main():
help='Log to file with rotation (default: stderr only)')
ap.add_argument('--log-max-mb', type=float, default=5, metavar='MB',
help='Max log file size in MB before rotation (default 5)')
ap.add_argument('--log-backups', type=int, default=0, metavar='N',
help='Number of rotated log files to keep (default 0)')
ap.add_argument('--log-backups', type=int, default=1, metavar='N',
help='Number of rotated log files to keep (min 1; '
'rotation needs at least one backup to bound size)')
ap.add_argument('--buf-kb', type=int, default=256, metavar='KB',
help='Socket send/recv buffer size in KB (default 256)')
ap.add_argument('--pool-size', type=int, default=4, metavar='N',
@@ -592,6 +593,10 @@ def main():
ap.add_argument('--proxy-protocol', action='store_true',
help='Accept PROXY protocol v1 header '
'(for use behind nginx/haproxy with proxy_protocol on)')
ap.add_argument('--ws-keepalive', type=float, default=30.0, metavar='SEC',
help='Seconds between WebSocket keepalive PINGs to the '
'upstream (default 30, 0 to disable). Keeps idle '
'sessions alive through NAT/firewall timeouts.')
args = ap.parse_args()
if not args.dc_ip:
@@ -628,6 +633,7 @@ def main():
proxy_config.cfproxy_worker_domains = coerce_domain_list(args.cfproxy_worker_domain)
proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
proxy_config.proxy_protocol = args.proxy_protocol
proxy_config.ws_keepalive_interval = max(0, args.ws_keepalive)
log_level = logging.DEBUG if args.verbose else logging.INFO
log_fmt = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s',
@@ -640,11 +646,11 @@ def main():
root.addHandler(console)
if args.log_file:
fh = logging.handlers.RotatingFileHandler(
from utils.logging_setup import build_log_handler
fh = build_log_handler(
args.log_file,
maxBytes=max(32 * 1024, int(args.log_max_mb * 1024 * 1024)),
backupCount=max(0, args.log_backups),
encoding='utf-8',
log_max_mb=args.log_max_mb,
backups=args.log_backups,
)
fh.setFormatter(log_fmt)
root.addHandler(fh)

View File

@@ -20,6 +20,7 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
"cfproxy": True,
"cfproxy_user_domain": [],
"cfproxy_worker_domain": [],
"ws_keepalive_interval": 30
}

57
utils/diagnostics.py Normal file
View File

@@ -0,0 +1,57 @@
from __future__ import annotations
import errno
import webbrowser
from typing import Optional, Tuple, Callable
MSG_PORT_BUSY = (
"Не удалось запустить прокси:\n"
"Порт уже используется другим приложением.\n\n"
"Закройте приложение, использующее этот порт, "
"или измените порт в настройках прокси и перезапустите."
)
MSG_PERMISSION = (
"Не удалось запустить прокси:\n"
"Доступ к адресу/порту запрещён "
"(брандмауэр, антивирус или права доступа).\n\n"
"Измените порт на случайный в диапазоне 1000050000 в настройках, "
"проверьте брандмауэр/антивирус и перезапустите."
)
MSG_BAD_ADDRESS = (
"Не удалось запустить прокси:\n"
"Некорректный или недоступный адрес для прослушивания.\n\n"
"Проверьте решение по открывшейся в браузере ссылке.\n"
"Проверьте host и порт в настройках прокси и перезапустите."
)
# Windows WinSock error codes (exc.winerror); errno may differ from POSIX.
_WSA_EACCES = 10013
_WSA_EFAULT = 10014
_WSA_EADDRINUSE = 10048
_WSA_EADDRNOTAVAIL = 10049
def diagnose_listen_error(exc: BaseException) -> Tuple[Optional[str], Optional[Callable]]:
"""Map a listen-socket bind failure to a user-facing message.
Returns None when the exception is not a recognizable bind failure,
so callers can fall back to generic handling.
"""
if not isinstance(exc, OSError):
return None
err = exc.errno
winerror = getattr(exc, "winerror", None)
if err == errno.EADDRINUSE or winerror == _WSA_EADDRINUSE:
return MSG_PORT_BUSY, None
if err == errno.EACCES or winerror == _WSA_EACCES:
return MSG_PERMISSION, None
if (winerror in (_WSA_EFAULT, _WSA_EADDRNOTAVAIL)
or err in (errno.EADDRNOTAVAIL, errno.EFAULT)):
return MSG_BAD_ADDRESS, lambda : webbrowser.open("https://github.com/Flowseal/tg-ws-proxy/issues/903#issuecomment-4726752103")
return None, None

39
utils/logging_setup.py Normal file
View File

@@ -0,0 +1,39 @@
"""Shared construction of the rotating log file handler.
Centralizes the rotation invariant so both the tray and the CLI log paths
behave identically and the file can never grow without bound (issue #885).
A ``RotatingFileHandler`` only rotates when ``backupCount >= 1``: CPython's
``doRollover`` skips the entire rotation block when ``backupCount == 0``, so
``maxBytes`` is silently ignored and the active file grows forever. We force
at least one backup here regardless of caller input.
"""
from __future__ import annotations
import logging.handlers
_MIN_BYTES = 32 * 1024
_MIN_BACKUPS = 1
def build_log_handler(
path: str,
log_max_mb: float = 5,
backups: int = 1,
) -> logging.handlers.RotatingFileHandler:
"""Create a RotatingFileHandler that actually rotates.
``backups`` is clamped to at least 1 so rotation is always active, and
``maxBytes`` keeps a small floor so a misconfigured tiny size can't cause
rotation on every line.
"""
max_bytes = max(_MIN_BYTES, int(log_max_mb * 1024 * 1024))
backup_count = max(_MIN_BACKUPS, int(backups))
return logging.handlers.RotatingFileHandler(
path,
maxBytes=max_bytes,
backupCount=backup_count,
encoding="utf-8",
)

View File

@@ -3,8 +3,8 @@ from __future__ import annotations
import asyncio
import json
import logging
import logging.handlers
import os
import shutil
import socket as _socket
import sys
import threading
@@ -17,13 +17,16 @@ import psutil
from proxy import __version__, get_link_host, parse_dc_ip_list, proxy_config, coerce_domain_list
from proxy.tg_ws_proxy import _run
from utils.default_config import default_tray_config
from utils.diagnostics import diagnose_listen_error
from utils.logging_setup import build_log_handler
log = logging.getLogger("tg-ws-tray")
APP_NAME = "TgWsProxy"
PORTABLE_DIR_NAME = "TgWsProxy_data"
def _app_dir() -> Path:
def _standard_app_dir() -> Path:
if sys.platform == "win32":
return Path(os.environ.get("APPDATA", Path.home())) / APP_NAME
if sys.platform == "darwin":
@@ -31,6 +34,61 @@ def _app_dir() -> Path:
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / APP_NAME
def _exe_dir() -> Optional[Path]:
try:
base = getattr(sys, "frozen", False) and sys.executable or sys.argv[0]
except Exception:
return None
if not base:
return None
p = Path(base).resolve()
return p.parent if p.is_file() else p
def _detect_portable() -> Optional[Path]:
exe_dir = _exe_dir()
if exe_dir is None:
return None
portable_dir = exe_dir / PORTABLE_DIR_NAME
if "--portable" in sys.argv:
try:
portable_dir.mkdir(parents=True, exist_ok=True)
except OSError as exc:
log.warning("Cannot create portable dir %s: %s", portable_dir, repr(exc))
return None
if portable_dir.is_dir():
_migrate_into_portable(portable_dir)
return portable_dir
return None
def _migrate_into_portable(portable_dir: Path) -> None:
try:
if any(portable_dir.iterdir()):
return
except OSError:
return
std = _standard_app_dir()
if not std.exists():
return
try:
for src in std.iterdir():
if ".log" in src.name:
continue
dst = portable_dir / src.name
try:
if not src.is_dir():
shutil.copy2(src, dst)
except OSError as exc:
log.warning("Portable migration: skip %s: %s", src.name, repr(exc))
except OSError as exc:
log.warning("Portable migration failed: %s", repr(exc))
def _app_dir() -> Path:
return _detect_portable() or _standard_app_dir()
APP_DIR = _app_dir()
CONFIG_FILE = APP_DIR / "config.json"
LOG_FILE = APP_DIR / "proxy.log"
@@ -155,12 +213,7 @@ def setup_logging(verbose: bool = False, log_max_mb: float = 5) -> None:
root.setLevel(level)
logging.getLogger('asyncio').setLevel(logging.WARNING)
fh = logging.handlers.RotatingFileHandler(
str(LOG_FILE),
maxBytes=max(32 * 1024, int(log_max_mb * 1024 * 1024)),
backupCount=0,
encoding="utf-8",
)
fh = build_log_handler(str(LOG_FILE), log_max_mb=log_max_mb, backups=1)
fh.setLevel(logging.DEBUG)
fh.setFormatter(logging.Formatter(_LOG_FMT_FILE, datefmt="%Y-%m-%d %H:%M:%S"))
root.addHandler(fh)
@@ -231,7 +284,7 @@ _proxy_thread: Optional[threading.Thread] = None
_async_stop: Optional[Tuple[asyncio.AbstractEventLoop, asyncio.Event]] = None
def _run_proxy_thread(on_port_busy: Callable[[str], None]) -> None:
def _run_proxy_thread(show_error: Callable[[str], None]) -> None:
global _async_stop
loop = asyncio.new_event_loop()
@@ -243,13 +296,11 @@ def _run_proxy_thread(on_port_busy: Callable[[str], None]) -> None:
loop.run_until_complete(_run(stop_event=stop_ev))
except Exception as exc:
log.error("Proxy thread crashed: %s", repr(exc))
if "Address already in use" in str(exc) or "10048" in str(exc):
on_port_busy(
"Не удалось запустить прокси:\n"
"Порт уже используется другим приложением.\n\n"
"Закройте приложение, использующее этот порт, "
"или измените порт в настройках прокси и перезапустите."
)
msg, diagnose_called = diagnose_listen_error(exc)
if msg:
show_error(msg)
if diagnose_called:
diagnose_called()
finally:
loop.close()
_async_stop = None
@@ -273,6 +324,7 @@ def apply_proxy_config(cfg: dict) -> bool:
pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"])
pc.cfproxy_user_domains = coerce_domain_list(cfg.get("cfproxy_user_domain", DEFAULT_CONFIG["cfproxy_user_domain"]))
pc.cfproxy_worker_domains = coerce_domain_list(cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG["cfproxy_worker_domain"]))
pc.ws_keepalive_interval = max(0, cfg.get("ws_keepalive_interval", DEFAULT_CONFIG["ws_keepalive_interval"]))
return True

View File

@@ -7,7 +7,6 @@
from __future__ import annotations
import json
import os
import sys
import time
from itertools import zip_longest
@@ -37,13 +36,8 @@ _state: Dict[str, Any] = {
def _cache_file() -> Optional[Path]:
try:
if sys.platform == "win32":
root = Path(os.environ.get("APPDATA", str(Path.home()))) / "TgWsProxy"
elif sys.platform == "darwin":
root = Path.home() / "Library/Application Support/TgWsProxy"
else:
xdg = os.environ.get("XDG_CONFIG_HOME")
root = (Path(xdg).expanduser() if xdg else Path.home() / ".config") / "TgWsProxy"
from utils.tray_common import APP_DIR
root = APP_DIR
root.mkdir(parents=True, exist_ok=True)
return root / ".update_check_cache.json"
except OSError:
@@ -258,19 +252,29 @@ def get_update_asset(exe_path: Path) -> Optional[Tuple[str, str]]:
pass
# Fallback
import platform
import struct
is_64 = struct.calcsize("P") * 8 == 64
machine = platform.machine().lower()
is_arm64 = machine in ("arm64", "aarch64")
try:
is_modern = sys.getwindowsversion().major >= 10
except Exception:
is_modern = True
if is_modern:
if is_arm64:
name = "TgWsProxy_windows_arm64.exe"
elif is_modern:
name = "TgWsProxy_windows.exe"
elif is_64:
name = "TgWsProxy_windows_7_64bit.exe"
else:
name = "TgWsProxy_windows_7_32bit.exe"
for a in assets:
if a.get("name") == name:
return a["url"], a["name"]
return None