mirror of
https://github.com/Flowseal/tg-ws-proxy.git
synced 2026-05-22 23:41:44 +03:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5915a0e1f3 | ||
|
|
7bc9e133c8 | ||
|
|
12d3d5e478 | ||
|
|
b7cca232ea | ||
|
|
0eebdff69e | ||
|
|
ab3bec967c | ||
|
|
a16f7dfc0b | ||
|
|
6f02fc1c46 | ||
|
|
884fffcc2f | ||
|
|
09ce00b2e0 | ||
|
|
362c5a4893 | ||
|
|
bff67b3ecf | ||
|
|
d5abfbf9c2 | ||
|
|
8269ebe3bb | ||
|
|
3770569789 | ||
|
|
e72a44d74b | ||
|
|
33d3147c0b |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 🐛 Проблема
|
||||
title: '[Проблема] '
|
||||
description: Сообщить о проблеме
|
||||
labels: ['type: проблема', 'status: нуждается в сортировке']
|
||||
labels: ['bug']
|
||||
|
||||
body:
|
||||
- type: input
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 🚀 Предложение
|
||||
title: '[Предложение] '
|
||||
description: Предложить улучшение или новую функциональность
|
||||
labels: ['type: предложение', 'status: нуждается в сортировке']
|
||||
labels: ['enhancement']
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -457,6 +457,10 @@ jobs:
|
||||
body: |
|
||||
##
|
||||
### [❤️ Поддержать развитие проекта](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: |
|
||||
dist/TgWsProxy_windows.exe
|
||||
dist/TgWsProxy_windows_7_64bit.exe
|
||||
|
||||
1
.github/workflows/issue-triage.yml
vendored
1
.github/workflows/issue-triage.yml
vendored
@@ -9,6 +9,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: contains(github.event.issue.labels.*.name, 'bug')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Comment on new issue
|
||||
|
||||
@@ -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 (параметр можно указывать несколько раз) |
|
||||
| `--no-cfproxy` | `false` | Отключить попытку [проксирования через 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-доменом |
|
||||
| `--proxy-protocol` | выкл. | Принимать HAProxy PROXY protocol v1 (для работы за nginx/haproxy с `proxy_protocol on`) |
|
||||
| `--buf-kb` | `256` | Размер буфера в КБ |
|
||||
|
||||
124
docs/CfWorker.md
Normal file
124
docs/CfWorker.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# 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/) (или войдите в существующий)
|
||||
* **После создания аккаунта подтвердите почту с помощью письма, который вам пришел на email**
|
||||
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
69
docs/README.docker.md
Normal 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:** из настроек или логов
|
||||
@@ -1,3 +1,12 @@
|
||||
<div align="center">
|
||||
<br />
|
||||
<p>
|
||||
<img width="1729" height="910" alt="tgwsproxy" src="./images/workflow.png" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
##
|
||||
|
||||
> [!TIP]
|
||||
>
|
||||
> ### [🎉 Поддержать меня](./Funding.md)
|
||||
@@ -24,8 +33,8 @@
|
||||
**Локальный MTProto-прокси** для Telegram Desktop, который **ускоряет работу Telegram**, перенаправляя трафик через WebSocket-соединения. Данные передаются в том же зашифрованном виде, а для работы не нужны сторонние серверы.
|
||||
|
||||
<picture>
|
||||
<source srcset="https://github.com/user-attachments/assets/17f1d15e-e1c2-41ea-a452-220d13359262" media="(prefers-color-scheme: dark)">
|
||||
<img src="https://github.com/user-attachments/assets/8d595468-83a1-4e4f-bac4-9ce4a07027bd">
|
||||
<source srcset="./images/preview-dark.png" media="(prefers-color-scheme: dark)">
|
||||
<img src="./images/preview-white.png">
|
||||
</picture>
|
||||
|
||||
## Навигация
|
||||
@@ -34,6 +43,8 @@
|
||||
- **[Windows](./README.windows.md)**
|
||||
- **[macOS](./README.macos.md)**
|
||||
- **[Linux](./README.linux.md)**
|
||||
- **[Docker](./README.docker.md)**
|
||||
- [Настройка Cloudflare Worker'а (бесплатный аналог CF-прокси)](./CfWorker.md)
|
||||
- [Настройка Cloudflare-домена (CF-прокси)](./CfProxy.md)
|
||||
- [Fake TLS + upstream в Nginx](./FakeTlsNginx.md)
|
||||
- [Файлы конфигурации Tray-приложения](./TrayConfig.md)
|
||||
|
||||
@@ -21,8 +21,8 @@ Tray-приложение хранит данные в:
|
||||
"log_max_mb": 5.0,
|
||||
"check_updates": true,
|
||||
"cfproxy": true,
|
||||
"cfproxy_priority": true,
|
||||
"cfproxy_user_domain": "",
|
||||
"cfproxy_worker_domain": "",
|
||||
"appearance": "auto"
|
||||
}
|
||||
```
|
||||
|
||||
BIN
docs/images/preview-dark.png
Normal file
BIN
docs/images/preview-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 245 KiB |
BIN
docs/images/preview-white.png
Normal file
BIN
docs/images/preview-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
BIN
docs/images/workflow.png
Normal file
BIN
docs/images/workflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
43
macos.py
43
macos.py
@@ -41,6 +41,8 @@ _app: Optional[object] = None
|
||||
_config: dict = {}
|
||||
_exiting: bool = False
|
||||
|
||||
_CFWORKER_HELP_URL = "https://github.com/Flowseal/tg-ws-proxy/blob/main/docs/CfWorker.md"
|
||||
|
||||
# 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")
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -396,13 +424,6 @@ def _edit_config_dialog() -> None:
|
||||
if cfproxy is None:
|
||||
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(
|
||||
"Свой CF-домен (оставьте пустым для автоматического выбора):\n"
|
||||
"DNS записи kws1-kws5,kws203 должны указывать на IP датацентров Telegram через Cloudflare.",
|
||||
@@ -412,6 +433,12 @@ def _edit_config_dialog() -> None:
|
||||
return
|
||||
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 = {
|
||||
"host": host,
|
||||
"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"])),
|
||||
"check_updates": cfg.get("check_updates", True),
|
||||
"cfproxy": cfproxy,
|
||||
"cfproxy_priority": cfproxy_priority,
|
||||
"cfproxy_user_domain": cfproxy_domain,
|
||||
"cfproxy_worker_domain": cfworker_domain,
|
||||
}
|
||||
save_config(new_cfg)
|
||||
log.info("Config saved: %s", new_cfg)
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
filevers=(1, 6, 5, 0),
|
||||
prodvers=(1, 6, 5, 0),
|
||||
filevers=(1, 7, 0, 0),
|
||||
prodvers=(1, 7, 0, 0),
|
||||
mask=0x3f,
|
||||
flags=0x0,
|
||||
OS=0x40004,
|
||||
@@ -21,12 +21,12 @@ VSVersionInfo(
|
||||
[
|
||||
StringStruct(u'CompanyName', u'Flowseal'),
|
||||
StringStruct(u'FileDescription', u'Telegram Desktop WebSocket Bridge Proxy'),
|
||||
StringStruct(u'FileVersion', u'1.6.6.0'),
|
||||
StringStruct(u'FileVersion', u'1.7.0.0'),
|
||||
StringStruct(u'InternalName', u'TgWsProxy'),
|
||||
StringStruct(u'LegalCopyright', u'Copyright (c) Flowseal. MIT License.'),
|
||||
StringStruct(u'OriginalFilename', u'TgWsProxy.exe'),
|
||||
StringStruct(u'ProductName', u'TG WS Proxy'),
|
||||
StringStruct(u'ProductVersion', u'1.6.6.0'),
|
||||
StringStruct(u'ProductVersion', u'1.7.0.0'),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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"]
|
||||
@@ -4,6 +4,7 @@ import struct
|
||||
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from typing import Dict, List, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from .utils import *
|
||||
from .stats import stats
|
||||
@@ -131,15 +132,26 @@ async def do_fallback(reader, writer, relay_init, label,
|
||||
ctx: CryptoCtx, splitter=None):
|
||||
fallback_dst = DC_DEFAULT_IPS.get(dc)
|
||||
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:
|
||||
methods.insert(0 if cf_first else 1, 'cf')
|
||||
methods.append('cf')
|
||||
if fallback_dst:
|
||||
methods.append('tcp')
|
||||
|
||||
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(
|
||||
reader, writer, relay_init, label, ctx,
|
||||
dc=dc, is_media=is_media,
|
||||
@@ -157,6 +169,42 @@ async def do_fallback(reader, writer, relay_init, label,
|
||||
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,
|
||||
ctx: CryptoCtx,
|
||||
dc: int, is_media: bool,
|
||||
|
||||
@@ -7,9 +7,10 @@ import threading
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.request import Request
|
||||
|
||||
from .balancer import balancer
|
||||
from .utils import build_github_opener
|
||||
|
||||
log = logging.getLogger('tg-mtproto-proxy')
|
||||
|
||||
@@ -57,8 +58,8 @@ class ProxyConfig:
|
||||
buffer_size: int = 256 * 1024
|
||||
pool_size: int = 4
|
||||
fallback_cfproxy: bool = True
|
||||
fallback_cfproxy_priority: bool = True
|
||||
cfproxy_user_domain: str = ''
|
||||
cfproxy_worker_domain: str = ''
|
||||
fake_tls_domain: str = ''
|
||||
proxy_protocol: bool = False
|
||||
|
||||
@@ -70,7 +71,7 @@ def _fetch_cfproxy_domain_list() -> List[str]:
|
||||
try:
|
||||
req = Request(CFPROXY_DOMAINS_URL + "?" + "".join(random.choices(string.ascii_letters, k=7)),
|
||||
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')
|
||||
encoded = [
|
||||
line.strip() for line in text.splitlines()
|
||||
|
||||
@@ -78,7 +78,8 @@ class RawWebSocket:
|
||||
self._closed = False
|
||||
|
||||
@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(
|
||||
asyncio.open_connection(host, 443, ssl=_ssl_ctx,
|
||||
server_hostname=domain),
|
||||
@@ -89,7 +90,7 @@ class RawWebSocket:
|
||||
ws_key = base64.b64encode(os.urandom(16)).decode()
|
||||
|
||||
req = (
|
||||
f'GET /apiws HTTP/1.1\r\n'
|
||||
f'GET {path} HTTP/1.1\r\n'
|
||||
f'Host: {domain}\r\n'
|
||||
f'Upgrade: websocket\r\n'
|
||||
f'Connection: Upgrade\r\n'
|
||||
|
||||
@@ -587,9 +587,11 @@ async def _run(stop_event: Optional[asyncio.Event] = None):
|
||||
ip = proxy_config.dc_redirects.get(dc)
|
||||
log.info(" DC%d: %s", dc, ip)
|
||||
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"
|
||||
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(" Connect:")
|
||||
if ftls:
|
||||
@@ -651,16 +653,6 @@ def run_proxy(stop_event: Optional[asyncio.Event] = None):
|
||||
|
||||
|
||||
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(
|
||||
description='Telegram MTProto WebSocket Bridge Proxy')
|
||||
ap.add_argument('--port', type=int, default=1443,
|
||||
@@ -687,10 +679,12 @@ def main():
|
||||
ap.add_argument('--cfproxy-domain', type=str, default='',
|
||||
metavar='DOMAIN',
|
||||
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',
|
||||
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='',
|
||||
metavar='DOMAIN',
|
||||
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.pool_size = max(0, args.pool_size)
|
||||
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_worker_domain = args.cfproxy_worker_domain.strip()
|
||||
proxy_config.fake_tls_domain = args.fake_tls_domain.strip()
|
||||
proxy_config.proxy_protocol = args.proxy_protocol
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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
|
||||
@@ -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'}
|
||||
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:
|
||||
for unit in ('B', 'KB', 'MB', 'GB'):
|
||||
@@ -46,3 +54,34 @@ def get_link_host(host: str) -> Optional[str]:
|
||||
return link_host
|
||||
else:
|
||||
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())
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import webbrowser
|
||||
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
|
||||
|
||||
log = logging.getLogger('tg-mtproto-proxy')
|
||||
|
||||
_TIP_HOST = (
|
||||
"Адрес, на котором прокси принимает подключения.\n"
|
||||
"Обычно 127.0.0.1 — локальная сеть, 0.0.0.0 - все интерфейсы"
|
||||
@@ -55,9 +58,6 @@ _TIP_CHECK_UPDATES = "При запуске проверять наличие о
|
||||
_TIP_CFPROXY = (
|
||||
"Использовать Cloudflare прокси для недоступных датацентров"
|
||||
)
|
||||
_TIP_CFPROXY_PRIORITY = (
|
||||
"Пробовать CF-прокси раньше прямого TCP-подключения"
|
||||
)
|
||||
_TIP_CFPROXY_DOMAIN = (
|
||||
"Ваш собственный домен, проксируемый через Cloudflare, для WS-подключения.\n"
|
||||
"Если не указан — выбирается автоматически из поддерживаемых доменов"
|
||||
@@ -65,14 +65,27 @@ _TIP_CFPROXY_DOMAIN = (
|
||||
_TIP_CFPROXY_USER_DOMAIN_CB = (
|
||||
"Указать свой домен вместо автоматического выбора"
|
||||
)
|
||||
_TIP_CFWORKER_DOMAIN = (
|
||||
"Домен Cloudflare Worker (например, name.account.workers.dev).\n"
|
||||
"Прокси передает через него подключение к Telegram DC по IP"
|
||||
)
|
||||
_TIP_SAVE = "Сохранить настройки"
|
||||
_TIP_CANCEL = "Закрыть окно без сохранения изменений"
|
||||
|
||||
_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]
|
||||
_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 ssl
|
||||
import socket as _socket
|
||||
@@ -81,15 +94,14 @@ def _run_cfproxy_connectivity_test(domain: str) -> dict:
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
results = {}
|
||||
for dc in _CFPROXY_TEST_DCS:
|
||||
host = f"kws{dc}.{domain}"
|
||||
for dc, connect_host, sni_host, req_host, path in cases:
|
||||
try:
|
||||
with _socket.create_connection((host, 443), timeout=5) as raw:
|
||||
with ctx.wrap_socket(raw, server_hostname=host) as ssock:
|
||||
with _socket.create_connection((connect_host, 443), timeout=5) as raw:
|
||||
with ctx.wrap_socket(raw, server_hostname=sni_host) as ssock:
|
||||
ws_key = base64.b64encode(os.urandom(16)).decode()
|
||||
req = (
|
||||
f"GET /apiws HTTP/1.1\r\n"
|
||||
f"Host: {host}\r\n"
|
||||
f"GET {path} HTTP/1.1\r\n"
|
||||
f"Host: {req_host}\r\n"
|
||||
f"Upgrade: websocket\r\n"
|
||||
f"Connection: Upgrade\r\n"
|
||||
f"Sec-WebSocket-Key: {ws_key}\r\n"
|
||||
@@ -120,6 +132,23 @@ def _run_cfproxy_connectivity_test(domain: str) -> dict:
|
||||
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:
|
||||
merged: dict = {}
|
||||
best_domain = None
|
||||
@@ -136,49 +165,39 @@ def _run_cfproxy_auto_test(domains: list) -> tuple:
|
||||
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
|
||||
from tkinter import messagebox as _mb
|
||||
|
||||
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 len(ok) == len(_CFPROXY_TEST_DCS):
|
||||
title = "CF-прокси: всё работает"
|
||||
msg = f"\u2713 Все {len(_CFPROXY_TEST_DCS)} серверов доступны через {domain}."
|
||||
elif not ok:
|
||||
title = "CF-прокси: недоступен"
|
||||
msg = f"\u2717 Ни один сервер не отвечает через {domain}.\n\nОшибки:\n"
|
||||
msg += "\n".join(f" kws{dc}: {v}" for dc, v in fail)
|
||||
if auto_mode:
|
||||
if domain:
|
||||
title = f"{title_base}: доступен"
|
||||
msg = f"\u2713 {title_base} работает. {len(ok)} из {len(_CFPROXY_TEST_DCS)} серверов доступны."
|
||||
else:
|
||||
title = f"{title_base}: недоступен"
|
||||
msg = unavailable_message
|
||||
else:
|
||||
title = "CF-прокси: частично работает"
|
||||
msg = (
|
||||
f"Домен: {domain}\n\n"
|
||||
f"\u2713 Работают: {', '.join(f'kws{dc}' for dc in ok)}\n\n"
|
||||
f"\u2717 Недоступны:\n"
|
||||
+ "\n".join(f" kws{dc}: {v}" for dc, v in fail)
|
||||
)
|
||||
root = _tk.Tk()
|
||||
root.withdraw()
|
||||
try:
|
||||
root.attributes("-topmost", True)
|
||||
except Exception:
|
||||
pass
|
||||
_mb.showinfo(title, msg, parent=root)
|
||||
root.destroy()
|
||||
fail = [(dc, v) for dc, v in results.items() if v is not True]
|
||||
if len(ok) == len(_CFPROXY_TEST_DCS):
|
||||
title = f"{title_base}: всё работает"
|
||||
msg = f"\u2713 Все {len(_CFPROXY_TEST_DCS)} серверов доступны через {domain}."
|
||||
elif not ok:
|
||||
title = f"{title_base}: недоступен"
|
||||
msg = f"\u2717 Ни один сервер не отвечает через {domain}.\n\nОшибки:\n"
|
||||
msg += "\n".join(f" {label_prefix}{dc}: {v}" for dc, v in fail)
|
||||
else:
|
||||
title = f"{title_base}: частично работает"
|
||||
msg = (
|
||||
f"Домен: {domain}\n\n"
|
||||
f"\u2713 Работают: {', '.join(f'{label_prefix}{dc}' for dc in ok)}\n\n"
|
||||
f"\u2717 Недоступны:\n"
|
||||
+ "\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.withdraw()
|
||||
try:
|
||||
@@ -296,8 +315,8 @@ class TrayConfigFormWidgets:
|
||||
autostart_var: Optional[Any]
|
||||
check_updates_var: Optional[Any]
|
||||
cfproxy_var: Optional[Any] = None
|
||||
cfproxy_priority_var: Optional[Any] = None
|
||||
cfproxy_user_domain_var: Optional[Any] = None
|
||||
cfproxy_worker_domain_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))
|
||||
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]
|
||||
|
||||
def _on_cf_test():
|
||||
@@ -448,7 +460,12 @@ def install_tray_config_form(
|
||||
try:
|
||||
res = _run_cfproxy_connectivity_test(user_domain)
|
||||
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:
|
||||
log.error("CF proxy test failed: %s", exc)
|
||||
finally:
|
||||
@@ -460,7 +477,17 @@ def install_tray_config_form(
|
||||
try:
|
||||
ok_domain, res = _run_cfproxy_auto_test(balancer.domains)
|
||||
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:
|
||||
log.error("CF proxy auto-test failed: %s", exc)
|
||||
finally:
|
||||
@@ -512,6 +539,80 @@ def install_tray_config_form(
|
||||
cf_custom_cb_var.trace_add("write", _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, "Логи и производительность")
|
||||
|
||||
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,
|
||||
autostart_var=autostart_var, check_updates_var=check_updates_var,
|
||||
cfproxy_var=cfproxy_var,
|
||||
cfproxy_priority_var=cfproxy_priority_var,
|
||||
cfproxy_user_domain_var=cfproxy_user_domain_var,
|
||||
cfproxy_worker_domain_var=cfproxy_worker_domain_var,
|
||||
appearance_var=appearance_var,
|
||||
)
|
||||
|
||||
@@ -682,10 +783,10 @@ def validate_config_form(
|
||||
new_cfg["check_updates"] = bool(widgets.check_updates_var.get())
|
||||
if widgets.cfproxy_var is not None:
|
||||
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:
|
||||
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:
|
||||
new_cfg["appearance"] = _APPEARANCE_TO_CFG.get(widgets.appearance_var.get(), "auto")
|
||||
return new_cfg
|
||||
|
||||
@@ -18,8 +18,8 @@ _TRAY_DEFAULTS_COMMON: Dict[str, Any] = {
|
||||
"buf_kb": 256,
|
||||
"pool_size": 4,
|
||||
"cfproxy": True,
|
||||
"cfproxy_priority": True,
|
||||
"cfproxy_user_domain": "",
|
||||
"cfproxy_worker_domain": "",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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.pool_size = max(0, cfg.get("pool_size", DEFAULT_CONFIG["pool_size"]))
|
||||
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_worker_domain = cfg.get("cfproxy_worker_domain", DEFAULT_CONFIG["cfproxy_worker_domain"])
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ from itertools import zip_longest
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
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"
|
||||
RELEASES_LATEST_API = f"https://api.github.com/repos/{REPO}/releases/latest"
|
||||
@@ -135,7 +136,7 @@ def fetch_latest_release(
|
||||
method="GET",
|
||||
)
|
||||
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()
|
||||
new_etag = resp.headers.get("ETag")
|
||||
raw = resp.read().decode("utf-8", errors="replace")
|
||||
|
||||
15
windows.py
15
windows.py
@@ -8,8 +8,11 @@ import threading
|
||||
import time
|
||||
import webbrowser
|
||||
import winreg
|
||||
import tempfile
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from proxy.utils import build_github_opener
|
||||
|
||||
try:
|
||||
import pyperclip
|
||||
@@ -219,9 +222,6 @@ def update_ctk_form(
|
||||
|
||||
|
||||
def _perform_update(download_url: str, set_status=None) -> None:
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
def _step(msg: str) -> None:
|
||||
log.info("Update: %s", msg)
|
||||
if set_status:
|
||||
@@ -244,7 +244,14 @@ def _perform_update(download_url: str, set_status=None) -> None:
|
||||
os.close(fd)
|
||||
tmp_path = Path(tmp_name)
|
||||
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:
|
||||
_err(f"Не удалось скачать:\n{exc}")
|
||||
if tmp_path:
|
||||
|
||||
Reference in New Issue
Block a user