teleproto 1.225.1 → 1.225.3
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/{LICENSE.txt → LICENSE} +3 -6
- package/README.md +67 -102
- package/Version.d.ts +1 -1
- package/Version.js +1 -1
- package/client/TelegramClient.d.ts +16 -0
- package/client/TelegramClient.js +18 -0
- package/client/auth.d.ts +2 -0
- package/client/auth.js +15 -0
- package/client/users.js +1 -1
- package/network/MTProtoState.d.ts +14 -18
- package/network/MTProtoState.js +14 -18
- package/network/connection/TCPMTProxy.d.ts +43 -52
- package/network/connection/TCPMTProxy.js +360 -93
- package/package.json +5 -7
- package/sessions/Abstract.d.ts +3 -3
- package/sessions/Memory.js +9 -1
- package/sessions/StoreSession.d.ts +1 -0
- package/sessions/StoreSession.js +4 -0
- package/sessions/StringSession.d.ts +1 -0
- package/sessions/StringSession.js +4 -0
package/{LICENSE.txt → LICENSE}
RENAMED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
4
|
-
|
|
5
|
-
This project, teleproto, is an independent work originally derived from GramJS.
|
|
6
|
-
GramJS is an open source project licensed under the MIT License.
|
|
7
|
-
Portions of teleproto are adapted from GramJS and remain subject to the MIT terms.
|
|
3
|
+
Copyright (c) 2019 GramJS
|
|
4
|
+
Copyright (c) 2025-present sanyok12345
|
|
8
5
|
|
|
9
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
7
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -22,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
22
19
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
20
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
21
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
-
SOFTWARE.
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,150 +1,115 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://www.npmjs.com/package/teleproto)
|
|
2
|
+
[](./LICENSE)
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
<img src="https://img.shields.io/npm/v/teleproto" alt="npm version">
|
|
5
|
-
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen" alt="node version">
|
|
6
|
-
<img src="https://img.shields.io/badge/language-TypeScript-3178c6" alt="typescript">
|
|
7
|
-
<img src="https://img.shields.io/badge/license-MIT-blue" alt="license">
|
|
8
|
-
<a href="https://t.me/teleproto"><img src="https://img.shields.io/badge/Telegram-Chat-26A5E4?logo=telegram" alt="telegram chat"></a>
|
|
9
|
-
</p>
|
|
4
|
+
This project was forked from the open source GramJS project in 2025 and is now developed independently.
|
|
10
5
|
|
|
11
|
-
|
|
12
|
-
`teleproto` is a high-performance fork of GramJS focused on clean API ergonomics, runtime reliability, and up-to-date Telegram layers.
|
|
6
|
+
This README is just a fast *quick start*. Ongoing discussion happens in the [Telegram chat](https://t.me/teleproto).
|
|
13
7
|
|
|
14
|
-
|
|
8
|
+
# What is teleproto?
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
- **TypeScript-friendly**: Strong typings across client methods, events, sessions, and TL objects.
|
|
18
|
-
- **Session options**: Use `StringSession` for portability or `StoreSession` for local persistence.
|
|
19
|
-
- **Event system**: Handle updates with builders like `NewMessage`, `EditedMessage`, `CallbackQuery`, and more.
|
|
20
|
-
- **Examples included**: Ready-to-run scripts in `teleproto_examples`.
|
|
10
|
+
teleproto is a TypeScript client for Telegram's MTProto API — the same protocol Telegram's own apps speak. Through it, your code gets the full account surface: userbots, multi-account automation, file transfer, raw TL invocation when you need it. If you only need to push notifications from a bot, the official Bot API is simpler; teleproto exists for everything *beyond* that.
|
|
21
11
|
|
|
22
|
-
|
|
12
|
+
# Installing teleproto
|
|
23
13
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
% npm install teleproto
|
|
15
|
+
|
|
16
|
+
Pure JavaScript, no native build step — installs cleanly on Alpine, ARM, and serverless runtimes.
|
|
27
17
|
|
|
28
|
-
|
|
18
|
+
# Connecting to Telegram
|
|
29
19
|
|
|
30
|
-
|
|
31
|
-
2. Create an app in **API development tools**
|
|
32
|
-
3. Copy your `api_id` and `api_hash`
|
|
20
|
+
You need an `api_id` and `api_hash` from <https://my.telegram.org>. Then:
|
|
33
21
|
|
|
34
22
|
```ts
|
|
35
23
|
import { TelegramClient } from "teleproto";
|
|
36
24
|
import { StringSession } from "teleproto/sessions";
|
|
37
|
-
import
|
|
25
|
+
import { createInterface } from "node:readline/promises";
|
|
26
|
+
|
|
27
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
38
28
|
|
|
39
|
-
const apiId =
|
|
40
|
-
const apiHash = "
|
|
29
|
+
const apiId = 0; // from https://my.telegram.org
|
|
30
|
+
const apiHash = ""; // from https://my.telegram.org
|
|
41
31
|
const session = new StringSession("");
|
|
42
32
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
output: process.stdout,
|
|
33
|
+
const client = new TelegramClient(session, apiId, apiHash, {
|
|
34
|
+
connectionRetries: 5,
|
|
46
35
|
});
|
|
47
36
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
await client.start({
|
|
57
|
-
phoneNumber: async () => await ask("Phone number: "),
|
|
58
|
-
password: async () => await ask("2FA password (if enabled): "),
|
|
59
|
-
phoneCode: async () => await ask("Code from Telegram: "),
|
|
60
|
-
onError: (err) => console.error(err),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
console.log("Connected as:", (await client.getMe())?.username || "unknown");
|
|
64
|
-
console.log("String session:\n", client.session.save());
|
|
37
|
+
await client.start({
|
|
38
|
+
phoneNumber: () => rl.question("Phone: "),
|
|
39
|
+
password: () => rl.question("2FA password: "),
|
|
40
|
+
phoneCode: () => rl.question("Code: "),
|
|
41
|
+
onError: console.error,
|
|
42
|
+
});
|
|
65
43
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
rl.close();
|
|
69
|
-
}
|
|
44
|
+
console.log(await client.getMe());
|
|
45
|
+
console.log("Session string:", client.session.save());
|
|
70
46
|
|
|
71
|
-
|
|
47
|
+
rl.close();
|
|
72
48
|
```
|
|
73
49
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Use `StringSession` when you want to store auth as a single string:
|
|
50
|
+
The session string is your saved login. Drop it back into `new StringSession(saved)` next time and skip the auth flow entirely.
|
|
77
51
|
|
|
78
|
-
|
|
79
|
-
import { StringSession } from "teleproto/sessions";
|
|
80
|
-
const session = new StringSession("");
|
|
81
|
-
```
|
|
52
|
+
# Sending and receiving
|
|
82
53
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
import { StoreSession } from "teleproto/sessions";
|
|
87
|
-
const session = new StoreSession("teleproto_session");
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Events
|
|
54
|
+
Send a message, listen for incoming ones:
|
|
91
55
|
|
|
92
56
|
```ts
|
|
93
57
|
import { NewMessage } from "teleproto/events";
|
|
94
58
|
|
|
59
|
+
await client.sendMessage("me", { message: "hello from teleproto" });
|
|
60
|
+
|
|
95
61
|
client.addEventHandler(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (/^hello$/i.test(text.trim())) {
|
|
99
|
-
await event.message.reply({ message: "Hi there!" });
|
|
100
|
-
}
|
|
101
|
-
},
|
|
102
|
-
new NewMessage({})
|
|
62
|
+
(event) => console.log(event.message.message),
|
|
63
|
+
new NewMessage({}),
|
|
103
64
|
);
|
|
104
65
|
```
|
|
105
66
|
|
|
106
|
-
|
|
67
|
+
# Raw MTProto API
|
|
68
|
+
|
|
69
|
+
Every method in Telegram's TL schema is callable directly through `Api.*`. teleproto follows the schema layer-for-layer, so what Telegram adds is usually available here within days.
|
|
107
70
|
|
|
108
71
|
```ts
|
|
109
72
|
import { Api } from "teleproto";
|
|
110
73
|
|
|
111
|
-
const
|
|
112
|
-
new Api.help.GetConfig()
|
|
113
|
-
);
|
|
114
|
-
console.log(result);
|
|
74
|
+
const config = await client.invoke(new Api.help.GetConfig());
|
|
115
75
|
```
|
|
116
76
|
|
|
117
|
-
|
|
77
|
+
# Versioning
|
|
118
78
|
|
|
119
|
-
|
|
79
|
+
teleproto uses a three-part version `MAJOR.LAYER.PATCH`:
|
|
120
80
|
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
|
|
124
|
-
-
|
|
81
|
+
- **MAJOR** — bumped on breaking API changes in teleproto itself.
|
|
82
|
+
- **LAYER** — the Telegram TL schema layer the release ships against
|
|
83
|
+
(e.g. `1.225.x` ships layer 225).
|
|
84
|
+
- **PATCH** — fixes and non-breaking improvements within the same layer.
|
|
125
85
|
|
|
126
|
-
|
|
86
|
+
This stays compatible with npm's range syntax:
|
|
127
87
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
88
|
+
- `^1.225.0` accepts new layers and patches — recommended default.
|
|
89
|
+
- `~1.225.0` sticks to layer 225 only; useful if you depend on
|
|
90
|
+
schema specifics that newer layers might change.
|
|
91
|
+
- `1.225.1` is an exact pin.
|
|
92
|
+
|
|
93
|
+
# Examples
|
|
94
|
+
|
|
95
|
+
Runnable scripts live in [teleproto_examples/](teleproto_examples/):
|
|
96
|
+
|
|
97
|
+
- `print_updates.ts` — log every update the client receives
|
|
98
|
+
- `print_messages.ts` — listen for new messages only
|
|
99
|
+
- `replier.ts` — auto-reply pattern for bots and userbots
|
|
100
|
+
- `interactive_terminal.ts` — REPL against a live client
|
|
131
101
|
|
|
132
|
-
|
|
102
|
+
Each is self-contained. Set your credentials at the top and run:
|
|
133
103
|
|
|
134
|
-
-
|
|
135
|
-
- [GitHub Issues](https://github.com/sanyok12345/teleproto/issues) — bug reports and feature requests
|
|
104
|
+
% npx ts-node --transpile-only teleproto_examples/print_updates.ts
|
|
136
105
|
|
|
137
|
-
|
|
106
|
+
# Code contributions
|
|
138
107
|
|
|
139
|
-
|
|
140
|
-
Every donation keeps the lights on and the commits flowing.
|
|
108
|
+
Please see [CONTRIBUTING.md][1].
|
|
141
109
|
|
|
142
|
-
|
|
143
|
-
|---------|---------|
|
|
144
|
-
| **TON** | `sanyok12345.ton` |
|
|
145
|
-
| **TRX** | `TXCtN1UrxST9ovq5tDN3Zb96eNF4164nK5` |
|
|
146
|
-
| **SOL** | `B3XcKmAR9aG2nBiWSMDe76GGa55hPNgLQ92QR2SjRR6F` |
|
|
110
|
+
# License
|
|
147
111
|
|
|
148
|
-
|
|
112
|
+
teleproto is distributed under the [MIT License][2].
|
|
149
113
|
|
|
150
|
-
|
|
114
|
+
[1]: ./CONTRIBUTING.md
|
|
115
|
+
[2]: ./LICENSE
|
package/Version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "1.225.
|
|
1
|
+
export declare const version = "1.225.3";
|
package/Version.js
CHANGED
|
@@ -85,6 +85,22 @@ export declare class TelegramClient extends TelegramBaseClient {
|
|
|
85
85
|
* @return boolean (true of authorized else false)
|
|
86
86
|
*/
|
|
87
87
|
checkAuthorization(): Promise<boolean>;
|
|
88
|
+
/**
|
|
89
|
+
* Logs out the currently authenticated user/bot. Invalidates the session
|
|
90
|
+
* on Telegram's side (via `auth.LogOut`), disconnects the client and wipes
|
|
91
|
+
* local session data so a subsequent `session.save()` returns nothing.
|
|
92
|
+
*
|
|
93
|
+
* After this call the client is disconnected and event handlers stop
|
|
94
|
+
* firing; reconnecting requires going through `start`/`signInUser` again.
|
|
95
|
+
*
|
|
96
|
+
* @returns `true` if the server-side log out succeeded, `false` if the
|
|
97
|
+
* `auth.LogOut` call failed. Local session data is wiped in both cases.
|
|
98
|
+
* @example
|
|
99
|
+
* ```ts
|
|
100
|
+
* await client.logOut();
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
logOut(): Promise<boolean>;
|
|
88
104
|
/**
|
|
89
105
|
* Logs in as a user. Should only be used when not already logged in.<br/>
|
|
90
106
|
* This method will send a code when needed.<br/>
|
package/client/TelegramClient.js
CHANGED
|
@@ -127,6 +127,24 @@ class TelegramClient extends telegramBaseClient_1.TelegramBaseClient {
|
|
|
127
127
|
checkAuthorization() {
|
|
128
128
|
return authMethods.checkAuthorization(this);
|
|
129
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Logs out the currently authenticated user/bot. Invalidates the session
|
|
132
|
+
* on Telegram's side (via `auth.LogOut`), disconnects the client and wipes
|
|
133
|
+
* local session data so a subsequent `session.save()` returns nothing.
|
|
134
|
+
*
|
|
135
|
+
* After this call the client is disconnected and event handlers stop
|
|
136
|
+
* firing; reconnecting requires going through `start`/`signInUser` again.
|
|
137
|
+
*
|
|
138
|
+
* @returns `true` if the server-side log out succeeded, `false` if the
|
|
139
|
+
* `auth.LogOut` call failed. Local session data is wiped in both cases.
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* await client.logOut();
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
logOut() {
|
|
146
|
+
return authMethods.logOut(this);
|
|
147
|
+
}
|
|
130
148
|
/**
|
|
131
149
|
* Logs in as a user. Should only be used when not already logged in.<br/>
|
|
132
150
|
* This method will send a code when needed.<br/>
|
package/client/auth.d.ts
CHANGED
|
@@ -115,6 +115,8 @@ export declare function start(client: TelegramClient, authParams: UserAuthParams
|
|
|
115
115
|
/** @hidden */
|
|
116
116
|
export declare function checkAuthorization(client: TelegramClient): Promise<boolean>;
|
|
117
117
|
/** @hidden */
|
|
118
|
+
export declare function logOut(client: TelegramClient): Promise<boolean>;
|
|
119
|
+
/** @hidden */
|
|
118
120
|
export declare function signInUser(client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams): Promise<Api.TypeUser>;
|
|
119
121
|
/** @hidden */
|
|
120
122
|
export declare function signInUserWithQrCode(client: TelegramClient, apiCredentials: ApiCredentials, authParams: QrCodeAuthParams): Promise<Api.TypeUser>;
|
package/client/auth.js
CHANGED
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.start = start;
|
|
37
37
|
exports.checkAuthorization = checkAuthorization;
|
|
38
|
+
exports.logOut = logOut;
|
|
38
39
|
exports.signInUser = signInUser;
|
|
39
40
|
exports.signInUserWithQrCode = signInUserWithQrCode;
|
|
40
41
|
exports.sendCode = sendCode;
|
|
@@ -75,6 +76,20 @@ async function checkAuthorization(client) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
/** @hidden */
|
|
79
|
+
async function logOut(client) {
|
|
80
|
+
let success = true;
|
|
81
|
+
try {
|
|
82
|
+
await client.invoke(new tl_1.Api.auth.LogOut());
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
client._log.warn("auth.LogOut failed: " + e.message);
|
|
86
|
+
success = false;
|
|
87
|
+
}
|
|
88
|
+
await client.disconnect();
|
|
89
|
+
await client.session.delete();
|
|
90
|
+
return success;
|
|
91
|
+
}
|
|
92
|
+
/** @hidden */
|
|
78
93
|
async function signInUser(client, apiCredentials, authParams) {
|
|
79
94
|
let phoneNumber = "";
|
|
80
95
|
let phoneCodeHash = "";
|
package/client/users.js
CHANGED
|
@@ -435,7 +435,7 @@ async function getInputEntity(client, peer) {
|
|
|
435
435
|
}
|
|
436
436
|
throw new Error(`Could not find the input entity for ${JSON.stringify(peer)}.
|
|
437
437
|
Please read https://` +
|
|
438
|
-
"docs.
|
|
438
|
+
"docs.teleproto.dev/concepts/entities to" +
|
|
439
439
|
" find out more details.");
|
|
440
440
|
}
|
|
441
441
|
/** @hidden */
|
|
@@ -14,26 +14,22 @@ export declare class MTProtoState {
|
|
|
14
14
|
private securityChecks;
|
|
15
15
|
/**
|
|
16
16
|
*
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
together all the required information.
|
|
17
|
+
`MTProtoSender` needs to hold a state in order to encrypt and decrypt
|
|
18
|
+
incoming/outgoing messages, as well as generate message IDs. Instances
|
|
19
|
+
of this class hold together all the required information.
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
the concept of "copying" sessions with the unnecessary entities or
|
|
28
|
-
updates state for these connections doesn't make sense.
|
|
21
|
+
A `Session` is intentionally not used here: the sender should *not* be
|
|
22
|
+
concerned with persisting this state to disk. Multiple senders may
|
|
23
|
+
exist for different data centers or CDNs, each requiring its own
|
|
24
|
+
authkey — "copying" a session along with unrelated entities or update
|
|
25
|
+
state would make no sense.
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
authentication process, at which point the `MTProtoPlainSender` is better
|
|
27
|
+
A `MTProtoPlainState` doing no encryption could in principle be used
|
|
28
|
+
through `MTProtoLayer` and remove the need for `MTProtoPlainSender`,
|
|
29
|
+
but `MTProtoLayer` targets efficient throughput and this state class
|
|
30
|
+
is also more advanced (gzipping, invoking after other message IDs).
|
|
31
|
+
Too many helper methods would be needed to make it convenient during
|
|
32
|
+
authentication, where `MTProtoPlainSender` is the better fit.
|
|
37
33
|
* @param authKey
|
|
38
34
|
* @param loggers
|
|
39
35
|
* @param securityChecks
|
package/network/MTProtoState.js
CHANGED
|
@@ -16,26 +16,22 @@ const ReceivedIdsManager_1 = require("./ReceivedIdsManager");
|
|
|
16
16
|
class MTProtoState {
|
|
17
17
|
/**
|
|
18
18
|
*
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
together all the required information.
|
|
19
|
+
`MTProtoSender` needs to hold a state in order to encrypt and decrypt
|
|
20
|
+
incoming/outgoing messages, as well as generate message IDs. Instances
|
|
21
|
+
of this class hold together all the required information.
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
the concept of "copying" sessions with the unnecessary entities or
|
|
30
|
-
updates state for these connections doesn't make sense.
|
|
23
|
+
A `Session` is intentionally not used here: the sender should *not* be
|
|
24
|
+
concerned with persisting this state to disk. Multiple senders may
|
|
25
|
+
exist for different data centers or CDNs, each requiring its own
|
|
26
|
+
authkey — "copying" a session along with unrelated entities or update
|
|
27
|
+
state would make no sense.
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
authentication process, at which point the `MTProtoPlainSender` is better
|
|
29
|
+
A `MTProtoPlainState` doing no encryption could in principle be used
|
|
30
|
+
through `MTProtoLayer` and remove the need for `MTProtoPlainSender`,
|
|
31
|
+
but `MTProtoLayer` targets efficient throughput and this state class
|
|
32
|
+
is also more advanced (gzipping, invoking after other message IDs).
|
|
33
|
+
Too many helper methods would be needed to make it convenient during
|
|
34
|
+
authentication, where `MTProtoPlainSender` is the better fit.
|
|
39
35
|
* @param authKey
|
|
40
36
|
* @param loggers
|
|
41
37
|
* @param securityChecks
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { ObfuscatedConnection } from "./Connection";
|
|
2
2
|
import { AbridgedPacketCodec } from "./TCPAbridged";
|
|
3
3
|
import { Logger, PromisedNetSockets } from "../../extensions";
|
|
4
|
-
/**
|
|
5
|
-
* Base proxy configuration shared by all proxy types.
|
|
6
|
-
*/
|
|
7
4
|
interface BasicProxyInterface {
|
|
8
5
|
/** Proxy server IP address. */
|
|
9
6
|
ip: string;
|
|
@@ -38,9 +35,13 @@ interface BasicProxyInterface {
|
|
|
38
35
|
*/
|
|
39
36
|
export type MTProxyType = BasicProxyInterface & {
|
|
40
37
|
/**
|
|
41
|
-
* The proxy secret (hex or base64 encoded).
|
|
42
|
-
*
|
|
43
|
-
*
|
|
38
|
+
* The proxy secret (hex or base64 encoded). Three formats are accepted:
|
|
39
|
+
* - 16-byte plain secret.
|
|
40
|
+
* - 17 bytes with a leading `dd` byte — random-padding mode.
|
|
41
|
+
* - A leading `ee` byte + 16 bytes of secret + UTF-8 SNI domain bytes —
|
|
42
|
+
* fake-TLS mode. The connection is wrapped in a spoofed TLS 1.2
|
|
43
|
+
* handshake and all MTProxy bytes are tunneled inside TLS
|
|
44
|
+
* application_data records.
|
|
44
45
|
*/
|
|
45
46
|
secret: string;
|
|
46
47
|
/** Must be `true` to identify this as an MTProxy configuration. */
|
|
@@ -50,12 +51,11 @@ export type MTProxyType = BasicProxyInterface & {
|
|
|
50
51
|
* Configuration for SOCKS4/SOCKS5 proxy connections.
|
|
51
52
|
*
|
|
52
53
|
* SOCKS proxies are handled at the socket level via the `socks` package and
|
|
53
|
-
* work transparently with any connection type
|
|
54
|
-
*
|
|
54
|
+
* work transparently with any connection type. SOCKS5 supports username/
|
|
55
|
+
* password authentication.
|
|
55
56
|
*
|
|
56
57
|
* @example
|
|
57
58
|
* ```ts
|
|
58
|
-
* // SOCKS5 with authentication
|
|
59
59
|
* const client = new TelegramClient(session, apiId, apiHash, {
|
|
60
60
|
* proxy: {
|
|
61
61
|
* socksType: 5,
|
|
@@ -65,15 +65,6 @@ export type MTProxyType = BasicProxyInterface & {
|
|
|
65
65
|
* password: "pass",
|
|
66
66
|
* },
|
|
67
67
|
* });
|
|
68
|
-
*
|
|
69
|
-
* // SOCKS4 (no authentication)
|
|
70
|
-
* const client = new TelegramClient(session, apiId, apiHash, {
|
|
71
|
-
* proxy: {
|
|
72
|
-
* socksType: 4,
|
|
73
|
-
* ip: "127.0.0.1",
|
|
74
|
-
* port: 1080,
|
|
75
|
-
* },
|
|
76
|
-
* });
|
|
77
68
|
* ```
|
|
78
69
|
*/
|
|
79
70
|
export type SocksProxyType = BasicProxyInterface & {
|
|
@@ -81,32 +72,10 @@ export type SocksProxyType = BasicProxyInterface & {
|
|
|
81
72
|
socksType: 4 | 5;
|
|
82
73
|
};
|
|
83
74
|
/**
|
|
84
|
-
* Union type for all supported proxy configurations.
|
|
85
|
-
*
|
|
86
|
-
* Pass to `TelegramClientParams.proxy` to route all connections through a proxy.
|
|
87
|
-
*
|
|
88
|
-
* - {@link MTProxyType} — Telegram MTProxy (obfuscated, secret-based)
|
|
89
|
-
* - {@link SocksProxyType} — SOCKS4/SOCKS5 (general-purpose, optional auth)
|
|
75
|
+
* Union type for all supported proxy configurations. Pass to
|
|
76
|
+
* `TelegramClientParams.proxy` to route all connections through a proxy.
|
|
90
77
|
*/
|
|
91
78
|
export type ProxyInterface = MTProxyType | SocksProxyType;
|
|
92
|
-
/**
|
|
93
|
-
* Handles the MTProxy obfuscation layer: generates the initial handshake header
|
|
94
|
-
* and wraps all reads/writes in AES-CTR encryption keyed by the proxy secret.
|
|
95
|
-
* @internal
|
|
96
|
-
*/
|
|
97
|
-
declare class MTProxyIO {
|
|
98
|
-
header?: Buffer;
|
|
99
|
-
private connection;
|
|
100
|
-
private _encrypt?;
|
|
101
|
-
private _decrypt?;
|
|
102
|
-
private _packetClass;
|
|
103
|
-
private _secret;
|
|
104
|
-
private _dcId;
|
|
105
|
-
constructor(connection: TCPMTProxy);
|
|
106
|
-
initHeader(): Promise<void>;
|
|
107
|
-
read(n: number): Promise<Buffer<any>>;
|
|
108
|
-
write(data: Buffer): void;
|
|
109
|
-
}
|
|
110
79
|
interface TCPMTProxyInterfaceParams {
|
|
111
80
|
ip: string;
|
|
112
81
|
port: number;
|
|
@@ -116,25 +85,47 @@ interface TCPMTProxyInterfaceParams {
|
|
|
116
85
|
socket: typeof PromisedNetSockets;
|
|
117
86
|
}
|
|
118
87
|
/**
|
|
119
|
-
*
|
|
88
|
+
* Generates the MTProxy obfuscation header and wraps every subsequent
|
|
89
|
+
* read/write in AES-CTR. MTProxy derives mirrored CTR streams from the same
|
|
90
|
+
* 64-byte header — bytes 8..56 supply the client→server key/iv, and the
|
|
91
|
+
* reversed copy supplies the server→client side.
|
|
120
92
|
*
|
|
121
|
-
*
|
|
122
|
-
|
|
123
|
-
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
95
|
+
declare class MTProxyIO {
|
|
96
|
+
header?: Buffer;
|
|
97
|
+
private readonly stream;
|
|
98
|
+
private readonly packetCodec;
|
|
99
|
+
private readonly secret;
|
|
100
|
+
private readonly dcId;
|
|
101
|
+
private encryptor?;
|
|
102
|
+
private decryptor?;
|
|
103
|
+
constructor(connection: TCPMTProxy);
|
|
104
|
+
initHeader(): Promise<void>;
|
|
105
|
+
read(n: number): Promise<Buffer>;
|
|
106
|
+
write(data: Buffer): void;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Connection that routes traffic through a Telegram MTProxy server. Connects
|
|
110
|
+
* to the proxy address (not Telegram directly), performs the MTProxy
|
|
111
|
+
* obfuscated handshake, and tunnels MTProto traffic through with AES-CTR
|
|
112
|
+
* encryption. When the secret carries a fake-TLS prefix the underlying
|
|
113
|
+
* socket is first wrapped in a {@link FakeTlsSocket}.
|
|
124
114
|
*
|
|
125
|
-
* Not intended to be used directly — use {@link ConnectionTCPMTProxyAbridged}
|
|
126
|
-
* or
|
|
115
|
+
* Not intended to be used directly — use {@link ConnectionTCPMTProxyAbridged}
|
|
116
|
+
* instead, or pass an {@link MTProxyType} config to `TelegramClientParams.proxy`
|
|
127
117
|
* and the client will select the correct connection class automatically.
|
|
128
118
|
*/
|
|
129
119
|
export declare class TCPMTProxy extends ObfuscatedConnection {
|
|
130
120
|
ObfuscatedIO: typeof MTProxyIO;
|
|
131
121
|
_secret: Buffer;
|
|
132
|
-
|
|
122
|
+
_fakeTlsDomain?: string;
|
|
123
|
+
constructor({ dcId, loggers, proxy, socket, }: TCPMTProxyInterfaceParams);
|
|
124
|
+
_initConn(): Promise<void>;
|
|
133
125
|
}
|
|
134
126
|
/**
|
|
135
|
-
* MTProxy connection using the Abridged packet codec
|
|
136
|
-
*
|
|
137
|
-
* This is the connection class automatically selected when `proxy.MTProxy` is `true`.
|
|
127
|
+
* MTProxy connection using the Abridged packet codec — automatically selected
|
|
128
|
+
* when `proxy.MTProxy` is `true`.
|
|
138
129
|
*/
|
|
139
130
|
export declare class ConnectionTCPMTProxyAbridged extends TCPMTProxy {
|
|
140
131
|
PacketCodecClass: typeof AbridgedPacketCodec;
|
|
@@ -1,136 +1,403 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectionTCPMTProxyAbridged = exports.TCPMTProxy = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
4
5
|
const Connection_1 = require("./Connection");
|
|
5
6
|
const TCPAbridged_1 = require("./TCPAbridged");
|
|
6
7
|
const Helpers_1 = require("../../Helpers");
|
|
7
8
|
const CTR_1 = require("../../crypto/CTR");
|
|
9
|
+
const SECRET_LEN = 16;
|
|
10
|
+
const PREFIX_FAKE_TLS = 0xee;
|
|
11
|
+
const PREFIX_DD_PADDING = 0xdd;
|
|
12
|
+
function decodeSecret(input) {
|
|
13
|
+
return /^[0-9a-f]+$/i.test(input)
|
|
14
|
+
? Buffer.from(input, "hex")
|
|
15
|
+
: Buffer.from(input, "base64");
|
|
16
|
+
}
|
|
17
|
+
function parseProxySecret(input) {
|
|
18
|
+
if (!input) {
|
|
19
|
+
throw new Error("MTProxy: secret is required");
|
|
20
|
+
}
|
|
21
|
+
const raw = decodeSecret(input);
|
|
22
|
+
if (raw.length > 1 + SECRET_LEN && raw[0] === PREFIX_FAKE_TLS) {
|
|
23
|
+
return {
|
|
24
|
+
key: Buffer.from(raw.subarray(1, 1 + SECRET_LEN)),
|
|
25
|
+
fakeTlsDomain: raw.subarray(1 + SECRET_LEN).toString("utf8"),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (raw.length === 1 + SECRET_LEN && raw[0] === PREFIX_DD_PADDING) {
|
|
29
|
+
return { key: Buffer.from(raw.subarray(1)) };
|
|
30
|
+
}
|
|
31
|
+
if (raw.length === SECRET_LEN) {
|
|
32
|
+
return { key: raw };
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`MTProxy: secret must be ${SECRET_LEN} bytes, ` +
|
|
35
|
+
`${1 + SECRET_LEN} with 0xdd prefix, ` +
|
|
36
|
+
`or 0xee + ${SECRET_LEN} bytes + domain — got ${raw.length} bytes`);
|
|
37
|
+
}
|
|
38
|
+
const OBF_HEADER_LEN = 64;
|
|
39
|
+
const OBF_TAG_OFFSET = 56;
|
|
40
|
+
const OBF_DC_OFFSET = 60;
|
|
41
|
+
/** Header prefixes that collide with HTTP or other protocol sniffers. */
|
|
42
|
+
const FORBIDDEN_HEADER_PREFIXES = [
|
|
43
|
+
Buffer.from("50567247", "hex"),
|
|
44
|
+
Buffer.from("474554", "hex"),
|
|
45
|
+
Buffer.from("504f5354", "hex"),
|
|
46
|
+
Buffer.from("eeeeeeee", "hex"),
|
|
47
|
+
];
|
|
48
|
+
function pickObfuscationHeader() {
|
|
49
|
+
while (true) {
|
|
50
|
+
const candidate = (0, Helpers_1.generateRandomBytes)(OBF_HEADER_LEN);
|
|
51
|
+
if (candidate[0] === 0xef)
|
|
52
|
+
continue;
|
|
53
|
+
if (candidate.subarray(4, 8).equals(Buffer.alloc(4)))
|
|
54
|
+
continue;
|
|
55
|
+
const head4 = candidate.subarray(0, 4);
|
|
56
|
+
if (FORBIDDEN_HEADER_PREFIXES.some((p) => p.equals(head4)))
|
|
57
|
+
continue;
|
|
58
|
+
return candidate;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
8
61
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
62
|
+
* Generates the MTProxy obfuscation header and wraps every subsequent
|
|
63
|
+
* read/write in AES-CTR. MTProxy derives mirrored CTR streams from the same
|
|
64
|
+
* 64-byte header — bytes 8..56 supply the client→server key/iv, and the
|
|
65
|
+
* reversed copy supplies the server→client side.
|
|
66
|
+
*
|
|
11
67
|
* @internal
|
|
12
68
|
*/
|
|
13
69
|
class MTProxyIO {
|
|
14
70
|
constructor(connection) {
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this._packetClass =
|
|
71
|
+
this.stream = connection.socket;
|
|
72
|
+
this.packetCodec =
|
|
18
73
|
connection.PacketCodecClass;
|
|
19
|
-
this.
|
|
20
|
-
this.
|
|
74
|
+
this.secret = connection._secret;
|
|
75
|
+
this.dcId = connection._dcId;
|
|
21
76
|
}
|
|
22
77
|
async initHeader() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
secret = isDD ? secret.slice(1) : secret;
|
|
26
|
-
if (secret.length != 16) {
|
|
27
|
-
throw new Error("MTProxy secret must be a hex-string representing 16 bytes");
|
|
28
|
-
}
|
|
29
|
-
const keywords = [
|
|
30
|
-
Buffer.from("50567247", "hex"),
|
|
31
|
-
Buffer.from("474554", "hex"),
|
|
32
|
-
Buffer.from("504f5354", "hex"),
|
|
33
|
-
Buffer.from("eeeeeeee", "hex"),
|
|
34
|
-
];
|
|
35
|
-
let random;
|
|
36
|
-
// eslint-disable-next-line no-constant-condition
|
|
37
|
-
while (true) {
|
|
38
|
-
random = (0, Helpers_1.generateRandomBytes)(64);
|
|
39
|
-
if (random[0] !== 0xef &&
|
|
40
|
-
!random.slice(4, 8).equals(Buffer.alloc(4))) {
|
|
41
|
-
let ok = true;
|
|
42
|
-
for (const key of keywords) {
|
|
43
|
-
if (key.equals(random.slice(0, 4))) {
|
|
44
|
-
ok = false;
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (ok) {
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
78
|
+
if (this.secret.length !== SECRET_LEN) {
|
|
79
|
+
throw new Error(`MTProxy: secret must be ${SECRET_LEN} bytes, got ${this.secret.length}`);
|
|
52
80
|
}
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
]);
|
|
74
|
-
random = Buffer.concat([
|
|
75
|
-
Buffer.from(random.slice(0, 56)),
|
|
76
|
-
Buffer.from(encryptor.encrypt(random).slice(56, 64)),
|
|
77
|
-
Buffer.from(random.slice(64)),
|
|
78
|
-
]);
|
|
79
|
-
this.header = random;
|
|
80
|
-
this._encrypt = encryptor;
|
|
81
|
-
this._decrypt = decryptor;
|
|
81
|
+
const header = pickObfuscationHeader();
|
|
82
|
+
const reversed = Buffer.from(header.subarray(8, 56)).reverse();
|
|
83
|
+
const encryptKey = await (0, Helpers_1.sha256)(Buffer.concat([header.subarray(8, 40), this.secret]));
|
|
84
|
+
const encryptIv = Buffer.from(header.subarray(40, 56));
|
|
85
|
+
const decryptKey = await (0, Helpers_1.sha256)(Buffer.concat([reversed.subarray(0, 32), this.secret]));
|
|
86
|
+
const decryptIv = Buffer.from(reversed.subarray(32, 48));
|
|
87
|
+
this.encryptor = new CTR_1.CTR(encryptKey, encryptIv);
|
|
88
|
+
this.decryptor = new CTR_1.CTR(decryptKey, decryptIv);
|
|
89
|
+
// Stamp protocol tag and DC id (low byte first, byte 61 zeroed —
|
|
90
|
+
// matches gramjs / official client behavior, even for negative DC ids).
|
|
91
|
+
this.packetCodec.obfuscateTag.copy(header, OBF_TAG_OFFSET);
|
|
92
|
+
header.writeInt8(this.dcId, OBF_DC_OFFSET);
|
|
93
|
+
header[OBF_DC_OFFSET + 1] = 0;
|
|
94
|
+
// Re-encrypt the stamped tail in place — the CTR counter ends up
|
|
95
|
+
// advanced by all 64 bytes, matching the server's view.
|
|
96
|
+
const encryptedTail = this.encryptor
|
|
97
|
+
.encrypt(header)
|
|
98
|
+
.subarray(OBF_TAG_OFFSET, OBF_HEADER_LEN);
|
|
99
|
+
encryptedTail.copy(header, OBF_TAG_OFFSET);
|
|
100
|
+
this.header = header;
|
|
82
101
|
}
|
|
83
102
|
async read(n) {
|
|
84
|
-
const data = await this.
|
|
85
|
-
return this.
|
|
103
|
+
const data = await this.stream.readExactly(n);
|
|
104
|
+
return this.decryptor.encrypt(data);
|
|
86
105
|
}
|
|
87
106
|
write(data) {
|
|
88
|
-
this.
|
|
107
|
+
this.stream.write(this.encryptor.encrypt(data));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const TLS_RECORD_HEADER_LEN = 5;
|
|
111
|
+
const TLS_RECORD_MAX_PAYLOAD = 16384;
|
|
112
|
+
const TLS_RECORD_HANDSHAKE = 0x16;
|
|
113
|
+
const TLS_RECORD_CHANGE_CIPHER_SPEC = 0x14;
|
|
114
|
+
const TLS_RECORD_APPLICATION_DATA = 0x17;
|
|
115
|
+
const TLS_APP_DATA_PREFIX = Buffer.from([0x17, 0x03, 0x03]);
|
|
116
|
+
const FAKE_TLS_HELLO_SIZE = 517;
|
|
117
|
+
const FAKE_TLS_HELLO_RANDOM_OFFSET = 11;
|
|
118
|
+
const FAKE_TLS_HELLO_RANDOM_LEN = 32;
|
|
119
|
+
/** 5-byte TLS record + 4-byte handshake header + 2-byte version = 114. */
|
|
120
|
+
const FAKE_TLS_FIXED_HEADER_SIZE = 114;
|
|
121
|
+
const FAKE_TLS_RANDOM_TIMESTAMP_XOR_OFFSET = 28;
|
|
122
|
+
const GREASE = 0x0a;
|
|
123
|
+
async function readTlsRecord(stream) {
|
|
124
|
+
const hdr = await stream.readExactly(TLS_RECORD_HEADER_LEN);
|
|
125
|
+
const type = hdr[0];
|
|
126
|
+
const len = hdr.readUInt16BE(3);
|
|
127
|
+
const payload = await stream.readExactly(len);
|
|
128
|
+
return { type, payload };
|
|
129
|
+
}
|
|
130
|
+
function tlsRecordTypeName(type) {
|
|
131
|
+
switch (type) {
|
|
132
|
+
case TLS_RECORD_HANDSHAKE:
|
|
133
|
+
return "Handshake";
|
|
134
|
+
case TLS_RECORD_CHANGE_CIPHER_SPEC:
|
|
135
|
+
return "ChangeCipherSpec";
|
|
136
|
+
case TLS_RECORD_APPLICATION_DATA:
|
|
137
|
+
return "ApplicationData";
|
|
138
|
+
default:
|
|
139
|
+
return `0x${type.toString(16).padStart(2, "0")}`;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function tlsExtension(type, body) {
|
|
143
|
+
const content = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
144
|
+
const out = Buffer.alloc(4 + content.length);
|
|
145
|
+
out.writeUInt16BE(type, 0);
|
|
146
|
+
out.writeUInt16BE(content.length, 2);
|
|
147
|
+
content.copy(out, 4);
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
function sniExtension(domain) {
|
|
151
|
+
const dom = Buffer.from(domain, "utf8");
|
|
152
|
+
const body = Buffer.alloc(5 + dom.length);
|
|
153
|
+
body.writeUInt16BE(dom.length + 3, 0);
|
|
154
|
+
body.writeUInt8(0x00, 2);
|
|
155
|
+
body.writeUInt16BE(dom.length, 3);
|
|
156
|
+
dom.copy(body, 5);
|
|
157
|
+
return tlsExtension(0x0000, body);
|
|
158
|
+
}
|
|
159
|
+
function keyShareExtension() {
|
|
160
|
+
const x25519Pub = (0, Helpers_1.generateRandomBytes)(32);
|
|
161
|
+
const entries = Buffer.concat([
|
|
162
|
+
Buffer.from([GREASE, GREASE, 0x00, 0x01, 0x00]),
|
|
163
|
+
Buffer.from([0x00, 0x1d, 0x00, 0x20]),
|
|
164
|
+
x25519Pub,
|
|
165
|
+
]);
|
|
166
|
+
const body = Buffer.alloc(2 + entries.length);
|
|
167
|
+
body.writeUInt16BE(entries.length, 0);
|
|
168
|
+
entries.copy(body, 2);
|
|
169
|
+
return tlsExtension(0x0033, body);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Input-independent extensions in Chrome 105+ order. SNI and key_share are
|
|
173
|
+
* spliced in at runtime (see `buildClientHello`). Layout is intentionally
|
|
174
|
+
* stable for JA3 fingerprint similarity.
|
|
175
|
+
*/
|
|
176
|
+
const STATIC_EXTENSIONS = [
|
|
177
|
+
tlsExtension(0x0a0a, []),
|
|
178
|
+
tlsExtension(0x0017, []),
|
|
179
|
+
tlsExtension(0xff01, [0x00]),
|
|
180
|
+
tlsExtension(0x000a, [
|
|
181
|
+
0x00, 0x08, GREASE, GREASE, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18,
|
|
182
|
+
]),
|
|
183
|
+
tlsExtension(0x000b, [0x01, 0x00]),
|
|
184
|
+
tlsExtension(0x0023, []),
|
|
185
|
+
tlsExtension(0x0010, [
|
|
186
|
+
0x00, 0x0c, 0x02, 0x68, 0x32, 0x08, 0x68, 0x74, 0x74, 0x70, 0x2f, 0x31,
|
|
187
|
+
0x2e, 0x31,
|
|
188
|
+
]),
|
|
189
|
+
tlsExtension(0x0005, [0x01, 0x00, 0x00, 0x00, 0x00]),
|
|
190
|
+
tlsExtension(0x000d, [
|
|
191
|
+
0x00, 0x10, 0x04, 0x03, 0x08, 0x04, 0x04, 0x01, 0x05, 0x03, 0x08, 0x05,
|
|
192
|
+
0x05, 0x01, 0x08, 0x06, 0x06, 0x01,
|
|
193
|
+
]),
|
|
194
|
+
tlsExtension(0x0012, []),
|
|
195
|
+
tlsExtension(0x002d, [0x01, 0x01]),
|
|
196
|
+
tlsExtension(0x002b, [0x06, GREASE, GREASE, 0x03, 0x04, 0x03, 0x03]),
|
|
197
|
+
tlsExtension(0x001b, [0x02, 0x00, 0x02]),
|
|
198
|
+
tlsExtension(0x0a0a, [0x00]),
|
|
199
|
+
];
|
|
200
|
+
const STATIC_EXTENSIONS_LEN = STATIC_EXTENSIONS.reduce((s, e) => s + e.length, 0);
|
|
201
|
+
const CIPHER_SUITES = Buffer.from([
|
|
202
|
+
GREASE, GREASE,
|
|
203
|
+
0x13, 0x01, 0x13, 0x02, 0x13, 0x03,
|
|
204
|
+
0xc0, 0x2b, 0xc0, 0x2f, 0xc0, 0x2c, 0xc0, 0x30,
|
|
205
|
+
0xcc, 0xa9, 0xcc, 0xa8,
|
|
206
|
+
0xc0, 0x13, 0xc0, 0x14,
|
|
207
|
+
0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35,
|
|
208
|
+
]);
|
|
209
|
+
/**
|
|
210
|
+
* Builds the 517-byte Chrome-like TLS 1.2 ClientHello. The 32-byte `random`
|
|
211
|
+
* field at offset 11 is left zeroed — the caller stamps it with the
|
|
212
|
+
* HMAC-derived value before sending.
|
|
213
|
+
*/
|
|
214
|
+
function buildClientHello(domain, sessionId) {
|
|
215
|
+
const sni = sniExtension(domain);
|
|
216
|
+
const keyShare = keyShareExtension();
|
|
217
|
+
let extensionsLen = STATIC_EXTENSIONS_LEN + sni.length + keyShare.length;
|
|
218
|
+
const paddingPayloadLen = FAKE_TLS_HELLO_SIZE - FAKE_TLS_FIXED_HEADER_SIZE - extensionsLen - 4;
|
|
219
|
+
if (paddingPayloadLen < 0) {
|
|
220
|
+
throw new Error(`FakeTLS: domain too long (${domain.length} bytes); ClientHello overflows by ${-paddingPayloadLen} bytes`);
|
|
221
|
+
}
|
|
222
|
+
const padding = tlsExtension(0x0015, Buffer.alloc(paddingPayloadLen));
|
|
223
|
+
extensionsLen += padding.length;
|
|
224
|
+
// SNI follows the leading GREASE; key_share follows the SCT extension.
|
|
225
|
+
const KEY_SHARE_INSERT_AFTER = 11;
|
|
226
|
+
const extensions = [
|
|
227
|
+
STATIC_EXTENSIONS[0],
|
|
228
|
+
sni,
|
|
229
|
+
...STATIC_EXTENSIONS.slice(1, KEY_SHARE_INSERT_AFTER + 1),
|
|
230
|
+
keyShare,
|
|
231
|
+
...STATIC_EXTENSIONS.slice(KEY_SHARE_INSERT_AFTER + 1),
|
|
232
|
+
padding,
|
|
233
|
+
];
|
|
234
|
+
const hello = Buffer.concat([
|
|
235
|
+
Buffer.from([0x16, 0x03, 0x01, 0x02, 0x00]),
|
|
236
|
+
Buffer.from([0x01, 0x00, 0x01, 0xfc]),
|
|
237
|
+
Buffer.from([0x03, 0x03]),
|
|
238
|
+
Buffer.alloc(FAKE_TLS_HELLO_RANDOM_LEN),
|
|
239
|
+
Buffer.from([sessionId.length]),
|
|
240
|
+
sessionId,
|
|
241
|
+
Buffer.from([
|
|
242
|
+
(CIPHER_SUITES.length >> 8) & 0xff,
|
|
243
|
+
CIPHER_SUITES.length & 0xff,
|
|
244
|
+
]),
|
|
245
|
+
CIPHER_SUITES,
|
|
246
|
+
Buffer.from([0x01, 0x00]),
|
|
247
|
+
Buffer.from([(extensionsLen >> 8) & 0xff, extensionsLen & 0xff]),
|
|
248
|
+
...extensions,
|
|
249
|
+
]);
|
|
250
|
+
if (hello.length !== FAKE_TLS_HELLO_SIZE) {
|
|
251
|
+
throw new Error(`FakeTLS: ClientHello size mismatch — got ${hello.length}, expected ${FAKE_TLS_HELLO_SIZE}`);
|
|
89
252
|
}
|
|
253
|
+
return hello;
|
|
90
254
|
}
|
|
91
255
|
/**
|
|
92
|
-
*
|
|
256
|
+
* Wraps a {@link PromisedNetSockets} in MTProto fake-TLS framing. On
|
|
257
|
+
* {@link handshake}: sends a Chrome-like ClientHello with the random field
|
|
258
|
+
* stamped to `HMAC-SHA256(secret, hello)` (last 4 bytes XOR'd with unix
|
|
259
|
+
* time), then verifies the server's random equals
|
|
260
|
+
* `HMAC-SHA256(secret, client_random || server_response_zeroed)`. Post-
|
|
261
|
+
* handshake, all bytes ride inside TLS application_data records.
|
|
93
262
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
263
|
+
* Exposes only the subset of {@link PromisedNetSockets} consumed by
|
|
264
|
+
* {@link MTProxyIO} and the {@link ObfuscatedConnection} base class so it
|
|
265
|
+
* can be hot-swapped in place of the raw socket.
|
|
97
266
|
*
|
|
98
|
-
*
|
|
99
|
-
|
|
267
|
+
* @internal
|
|
268
|
+
*/
|
|
269
|
+
class FakeTlsSocket {
|
|
270
|
+
constructor(inner, secret, domain) {
|
|
271
|
+
this.clientRandom = Buffer.alloc(0);
|
|
272
|
+
this.readBuf = Buffer.alloc(0);
|
|
273
|
+
if (secret.length !== SECRET_LEN) {
|
|
274
|
+
throw new Error(`FakeTLS: secret must be ${SECRET_LEN} bytes, got ${secret.length}`);
|
|
275
|
+
}
|
|
276
|
+
this.inner = inner;
|
|
277
|
+
this.secret = secret;
|
|
278
|
+
this.domain = domain;
|
|
279
|
+
}
|
|
280
|
+
async handshake() {
|
|
281
|
+
const hello = this.sendClientHello();
|
|
282
|
+
this.inner.write(hello);
|
|
283
|
+
await this.readAndVerifyServerHandshake();
|
|
284
|
+
}
|
|
285
|
+
write(data) {
|
|
286
|
+
for (let off = 0; off < data.length; off += TLS_RECORD_MAX_PAYLOAD) {
|
|
287
|
+
const chunkLen = Math.min(TLS_RECORD_MAX_PAYLOAD, data.length - off);
|
|
288
|
+
const hdr = Buffer.alloc(TLS_RECORD_HEADER_LEN);
|
|
289
|
+
TLS_APP_DATA_PREFIX.copy(hdr, 0);
|
|
290
|
+
hdr.writeUInt16BE(chunkLen, 3);
|
|
291
|
+
this.inner.write(Buffer.concat([hdr, data.subarray(off, off + chunkLen)]));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async readExactly(n) {
|
|
295
|
+
while (this.readBuf.length < n) {
|
|
296
|
+
const { type, payload } = await readTlsRecord(this.inner);
|
|
297
|
+
if (type !== TLS_RECORD_APPLICATION_DATA)
|
|
298
|
+
continue;
|
|
299
|
+
this.readBuf = Buffer.concat([this.readBuf, payload]);
|
|
300
|
+
}
|
|
301
|
+
const out = Buffer.from(this.readBuf.subarray(0, n));
|
|
302
|
+
this.readBuf = this.readBuf.subarray(n);
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
async close() {
|
|
306
|
+
await this.inner.close();
|
|
307
|
+
}
|
|
308
|
+
sendClientHello() {
|
|
309
|
+
const sessionId = (0, Helpers_1.generateRandomBytes)(32);
|
|
310
|
+
const hello = buildClientHello(this.domain, sessionId);
|
|
311
|
+
const stamp = (0, node_crypto_1.createHmac)("sha256", this.secret).update(hello).digest();
|
|
312
|
+
const now = Math.floor(Date.now() / 1000) >>> 0;
|
|
313
|
+
const lastWord = (stamp.readUInt32BE(FAKE_TLS_RANDOM_TIMESTAMP_XOR_OFFSET) ^ now) >>>
|
|
314
|
+
0;
|
|
315
|
+
stamp.writeUInt32BE(lastWord, FAKE_TLS_RANDOM_TIMESTAMP_XOR_OFFSET);
|
|
316
|
+
stamp.copy(hello, FAKE_TLS_HELLO_RANDOM_OFFSET);
|
|
317
|
+
this.clientRandom = stamp;
|
|
318
|
+
return hello;
|
|
319
|
+
}
|
|
320
|
+
// Expects the canonical 3-record server response. Anything else fails
|
|
321
|
+
// loudly rather than silently corrupting the downstream MTProxy stream.
|
|
322
|
+
async readAndVerifyServerHandshake() {
|
|
323
|
+
const expectedTypes = [
|
|
324
|
+
TLS_RECORD_HANDSHAKE,
|
|
325
|
+
TLS_RECORD_CHANGE_CIPHER_SPEC,
|
|
326
|
+
TLS_RECORD_APPLICATION_DATA,
|
|
327
|
+
];
|
|
328
|
+
const chunks = [];
|
|
329
|
+
let serverRandomOffset = -1;
|
|
330
|
+
let offset = 0;
|
|
331
|
+
for (let i = 0; i < expectedTypes.length; i++) {
|
|
332
|
+
const { type, payload } = await readTlsRecord(this.inner);
|
|
333
|
+
if (type !== expectedTypes[i]) {
|
|
334
|
+
throw new Error(`FakeTLS: expected ${tlsRecordTypeName(expectedTypes[i])} at record ${i}, got ${tlsRecordTypeName(type)}`);
|
|
335
|
+
}
|
|
336
|
+
if (i === 0) {
|
|
337
|
+
serverRandomOffset = offset + FAKE_TLS_HELLO_RANDOM_OFFSET;
|
|
338
|
+
}
|
|
339
|
+
const hdr = Buffer.alloc(TLS_RECORD_HEADER_LEN);
|
|
340
|
+
hdr[0] = type;
|
|
341
|
+
hdr[1] = 0x03;
|
|
342
|
+
hdr[2] = 0x03;
|
|
343
|
+
hdr.writeUInt16BE(payload.length, 3);
|
|
344
|
+
chunks.push(hdr, payload);
|
|
345
|
+
offset += TLS_RECORD_HEADER_LEN + payload.length;
|
|
346
|
+
}
|
|
347
|
+
const serverResp = Buffer.concat(chunks);
|
|
348
|
+
const serverRandom = Buffer.from(serverResp.subarray(serverRandomOffset, serverRandomOffset + FAKE_TLS_HELLO_RANDOM_LEN));
|
|
349
|
+
serverResp.fill(0, serverRandomOffset, serverRandomOffset + FAKE_TLS_HELLO_RANDOM_LEN);
|
|
350
|
+
const expected = (0, node_crypto_1.createHmac)("sha256", this.secret)
|
|
351
|
+
.update(this.clientRandom)
|
|
352
|
+
.update(serverResp)
|
|
353
|
+
.digest();
|
|
354
|
+
if (!expected.equals(serverRandom)) {
|
|
355
|
+
throw new Error("FakeTLS: server HMAC verification failed");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Connection that routes traffic through a Telegram MTProxy server. Connects
|
|
361
|
+
* to the proxy address (not Telegram directly), performs the MTProxy
|
|
362
|
+
* obfuscated handshake, and tunnels MTProto traffic through with AES-CTR
|
|
363
|
+
* encryption. When the secret carries a fake-TLS prefix the underlying
|
|
364
|
+
* socket is first wrapped in a {@link FakeTlsSocket}.
|
|
365
|
+
*
|
|
366
|
+
* Not intended to be used directly — use {@link ConnectionTCPMTProxyAbridged}
|
|
367
|
+
* instead, or pass an {@link MTProxyType} config to `TelegramClientParams.proxy`
|
|
100
368
|
* and the client will select the correct connection class automatically.
|
|
101
369
|
*/
|
|
102
370
|
class TCPMTProxy extends Connection_1.ObfuscatedConnection {
|
|
103
|
-
constructor({
|
|
371
|
+
constructor({ dcId, loggers, proxy, socket, }) {
|
|
104
372
|
super({
|
|
105
373
|
ip: proxy.ip,
|
|
106
374
|
port: proxy.port,
|
|
107
|
-
dcId
|
|
108
|
-
loggers
|
|
109
|
-
socket
|
|
110
|
-
proxy
|
|
375
|
+
dcId,
|
|
376
|
+
loggers,
|
|
377
|
+
socket,
|
|
378
|
+
proxy,
|
|
111
379
|
});
|
|
112
380
|
this.ObfuscatedIO = MTProxyIO;
|
|
113
381
|
if (!("MTProxy" in proxy)) {
|
|
114
|
-
throw new Error("This connection only supports
|
|
115
|
-
}
|
|
116
|
-
if (!proxy.secret) {
|
|
117
|
-
throw new Error("You need to provide the secret for the MTProxy");
|
|
382
|
+
throw new Error("This connection only supports MTProxies");
|
|
118
383
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
384
|
+
const parsed = parseProxySecret(proxy.secret);
|
|
385
|
+
this._secret = parsed.key;
|
|
386
|
+
this._fakeTlsDomain = parsed.fakeTlsDomain;
|
|
387
|
+
}
|
|
388
|
+
async _initConn() {
|
|
389
|
+
if (this._fakeTlsDomain) {
|
|
390
|
+
const tls = new FakeTlsSocket(this.socket, this._secret, this._fakeTlsDomain);
|
|
391
|
+
await tls.handshake();
|
|
392
|
+
this.socket = tls;
|
|
126
393
|
}
|
|
394
|
+
await super._initConn();
|
|
127
395
|
}
|
|
128
396
|
}
|
|
129
397
|
exports.TCPMTProxy = TCPMTProxy;
|
|
130
398
|
/**
|
|
131
|
-
* MTProxy connection using the Abridged packet codec
|
|
132
|
-
*
|
|
133
|
-
* This is the connection class automatically selected when `proxy.MTProxy` is `true`.
|
|
399
|
+
* MTProxy connection using the Abridged packet codec — automatically selected
|
|
400
|
+
* when `proxy.MTProxy` is `true`.
|
|
134
401
|
*/
|
|
135
402
|
class ConnectionTCPMTProxyAbridged extends TCPMTProxy {
|
|
136
403
|
constructor() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teleproto",
|
|
3
|
-
"version": "1.225.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.225.3",
|
|
4
|
+
"description": "Telegram MTProto API client library written in TypeScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"repository": {
|
|
@@ -15,13 +15,11 @@
|
|
|
15
15
|
"keywords": [
|
|
16
16
|
"telegram",
|
|
17
17
|
"mtproto",
|
|
18
|
-
"
|
|
19
|
-
"client",
|
|
20
|
-
"nodejs",
|
|
18
|
+
"userbot",
|
|
21
19
|
"typescript",
|
|
22
|
-
"
|
|
20
|
+
"nodejs"
|
|
23
21
|
],
|
|
24
|
-
"homepage": "https://
|
|
22
|
+
"homepage": "https://docs.teleproto.dev",
|
|
25
23
|
"dependencies": {
|
|
26
24
|
"async-mutex": "^0.3.0",
|
|
27
25
|
"big-integer": "^1.6.48",
|
package/sessions/Abstract.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export declare abstract class Session {
|
|
|
63
63
|
* to suit several purposes (e.g. user only provided its ID or wishes
|
|
64
64
|
* to use a cached username to avoid extra RPC).
|
|
65
65
|
*/
|
|
66
|
-
abstract getInputEntity(key: EntityLike): Api.TypeInputPeer
|
|
66
|
+
abstract getInputEntity(key: EntityLike): Api.TypeInputPeer | Promise<Api.TypeInputPeer>;
|
|
67
67
|
/**
|
|
68
68
|
* Returns an ID of the takeout process initialized for this session,
|
|
69
69
|
* or `None` if there's no were any unfinished takeout requests.
|
|
@@ -97,10 +97,10 @@ export declare abstract class Session {
|
|
|
97
97
|
*/
|
|
98
98
|
abstract save(): void;
|
|
99
99
|
/**
|
|
100
|
-
* Called upon client.
|
|
100
|
+
* Called upon client.logOut(). Should delete the stored
|
|
101
101
|
* information from disk since it's not valid anymore.
|
|
102
102
|
*/
|
|
103
|
-
abstract delete(): void
|
|
103
|
+
abstract delete(): void | Promise<void>;
|
|
104
104
|
/**
|
|
105
105
|
* Processes the input ``TLObject`` or ``list`` and saves
|
|
106
106
|
* whatever information is relevant (e.g., ID or access hash).
|
package/sessions/Memory.js
CHANGED
|
@@ -115,7 +115,15 @@ class MemorySession extends Abstract_1.Session {
|
|
|
115
115
|
close() { }
|
|
116
116
|
save() { }
|
|
117
117
|
async load() { }
|
|
118
|
-
delete() {
|
|
118
|
+
delete() {
|
|
119
|
+
this._authKey = undefined;
|
|
120
|
+
this._dcId = 0;
|
|
121
|
+
this._serverAddress = undefined;
|
|
122
|
+
this._port = undefined;
|
|
123
|
+
this._dcAuthKeys.clear();
|
|
124
|
+
this._entities.clear();
|
|
125
|
+
this._updateStates = {};
|
|
126
|
+
}
|
|
119
127
|
_entityValuesToRow(id, hash, username, phone, name) {
|
|
120
128
|
// While this is a simple implementation it might be overrode by,
|
|
121
129
|
// other classes so they don't need to implement the plural form
|
|
@@ -24,6 +24,7 @@ export declare class StoreSession extends MemorySession {
|
|
|
24
24
|
get testServers(): boolean;
|
|
25
25
|
set authKey(value: AuthKey | undefined);
|
|
26
26
|
get authKey(): AuthKey | undefined;
|
|
27
|
+
delete(): void;
|
|
27
28
|
processEntities(tlo: any): void;
|
|
28
29
|
getEntityRowsById(id: string | bigInt.BigInteger, exact?: boolean): any;
|
|
29
30
|
}
|
package/sessions/StoreSession.js
CHANGED
|
@@ -113,6 +113,10 @@ class StoreSession extends Memory_1.MemorySession {
|
|
|
113
113
|
get authKey() {
|
|
114
114
|
return this._authKey;
|
|
115
115
|
}
|
|
116
|
+
delete() {
|
|
117
|
+
this.store.clearAll();
|
|
118
|
+
super.delete();
|
|
119
|
+
}
|
|
116
120
|
processEntities(tlo) {
|
|
117
121
|
const rows = this._entitiesToRows(tlo);
|
|
118
122
|
if (!rows) {
|
|
@@ -88,6 +88,10 @@ class StringSession extends Memory_1.MemorySession {
|
|
|
88
88
|
await this._authKey.setKey(this._key);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
delete() {
|
|
92
|
+
this._key = undefined;
|
|
93
|
+
super.delete();
|
|
94
|
+
}
|
|
91
95
|
save() {
|
|
92
96
|
if (!this.authKey || !this.serverAddress || !this.port) {
|
|
93
97
|
return "";
|