mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-06-18 04:28:27 +03:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2afd80825b | ||
|
|
12fafbc8f4 | ||
|
|
5839ca2564 | ||
|
|
e40c571009 | ||
|
|
96e5b4b639 | ||
|
|
13d2b1db6d | ||
|
|
a29a1a8610 | ||
|
|
94010f1481 | ||
|
|
42172235c7 | ||
|
|
b0010af130 | ||
|
|
784a7f659b | ||
|
|
21fe672963 | ||
|
|
ed46ecce5a | ||
|
|
9562b11101 | ||
|
|
dfdb993da5 |
5
.github/cfproxy-domains.txt
vendored
5
.github/cfproxy-domains.txt
vendored
@@ -8,3 +8,8 @@ clngqrflngqin.com
|
||||
tjacxbqtj.com
|
||||
bxaxtxmrw.com
|
||||
dmohrsgmohcrwb.com
|
||||
vwbmtmoi.com
|
||||
khgrre.com
|
||||
ulihssf.com
|
||||
tmhqsdqmfpmk.com
|
||||
xwuwoqbm.com
|
||||
|
||||
83
.github/workflows/build.yml
vendored
83
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 []
|
||||
@@ -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` |
|
||||
|
||||
Пример с ручным указанием секрета:
|
||||
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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)
|
||||
|
||||
11
macos.py
11
macos.py
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
@@ -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"]
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
57
utils/diagnostics.py
Normal 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"
|
||||
"Измените порт на случайный в диапазоне 10000–50000 в настройках, "
|
||||
"проверьте брандмауэр/антивирус и перезапустите."
|
||||
)
|
||||
|
||||
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
39
utils/logging_setup.py
Normal 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",
|
||||
)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user