13 Commits

Author SHA1 Message Date
Flowseal
0eebdff69e Version bump 2026-05-16 11:32:48 +03:00
Flowseal
ab3bec967c Update CfWorker.md 2026-05-16 11:32:12 +03:00
Flowseal
a16f7dfc0b Update CfWorker.md 2026-05-16 11:31:21 +03:00
Flowseal
6f02fc1c46 remove cf priority flag, cf worker ui setup 2026-05-16 11:17:42 +03:00
Flowseal
884fffcc2f cf worker mention in readme 2026-05-16 11:17:42 +03:00
Flowseal
09ce00b2e0 worker's code cleanup 2026-05-16 11:17:21 +03:00
Flowseal
362c5a4893 cloudflare worker implementation 2026-05-16 11:17:21 +03:00
Kira
bff67b3ecf Docs/readme docker (#843) 2026-05-13 09:20:10 +03:00
Flowseal
d5abfbf9c2 github connection fallback 2026-05-09 16:47:56 +03:00
Flowseal
8269ebe3bb download ways mention on build's page 2026-05-08 20:42:31 +03:00
Flowseal
3770569789 revert version 2026-05-08 14:49:19 +03:00
Flowseal
e72a44d74b github downloader fix 2026-05-08 14:36:54 +03:00
deexsed
33d3147c0b fix: автоответы только для label "bug" (#826) 2026-05-08 12:19:20 +03:00
22 changed files with 531 additions and 113 deletions

View File

@@ -1,7 +1,7 @@
name: 🐛 Проблема name: 🐛 Проблема
title: '[Проблема] ' title: '[Проблема] '
description: Сообщить о проблеме description: Сообщить о проблеме
labels: ['type: проблема', 'status: нуждается в сортировке'] labels: ['bug']
body: body:
- type: input - type: input

View File

@@ -1,7 +1,7 @@
name: 🚀 Предложение name: 🚀 Предложение
title: '[Предложение] ' title: '[Предложение] '
description: Предложить улучшение или новую функциональность description: Предложить улучшение или новую функциональность
labels: ['type: предложение', 'status: нуждается в сортировке'] labels: ['enhancement']
body: body:
- type: textarea - type: textarea

View File

@@ -457,6 +457,10 @@ jobs:
body: | body: |
## ##
### [❤️ Поддержать развитие проекта](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/Funding.md) ### [❤️ Поддержать развитие проекта](https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/Funding.md)
> [!TIP]
> Не можете скачать?
> Добавьте `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_7_64bit.exe dist/TgWsProxy_windows_7_64bit.exe

View File

@@ -9,6 +9,7 @@ permissions:
jobs: jobs:
comment: comment:
if: contains(github.event.issue.labels.*.name, 'bug')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Comment on new issue - name: Comment on new issue

View File

@@ -48,7 +48,7 @@ tg-ws-proxy [--port PORT] [--host HOST] [--dc-ip DC:IP ...] [-v]
| `--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 (параметр можно указывать несколько раз) |
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) | | `--no-cfproxy` | `false` | Отключить попытку [проксирования через Cloudflare](./CfProxy.md) |
| `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare. [Подробнее](./CfProxy.md) | | `--cfproxy-domain` | | Указать свой домен для проксирования через Cloudflare. [Подробнее](./CfProxy.md) |
| `--cfproxy-priority` | `true` | Пробовать проксировать через Cloudflare перед прямым TCP подключением | | `--cfproxy-worker-domain` | | Домен Cloudflare Worker [Подробнее](./CfWorker.md) |
| `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом | | `--fake-tls-domain` | | Включить маскировку Fake TLS (ee-secret) с указанным SNI-доменом |
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) | | `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |
| `--buf-kb` | `256` | Размер буфера в КБ | | `--buf-kb` | `256` | Размер буфера в КБ |

123
docs/CfWorker.md Normal file
View File

@@ -0,0 +1,123 @@
# Cloudflare Worker
Альтернативный (полностью бесплатный, не нужно покупать домен в отличии от [CfProxy](./CfProxy.md)) способ проксирования.
Прокси возвращает доступ к тому, что раньше не загружалось (реакции, некоторые стикеры). Если на аккаунте без Premium с данным способом все еще не загружаются фото/видео, оставьте в блоке `DC → IP` только `4:149.154.167.220`
##
1. **Добавьте в [zapret](https://github.com/Flowseal/zapret-discord-youtube/) или в любое другое ПО следующие домены:**
```
cloudflare.com
cloudflare.dev
workers.dev
```
2. Создайте аккаунт в [Cloudflare](https://dash.cloudflare.com/) (или войдите в существующий)
3. Слева в панели выберите `Compute``Workers & Pages`
<img width="250" height="768" alt="image" src="https://github.com/user-attachments/assets/d81e3522-045a-4e65-9c2e-5545b7ad409a" />
4. Нажмите сверху справа кнопку **`Create application`** → `Start with Hello World!``Deploy`
<img width="1406" height="193" alt="image" src="https://github.com/user-attachments/assets/7ac65944-8761-42a6-ab6d-ba5f9080c883" />
<img width="586" height="379" alt="image" src="https://github.com/user-attachments/assets/ff901439-c2a1-4867-95de-e11b82a37044" />
<img width="624" height="694" alt="image" src="https://github.com/user-attachments/assets/bb68d49a-166d-42a0-8fe2-bd2b16c0d066" />
5. Сверху справа нажмите кнопку **`Edit code`**, замените код слева на тот, [что находится внизу этой страницы](./CfWorker.md#код-workerа)
* Если у вас не загружается код, то вы не выполнили первый пункт
<img width="911" height="117" alt="image" src="https://github.com/user-attachments/assets/6bcdf839-d776-47e9-9d18-ba0efdf53244" />
<img width="1027" height="512" alt="image" src="https://github.com/user-attachments/assets/daf131ed-82d5-40f0-a7eb-daeb598bea40" />
6. Нажмите сверху справа кнопку **`Deploy`**
<img width="415" height="138" alt="image" src="https://github.com/user-attachments/assets/58d8f83e-d8b5-40cf-a30f-741d7311047b" />
7. Скопируйте домен из поля справа и укажите его в настройках **Cloudflare Worker** (или через аргумент `--cfproxy-worker-domain`)
* Пример домена: `random-symbols-1234.username.workers.dev`
<img width="414" height="182" alt="image" src="https://github.com/user-attachments/assets/4fb0b111-8026-4d17-b993-6c70ec37f1f5" />
### Код Worker'а
```javascript
import { connect } from "cloudflare:sockets";
function toBytes(data) {
if (data instanceof ArrayBuffer) {
return new Uint8Array(data);
}
if (typeof data === "string") {
return new TextEncoder().encode(data);
}
if (data && typeof data.arrayBuffer === "function") {
return data.arrayBuffer().then((ab) => new Uint8Array(ab));
}
return new Uint8Array();
}
export default {
async fetch(request) {
if ((request.headers.get("Upgrade") || "").toLowerCase() !== "websocket") {
return new Response("Expected websocket", { status: 426 });
}
const url = new URL(request.url);
if (url.pathname !== "/apiws") {
return new Response("Not found", { status: 404 });
}
const dst = url.searchParams.get("dst");
const pair = new WebSocketPair();
const client = pair[0];
const server = pair[1];
server.accept();
const socket = connect({ hostname: dst, port: 443 });
const tcpReader = socket.readable.getReader();
const tcpWriter = socket.writable.getWriter();
server.addEventListener("message", async (event) => {
try {
await tcpWriter.write(await toBytes(event.data));
} catch {
try {
server.close(1011, "tcp write failed");
} catch {}
}
});
server.addEventListener("close", async () => {
try {
await tcpWriter.close();
} catch {}
try {
socket.close();
} catch {}
});
(async () => {
try {
while (true) {
const { value, done } = await tcpReader.read();
if (done) {
break;
}
if (value) {
server.send(value);
}
}
} catch {
} finally {
try {
server.close();
} catch {}
try {
tcpReader.releaseLock();
} catch {}
try {
socket.close();
} catch {}
}
})();
return new Response(null, { status: 101, webSocket: client });
},
};
```

69
docs/README.docker.md Normal file
View File

@@ -0,0 +1,69 @@
# TG WS Proxy для Docker
## Установка из исходников
Вводите команды последовательно, одну за другой:
```bash
# Скачиваем репозиторий
git clone https://github.com/Flowseal/tg-ws-proxy.git
# Переходим в папку с проектом
cd tg-ws-proxy
# Собираем образ
docker build -t tg-ws-proxy .
# Запускаем контейнер
docker run -d \
--name tg-ws-proxy \
--restart=always \
-p 1443:1443 \
tg-ws-proxy:latest
# Получаем ссылку для подключения
docker logs tg-ws-proxy 2>&1 | grep 'tg://proxy'
```
После выполнения последней команды вы увидите ссылку вида:
```text
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`|
Пример с ручным указанием секрета:
```bash
docker run -d \
--name tg-ws-proxy \
--restart=always \
-p 1443:1443 \
-e TG_WS_PROXY_SECRET=аш_секрет" \
tg-ws-proxy:latest
```
Для генерации секрета можно использовать:
```bash
openssl rand -hex 16
```
## Настройка Telegram Desktop
1. Telegram → **Настройки** → **Продвинутые настройки** → **Тип подключения** → **Прокси**
2. Добавьте прокси:
- **Тип:** MTProto
- **Сервер:** `127.0.0.1` (или переопределенный вами)
- **Порт:** `1443` (или переопределенный вами)
- **Secret:** из настроек или логов

View File

@@ -34,6 +34,8 @@
- **[Windows](./README.windows.md)** - **[Windows](./README.windows.md)**
- **[macOS](./README.macos.md)** - **[macOS](./README.macos.md)**
- **[Linux](./README.linux.md)** - **[Linux](./README.linux.md)**
- **[Docker](./README.docker.md)**
- [Настройка Cloudflare Worker'а (бесплатный аналог CF-прокси)](./CfWorker.md)
- [Настройка Cloudflare-домена (CF-прокси)](./CfProxy.md) - [Настройка Cloudflare-домена (CF-прокси)](./CfProxy.md)
- [Fake TLS + upstream в Nginx](./FakeTlsNginx.md) - [Fake TLS + upstream в Nginx](./FakeTlsNginx.md)
- [Файлы конфигурации Tray-приложения](./TrayConfig.md) - [Файлы конфигурации Tray-приложения](./TrayConfig.md)

View File

@@ -21,8 +21,8 @@ Tray-приложение хранит данные в:
"log_max_mb": 5.0, "log_max_mb": 5.0,
"check_updates": true, "check_updates": true,
"cfproxy": true, "cfproxy": true,
"cfproxy_priority": true,
"cfproxy_user_domain": "", "cfproxy_user_domain": "",
"cfproxy_worker_domain": "",
"appearance": "auto" "appearance": "auto"
} }
``` ```

View File

@@ -41,6 +41,8 @@ _app: Optional[object] = None
_config: dict = {} _config: dict = {}
_exiting: bool = False _exiting: bool = False
_CFWORKER_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfWorker.md"
# osascript dialogs # osascript dialogs
@@ -109,6 +111,32 @@ def _osascript_input(prompt: str, default: str, title: str = "TG WS Proxy") -> O
return r.stdout.rstrip("\r\n") return r.stdout.rstrip("\r\n")
def _ask_cfworker_domain(default: str) -> Optional[str]:
value = default
while True:
script = (
f'set d to display dialog "{_esc("Cloudflare Worker домен (например, name.account.workers.dev):")}" '
f'default answer "{_esc(value)}" '
f'with title "TG WS Proxy" '
f'buttons {{"Закрыть", "?", "OK"}} '
f'default button "OK" cancel button "Закрыть"\n'
f'return (button returned of d) & "\\n" & (text returned of d)'
)
r = subprocess.run(["osascript", "-e", script], capture_output=True, text=True)
if r.returncode != 0:
return None
out_lines = r.stdout.splitlines()
button = out_lines[0].strip() if out_lines else ""
value = out_lines[1].strip() if len(out_lines) > 1 else value
if button == "?":
webbrowser.open(_CFWORKER_HELP_URL)
continue
if button == "OK":
return value.strip()
# menubar icon # menubar icon
@@ -396,13 +424,6 @@ def _edit_config_dialog() -> None:
if cfproxy is None: if cfproxy is None:
return return
cfproxy_priority = True
if cfproxy:
cfproxy_priority_result = _ask_yes_no_close("Приоритет CfProxy (пробовать раньше прямого TCP)?")
if cfproxy_priority_result is None:
return
cfproxy_priority = cfproxy_priority_result
cfproxy_domain = _osascript_input( cfproxy_domain = _osascript_input(
"Свой CF-домен (оставьте пустым для автоматического выбора):\n" "Свой CF-домен (оставьте пустым для автоматического выбора):\n"
"DNS записи kws1-kws5,kws203 должны указывать на IP датацентров Telegram через Cloudflare.", "DNS записи kws1-kws5,kws203 должны указывать на IP датацентров Telegram через Cloudflare.",
@@ -412,6 +433,12 @@ def _edit_config_dialog() -> None:
return return
cfproxy_domain = cfproxy_domain.strip() cfproxy_domain = cfproxy_domain.strip()
cfworker_domain = _ask_cfworker_domain(
cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG.get("cfproxy_worker_domain", ""))
)
if cfworker_domain is None:
return
new_cfg = { new_cfg = {
"host": host, "host": host,
"port": port, "port": port,
@@ -423,8 +450,8 @@ def _edit_config_dialog() -> None:
"log_max_mb": adv.get("log_max_mb", cfg.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])), "log_max_mb": adv.get("log_max_mb", cfg.get("log_max_mb", DEFAULT_CONFIG["log_max_mb"])),
"check_updates": cfg.get("check_updates", True), "check_updates": cfg.get("check_updates", True),
"cfproxy": cfproxy, "cfproxy": cfproxy,
"cfproxy_priority": cfproxy_priority,
"cfproxy_user_domain": cfproxy_domain, "cfproxy_user_domain": cfproxy_domain,
"cfproxy_worker_domain": cfworker_domain,
} }
save_config(new_cfg) save_config(new_cfg)
log.info("Config saved: %s", new_cfg) log.info("Config saved: %s", new_cfg)

View File

@@ -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, 6, 5, 0), filevers=(1, 7, 0, 0),
prodvers=(1, 6, 5, 0), prodvers=(1, 7, 0, 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.6.6.0'), StringStruct(u'FileVersion', u'1.7.0.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.6.6.0'), StringStruct(u'ProductVersion', u'1.7.0.0'),
] ]
) )
] ]

View File

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

View File

@@ -4,6 +4,7 @@ import struct
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from typing import Dict, List, Optional from typing import Dict, List, Optional
from urllib.parse import urlencode
from .utils import * from .utils import *
from .stats import stats from .stats import stats
@@ -131,15 +132,26 @@ async def do_fallback(reader, writer, relay_init, label,
ctx: CryptoCtx, splitter=None): ctx: CryptoCtx, splitter=None):
fallback_dst = DC_DEFAULT_IPS.get(dc) fallback_dst = DC_DEFAULT_IPS.get(dc)
use_cf = proxy_config.fallback_cfproxy use_cf = proxy_config.fallback_cfproxy
cf_first = proxy_config.fallback_cfproxy_priority worker_domain = proxy_config.cfproxy_worker_domain
methods: List[str] = ['tcp'] methods: List[str] = []
if worker_domain and fallback_dst:
methods.append('cf_worker')
if use_cf: if use_cf:
methods.insert(0 if cf_first else 1, 'cf') methods.append('cf')
if fallback_dst:
methods.append('tcp')
for method in methods: for method in methods:
if method == 'cf': if method == 'cf_worker' and fallback_dst:
ok = await _cfproxy_worker_fallback(
reader, writer, relay_init, label, ctx,
dc=dc, is_media=is_media, fallback_dst=fallback_dst,
splitter=splitter)
if ok:
return True
elif method == 'cf':
ok = await _cfproxy_fallback( ok = await _cfproxy_fallback(
reader, writer, relay_init, label, ctx, reader, writer, relay_init, label, ctx,
dc=dc, is_media=is_media, dc=dc, is_media=is_media,
@@ -157,6 +169,42 @@ async def do_fallback(reader, writer, relay_init, label,
return False return False
async def _cfproxy_worker_fallback(reader, writer, relay_init, label,
ctx: CryptoCtx,
dc: int, is_media: bool,
fallback_dst: str,
splitter=None):
media_tag = ' media' if is_media else ''
worker_domain = proxy_config.cfproxy_worker_domain
if not worker_domain:
return False
query = urlencode({
'dst': fallback_dst,
'dc': str(dc),
'media': '1' if is_media else '0',
})
path = f'/apiws?{query}'
log.info("[%s] DC%d%s -> trying CF worker for %s",
label, dc, media_tag, fallback_dst)
try:
ws = await RawWebSocket.connect(worker_domain, worker_domain,
timeout=10.0, path=path)
except Exception as exc:
log.warning("[%s] DC%d%s CF worker failed: %s",
label, dc, media_tag, repr(exc))
return False
stats.connections_cfproxy += 1
await ws.send(relay_init)
await bridge_ws_reencrypt(reader, writer, ws, label, ctx,
dc=dc, is_media=is_media,
splitter=splitter)
return True
async def _cfproxy_fallback(reader, writer, relay_init, label, async def _cfproxy_fallback(reader, writer, relay_init, label,
ctx: CryptoCtx, ctx: CryptoCtx,
dc: int, is_media: bool, dc: int, is_media: bool,

View File

@@ -7,9 +7,10 @@ import threading
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Dict, List from typing import Dict, List
from urllib.request import Request, urlopen from urllib.request import Request
from .balancer import balancer from .balancer import balancer
from .utils import build_github_opener
log = logging.getLogger('tg-mtproto-proxy') log = logging.getLogger('tg-mtproto-proxy')
@@ -57,8 +58,8 @@ class ProxyConfig:
buffer_size: int = 256 * 1024 buffer_size: int = 256 * 1024
pool_size: int = 4 pool_size: int = 4
fallback_cfproxy: bool = True fallback_cfproxy: bool = True
fallback_cfproxy_priority: bool = True
cfproxy_user_domain: str = '' cfproxy_user_domain: str = ''
cfproxy_worker_domain: str = ''
fake_tls_domain: str = '' fake_tls_domain: str = ''
proxy_protocol: bool = False proxy_protocol: bool = False
@@ -70,7 +71,7 @@ def _fetch_cfproxy_domain_list() -> List[str]:
try: try:
req = Request(CFPROXY_DOMAINS_URL + "?" + "".join(random.choices(string.ascii_letters, k=7)), req = Request(CFPROXY_DOMAINS_URL + "?" + "".join(random.choices(string.ascii_letters, k=7)),
headers={'User-Agent': 'tg-ws-proxy'}) headers={'User-Agent': 'tg-ws-proxy'})
with urlopen(req, timeout=10) as resp: with build_github_opener().open(req, timeout=10) as resp:
text = resp.read().decode('utf-8', errors='replace') text = resp.read().decode('utf-8', errors='replace')
encoded = [ encoded = [
line.strip() for line in text.splitlines() line.strip() for line in text.splitlines()

View File

@@ -78,7 +78,8 @@ class RawWebSocket:
self._closed = False self._closed = False
@staticmethod @staticmethod
async def connect(host: str, domain: str, timeout: float = 10.0) -> 'RawWebSocket': async def connect(host: str, domain: str, timeout: float = 10.0,
path: str = '/apiws') -> 'RawWebSocket':
reader, writer = await asyncio.wait_for( reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, 443, ssl=_ssl_ctx, asyncio.open_connection(host, 443, ssl=_ssl_ctx,
server_hostname=domain), server_hostname=domain),
@@ -89,7 +90,7 @@ class RawWebSocket:
ws_key = base64.b64encode(os.urandom(16)).decode() ws_key = base64.b64encode(os.urandom(16)).decode()
req = ( req = (
f'GET /apiws HTTP/1.1\r\n' f'GET {path} HTTP/1.1\r\n'
f'Host: {domain}\r\n' f'Host: {domain}\r\n'
f'Upgrade: websocket\r\n' f'Upgrade: websocket\r\n'
f'Connection: Upgrade\r\n' f'Connection: Upgrade\r\n'

View File

@@ -587,9 +587,11 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
ip = proxy_config.dc_redirects.get(dc) ip = proxy_config.dc_redirects.get(dc)
log.info(" DC%d: %s", dc, ip) log.info(" DC%d: %s", dc, ip)
if proxy_config.fallback_cfproxy: if proxy_config.fallback_cfproxy:
prio = 'CF first' if proxy_config.fallback_cfproxy_priority else 'TCP first'
user_domain = "user" if proxy_config.cfproxy_user_domain else "auto" user_domain = "user" if proxy_config.cfproxy_user_domain else "auto"
log.info(" CF proxy: enabled (%s | %s)", prio, user_domain) log.info(" CF proxy: enabled (%s)", user_domain)
if proxy_config.cfproxy_worker_domain:
log.info(" CF worker: enabled (%s)",
proxy_config.cfproxy_worker_domain)
log.info("=" * 60) log.info("=" * 60)
log.info(" Connect:") log.info(" Connect:")
if ftls: if ftls:
@@ -651,16 +653,6 @@ def run_proxy(stop_event: Optional[asyncio.Event] = None):
def main(): def main():
def _parse_bool(value: str) -> bool:
lowered = value.strip().lower()
if lowered == 'true':
return True
if lowered == 'false':
return False
raise argparse.ArgumentTypeError(
"Expected boolean value: true or false",
)
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(
description='Telegram MTProto WebSocket Bridge Proxy') description='Telegram MTProto WebSocket Bridge Proxy')
ap.add_argument('--port', type=int, default=1443, ap.add_argument('--port', type=int, default=1443,
@@ -687,10 +679,12 @@ def main():
ap.add_argument('--cfproxy-domain', type=str, default='', ap.add_argument('--cfproxy-domain', type=str, default='',
metavar='DOMAIN', metavar='DOMAIN',
help='User defined Cloudflare-proxied domain for WS fallback') help='User defined Cloudflare-proxied domain for WS fallback')
ap.add_argument('--cfproxy-worker-domain', type=str, default='',
metavar='DOMAIN',
help='Cloudflare Worker domain for WS fallback '
'(tried before other fallback methods)')
ap.add_argument('--no-cfproxy', action='store_true', ap.add_argument('--no-cfproxy', action='store_true',
help='Disable Cloudflare proxy fallback') help='Disable Cloudflare proxy fallback')
ap.add_argument('--cfproxy-priority', type=_parse_bool, default=True,
help='Try cfproxy before tcp fallback (default: true)')
ap.add_argument('--fake-tls-domain', type=str, default='', ap.add_argument('--fake-tls-domain', type=str, default='',
metavar='DOMAIN', metavar='DOMAIN',
help='Enable Fake TLS (ee-secret) masking with the given ' help='Enable Fake TLS (ee-secret) masking with the given '
@@ -730,8 +724,8 @@ def main():
proxy_config.buffer_size = max(4, args.buf_kb) * 1024 proxy_config.buffer_size = max(4, args.buf_kb) * 1024
proxy_config.pool_size = max(0, args.pool_size) proxy_config.pool_size = max(0, args.pool_size)
proxy_config.fallback_cfproxy = not args.no_cfproxy proxy_config.fallback_cfproxy = not args.no_cfproxy
proxy_config.fallback_cfproxy_priority = args.cfproxy_priority
proxy_config.cfproxy_user_domain = args.cfproxy_domain.strip() proxy_config.cfproxy_user_domain = args.cfproxy_domain.strip()
proxy_config.cfproxy_worker_domain = args.cfproxy_worker_domain.strip()
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

View File

@@ -1,6 +1,9 @@
import socket as _socket import socket as _socket
import urllib.request
import http.client
from typing import Optional from typing import Optional, Dict
from urllib.request import Request
ZERO_64 = b'\x00' * 64 ZERO_64 = b'\x00' * 64
@@ -26,6 +29,11 @@ RESERVED_STARTS = {b'\x48\x45\x41\x44', b'\x50\x4F\x53\x54',
b'\xdd\xdd\xdd\xdd', b'\x16\x03\x01\x02'} b'\xdd\xdd\xdd\xdd', b'\x16\x03\x01\x02'}
RESERVED_CONTINUE = b'\x00\x00\x00\x00' RESERVED_CONTINUE = b'\x00\x00\x00\x00'
_GITHUB_IPS: Dict[str, str] = {
"release-assets.githubusercontent.com": "185.199.109.133",
"raw.githubusercontent.com": "185.199.109.133",
}
def human_bytes(n: int) -> str: def human_bytes(n: int) -> str:
for unit in ('B', 'KB', 'MB', 'GB'): for unit in ('B', 'KB', 'MB', 'GB'):
@@ -45,4 +53,35 @@ def get_link_host(host: str) -> Optional[str]:
link_host = '127.0.0.1' link_host = '127.0.0.1'
return link_host return link_host
else: else:
return host return host
class _PinnedHTTPSHandler(urllib.request.HTTPSHandler):
def https_open(self, req: Request):
host = req.host.split(":")[0]
ip = _GITHUB_IPS.get(host)
if not ip:
return super().https_open(req)
pinned = ip
class _Conn(http.client.HTTPSConnection):
def connect(self):
self.sock = _socket.create_connection(
(pinned, self.port or 443),
self.timeout,
self.source_address,
)
if self._tunnel_host:
self._tunnel()
self.sock = self._context.wrap_socket(
self.sock, server_hostname=self._tunnel_host or self.host
)
try:
return self.do_open(_Conn, req)
except Exception:
return super().https_open(req)
def build_github_opener() -> urllib.request.OpenerDirector:
return urllib.request.build_opener(_PinnedHTTPSHandler())

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import logging
import os import os
import webbrowser import webbrowser
from dataclasses import dataclass from dataclasses import dataclass
@@ -17,6 +18,8 @@ from ui.ctk_theme import (
) )
from ui.ctk_tooltip import attach_ctk_tooltip, attach_tooltip_to_widgets from ui.ctk_tooltip import attach_ctk_tooltip, attach_tooltip_to_widgets
log = logging.getLogger('tg-mtproto-proxy')
_TIP_HOST = ( _TIP_HOST = (
"Адрес, на котором прокси принимает подключения.\n" "Адрес, на котором прокси принимает подключения.\n"
"Обычно 127.0.0.1 — локальная сеть, 0.0.0.0 - все интерфейсы" "Обычно 127.0.0.1 — локальная сеть, 0.0.0.0 - все интерфейсы"
@@ -55,9 +58,6 @@ _TIP_CHECK_UPDATES = "При запуске проверять наличие о
_TIP_CFPROXY = ( _TIP_CFPROXY = (
"Использовать Cloudflare прокси для недоступных датацентров" "Использовать Cloudflare прокси для недоступных датацентров"
) )
_TIP_CFPROXY_PRIORITY = (
"Пробовать CF-прокси раньше прямого TCP-подключения"
)
_TIP_CFPROXY_DOMAIN = ( _TIP_CFPROXY_DOMAIN = (
"Ваш собственный домен, проксируемый через Cloudflare, для WS-подключения.\n" "Ваш собственный домен, проксируемый через Cloudflare, для WS-подключения.\n"
"Если не указан — выбирается автоматически из поддерживаемых доменов" "Если не указан — выбирается автоматически из поддерживаемых доменов"
@@ -65,14 +65,27 @@ _TIP_CFPROXY_DOMAIN = (
_TIP_CFPROXY_USER_DOMAIN_CB = ( _TIP_CFPROXY_USER_DOMAIN_CB = (
"Указать свой домен вместо автоматического выбора" "Указать свой домен вместо автоматического выбора"
) )
_TIP_CFWORKER_DOMAIN = (
"Домен Cloudflare Worker (например, name.account.workers.dev).\n"
"Прокси передает через него подключение к Telegram DC по IP"
)
_TIP_SAVE = "Сохранить настройки" _TIP_SAVE = "Сохранить настройки"
_TIP_CANCEL = "Закрыть окно без сохранения изменений" _TIP_CANCEL = "Закрыть окно без сохранения изменений"
_CFPROXY_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md" _CFPROXY_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfProxy.md"
_CFWORKER_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfWorker.md"
_CFPROXY_TEST_DCS = [1, 2, 3, 4, 5, 203] _CFPROXY_TEST_DCS = [1, 2, 3, 4, 5, 203]
_CFWORKER_TEST_DST = {
1: '149.154.175.50',
2: '149.154.167.51',
3: '149.154.175.100',
4: '149.154.167.91',
5: '149.154.171.5',
203: '91.105.192.100',
}
def _run_cfproxy_connectivity_test(domain: str) -> dict: def _run_connectivity_test(cases: list) -> dict:
import base64 import base64
import ssl import ssl
import socket as _socket import socket as _socket
@@ -81,15 +94,14 @@ def _run_cfproxy_connectivity_test(domain: str) -> dict:
ctx.check_hostname = False ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE ctx.verify_mode = ssl.CERT_NONE
results = {} results = {}
for dc in _CFPROXY_TEST_DCS: for dc, connect_host, sni_host, req_host, path in cases:
host = f"kws{dc}.{domain}"
try: try:
with _socket.create_connection((host, 443), timeout=5) as raw: with _socket.create_connection((connect_host, 443), timeout=5) as raw:
with ctx.wrap_socket(raw, server_hostname=host) as ssock: with ctx.wrap_socket(raw, server_hostname=sni_host) as ssock:
ws_key = base64.b64encode(os.urandom(16)).decode() ws_key = base64.b64encode(os.urandom(16)).decode()
req = ( req = (
f"GET /apiws HTTP/1.1\r\n" f"GET {path} HTTP/1.1\r\n"
f"Host: {host}\r\n" f"Host: {req_host}\r\n"
f"Upgrade: websocket\r\n" f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n" f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {ws_key}\r\n" f"Sec-WebSocket-Key: {ws_key}\r\n"
@@ -120,6 +132,23 @@ def _run_cfproxy_connectivity_test(domain: str) -> dict:
return results return results
def _run_cfproxy_connectivity_test(domain: str) -> dict:
cases = []
for dc in _CFPROXY_TEST_DCS:
host = f"kws{dc}.{domain}"
cases.append((dc, host, host, host, "/apiws"))
return _run_connectivity_test(cases)
def _run_cfworker_connectivity_test(domain: str) -> dict:
cases = []
for dc in _CFPROXY_TEST_DCS:
dst = _CFWORKER_TEST_DST[dc]
path = f"/apiws?dst={dst}&dc={dc}&media=0"
cases.append((dc, domain, domain, domain, path))
return _run_connectivity_test(cases)
def _run_cfproxy_auto_test(domains: list) -> tuple: def _run_cfproxy_auto_test(domains: list) -> tuple:
merged: dict = {} merged: dict = {}
best_domain = None best_domain = None
@@ -136,49 +165,39 @@ def _run_cfproxy_auto_test(domains: list) -> tuple:
return best_domain, merged return best_domain, merged
def _cfproxy_show_test_results(domain: str, results: dict) -> None: def _show_connectivity_results(title_base: str, results: dict,
domain: str = '', label_prefix: str = 'DC',
auto_mode: bool = False,
unavailable_message: str = '') -> None:
import tkinter as _tk import tkinter as _tk
from tkinter import messagebox as _mb from tkinter import messagebox as _mb
ok = [dc for dc, v in results.items() if v is True] ok = [dc for dc, v in results.items() if v is True]
fail = [(dc, v) for dc, v in results.items() if v is not True] if auto_mode:
if len(ok) == len(_CFPROXY_TEST_DCS): if domain:
title = "CF-прокси: всё работает" title = f"{title_base}: доступен"
msg = f"\u2713 Все {len(_CFPROXY_TEST_DCS)} серверов доступны через {domain}." msg = f"\u2713 {title_base} работает. {len(ok)} из {len(_CFPROXY_TEST_DCS)} серверов доступны."
elif not ok: else:
title = "CF-прокси: недоступен" title = f"{title_base}: недоступен"
msg = f"\u2717 Ни один сервер не отвечает через {domain}.\n\nОшибки:\n" msg = unavailable_message
msg += "\n".join(f" kws{dc}: {v}" for dc, v in fail)
else: else:
title = "CF-прокси: частично работает" fail = [(dc, v) for dc, v in results.items() if v is not True]
msg = ( if len(ok) == len(_CFPROXY_TEST_DCS):
f"Домен: {domain}\n\n" title = f"{title_base}: всё работает"
f"\u2713 Работают: {', '.join(f'kws{dc}' for dc in ok)}\n\n" msg = f"\u2713 Все {len(_CFPROXY_TEST_DCS)} серверов доступны через {domain}."
f"\u2717 Недоступны:\n" elif not ok:
+ "\n".join(f" kws{dc}: {v}" for dc, v in fail) title = f"{title_base}: недоступен"
) msg = f"\u2717 Ни один сервер не отвечает через {domain}.\n\nОшибки:\n"
root = _tk.Tk() msg += "\n".join(f" {label_prefix}{dc}: {v}" for dc, v in fail)
root.withdraw() else:
try: title = f"{title_base}: частично работает"
root.attributes("-topmost", True) msg = (
except Exception: f"Домен: {domain}\n\n"
pass f"\u2713 Работают: {', '.join(f'{label_prefix}{dc}' for dc in ok)}\n\n"
_mb.showinfo(title, msg, parent=root) f"\u2717 Недоступны:\n"
root.destroy() + "\n".join(f" {label_prefix}{dc}: {v}" for dc, v in fail)
)
def _cfproxy_show_auto_test_results(ok_domain, results: dict) -> None:
import tkinter as _tk
from tkinter import messagebox as _mb
if ok_domain is not None:
title = "CF-прокси: доступен"
ok = [dc for dc, v in results.items() if v is True]
msg = f"\u2713 CF-прокси работает. {len(ok)} из {len(_CFPROXY_TEST_DCS)} серверов доступны."
else:
title = "CF-прокси: недоступен"
msg = "\u2717 Ни один из автоматических CF-доменов не отвечает.\n"
msg += "Возможно, блокировка или проблемы с сетью."
root = _tk.Tk() root = _tk.Tk()
root.withdraw() root.withdraw()
try: try:
@@ -296,8 +315,8 @@ class TrayConfigFormWidgets:
autostart_var: Optional[Any] autostart_var: Optional[Any]
check_updates_var: Optional[Any] check_updates_var: Optional[Any]
cfproxy_var: Optional[Any] = None cfproxy_var: Optional[Any] = None
cfproxy_priority_var: Optional[Any] = None
cfproxy_user_domain_var: Optional[Any] = None cfproxy_user_domain_var: Optional[Any] = None
cfproxy_worker_domain_var: Optional[Any] = None
appearance_var: Optional[Any] = None appearance_var: Optional[Any] = None
@@ -428,13 +447,6 @@ def install_tray_config_form(
cf_cb.pack(side="left", padx=(0, 16)) cf_cb.pack(side="left", padx=(0, 16))
attach_ctk_tooltip(cf_cb, _TIP_CFPROXY) attach_ctk_tooltip(cf_cb, _TIP_CFPROXY)
cfproxy_priority_var = ctk.BooleanVar(
value=cfg.get("cfproxy_priority", default_config.get("cfproxy_priority", True))
)
cf_prio_cb = _checkbox(ctk, cf_row, theme, "Приоритет", cfproxy_priority_var)
cf_prio_cb.pack(side="left")
attach_ctk_tooltip(cf_prio_cb, _TIP_CFPROXY_PRIORITY)
_cf_test_btn = [None] _cf_test_btn = [None]
def _on_cf_test(): def _on_cf_test():
@@ -448,7 +460,12 @@ def install_tray_config_form(
try: try:
res = _run_cfproxy_connectivity_test(user_domain) res = _run_cfproxy_connectivity_test(user_domain)
if btn: if btn:
btn.after(0, lambda: _cfproxy_show_test_results(user_domain, res)) btn.after(
0,
lambda: _show_connectivity_results(
"CF-прокси", res, domain=user_domain, label_prefix='kws',
),
)
except Exception as exc: except Exception as exc:
log.error("CF proxy test failed: %s", exc) log.error("CF proxy test failed: %s", exc)
finally: finally:
@@ -460,7 +477,17 @@ def install_tray_config_form(
try: try:
ok_domain, res = _run_cfproxy_auto_test(balancer.domains) ok_domain, res = _run_cfproxy_auto_test(balancer.domains)
if btn: if btn:
btn.after(0, lambda: _cfproxy_show_auto_test_results(ok_domain, res)) btn.after(
0,
lambda: _show_connectivity_results(
"CF-прокси", res,
domain=ok_domain or '',
auto_mode=True,
unavailable_message=(
"\u2717 Ни один из автоматических CF-доменов не отвечает."
),
),
)
except Exception as exc: except Exception as exc:
log.error("CF proxy auto-test failed: %s", exc) log.error("CF proxy auto-test failed: %s", exc)
finally: finally:
@@ -512,6 +539,80 @@ def install_tray_config_form(
cf_custom_cb_var.trace_add("write", _sync_domain_entry) cf_custom_cb_var.trace_add("write", _sync_domain_entry)
_sync_domain_entry() _sync_domain_entry()
cf_worker_inner = _config_section(ctk, frame, theme, "Cloudflare Worker")
cf_worker_row = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
cf_worker_row.pack(fill="x", pady=(0, 4))
cf_worker_lbl = _label(ctk, cf_worker_row, theme, "Cloudflare Worker домен", size=11)
cf_worker_lbl.pack(anchor="w", pady=(0, 2))
cf_worker_input = ctk.CTkFrame(cf_worker_inner, fg_color="transparent")
cf_worker_input.pack(fill="x")
cfproxy_worker_domain_var = ctk.StringVar(
value=cfg.get("cfproxy_worker_domain", default_config.get("cfproxy_worker_domain", ""))
)
cf_worker_entry = _entry(
ctk, cf_worker_input, theme, var=cfproxy_worker_domain_var,
height=32, radius=8,
)
cf_worker_entry.pack(side="left", fill="x", expand=True, padx=(0, 6))
attach_tooltip_to_widgets([cf_worker_lbl, cf_worker_entry], _TIP_CFWORKER_DOMAIN)
_cfworker_test_btn = [None]
def _sync_cfworker_test_button(*_):
btn = _cfworker_test_btn[0]
if btn is None:
return
enabled = bool(cfproxy_worker_domain_var.get().strip())
btn.configure(state="normal" if enabled else "disabled")
def _on_cfworker_test():
domain = cfproxy_worker_domain_var.get().strip()
btn = _cfworker_test_btn[0]
if not domain or btn is None:
return
btn.configure(text="...", state="disabled")
import threading as _threading
def _worker():
try:
res = _run_cfworker_connectivity_test(domain)
btn.after(
0,
lambda: _show_connectivity_results(
"CF Worker", res, domain=domain, label_prefix='DC',
),
)
except Exception as exc:
log.error("CF worker test failed: %s", exc)
finally:
btn.after(0, lambda: btn.configure(text="Тест"))
btn.after(0, _sync_cfworker_test_button)
_threading.Thread(target=_worker, daemon=True).start()
ctk.CTkButton(
cf_worker_input, text="?", width=28, height=32,
font=(theme.ui_font_family, 14), corner_radius=8,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff", border_width=1, border_color=theme.field_border,
command=lambda: webbrowser.open(_CFWORKER_HELP_URL),
).pack(side="right")
_cfworker_test_widget = ctk.CTkButton(
cf_worker_input, text="Тест", width=56, height=32,
font=(theme.ui_font_family, 13), corner_radius=8,
fg_color=theme.tg_blue, hover_color=theme.tg_blue_hover,
text_color="#ffffff", border_width=1, border_color=theme.field_border,
command=_on_cfworker_test,
)
_cfworker_test_widget.pack(side="right", padx=(0, 6))
_cfworker_test_btn[0] = _cfworker_test_widget
cfproxy_worker_domain_var.trace_add("write", _sync_cfworker_test_button)
_sync_cfworker_test_button()
log_inner = _config_section(ctk, frame, theme, "Логи и производительность") log_inner = _config_section(ctk, frame, theme, "Логи и производительность")
verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False)) verbose_var = ctk.BooleanVar(value=cfg.get("verbose", False))
@@ -601,8 +702,8 @@ def install_tray_config_form(
adv_entries=adv_entries, adv_keys=adv_keys, adv_entries=adv_entries, adv_keys=adv_keys,
autostart_var=autostart_var, check_updates_var=check_updates_var, autostart_var=autostart_var, check_updates_var=check_updates_var,
cfproxy_var=cfproxy_var, cfproxy_var=cfproxy_var,
cfproxy_priority_var=cfproxy_priority_var,
cfproxy_user_domain_var=cfproxy_user_domain_var, cfproxy_user_domain_var=cfproxy_user_domain_var,
cfproxy_worker_domain_var=cfproxy_worker_domain_var,
appearance_var=appearance_var, appearance_var=appearance_var,
) )
@@ -682,10 +783,10 @@ def validate_config_form(
new_cfg["check_updates"] = bool(widgets.check_updates_var.get()) new_cfg["check_updates"] = bool(widgets.check_updates_var.get())
if widgets.cfproxy_var is not None: if widgets.cfproxy_var is not None:
new_cfg["cfproxy"] = bool(widgets.cfproxy_var.get()) new_cfg["cfproxy"] = bool(widgets.cfproxy_var.get())
if widgets.cfproxy_priority_var is not None:
new_cfg["cfproxy_priority"] = bool(widgets.cfproxy_priority_var.get())
if widgets.cfproxy_user_domain_var is not None: if widgets.cfproxy_user_domain_var is not None:
new_cfg["cfproxy_user_domain"] = widgets.cfproxy_user_domain_var.get().strip() new_cfg["cfproxy_user_domain"] = widgets.cfproxy_user_domain_var.get().strip()
if widgets.cfproxy_worker_domain_var is not None:
new_cfg["cfproxy_worker_domain"] = widgets.cfproxy_worker_domain_var.get().strip()
if widgets.appearance_var is not None: if widgets.appearance_var is not None:
new_cfg["appearance"] = _APPEARANCE_TO_CFG.get(widgets.appearance_var.get(), "auto") new_cfg["appearance"] = _APPEARANCE_TO_CFG.get(widgets.appearance_var.get(), "auto")
return new_cfg return new_cfg

View File

@@ -18,8 +18,8 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
"buf_kb": 256, "buf_kb": 256,
"pool_size": 4, "pool_size": 4,
"cfproxy": True, "cfproxy": True,
"cfproxy_priority": True,
"cfproxy_user_domain": "", "cfproxy_user_domain": "",
"cfproxy_worker_domain": "",
} }

View File

@@ -271,8 +271,8 @@ def apply_proxy_config(cfg: dict) -> bool:
pc.buffer_size = max(4, cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])) * 1024 pc.buffer_size = max(4, cfg.get("buf_kb", DEFAULT_CONFIG["buf_kb"])) * 1024
pc.pool_size = max(0, cfg.get("pool_size", DEFAULT_CONFIG["pool_size"])) pc.pool_size = max(0, cfg.get("pool_size", DEFAULT_CONFIG["pool_size"]))
pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"]) pc.fallback_cfproxy = cfg.get("cfproxy", DEFAULT_CONFIG["cfproxy"])
pc.fallback_cfproxy_priority = cfg.get("cfproxy_priority", DEFAULT_CONFIG["cfproxy_priority"])
pc.cfproxy_user_domain = cfg.get("cfproxy_user_domain", DEFAULT_CONFIG["cfproxy_user_domain"]) pc.cfproxy_user_domain = cfg.get("cfproxy_user_domain", DEFAULT_CONFIG["cfproxy_user_domain"])
pc.cfproxy_worker_domain = cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG["cfproxy_worker_domain"])
return True return True

View File

@@ -14,7 +14,8 @@ from itertools import zip_longest
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional, Tuple from typing import Any, Dict, Optional, Tuple
from urllib.error import HTTPError, URLError from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen from urllib.request import Request
from proxy.utils import build_github_opener
REPO = "Flowseal/tg-ws-proxy" REPO = "Flowseal/tg-ws-proxy"
RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest" RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest"
@@ -135,7 +136,7 @@ def fetch_latest_release(
method="GET", method="GET",
) )
try: try:
with urlopen(req, timeout=timeout) as resp: with build_github_opener().open(req, timeout=timeout) as resp:
code = getattr(resp, "status", None) or resp.getcode() code = getattr(resp, "status", None) or resp.getcode()
new_etag = resp.headers.get("ETag") new_etag = resp.headers.get("ETag")
raw = resp.read().decode("utf-8", errors="replace") raw = resp.read().decode("utf-8", errors="replace")

View File

@@ -8,8 +8,11 @@ import threading
import time import time
import webbrowser import webbrowser
import winreg import winreg
import tempfile
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from proxy.utils import build_github_opener
try: try:
import pyperclip import pyperclip
@@ -219,9 +222,6 @@ def update_ctk_form(
def _perform_update(download_url: str, set_status=None) -> None: def _perform_update(download_url: str, set_status=None) -> None:
import tempfile
import urllib.request
def _step(msg: str) -> None: def _step(msg: str) -> None:
log.info("Update: %s", msg) log.info("Update: %s", msg)
if set_status: if set_status:
@@ -244,7 +244,14 @@ def _perform_update(download_url: str, set_status=None) -> None:
os.close(fd) os.close(fd)
tmp_path = Path(tmp_name) tmp_path = Path(tmp_name)
log.info("Downloading update from %s", download_url) log.info("Downloading update from %s", download_url)
urllib.request.urlretrieve(download_url, str(tmp_path)) opener = build_github_opener()
with opener.open(download_url) as _resp:
with open(str(tmp_path), "wb") as _fout:
while True:
_chunk = _resp.read(65536)
if not _chunk:
break
_fout.write(_chunk)
except Exception as exc: except Exception as exc:
_err(f"Не удалось скачать:\n{exc}") _err(f"Не удалось скачать:\n{exc}")
if tmp_path: if tmp_path: