docker-auth
This commit is contained in:
parent
4ae7cb92f7
commit
99c5340f09
|
|
@ -0,0 +1,28 @@
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
packaging/
|
||||||
|
windows.py
|
||||||
|
icon.ico
|
||||||
|
*.spec
|
||||||
|
*.spec.bak
|
||||||
|
*.manifest
|
||||||
|
*.log
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
FROM python:3.12-slim AS builder
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||||
|
PIP_NO_CACHE_DIR=1 \
|
||||||
|
VIRTUAL_ENV=/opt/venv
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends build-essential cargo libffi-dev libssl-dev \
|
||||||
|
&& python -m venv "$VIRTUAL_ENV" \
|
||||||
|
&& "$VIRTUAL_ENV/bin/pip" install --upgrade pip setuptools wheel \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN "$VIRTUAL_ENV/bin/pip" install cryptography==46.0.5
|
||||||
|
|
||||||
|
FROM python:3.12-slim AS runtime
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PATH=/opt/venv/bin:$PATH \
|
||||||
|
TG_WS_PROXY_HOST=0.0.0.0 \
|
||||||
|
TG_WS_PROXY_PORT=1080 \
|
||||||
|
TG_WS_PROXY_DC_IPS="2:149.154.167.220 4:149.154.167.220"
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends tini ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& groupadd --system app \
|
||||||
|
&& useradd --system --gid app --create-home --home-dir /home/app app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
COPY proxy ./proxy
|
||||||
|
COPY README.md LICENSE ./
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
EXPOSE 1080/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 \"$@\"", "--"]
|
||||||
|
CMD []
|
||||||
116
README.md
116
README.md
|
|
@ -1,10 +1,8 @@
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
>
|
>
|
||||||
> ### Реакция антивирусов
|
> ### Реакция антивирусов
|
||||||
>
|
|
||||||
> Windows Defender часто ошибочно помечает приложение как **Wacatac**.
|
> Windows Defender часто ошибочно помечает приложение как **Wacatac**.
|
||||||
> Если вы не можете скачать из-за блокировки, то:
|
> Если вы не можете скачать из-за блокировки, то:
|
||||||
>
|
|
||||||
> 1) Попробуйте скачать версию win7 (она ничем не отличается в плане функционала)
|
> 1) Попробуйте скачать версию win7 (она ничем не отличается в плане функционала)
|
||||||
> 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно
|
> 2) Отключите антивирус на время скачивания, добавьте файл в исключения и включите обратно
|
||||||
>
|
>
|
||||||
|
|
@ -12,110 +10,63 @@
|
||||||
|
|
||||||
# TG WS Proxy
|
# TG WS Proxy
|
||||||
|
|
||||||
**Локальный SOCKS5-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние сервера.
|
Локальный SOCKS5-прокси для Telegram Desktop, который перенаправляет трафик через WebSocket-соединения к указанным серверам, помогая частично ускорить работу Telegram.
|
||||||
|
|
||||||
|
**Ожидаемый результат аналогичен прокидыванию hosts для Web Telegram**: ускорение загрузки и скачивания файлов, загрузки сообщений и части медиа.
|
||||||
|
|
||||||
<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 → SOCKS5 (127.0.0.1:1080) → TG WS Proxy → WSS (kws*.web.telegram.org) → Telegram DC
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Приложение поднимает локальный SOCKS5-прокси на `127.0.0.1:1080`
|
1. Приложение поднимает локальный SOCKS5-прокси на `127.0.0.1:1080`
|
||||||
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 через домены `kws{N}.web.telegram.org`
|
||||||
5. Если WS недоступен (302 redirect) — автоматически переключается на прямое TCP-соединение
|
5. Если WS недоступен (302 redirect) — автоматически переключается на прямое TCP-соединение
|
||||||
|
|
||||||
## 🚀 Быстрый старт
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy.exe`**. Он собирается автоматически через [Github Actions](https://github.com/Flowseal/tg-ws-proxy/actions) из открытого исходного кода.
|
||||||
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_windows.exe`**. Он собирается автоматически через [Github Actions](https://github.com/Flowseal/tg-ws-proxy/actions) из открытого исходного кода.
|
|
||||||
|
|
||||||
При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей.
|
При первом запуске откроется окно с инструкцией по подключению Telegram Desktop. Приложение сворачивается в системный трей.
|
||||||
|
|
||||||
**Меню трея:**
|
**Меню трея:**
|
||||||
|
|
||||||
- **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку
|
- **Открыть в Telegram** — автоматически настроить прокси через `tg://socks` ссылку
|
||||||
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
- **Перезапустить прокси** — перезапуск без выхода из приложения
|
||||||
- **Настройки...** — GUI-редактор конфигурации
|
- **Настройки...** — GUI-редактор конфигурации
|
||||||
- **Открыть логи** — открыть файл логов
|
- **Открыть логи** — открыть файл логов
|
||||||
- **Выход** — остановить прокси и закрыть приложение
|
- **Выход** — остановить прокси и закрыть приложение
|
||||||
|
|
||||||
### macOS
|
|
||||||
|
|
||||||
Перейдите на [страницу релизов](https://github.com/Flowseal/tg-ws-proxy/releases) и скачайте **`TgWsProxy_macos_universal.dmg`** — универсальная сборка для Apple Silicon и Intel.
|
|
||||||
|
|
||||||
1. Открыть образ
|
|
||||||
2. Перенести **TG WS Proxy.app** в папку **Applications**
|
|
||||||
3. При первом запуске macOS может попросить подтвердить открытие: **Системные настройки → Конфиденциальность и безопасность → Всё равно открыть**
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
Для Debian/Ubuntu скачайте со [страницы релизов](https://github.com/Flowseal/tg-ws-proxy/releases) пакет **`TgWsProxy_linux_amd64.deb`**.
|
|
||||||
|
|
||||||
Для остальных дистрибутивов можно использовать **`TgWsProxy_linux_amd64`** (бинарный файл для x86_64).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
chmod +x TgWsProxy_linux_amd64
|
|
||||||
./TgWsProxy_linux_amd64
|
|
||||||
```
|
|
||||||
|
|
||||||
При первом запуске откроется окно с инструкцией. Приложение работает в системном трее (требуется AppIndicator).
|
|
||||||
|
|
||||||
## Установка из исходников
|
## Установка из исходников
|
||||||
|
|
||||||
### Консольный proxy
|
|
||||||
|
|
||||||
Для запуска только SOCKS5/WebSocket proxy без tray-интерфейса достаточно базовой установки:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -e .
|
pip install -r requirements.txt
|
||||||
tg-ws-proxy
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows 10+
|
### Windows (Tray-приложение)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -e ".[win10]"
|
python windows.py
|
||||||
tg-ws-proxy-tray-win
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Windows 7
|
### Консольный режим
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -e ".[win7]"
|
python proxy/tg_ws_proxy.py [--port PORT] [--dc-ip DC:IP ...] [-v]
|
||||||
tg-ws-proxy-tray-win
|
|
||||||
```
|
|
||||||
|
|
||||||
### macOS
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -e ".[macos]"
|
|
||||||
tg-ws-proxy-tray-macos
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -e ".[linux]"
|
|
||||||
tg-ws-proxy-tray-linux
|
|
||||||
```
|
|
||||||
|
|
||||||
### Консольный режим из исходников
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Аргументы:**
|
**Аргументы:**
|
||||||
|
|
||||||
| Аргумент | По умолчанию | Описание |
|
| Аргумент | По умолчанию | Описание |
|
||||||
|---|---|---|
|
|-------------------------------------|------------------------------------------|------------------------------------------------|
|
||||||
|
| `--host` | `127.0.0.1` | IP-адрес SOCKS5-прокси |
|
||||||
| `--port` | `1080` | Порт SOCKS5-прокси |
|
| `--port` | `1080` | Порт SOCKS5-прокси |
|
||||||
| `--host` | `127.0.0.1` | Хост SOCKS5-прокси |
|
| `-u`, `-user`<br/>`-P`,`--password` | выкл. | Логин и Пароль для авторизации в SOCKS5-прокси |
|
||||||
| `--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 (можно указать несколько раз)|
|
||||||
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
|
| `-v`, `--verbose` | выкл. | Подробное логирование (DEBUG) |
|
||||||
|
|
||||||
|
|
@ -123,27 +74,13 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Стандартный запуск
|
# Стандартный запуск
|
||||||
tg-ws-proxy
|
python proxy/tg_ws_proxy.py
|
||||||
|
|
||||||
# Другой порт и дополнительные DC
|
# Другой порт и дополнительные DC
|
||||||
tg-ws-proxy --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220
|
python proxy/tg_ws_proxy.py --port 9050 --dc-ip 1:149.154.175.205 --dc-ip 2:149.154.167.220
|
||||||
|
|
||||||
# С подробным логированием
|
# С подробным логированием
|
||||||
tg-ws-proxy -v
|
python proxy/tg_ws_proxy.py -v
|
||||||
```
|
|
||||||
|
|
||||||
## CLI-скрипты (pyproject.toml)
|
|
||||||
|
|
||||||
CLI команды объявляются в `pyproject.toml` в секции `[project.scripts]` и должны указывать на `module:function`.
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[project.scripts]
|
|
||||||
tg-ws-proxy = "proxy.tg_ws_proxy:main"
|
|
||||||
tg-ws-proxy-tray-win = "windows:main"
|
|
||||||
tg-ws-proxy-tray-macos = "macos:main"
|
|
||||||
tg-ws-proxy-tray-linux = "linux:main"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Настройка Telegram Desktop
|
## Настройка Telegram Desktop
|
||||||
|
|
@ -163,11 +100,7 @@ tg-ws-proxy-tray-linux = "linux:main"
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
Tray-приложение хранит данные в:
|
Tray-приложение хранит данные в `%APPDATA%/TgWsProxy`:
|
||||||
|
|
||||||
- **Windows:** `%APPDATA%/TgWsProxy`
|
|
||||||
- **macOS:** `~/Library/Application Support/TgWsProxy`
|
|
||||||
- **Linux:** `~/.config/TgWsProxy` (или `$XDG_CONFIG_HOME/TgWsProxy`)
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -182,15 +115,12 @@ Tray-приложение хранит данные в:
|
||||||
|
|
||||||
## Автоматическая сборка
|
## Автоматическая сборка
|
||||||
|
|
||||||
Проект содержит спецификации PyInstaller ([`packaging/windows.spec`](packaging/windows.spec), [`packaging/macos.spec`](packaging/macos.spec), [`packaging/linux.spec`](packaging/linux.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки.
|
Проект содержит спецификацию PyInstaller ([`windows.spec`](packaging/windows.spec)) и GitHub Actions workflow ([`.github/workflows/build.yml`](.github/workflows/build.yml)) для автоматической сборки.
|
||||||
|
|
||||||
Минимально поддерживаемые версии ОС для текущих бинарных сборок:
|
```bash
|
||||||
|
pip install pyinstaller
|
||||||
- Windows 10+ для `TgWsProxy_windows.exe`
|
pyinstaller packaging/windows.spec
|
||||||
- Windows 7 для `TgWsProxy_windows_7.exe`
|
```
|
||||||
- Intel macOS 10.15+
|
|
||||||
- Apple Silicon macOS 11.0+
|
|
||||||
- Linux x86_64 (требуется AppIndicator для системного трея)
|
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,14 +12,15 @@ import sys
|
||||||
import time
|
import time
|
||||||
from typing import Dict, List, Optional, Set, Tuple
|
from typing import Dict, List, Optional, Set, Tuple
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_PORT = 1080
|
DEFAULT_PORT = 1080
|
||||||
log = logging.getLogger('tg-ws-proxy')
|
log = logging.getLogger('tg-ws-proxy')
|
||||||
|
|
||||||
_TCP_NODELAY = True
|
_TCP_NODELAY = True
|
||||||
_RECV_BUF = 256 * 1024
|
_RECV_BUF = 131072
|
||||||
_SEND_BUF = 256 * 1024
|
_SEND_BUF = 131072
|
||||||
_WS_POOL_SIZE = 4
|
_WS_POOL_SIZE = 4
|
||||||
_WS_POOL_MAX_AGE = 120.0
|
_WS_POOL_MAX_AGE = 120.0
|
||||||
|
|
||||||
|
|
@ -68,13 +69,11 @@ _IP_TO_DC: Dict[str, Tuple[int, bool]] = {
|
||||||
'91.105.192.100': (203, False),
|
'91.105.192.100': (203, False),
|
||||||
}
|
}
|
||||||
|
|
||||||
# This case might work but not actually sure
|
|
||||||
_DC_OVERRIDES: Dict[int, int] = {
|
|
||||||
203: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
_dc_opt: Dict[int, Optional[str]] = {}
|
_dc_opt: Dict[int, Optional[str]] = {}
|
||||||
|
|
||||||
|
_auth_user = ''
|
||||||
|
_auth_password = ''
|
||||||
|
|
||||||
# DCs where WS is known to fail (302 redirect)
|
# DCs where WS is known to fail (302 redirect)
|
||||||
# Raw TCP fallback will be used instead
|
# Raw TCP fallback will be used instead
|
||||||
# Keyed by (dc, is_media)
|
# Keyed by (dc, is_media)
|
||||||
|
|
@ -82,8 +81,7 @@ _ws_blacklist: Set[Tuple[int, bool]] = set()
|
||||||
|
|
||||||
# Rate-limit re-attempts per (dc, is_media)
|
# Rate-limit re-attempts per (dc, is_media)
|
||||||
_dc_fail_until: Dict[Tuple[int, bool], float] = {}
|
_dc_fail_until: Dict[Tuple[int, bool], float] = {}
|
||||||
_DC_FAIL_COOLDOWN = 30.0 # seconds to keep reduced WS timeout after failure
|
_DC_FAIL_COOLDOWN = 60.0 # seconds
|
||||||
_WS_FAIL_TIMEOUT = 2.0 # quick-retry timeout after a recent WS failure
|
|
||||||
|
|
||||||
|
|
||||||
_ssl_ctx = ssl.create_default_context()
|
_ssl_ctx = ssl.create_default_context()
|
||||||
|
|
@ -383,7 +381,7 @@ def _dc_from_init(data: bytes) -> Tuple[Optional[int], bool]:
|
||||||
proto, dc_raw, plain.hex())
|
proto, dc_raw, plain.hex())
|
||||||
if proto in (0xEFEFEFEF, 0xEEEEEEEE, 0xDDDDDDDD):
|
if proto in (0xEFEFEFEF, 0xEEEEEEEE, 0xDDDDDDDD):
|
||||||
dc = abs(dc_raw)
|
dc = abs(dc_raw)
|
||||||
if 1 <= dc <= 5 or dc == 203:
|
if 1 <= dc <= 1000:
|
||||||
return dc, (dc_raw < 0)
|
return dc, (dc_raw < 0)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
log.debug("DC extraction failed: %s", exc)
|
log.debug("DC extraction failed: %s", exc)
|
||||||
|
|
@ -470,10 +468,16 @@ class _MsgSplitter:
|
||||||
|
|
||||||
|
|
||||||
def _ws_domains(dc: int, is_media) -> List[str]:
|
def _ws_domains(dc: int, is_media) -> List[str]:
|
||||||
dc = _DC_OVERRIDES.get(dc, dc)
|
"""
|
||||||
|
Return domain names to try for WebSocket connection to a DC.
|
||||||
|
|
||||||
|
DC 1-5: kws{N}[-1].web.telegram.org
|
||||||
|
DC >5: kws{N}[-1].telegram.org
|
||||||
|
"""
|
||||||
|
base = 'telegram.org' if dc > 5 else 'web.telegram.org'
|
||||||
if is_media is None or is_media:
|
if is_media is None or is_media:
|
||||||
return [f'kws{dc}-1.web.telegram.org', f'kws{dc}.web.telegram.org']
|
return [f'kws{dc}-1.{base}', f'kws{dc}.{base}']
|
||||||
return [f'kws{dc}.web.telegram.org', f'kws{dc}-1.web.telegram.org']
|
return [f'kws{dc}.{base}', f'kws{dc}-1.{base}']
|
||||||
|
|
||||||
|
|
||||||
class Stats:
|
class Stats:
|
||||||
|
|
@ -614,7 +618,7 @@ async def _bridge_ws(reader, writer, ws: RawWebSocket, label,
|
||||||
nonlocal up_bytes, up_packets
|
nonlocal up_bytes, up_packets
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
chunk = await reader.read(65536)
|
chunk = await reader.read(131072)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
_stats.bytes_up += len(chunk)
|
_stats.bytes_up += len(chunk)
|
||||||
|
|
@ -787,10 +791,53 @@ async def _handle_client(reader, writer):
|
||||||
log.debug("[%s] not SOCKS5 (ver=%d)", label, hdr[0])
|
log.debug("[%s] not SOCKS5 (ver=%d)", label, hdr[0])
|
||||||
writer.close()
|
writer.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Auth check
|
||||||
nmethods = hdr[1]
|
nmethods = hdr[1]
|
||||||
await reader.readexactly(nmethods)
|
auth_methods = await reader.readexactly(nmethods)
|
||||||
|
|
||||||
|
if not _auth_user and not _auth_password:
|
||||||
writer.write(b'\x05\x00') # no-auth
|
writer.write(b'\x05\x00') # no-auth
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
elif _auth_user and _auth_password:
|
||||||
|
if b'\x02' not in auth_methods:
|
||||||
|
writer.write(b'\x05\xff')
|
||||||
|
await writer.drain()
|
||||||
|
writer.close()
|
||||||
|
return
|
||||||
|
writer.write(b'\x05\x02') # username/password required
|
||||||
|
log.debug("[%s] auth required", label)
|
||||||
|
|
||||||
|
# Username/password subnegotiation
|
||||||
|
auth_hdr = await asyncio.wait_for(reader.readexactly(1), timeout=10)
|
||||||
|
if auth_hdr[0] != 1:
|
||||||
|
log.debug("[%s] bad auth request ver: %s", label, auth_hdr)
|
||||||
|
writer.write(b'\x01\x01')
|
||||||
|
await writer.drain()
|
||||||
|
writer.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
ulen = (await reader.readexactly(1))[0]
|
||||||
|
client_username = (await reader.readexactly(ulen)).decode('utf-8', errors='ignore')
|
||||||
|
plen = (await reader.readexactly(1))[0]
|
||||||
|
client_password = (await reader.readexactly(plen)).decode('utf-8', errors='ignore')
|
||||||
|
|
||||||
|
if client_username != _auth_user or client_password != _auth_password:
|
||||||
|
log.warning("[%s] auth failed with creds: %s/%s", label, client_username, client_password)
|
||||||
|
writer.write(b'\x01\x01')
|
||||||
|
await writer.drain()
|
||||||
|
writer.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
log.debug("[%s] auth success", label)
|
||||||
|
writer.write(b'\x01\x00') # Success
|
||||||
|
else:
|
||||||
|
# If have some problems
|
||||||
|
writer.write(b'\x05\xff')
|
||||||
|
await writer.drain()
|
||||||
|
writer.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
# -- SOCKS5 CONNECT request --
|
# -- SOCKS5 CONNECT request --
|
||||||
req = await asyncio.wait_for(reader.readexactly(4), timeout=10)
|
req = await asyncio.wait_for(reader.readexactly(4), timeout=10)
|
||||||
|
|
@ -911,10 +958,20 @@ async def _handle_client(reader, writer):
|
||||||
label, dc, media_tag)
|
label, dc, media_tag)
|
||||||
return
|
return
|
||||||
|
|
||||||
# -- Try WebSocket via direct connection --
|
# -- Cooldown check --
|
||||||
fail_until = _dc_fail_until.get(dc_key, 0)
|
fail_until = _dc_fail_until.get(dc_key, 0)
|
||||||
ws_timeout = _WS_FAIL_TIMEOUT if now < fail_until else 10.0
|
if now < fail_until:
|
||||||
|
remaining = fail_until - now
|
||||||
|
log.debug("[%s] DC%d%s WS cooldown (%.0fs) -> TCP",
|
||||||
|
label, dc, media_tag, remaining)
|
||||||
|
ok = await _tcp_fallback(reader, writer, dst, port, init,
|
||||||
|
label, dc=dc, is_media=is_media)
|
||||||
|
if ok:
|
||||||
|
log.info("[%s] DC%d%s TCP fallback closed",
|
||||||
|
label, dc, media_tag)
|
||||||
|
return
|
||||||
|
|
||||||
|
# -- Try WebSocket via direct connection --
|
||||||
domains = _ws_domains(dc, is_media)
|
domains = _ws_domains(dc, is_media)
|
||||||
target = _dc_opt[dc]
|
target = _dc_opt[dc]
|
||||||
ws = None
|
ws = None
|
||||||
|
|
@ -932,7 +989,7 @@ async def _handle_client(reader, writer):
|
||||||
label, dc, media_tag, dst, port, url, target)
|
label, dc, media_tag, dst, port, url, target)
|
||||||
try:
|
try:
|
||||||
ws = await RawWebSocket.connect(target, domain,
|
ws = await RawWebSocket.connect(target, domain,
|
||||||
timeout=ws_timeout)
|
timeout=10)
|
||||||
all_redirects = False
|
all_redirects = False
|
||||||
break
|
break
|
||||||
except WsHandshakeError as exc:
|
except WsHandshakeError as exc:
|
||||||
|
|
@ -1050,6 +1107,7 @@ async def _run(port: int, dc_opt: Dict[int, Optional[str]],
|
||||||
log.info("=" * 60)
|
log.info("=" * 60)
|
||||||
log.info(" Configure Telegram Desktop:")
|
log.info(" Configure Telegram Desktop:")
|
||||||
log.info(" SOCKS5 proxy -> %s:%d (no user/pass)", host, port)
|
log.info(" SOCKS5 proxy -> %s:%d (no user/pass)", host, port)
|
||||||
|
log.info(f" tg://socks/?server={host}&port={port}&user={quote(_auth_user)}&pass={quote(_auth_password)}")
|
||||||
log.info("=" * 60)
|
log.info("=" * 60)
|
||||||
|
|
||||||
async def log_stats():
|
async def log_stats():
|
||||||
|
|
@ -1086,6 +1144,20 @@ async def _run(port: int, dc_opt: Dict[int, Optional[str]],
|
||||||
_server_instance = None
|
_server_instance = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_auth_credentials(username: str, password: str):
|
||||||
|
"""Validate 'user' and 'password': both are specified, or both aren't"""
|
||||||
|
empty_user = username is None or not str(username).strip()
|
||||||
|
empty_pass = password is None or not str(password).strip()
|
||||||
|
|
||||||
|
if empty_user != empty_pass:
|
||||||
|
raise ValueError(
|
||||||
|
"Both --username (-u) and --password (-P) must be specified together, or neither should be used")
|
||||||
|
|
||||||
|
global _auth_user, _auth_password
|
||||||
|
_auth_user = username
|
||||||
|
_auth_password = password
|
||||||
|
|
||||||
|
|
||||||
def parse_dc_ip_list(dc_ip_list: List[str]) -> Dict[int, str]:
|
def parse_dc_ip_list(dc_ip_list: List[str]) -> Dict[int, str]:
|
||||||
"""Parse list of 'DC:IP' strings into {dc: ip} dict."""
|
"""Parse list of 'DC:IP' strings into {dc: ip} dict."""
|
||||||
dc_opt: Dict[int, str] = {}
|
dc_opt: Dict[int, str] = {}
|
||||||
|
|
@ -1112,23 +1184,25 @@ def run_proxy(port: int, dc_opt: Dict[int, str],
|
||||||
def main():
|
def main():
|
||||||
ap = argparse.ArgumentParser(
|
ap = argparse.ArgumentParser(
|
||||||
description='Telegram Desktop WebSocket Bridge Proxy')
|
description='Telegram Desktop WebSocket Bridge Proxy')
|
||||||
ap.add_argument('--port', type=int, default=DEFAULT_PORT,
|
|
||||||
help=f'Listen port (default {DEFAULT_PORT})')
|
|
||||||
ap.add_argument('--host', type=str, default='127.0.0.1',
|
ap.add_argument('--host', type=str, default='127.0.0.1',
|
||||||
help='Listen host (default 127.0.0.1)')
|
help='Listen host (default 127.0.0.1)')
|
||||||
|
ap.add_argument('--port', type=int, default=DEFAULT_PORT,
|
||||||
|
help=f'Listen port (default {DEFAULT_PORT})')
|
||||||
|
ap.add_argument('-u', '--user', type=str, default='',
|
||||||
|
help='User for proxy')
|
||||||
|
ap.add_argument('-P', '--password', type=str, default='',
|
||||||
|
help='Password for proxy')
|
||||||
ap.add_argument('--dc-ip', metavar='DC:IP', action='append',
|
ap.add_argument('--dc-ip', metavar='DC:IP', action='append',
|
||||||
default=[],
|
default=['2:149.154.167.220', '4:149.154.167.220'],
|
||||||
help='Target IP for a DC, e.g. --dc-ip 1:149.154.175.205'
|
help='Target IP for a DC, e.g. --dc-ip 1:149.154.175.205'
|
||||||
' --dc-ip 2:149.154.167.220')
|
' --dc-ip 2:149.154.167.220')
|
||||||
ap.add_argument('-v', '--verbose', action='store_true',
|
ap.add_argument('-v', '--verbose', action='store_true',
|
||||||
help='Debug logging')
|
help='Debug logging')
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
|
|
||||||
if not args.dc_ip:
|
|
||||||
args.dc_ip = ['2:149.154.167.220', '4:149.154.167.220']
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dc_opt = parse_dc_ip_list(args.dc_ip)
|
dc_opt = parse_dc_ip_list(args.dc_ip)
|
||||||
|
set_auth_credentials(args.user, args.password)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
log.error(str(e))
|
log.error(str(e))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue