@emaxe/tuigram 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +17 -0
- package/CHANGELOG.md +10 -1
- package/CHANGELOG.ru.md +10 -1
- package/README.md +31 -0
- package/README.ru.md +31 -0
- package/package.json +1 -1
- package/src/cli/init.js +2 -1
- package/src/config.js +125 -0
- package/src/telegram/client.js +15 -7
- package/src/telegram/socket.js +224 -0
package/.env.example
CHANGED
|
@@ -27,3 +27,20 @@ AUTO_SCROLL=true
|
|
|
27
27
|
|
|
28
28
|
# Показывать уведомления о наборе текста (true/false)
|
|
29
29
|
SHOW_TYPING=true
|
|
30
|
+
|
|
31
|
+
# ── Proxy Settings ────────────────────────────────────────────────────────────
|
|
32
|
+
# Поддерживаются HTTP и SOCKS5 (как с авторизацией, так и без).
|
|
33
|
+
# Формат единого URL:
|
|
34
|
+
# PROXY_URL=http://127.0.0.1:8080
|
|
35
|
+
# PROXY_URL=http://user:password@proxy.example.com:8080
|
|
36
|
+
# PROXY_URL=socks5://127.0.0.1:1080
|
|
37
|
+
# PROXY_URL=socks5://user:password@127.0.0.1:1080
|
|
38
|
+
#
|
|
39
|
+
# Либо отдельными переменными:
|
|
40
|
+
# PROXY_TYPE=http
|
|
41
|
+
# PROXY_HOST=127.0.0.1
|
|
42
|
+
# PROXY_PORT=8080
|
|
43
|
+
# PROXY_USERNAME=
|
|
44
|
+
# PROXY_PASSWORD=
|
|
45
|
+
# PROXY_TIMEOUT=10
|
|
46
|
+
|
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
13
13
|
|
|
14
14
|
- English localization of the TUI and CLI interface strings.
|
|
15
15
|
|
|
16
|
+
## [1.1.0] — 2026-08-31
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Support for HTTP/HTTPS (via HTTP CONNECT) and SOCKS5/SOCKS4 proxies with optional username and password authentication.
|
|
21
|
+
- Proxy configuration options via `.env` (`PROXY_URL` or `PROXY_TYPE`/`PROXY_HOST`/`PROXY_PORT`/`PROXY_USERNAME`/`PROXY_PASSWORD`) and standard environment variables (`HTTPS_PROXY`, `HTTP_PROXY`, `ALL_PROXY`).
|
|
22
|
+
- Display of active proxy status with password masking in `tuigram paths`.
|
|
23
|
+
|
|
16
24
|
## [1.0.1] — 2026-08-29
|
|
17
25
|
|
|
18
26
|
Documentation and packaging only — no runtime changes.
|
|
@@ -103,6 +111,7 @@ First public release.
|
|
|
103
111
|
- A WCAG contrast test (3:1 threshold) applied to every theme **after** conversion
|
|
104
112
|
to xterm-256 — the colors are checked exactly as the user sees them.
|
|
105
113
|
|
|
106
|
-
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.0
|
|
114
|
+
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.1.0...HEAD
|
|
115
|
+
[1.1.0]: https://github.com/emaxe/tuigram/compare/v1.0.1...v1.1.0
|
|
107
116
|
[1.0.1]: https://github.com/emaxe/tuigram/compare/v1.0.0...v1.0.1
|
|
108
117
|
[1.0.0]: https://github.com/emaxe/tuigram/releases/tag/v1.0.0
|
package/CHANGELOG.ru.md
CHANGED
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
|
|
14
14
|
- Английская локализация строк интерфейса TUI и CLI.
|
|
15
15
|
|
|
16
|
+
## [1.1.0] — 2026-08-31
|
|
17
|
+
|
|
18
|
+
### Добавлено
|
|
19
|
+
|
|
20
|
+
- Поддержка работы через HTTP/HTTPS (HTTP CONNECT) и SOCKS5/SOCKS4 прокси, как с авторизацией по логину и паролю, так и без неё.
|
|
21
|
+
- Настройка прокси через `.env` (`PROXY_URL` или `PROXY_TYPE`/`PROXY_HOST`/`PROXY_PORT`/`PROXY_USERNAME`/`PROXY_PASSWORD`) и стандартные переменные окружения (`HTTPS_PROXY`, `HTTP_PROXY`, `ALL_PROXY`).
|
|
22
|
+
- Отображение активного прокси с маскированием пароля в выводе команды `tuigram paths`.
|
|
23
|
+
|
|
16
24
|
## [1.0.1] — 2026-08-29
|
|
17
25
|
|
|
18
26
|
Только документация и упаковка — поведение клиента не менялось.
|
|
@@ -105,6 +113,7 @@
|
|
|
105
113
|
- Тест контраста по WCAG (порог 3:1) для каждой темы — **после** конверсии в xterm-256,
|
|
106
114
|
то есть ровно в том виде, в каком цвет увидит пользователь.
|
|
107
115
|
|
|
108
|
-
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.0
|
|
116
|
+
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.1.0...HEAD
|
|
117
|
+
[1.1.0]: https://github.com/emaxe/tuigram/compare/v1.0.1...v1.1.0
|
|
109
118
|
[1.0.1]: https://github.com/emaxe/tuigram/compare/v1.0.0...v1.0.1
|
|
110
119
|
[1.0.0]: https://github.com/emaxe/tuigram/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -55,6 +55,7 @@ TuiGram lets you use Telegram entirely from the terminal: browse your dialog lis
|
|
|
55
55
|
- [Slash commands](#-slash-commands-in-the-input-box)
|
|
56
56
|
- [Sending files and images](#-sending-files-and-images)
|
|
57
57
|
- [Themes](#-themes)
|
|
58
|
+
- [Proxy configuration](#-proxy-configuration)
|
|
58
59
|
- [Command line usage (CLI)](#️-command-line-usage-cli)
|
|
59
60
|
- [Project structure](#-project-structure)
|
|
60
61
|
- [Security](#-security)
|
|
@@ -511,6 +512,35 @@ The contrast of every "text on background" pair is verified by an automated WCAG
|
|
|
511
512
|
|
|
512
513
|
---
|
|
513
514
|
|
|
515
|
+
## 🌐 Proxy configuration
|
|
516
|
+
|
|
517
|
+
TuiGram supports routing MTProto connections through HTTP (including HTTPS CONNECT) and SOCKS5/SOCKS4 proxies — both with and without username/password authentication.
|
|
518
|
+
|
|
519
|
+
Proxy options can be configured in `.env` (or via environment variables):
|
|
520
|
+
|
|
521
|
+
**Single URL:**
|
|
522
|
+
```env
|
|
523
|
+
PROXY_URL=http://127.0.0.1:8080
|
|
524
|
+
PROXY_URL=http://user:password@proxy.example.com:8080
|
|
525
|
+
PROXY_URL=socks5://127.0.0.1:1080
|
|
526
|
+
PROXY_URL=socks5://user:password@127.0.0.1:1080
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Or separate variables:**
|
|
530
|
+
```env
|
|
531
|
+
PROXY_TYPE=http # http, https, socks5, socks4
|
|
532
|
+
PROXY_HOST=127.0.0.1
|
|
533
|
+
PROXY_PORT=8080
|
|
534
|
+
PROXY_USERNAME=user # optional
|
|
535
|
+
PROXY_PASSWORD=password # optional
|
|
536
|
+
PROXY_TIMEOUT=10 # timeout in seconds (default: 10)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
Standard environment variables `HTTPS_PROXY`, `HTTP_PROXY`, and `ALL_PROXY` are also supported as fallbacks.
|
|
540
|
+
The active proxy status can be inspected using `tuigram paths`.
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
514
544
|
## 🛠️ Command line usage (CLI)
|
|
515
545
|
|
|
516
546
|
TuiGram can be used as a set of console utilities (when running from a repository
|
|
@@ -554,6 +584,7 @@ TuiGram/
|
|
|
554
584
|
│ ├── state.js # Reactive centralized state store
|
|
555
585
|
│ ├── telegram/
|
|
556
586
|
│ │ ├── client.js # MTProto client creation and management
|
|
587
|
+
│ │ ├── socket.js # MTProto network transport and proxy tunneling (HTTP/SOCKS5)
|
|
557
588
|
│ │ ├── auth.js # Interactive login wizard and 2FA
|
|
558
589
|
│ │ ├── dialogs.js # Fetching, filtering and searching dialogs
|
|
559
590
|
│ │ ├── messages.js # History loading, sending, editing, files, reactions
|
package/README.ru.md
CHANGED
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
- [Слэш-команды](#-слэш-команды-в-поле-ввода)
|
|
52
52
|
- [Отправка файлов и картинок](#-отправка-файлов-и-картинок)
|
|
53
53
|
- [Темы оформления](#-темы-оформления)
|
|
54
|
+
- [Прокси-сервер](#-прокси-сервер)
|
|
54
55
|
- [Использование через консоль (CLI)](#️-использование-через-консоль-cli)
|
|
55
56
|
- [Структура проекта](#-структура-проекта)
|
|
56
57
|
- [Безопасность](#-безопасность)
|
|
@@ -505,6 +506,35 @@ TUI_THEME=light # светлая
|
|
|
505
506
|
|
|
506
507
|
---
|
|
507
508
|
|
|
509
|
+
## 🌐 Прокси-сервер
|
|
510
|
+
|
|
511
|
+
TuiGram поддерживает маршрутизацию MTProto-соединений через HTTP (включая HTTPS CONNECT) и SOCKS5/SOCKS4 прокси — как с авторизацией по логину и паролю, так и без неё.
|
|
512
|
+
|
|
513
|
+
Настройки прокси задаются в `.env` (или через переменные окружения):
|
|
514
|
+
|
|
515
|
+
**Единый URL:**
|
|
516
|
+
```env
|
|
517
|
+
PROXY_URL=http://127.0.0.1:8080
|
|
518
|
+
PROXY_URL=http://user:password@proxy.example.com:8080
|
|
519
|
+
PROXY_URL=socks5://127.0.0.1:1080
|
|
520
|
+
PROXY_URL=socks5://user:password@127.0.0.1:1080
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
**Либо отдельными переменными:**
|
|
524
|
+
```env
|
|
525
|
+
PROXY_TYPE=http # http, https, socks5, socks4
|
|
526
|
+
PROXY_HOST=127.0.0.1
|
|
527
|
+
PROXY_PORT=8080
|
|
528
|
+
PROXY_USERNAME=user # опционально
|
|
529
|
+
PROXY_PASSWORD=password # опционально
|
|
530
|
+
PROXY_TIMEOUT=10 # таймаут в секундах (по умолчанию 10)
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
Также в качестве фолбэка поддерживаются стандартные переменные `HTTPS_PROXY`, `HTTP_PROXY` и `ALL_PROXY`.
|
|
534
|
+
Проверить активное состояние прокси можно командой `tuigram paths`.
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
508
538
|
## 🛠️ Использование через консоль (CLI)
|
|
509
539
|
|
|
510
540
|
TuiGram можно запускать в режиме консольных утилит (при запуске из клона
|
|
@@ -548,6 +578,7 @@ TuiGram/
|
|
|
548
578
|
│ ├── state.js # Реактивное централизованное хранилище состояния
|
|
549
579
|
│ ├── telegram/
|
|
550
580
|
│ │ ├── client.js # Создание и управление MTProto клиентом
|
|
581
|
+
│ │ ├── socket.js # Сетевой транспорт MTProto и туннелирование прокси (HTTP/SOCKS5)
|
|
551
582
|
│ │ ├── auth.js # Интерактивный логин-визард и 2FA
|
|
552
583
|
│ │ ├── dialogs.js # Получение, фильтрация и поиск диалогов
|
|
553
584
|
│ │ ├── messages.js # Загрузка истории, отправка, правка, файлы, реакции
|
package/package.json
CHANGED
package/src/cli/init.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import input from "input";
|
|
4
4
|
import { bold, cyan, green, yellow, dim } from "colorette";
|
|
5
|
-
import { config } from "../config.js";
|
|
5
|
+
import { config, formatProxyUrl } from "../config.js";
|
|
6
6
|
|
|
7
7
|
/** Сколько раз переспрашивать значение, прежде чем сдаться. */
|
|
8
8
|
const MAX_ATTEMPTS = 3;
|
|
@@ -193,5 +193,6 @@ export function cmdPaths() {
|
|
|
193
193
|
console.log(` Сессия: ${config.sessionPath} ${sessionExists ? green("(есть)") : dim("(нет)")}`);
|
|
194
194
|
console.log(` Загрузки: ${config.downloadsDir}`);
|
|
195
195
|
console.log(` Ключи API: ${config.apiId && config.apiHash ? green("заданы") : yellow("не заданы — запустите tuigram init")}`);
|
|
196
|
+
console.log(` Прокси: ${config.proxy ? green(formatProxyUrl(config.proxy)) : dim("не используется")}`);
|
|
196
197
|
console.log("");
|
|
197
198
|
}
|
package/src/config.js
CHANGED
|
@@ -110,6 +110,129 @@ function readVersion() {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* @typedef {Object} ProxyConfig
|
|
115
|
+
* @property {"http"|"https"|"socks5"|"socks4"} type - Протокол прокси
|
|
116
|
+
* @property {string} host - Адрес сервера прокси
|
|
117
|
+
* @property {number} port - Порт сервера прокси
|
|
118
|
+
* @property {string} [username] - Логин пользователя
|
|
119
|
+
* @property {string} [password] - Пароль пользователя
|
|
120
|
+
* @property {number} [timeout] - Таймаут в секундах
|
|
121
|
+
* @property {string} ip - Хост прокси (для совместимости с MTProto)
|
|
122
|
+
* @property {4|5} [socksType] - Тип SOCKS-прокси
|
|
123
|
+
* @property {boolean} [http] - Флаг HTTP-прокси
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Нормализует тип прокси из строки протокола.
|
|
128
|
+
* @param {string} [raw]
|
|
129
|
+
* @returns {"http"|"https"|"socks5"|"socks4"}
|
|
130
|
+
*/
|
|
131
|
+
function normalizeProxyType(raw) {
|
|
132
|
+
const clean = String(raw || "").toLowerCase().replace(/:$/, "").trim();
|
|
133
|
+
if (clean === "https") return "https";
|
|
134
|
+
if (clean === "socks4" || clean === "socks4a") return "socks4";
|
|
135
|
+
if (clean === "socks5" || clean === "socks5h" || clean === "socks") return "socks5";
|
|
136
|
+
return "http";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Разбирает параметры прокси из переменных окружения или переданного объекта.
|
|
141
|
+
* Поддерживает URL-формат (PROXY_URL, HTTPS_PROXY, HTTP_PROXY, ALL_PROXY)
|
|
142
|
+
* и отдельные переменные (PROXY_TYPE, PROXY_HOST, PROXY_PORT, PROXY_USERNAME, PROXY_PASSWORD).
|
|
143
|
+
* @param {Record<string, string|undefined>} [env=process.env]
|
|
144
|
+
* @returns {ProxyConfig|null}
|
|
145
|
+
*/
|
|
146
|
+
export function parseProxyConfig(env = process.env) {
|
|
147
|
+
const rawUrl = env.PROXY_URL || env.proxy_url || (!env.PROXY_HOST && !env.PROXY_IP && !env.proxy_host && !env.proxy_ip
|
|
148
|
+
? (env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy || env.ALL_PROXY || env.all_proxy)
|
|
149
|
+
: undefined);
|
|
150
|
+
|
|
151
|
+
let type = "http";
|
|
152
|
+
let host = "";
|
|
153
|
+
let port = 0;
|
|
154
|
+
let username = "";
|
|
155
|
+
let password = "";
|
|
156
|
+
|
|
157
|
+
if (rawUrl && typeof rawUrl === "string" && rawUrl.trim()) {
|
|
158
|
+
let urlStr = rawUrl.trim();
|
|
159
|
+
// Добавляем схему по умолчанию, если передан host:port без префикса
|
|
160
|
+
if (!/^[a-zA-Z0-9+-.]+:\/\//.test(urlStr)) {
|
|
161
|
+
const defaultScheme = (env.PROXY_TYPE || "http").toLowerCase().replace(/:$/, "");
|
|
162
|
+
urlStr = `${defaultScheme}://${urlStr}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const parsed = new URL(urlStr);
|
|
167
|
+
type = normalizeProxyType(parsed.protocol);
|
|
168
|
+
host = parsed.hostname.replace(/^\[(.*)\]$/, "$1");
|
|
169
|
+
port = parsed.port ? parseInt(parsed.port, 10) : (type === "https" ? 443 : (type.startsWith("socks") ? 1080 : 8080));
|
|
170
|
+
if (parsed.username) username = decodeURIComponent(parsed.username);
|
|
171
|
+
if (parsed.password) password = decodeURIComponent(parsed.password);
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
} else if (env.PROXY_HOST || env.PROXY_IP) {
|
|
176
|
+
type = normalizeProxyType(env.PROXY_TYPE || "http");
|
|
177
|
+
host = String(env.PROXY_HOST || env.PROXY_IP).trim();
|
|
178
|
+
const defaultPort = type === "https" ? 443 : (type.startsWith("socks") ? 1080 : 8080);
|
|
179
|
+
port = env.PROXY_PORT ? parseInt(String(env.PROXY_PORT).trim(), 10) : defaultPort;
|
|
180
|
+
username = String(env.PROXY_USERNAME || env.PROXY_USER || "").trim();
|
|
181
|
+
password = String(env.PROXY_PASSWORD || env.PROXY_PASS || "").trim();
|
|
182
|
+
} else {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!host || !port || isNaN(port) || port <= 0 || port > 65535) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const rawTimeout = env.PROXY_TIMEOUT ? parseInt(String(env.PROXY_TIMEOUT).trim(), 10) : 10;
|
|
191
|
+
const timeout = isNaN(rawTimeout) || rawTimeout <= 0 ? 10 : rawTimeout;
|
|
192
|
+
|
|
193
|
+
/** @type {ProxyConfig} */
|
|
194
|
+
const result = {
|
|
195
|
+
type,
|
|
196
|
+
host,
|
|
197
|
+
port,
|
|
198
|
+
ip: host,
|
|
199
|
+
timeout,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (username) result.username = username;
|
|
203
|
+
if (password) result.password = password;
|
|
204
|
+
|
|
205
|
+
if (type === "socks5") {
|
|
206
|
+
result.socksType = 5;
|
|
207
|
+
} else if (type === "socks4") {
|
|
208
|
+
result.socksType = 4;
|
|
209
|
+
} else {
|
|
210
|
+
result.http = true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Форматирует конфигурацию прокси в строку для вывода в терминал.
|
|
218
|
+
* Пароль маскируется звездочками в целях безопасности.
|
|
219
|
+
* @param {ProxyConfig|null} proxy
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
export function formatProxyUrl(proxy) {
|
|
223
|
+
if (!proxy) return "не используется";
|
|
224
|
+
|
|
225
|
+
let auth = "";
|
|
226
|
+
if (proxy.username) {
|
|
227
|
+
auth = proxy.password
|
|
228
|
+
? `${encodeURIComponent(proxy.username)}:***@`
|
|
229
|
+
: `${encodeURIComponent(proxy.username)}@`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const host = proxy.host.includes(":") ? `[${proxy.host}]` : proxy.host;
|
|
233
|
+
return `${proxy.type}://${auth}${host}:${proxy.port}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
113
236
|
export const config = {
|
|
114
237
|
packageRoot,
|
|
115
238
|
/** @deprecated оставлено для обратной совместимости, равно packageRoot */
|
|
@@ -129,6 +252,8 @@ export const config = {
|
|
|
129
252
|
autoScroll: String(process.env.AUTO_SCROLL || "true").toLowerCase() === "true",
|
|
130
253
|
showTyping: String(process.env.SHOW_TYPING || "true").toLowerCase() === "true",
|
|
131
254
|
|
|
255
|
+
proxy: parseProxyConfig(),
|
|
256
|
+
|
|
132
257
|
/**
|
|
133
258
|
* Путь к файлу сессии.
|
|
134
259
|
* @returns {string}
|
package/src/telegram/client.js
CHANGED
|
@@ -4,6 +4,7 @@ import { StringSession } from "teleproto/sessions/index.js";
|
|
|
4
4
|
import { Logger, LogLevel } from "teleproto/extensions/Logger.js";
|
|
5
5
|
import { config } from "../config.js";
|
|
6
6
|
import { saveSessionFile, readFileSafe } from "../utils/storage.js";
|
|
7
|
+
import { TuiGramNetSockets } from "./socket.js";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Читает сохранённую строку сессии.
|
|
@@ -42,17 +43,24 @@ export function clearSession() {
|
|
|
42
43
|
export function buildClient(sessionString = readSession()) {
|
|
43
44
|
config.assertCredentials();
|
|
44
45
|
|
|
46
|
+
const clientParams = {
|
|
47
|
+
connectionRetries: 10,
|
|
48
|
+
autoReconnect: true,
|
|
49
|
+
retryDelay: 1500,
|
|
50
|
+
baseLogger: new Logger(LogLevel.ERROR),
|
|
51
|
+
useWSS: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (config.proxy) {
|
|
55
|
+
clientParams.proxy = config.proxy;
|
|
56
|
+
clientParams.networkSocket = TuiGramNetSockets;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
const client = new TelegramClient(
|
|
46
60
|
new StringSession(sessionString),
|
|
47
61
|
config.apiId,
|
|
48
62
|
config.apiHash,
|
|
49
|
-
|
|
50
|
-
connectionRetries: 10,
|
|
51
|
-
autoReconnect: true,
|
|
52
|
-
retryDelay: 1500,
|
|
53
|
-
baseLogger: new Logger(LogLevel.ERROR),
|
|
54
|
-
useWSS: false,
|
|
55
|
-
}
|
|
63
|
+
clientParams
|
|
56
64
|
);
|
|
57
65
|
|
|
58
66
|
return client;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Сетевой транспорт для MTProto с поддержкой прокси (HTTP CONNECT, SOCKS5, SOCKS4).
|
|
3
|
+
*/
|
|
4
|
+
import http from "node:http";
|
|
5
|
+
import https from "node:https";
|
|
6
|
+
import net from "node:net";
|
|
7
|
+
import { PromisedNetSockets } from "teleproto/extensions/PromisedNetSockets.js";
|
|
8
|
+
import { SocksClient } from "socks";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Создаёт TCP-туннель через HTTP/HTTPS прокси методом HTTP CONNECT.
|
|
12
|
+
* Поддерживает как прокси с Basic-авторизацией, так и без неё.
|
|
13
|
+
* @param {object} params
|
|
14
|
+
* @param {boolean} [params.isHttps=false] подключение к прокси-серверу по TLS
|
|
15
|
+
* @param {string} params.proxyHost адрес прокси-сервера
|
|
16
|
+
* @param {number} params.proxyPort порт прокси-сервера
|
|
17
|
+
* @param {string} params.targetHost адрес целевого сервера Telegram MTProto
|
|
18
|
+
* @param {number} params.targetPort порт целевого сервера Telegram MTProto
|
|
19
|
+
* @param {string} [params.username] логин для авторизации на прокси
|
|
20
|
+
* @param {string} [params.password] пароль для авторизации на прокси
|
|
21
|
+
* @param {number} [params.timeout=10000] таймаут установки соединения в миллисекундах
|
|
22
|
+
* @returns {Promise<import("node:net").Socket>}
|
|
23
|
+
*/
|
|
24
|
+
export function createHttpConnectSocket({
|
|
25
|
+
isHttps = false,
|
|
26
|
+
proxyHost,
|
|
27
|
+
proxyPort,
|
|
28
|
+
targetHost,
|
|
29
|
+
targetPort,
|
|
30
|
+
username,
|
|
31
|
+
password,
|
|
32
|
+
timeout = 10000,
|
|
33
|
+
}) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const clientModule = isHttps ? https : http;
|
|
36
|
+
|
|
37
|
+
const headers = {
|
|
38
|
+
Host: `${targetHost}:${targetPort}`,
|
|
39
|
+
"User-Agent": "TuiGram",
|
|
40
|
+
"Proxy-Connection": "Keep-Alive",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
if (username || password) {
|
|
44
|
+
const auth = Buffer.from(`${username || ""}:${password || ""}`).toString("base64");
|
|
45
|
+
headers["Proxy-Authorization"] = `Basic ${auth}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const req = clientModule.request({
|
|
49
|
+
host: proxyHost,
|
|
50
|
+
port: proxyPort,
|
|
51
|
+
method: "CONNECT",
|
|
52
|
+
path: `${targetHost}:${targetPort}`,
|
|
53
|
+
headers,
|
|
54
|
+
timeout,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
let finished = false;
|
|
58
|
+
|
|
59
|
+
const cleanup = () => {
|
|
60
|
+
req.removeAllListeners();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
req.on("connect", (res, socket, head) => {
|
|
64
|
+
if (finished) return;
|
|
65
|
+
finished = true;
|
|
66
|
+
cleanup();
|
|
67
|
+
|
|
68
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
69
|
+
// Если в заголовке ответа уже были данные следующего протокола, возвращаем их в сокет
|
|
70
|
+
if (head && head.length > 0) {
|
|
71
|
+
socket.unshift(head);
|
|
72
|
+
}
|
|
73
|
+
resolve(socket);
|
|
74
|
+
} else if (res.statusCode === 407) {
|
|
75
|
+
socket.destroy();
|
|
76
|
+
reject(new Error("Ошибка авторизации на HTTP-прокси (407 Proxy Authentication Required)"));
|
|
77
|
+
} else {
|
|
78
|
+
socket.destroy();
|
|
79
|
+
reject(new Error(`HTTP-прокси вернул код ошибки: ${res.statusCode} ${res.statusMessage || ""}`.trim()));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
req.on("response", (res) => {
|
|
84
|
+
if (finished) return;
|
|
85
|
+
finished = true;
|
|
86
|
+
cleanup();
|
|
87
|
+
if (res.statusCode === 407) {
|
|
88
|
+
reject(new Error("Ошибка авторизации на HTTP-прокси (407 Proxy Authentication Required)"));
|
|
89
|
+
} else {
|
|
90
|
+
reject(new Error(`HTTP-прокси вернул код ошибки: ${res.statusCode} ${res.statusMessage || ""}`.trim()));
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
req.on("timeout", () => {
|
|
95
|
+
if (finished) return;
|
|
96
|
+
finished = true;
|
|
97
|
+
cleanup();
|
|
98
|
+
req.destroy(new Error(`Таймаут подключения к HTTP-прокси (${proxyHost}:${proxyPort})`));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
req.on("error", (err) => {
|
|
102
|
+
if (finished) return;
|
|
103
|
+
finished = true;
|
|
104
|
+
cleanup();
|
|
105
|
+
reject(new Error(`Не удалось подключиться к HTTP-прокси (${proxyHost}:${proxyPort}): ${err.message}`));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
req.end();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Расширенный сокет для teleproto с поддержкой HTTP CONNECT и SOCKS5/4 туннелирования.
|
|
114
|
+
*/
|
|
115
|
+
export class TuiGramNetSockets extends PromisedNetSockets {
|
|
116
|
+
/**
|
|
117
|
+
* @param {object} [proxy]
|
|
118
|
+
* @param {number} [keepAliveInterval]
|
|
119
|
+
*/
|
|
120
|
+
constructor(proxy, keepAliveInterval) {
|
|
121
|
+
// Передаём undefined в базовый класс, чтобы обойти валидацию socksType для HTTP-прокси
|
|
122
|
+
super(undefined, keepAliveInterval);
|
|
123
|
+
this.proxy = proxy;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Устанавливает сетевое соединение с дата-центром Telegram.
|
|
128
|
+
* @param {number} port
|
|
129
|
+
* @param {string} ip
|
|
130
|
+
* @returns {Promise<this>}
|
|
131
|
+
*/
|
|
132
|
+
async connect(port, ip) {
|
|
133
|
+
this.chunks = [];
|
|
134
|
+
this.headOffset = 0;
|
|
135
|
+
this.available = 0;
|
|
136
|
+
let connected = false;
|
|
137
|
+
|
|
138
|
+
if (this.proxy) {
|
|
139
|
+
const proxyType = (this.proxy.type || "").toLowerCase();
|
|
140
|
+
const host = this.proxy.host || this.proxy.ip;
|
|
141
|
+
const proxyPort = Number(this.proxy.port);
|
|
142
|
+
const timeout = (this.proxy.timeout || 10) * 1000;
|
|
143
|
+
|
|
144
|
+
if (proxyType === "socks5" || proxyType === "socks4" || this.proxy.socksType) {
|
|
145
|
+
const socksType = this.proxy.socksType || (proxyType === "socks4" ? 4 : 5);
|
|
146
|
+
const info = await SocksClient.createConnection({
|
|
147
|
+
proxy: {
|
|
148
|
+
host,
|
|
149
|
+
port: proxyPort,
|
|
150
|
+
type: socksType,
|
|
151
|
+
userId: this.proxy.username,
|
|
152
|
+
password: this.proxy.password,
|
|
153
|
+
},
|
|
154
|
+
command: "connect",
|
|
155
|
+
timeout,
|
|
156
|
+
destination: {
|
|
157
|
+
host: ip,
|
|
158
|
+
port: port,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
this.client = info.socket;
|
|
162
|
+
connected = true;
|
|
163
|
+
} else if (proxyType === "http" || proxyType === "https" || this.proxy.http) {
|
|
164
|
+
this.client = await createHttpConnectSocket({
|
|
165
|
+
isHttps: proxyType === "https",
|
|
166
|
+
proxyHost: host,
|
|
167
|
+
proxyPort: proxyPort,
|
|
168
|
+
targetHost: ip,
|
|
169
|
+
targetPort: port,
|
|
170
|
+
username: this.proxy.username,
|
|
171
|
+
password: this.proxy.password,
|
|
172
|
+
timeout,
|
|
173
|
+
});
|
|
174
|
+
connected = true;
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error(`Неподдерживаемый тип прокси: ${this.proxy.type}`);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
this.client = new net.Socket();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.canRead = new Promise((resolve) => {
|
|
183
|
+
this.resolveRead = resolve;
|
|
184
|
+
});
|
|
185
|
+
this.closed = false;
|
|
186
|
+
|
|
187
|
+
return new Promise((resolve, reject) => {
|
|
188
|
+
if (!this.client) {
|
|
189
|
+
return reject(new Error("Сетевой сокет не инициализирован"));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const tune = (socket) => {
|
|
193
|
+
socket.setNoDelay(true);
|
|
194
|
+
socket.setKeepAlive(this.keepAliveInterval > 0, Math.max(0, this.keepAliveInterval));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (connected) {
|
|
198
|
+
tune(this.client);
|
|
199
|
+
this.receive();
|
|
200
|
+
resolve(this);
|
|
201
|
+
} else {
|
|
202
|
+
this.client.connect(port, ip, () => {
|
|
203
|
+
tune(this.client);
|
|
204
|
+
this.receive();
|
|
205
|
+
resolve(this);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.client.on("error", reject);
|
|
210
|
+
this.client.on("close", () => {
|
|
211
|
+
if (this.client && this.client.destroyed) {
|
|
212
|
+
if (this.resolveRead) {
|
|
213
|
+
this.resolveRead(false);
|
|
214
|
+
}
|
|
215
|
+
this.closed = true;
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
toString() {
|
|
222
|
+
return "TuiGramNetSocket";
|
|
223
|
+
}
|
|
224
|
+
}
|