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
|
tjacxbqtj.com
|
||||||
bxaxtxmrw.com
|
bxaxtxmrw.com
|
||||||
dmohrsgmohcrwb.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
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-windows:
|
build-windows-x64:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -73,9 +73,85 @@ jobs:
|
|||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: TgWsProxy
|
name: TgWsProxy-windows-x64
|
||||||
path: dist/TgWsProxy_windows.exe
|
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:
|
build-win7:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
strategy:
|
strategy:
|
||||||
@@ -439,7 +515,7 @@ jobs:
|
|||||||
dist/TgWsProxy_linux_amd64.rpm
|
dist/TgWsProxy_linux_amd64.rpm
|
||||||
|
|
||||||
release:
|
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
|
runs-on: ubuntu-latest
|
||||||
if: ${{ github.event.inputs.make_release == 'true' }}
|
if: ${{ github.event.inputs.make_release == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
@@ -463,6 +539,7 @@ jobs:
|
|||||||
> Добавьте `185.199.109.133 release-assets.githubusercontent.com` в hosts или воспользуйтесь зеркалом: https://sourceforge.net/projects/tg-ws-proxy.mirror/files/
|
> Добавьте `185.199.109.133 release-assets.githubusercontent.com` в hosts или воспользуйтесь зеркалом: https://sourceforge.net/projects/tg-ws-proxy.mirror/files/
|
||||||
files: |
|
files: |
|
||||||
dist/TgWsProxy_windows.exe
|
dist/TgWsProxy_windows.exe
|
||||||
|
dist/TgWsProxy_windows_arm64.exe
|
||||||
dist/TgWsProxy_windows_7_64bit.exe
|
dist/TgWsProxy_windows_7_64bit.exe
|
||||||
dist/TgWsProxy_windows_7_32bit.exe
|
dist/TgWsProxy_windows_7_32bit.exe
|
||||||
dist/TgWsProxy_macos_universal.dmg
|
dist/TgWsProxy_macos_universal.dmg
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
TG_WS_PROXY_HOST=0.0.0.0 \
|
TG_WS_PROXY_HOST=0.0.0.0 \
|
||||||
TG_WS_PROXY_PORT=1443 \
|
TG_WS_PROXY_PORT=1443 \
|
||||||
TG_WS_PROXY_SECRET="" \
|
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 \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends tini ca-certificates \
|
&& apt-get install -y --no-install-recommends tini ca-certificates \
|
||||||
@@ -42,5 +43,5 @@ USER app
|
|||||||
|
|
||||||
EXPOSE 1443/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; 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 []
|
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_HOST` | `Адрес для приёма подключений` | `0.0.0.0` |
|
||||||
| `TG_WS_PROXY_PORT` | Порт внутри контейнера | `1443` |
|
| `TG_WS_PROXY_PORT` | `Порт внутри контейнера` | `1443` |
|
||||||
| `TG_WS_PROXY_SECRET` | Секретный ключ | `random` |
|
| `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_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)
|
- [Fake TLS + upstream в Nginx](./FakeTlsNginx.md)
|
||||||
- [Файлы конфигурации Tray-приложения](./TrayConfig.md)
|
- [Файлы конфигурации Tray-приложения](./TrayConfig.md)
|
||||||
- [Установка из исходников](./BuildFromSource.md)
|
- [Установка из исходников](./BuildFromSource.md)
|
||||||
- [Руководство для контрибьюторов](../CONTRIBUTING.md)
|
- [Руководство для контрибьюторов](./CONTRIBUTING.md)
|
||||||
|
|
||||||
## Windows: быстрый вход
|
## Windows: быстрый вход
|
||||||
|
|
||||||
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте:
|
Перейдите на [страницу релизов](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_64bit.exe` (Windows 7 x64)
|
||||||
- `TgWsProxy_windows_7_32bit.exe` (Windows 7 x32)
|
- `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 (x64) для `TgWsProxy_windows_7_64bit.exe`
|
||||||
- Windows 7 (x32) для `TgWsProxy_windows_7_32bit.exe`
|
- Windows 7 (x32) для `TgWsProxy_windows_7_32bit.exe`
|
||||||
- Intel macOS 10.15+
|
- Intel macOS 10.15+
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте:
|
Перейдите на [страницу релизов](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_64bit.exe` (Windows 7 x64)
|
||||||
- `TgWsProxy_windows_7_32bit.exe` (Windows 7 x32)
|
- `TgWsProxy_windows_7_32bit.exe` (Windows 7 x32)
|
||||||
|
|
||||||
@@ -42,6 +43,10 @@
|
|||||||
- **Порт:** `1443` (или переопределенный вами)
|
- **Порт:** `1443` (или переопределенный вами)
|
||||||
- **Secret:** из настроек или логов
|
- **Secret:** из настроек или логов
|
||||||
|
|
||||||
|
## Портативный режим
|
||||||
|
Портативный режим автоматически включается, если рядом с исполняемым файлом есть папка с названием `TgWsProxy_data`.
|
||||||
|
Либо можно принудительно включить портативный режим (который сам создаст папку), запустив исполняемый файл с параметром `--portable`.
|
||||||
|
|
||||||
## Установка из исходников
|
## Установка из исходников
|
||||||
|
|
||||||
Подробная инструкция: [BuildFromSource.md](./BuildFromSource.md)
|
Подробная инструкция: [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_FILE, acquire_lock, apply_proxy_config, ensure_dirs, load_config,
|
||||||
log, release_lock, save_config, setup_logging, stop_proxy, tg_proxy_url,
|
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"
|
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))
|
loop.run_until_complete(_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):
|
msg, _ = diagnose_listen_error(exc)
|
||||||
_show_error(
|
if msg:
|
||||||
"Не удалось запустить прокси:\n"
|
_show_error(msg)
|
||||||
"Порт уже используется другим приложением.\n\n"
|
|
||||||
"Закройте приложение, использующее этот порт, "
|
|
||||||
"или измените порт в настройках прокси и перезапустите."
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
loop.close()
|
loop.close()
|
||||||
_async_stop = None
|
_async_stop = None
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
||||||
VSVersionInfo(
|
VSVersionInfo(
|
||||||
ffi=FixedFileInfo(
|
ffi=FixedFileInfo(
|
||||||
filevers=(1, 7, 1, 0),
|
filevers=(1, 7, 3, 0),
|
||||||
prodvers=(1, 7, 1, 0),
|
prodvers=(1, 7, 3, 0),
|
||||||
mask=0x3f,
|
mask=0x3f,
|
||||||
flags=0x0,
|
flags=0x0,
|
||||||
OS=0x40004,
|
OS=0x40004,
|
||||||
@@ -21,12 +21,12 @@ VSVersionInfo(
|
|||||||
[
|
[
|
||||||
StringStruct(u'CompanyName', u'Flowseal'),
|
StringStruct(u'CompanyName', u'Flowseal'),
|
||||||
StringStruct(u'FileDescription', u'Telegram Desktop WebSocket Bridge Proxy'),
|
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'InternalName', u'TgWsProxy'),
|
||||||
StringStruct(u'LegalCopyright', u'Copyright (c) Flowseal. MIT License.'),
|
StringStruct(u'LegalCopyright', u'Copyright (c) Flowseal. MIT License.'),
|
||||||
StringStruct(u'OriginalFilename', u'TgWsProxy.exe'),
|
StringStruct(u'OriginalFilename', u'TgWsProxy.exe'),
|
||||||
StringStruct(u'ProductName', u'TG WS Proxy'),
|
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 .config import parse_dc_ip_list, proxy_config, coerce_domain_list
|
||||||
from .utils import get_link_host, build_github_opener
|
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"]
|
__all__ = ["__version__", "get_link_host", "proxy_config", "parse_dc_ip_list", "build_github_opener", "coerce_domain_list"]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
import random
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
@@ -179,6 +180,8 @@ async def _cfproxy_worker_fallback(reader, writer, relay_init, label,
|
|||||||
worker_domains = proxy_config.cfproxy_worker_domains
|
worker_domains = proxy_config.cfproxy_worker_domains
|
||||||
if not worker_domains:
|
if not worker_domains:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
random.shuffle(worker_domains)
|
||||||
|
|
||||||
for worker_domain in worker_domains:
|
for worker_domain in worker_domains:
|
||||||
ws = await cf_worker_pool.get(dc, worker_domain, fallback_dst)
|
ws = await cf_worker_pool.get(dc, worker_domain, fallback_dst)
|
||||||
@@ -263,6 +266,26 @@ async def _tcp_fallback(reader, writer, dst, port, relay_init, label, ctx: Crypt
|
|||||||
return True
|
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,
|
async def bridge_ws_reencrypt(reader, writer, ws: RawWebSocket, label,
|
||||||
ctx: CryptoCtx,
|
ctx: CryptoCtx,
|
||||||
dc=None, is_media=False,
|
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()),
|
tasks = [asyncio.create_task(tcp_to_ws()),
|
||||||
asyncio.create_task(ws_to_tcp())]
|
asyncio.create_task(ws_to_tcp())]
|
||||||
|
keepalive = asyncio.create_task(
|
||||||
|
_ws_keepalive(ws, proxy_config.ws_keepalive_interval))
|
||||||
try:
|
try:
|
||||||
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
||||||
finally:
|
finally:
|
||||||
|
keepalive.cancel()
|
||||||
for t in tasks:
|
for t in tasks:
|
||||||
t.cancel()
|
t.cancel()
|
||||||
for t in tasks:
|
for t in (*tasks, keepalive):
|
||||||
try:
|
try:
|
||||||
await t
|
await t
|
||||||
except BaseException:
|
except BaseException:
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ _CFPROXY_ENC: List[str] = [
|
|||||||
'clngqrflngqin.com',
|
'clngqrflngqin.com',
|
||||||
'tjacxbqtj.com',
|
'tjacxbqtj.com',
|
||||||
'bxaxtxmrw.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))
|
_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)
|
cfproxy_worker_domains: List[str] = field(default_factory=list)
|
||||||
fake_tls_domain: str = ''
|
fake_tls_domain: str = ''
|
||||||
proxy_protocol: bool = False
|
proxy_protocol: bool = False
|
||||||
|
ws_keepalive_interval: float = 30.0
|
||||||
|
|
||||||
|
|
||||||
proxy_config = ProxyConfig()
|
proxy_config = ProxyConfig()
|
||||||
@@ -199,4 +205,4 @@ def parse_dc_ip_list(dc_ip_list: List[str]) -> Dict[int, str]:
|
|||||||
except (ValueError, OSError):
|
except (ValueError, OSError):
|
||||||
raise ValueError(f"Invalid --dc-ip {entry!r}")
|
raise ValueError(f"Invalid --dc-ip {entry!r}")
|
||||||
dc_redirects[dc_n] = ip_s
|
dc_redirects[dc_n] = ip_s
|
||||||
return dc_redirects
|
return dc_redirects
|
||||||
|
|||||||
@@ -154,6 +154,13 @@ class RawWebSocket:
|
|||||||
self._build_frame(self.OP_BINARY, part, mask=True))
|
self._build_frame(self.OP_BINARY, part, mask=True))
|
||||||
await self.writer.drain()
|
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]:
|
async def recv(self) -> Optional[bytes]:
|
||||||
while not self._closed:
|
while not self._closed:
|
||||||
opcode, payload = await self._read_frame()
|
opcode, payload = await self._read_frame()
|
||||||
|
|||||||
@@ -568,8 +568,9 @@ def main():
|
|||||||
help='Log to file with rotation (default: stderr only)')
|
help='Log to file with rotation (default: stderr only)')
|
||||||
ap.add_argument('--log-max-mb', type=float, default=5, metavar='MB',
|
ap.add_argument('--log-max-mb', type=float, default=5, metavar='MB',
|
||||||
help='Max log file size in MB before rotation (default 5)')
|
help='Max log file size in MB before rotation (default 5)')
|
||||||
ap.add_argument('--log-backups', type=int, default=0, metavar='N',
|
ap.add_argument('--log-backups', type=int, default=1, metavar='N',
|
||||||
help='Number of rotated log files to keep (default 0)')
|
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',
|
ap.add_argument('--buf-kb', type=int, default=256, metavar='KB',
|
||||||
help='Socket send/recv buffer size in KB (default 256)')
|
help='Socket send/recv buffer size in KB (default 256)')
|
||||||
ap.add_argument('--pool-size', type=int, default=4, metavar='N',
|
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',
|
ap.add_argument('--proxy-protocol', action='store_true',
|
||||||
help='Accept PROXY protocol v1 header '
|
help='Accept PROXY protocol v1 header '
|
||||||
'(for use behind nginx/haproxy with proxy_protocol on)')
|
'(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()
|
args = ap.parse_args()
|
||||||
|
|
||||||
if not args.dc_ip:
|
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.cfproxy_worker_domains = coerce_domain_list(args.cfproxy_worker_domain)
|
||||||
proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
|
proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
|
||||||
proxy_config.proxy_protocol = args.proxy_protocol
|
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_level = logging.DEBUG if args.verbose else logging.INFO
|
||||||
log_fmt = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s',
|
log_fmt = logging.Formatter('%(asctime)s %(levelname)-5s %(message)s',
|
||||||
@@ -640,11 +646,11 @@ def main():
|
|||||||
root.addHandler(console)
|
root.addHandler(console)
|
||||||
|
|
||||||
if args.log_file:
|
if args.log_file:
|
||||||
fh = logging.handlers.RotatingFileHandler(
|
from utils.logging_setup import build_log_handler
|
||||||
|
fh = build_log_handler(
|
||||||
args.log_file,
|
args.log_file,
|
||||||
maxBytes=max(32 * 1024, int(args.log_max_mb * 1024 * 1024)),
|
log_max_mb=args.log_max_mb,
|
||||||
backupCount=max(0, args.log_backups),
|
backups=args.log_backups,
|
||||||
encoding='utf-8',
|
|
||||||
)
|
)
|
||||||
fh.setFormatter(log_fmt)
|
fh.setFormatter(log_fmt)
|
||||||
root.addHandler(fh)
|
root.addHandler(fh)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
|||||||
"cfproxy": True,
|
"cfproxy": True,
|
||||||
"cfproxy_user_domain": [],
|
"cfproxy_user_domain": [],
|
||||||
"cfproxy_worker_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 asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import socket as _socket
|
import socket as _socket
|
||||||
import sys
|
import sys
|
||||||
import threading
|
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 import __version__, get_link_host, parse_dc_ip_list, proxy_config, coerce_domain_list
|
||||||
from proxy.tg_ws_proxy import _run
|
from proxy.tg_ws_proxy import _run
|
||||||
from utils.default_config import default_tray_config
|
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")
|
log = logging.getLogger("tg-ws-tray")
|
||||||
|
|
||||||
APP_NAME = "TgWsProxy"
|
APP_NAME = "TgWsProxy"
|
||||||
|
PORTABLE_DIR_NAME = "TgWsProxy_data"
|
||||||
|
|
||||||
|
|
||||||
def _app_dir() -> Path:
|
def _standard_app_dir() -> Path:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
return Path(os.environ.get("APPDATA", Path.home())) / APP_NAME
|
return Path(os.environ.get("APPDATA", Path.home())) / APP_NAME
|
||||||
if sys.platform == "darwin":
|
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
|
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()
|
APP_DIR = _app_dir()
|
||||||
CONFIG_FILE = APP_DIR / "config.json"
|
CONFIG_FILE = APP_DIR / "config.json"
|
||||||
LOG_FILE = APP_DIR / "proxy.log"
|
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)
|
root.setLevel(level)
|
||||||
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
logging.getLogger('asyncio').setLevel(logging.WARNING)
|
||||||
|
|
||||||
fh = logging.handlers.RotatingFileHandler(
|
fh = build_log_handler(str(LOG_FILE), log_max_mb=log_max_mb, backups=1)
|
||||||
str(LOG_FILE),
|
|
||||||
maxBytes=max(32 * 1024, int(log_max_mb * 1024 * 1024)),
|
|
||||||
backupCount=0,
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
fh.setLevel(logging.DEBUG)
|
fh.setLevel(logging.DEBUG)
|
||||||
fh.setFormatter(logging.Formatter(_LOG_FMT_FILE, datefmt="%Y-%m-%d %H:%M:%S"))
|
fh.setFormatter(logging.Formatter(_LOG_FMT_FILE, datefmt="%Y-%m-%d %H:%M:%S"))
|
||||||
root.addHandler(fh)
|
root.addHandler(fh)
|
||||||
@@ -231,7 +284,7 @@ _proxy_thread: Optional[threading.Thread] = None
|
|||||||
_async_stop: Optional[Tuple[asyncio.AbstractEventLoop, asyncio.Event]] = 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
|
global _async_stop
|
||||||
|
|
||||||
loop = asyncio.new_event_loop()
|
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))
|
loop.run_until_complete(_run(stop_event=stop_ev))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.error("Proxy thread crashed: %s", repr(exc))
|
log.error("Proxy thread crashed: %s", repr(exc))
|
||||||
if "Address already in use" in str(exc) or "10048" in str(exc):
|
msg, diagnose_called = diagnose_listen_error(exc)
|
||||||
on_port_busy(
|
if msg:
|
||||||
"Не удалось запустить прокси:\n"
|
show_error(msg)
|
||||||
"Порт уже используется другим приложением.\n\n"
|
if diagnose_called:
|
||||||
"Закройте приложение, использующее этот порт, "
|
diagnose_called()
|
||||||
"или измените порт в настройках прокси и перезапустите."
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
loop.close()
|
loop.close()
|
||||||
_async_stop = None
|
_async_stop = None
|
||||||
@@ -273,6 +324,7 @@ def apply_proxy_config(cfg: dict) -> bool:
|
|||||||
pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"])
|
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_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.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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest
|
||||||
@@ -37,13 +36,8 @@ _state: Dict[str, Any] = {
|
|||||||
|
|
||||||
def _cache_file() -> Optional[Path]:
|
def _cache_file() -> Optional[Path]:
|
||||||
try:
|
try:
|
||||||
if sys.platform == "win32":
|
from utils.tray_common import APP_DIR
|
||||||
root = Path(os.environ.get("APPDATA", str(Path.home()))) / "TgWsProxy"
|
root = APP_DIR
|
||||||
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"
|
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
return root / ".update_check_cache.json"
|
return root / ".update_check_cache.json"
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -258,19 +252,29 @@ def get_update_asset(exe_path: Path) -> Optional[Tuple[str, str]]:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback
|
# Fallback
|
||||||
|
import platform
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
is_64 = struct.calcsize("P") * 8 == 64
|
is_64 = struct.calcsize("P") * 8 == 64
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
is_arm64 = machine in ("arm64", "aarch64")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
is_modern = sys.getwindowsversion().major >= 10
|
is_modern = sys.getwindowsversion().major >= 10
|
||||||
except Exception:
|
except Exception:
|
||||||
is_modern = True
|
is_modern = True
|
||||||
if is_modern:
|
|
||||||
|
if is_arm64:
|
||||||
|
name = "TgWsProxy_windows_arm64.exe"
|
||||||
|
elif is_modern:
|
||||||
name = "TgWsProxy_windows.exe"
|
name = "TgWsProxy_windows.exe"
|
||||||
elif is_64:
|
elif is_64:
|
||||||
name = "TgWsProxy_windows_7_64bit.exe"
|
name = "TgWsProxy_windows_7_64bit.exe"
|
||||||
else:
|
else:
|
||||||
name = "TgWsProxy_windows_7_32bit.exe"
|
name = "TgWsProxy_windows_7_32bit.exe"
|
||||||
|
|
||||||
for a in assets:
|
for a in assets:
|
||||||
if a.get("name") == name:
|
if a.get("name") == name:
|
||||||
return a["url"], a["name"]
|
return a["url"], a["name"]
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user