teleproto 1.225.2 → 1.225.4
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/Version.d.ts +1 -1
- package/Version.js +1 -1
- package/client/TelegramClient.d.ts +34 -6
- package/client/TelegramClient.js +36 -6
- package/client/UpdateManager.d.ts +3 -0
- package/client/UpdateManager.js +33 -0
- package/client/auth.d.ts +5 -0
- package/client/auth.js +40 -16
- package/network/connection/TCPMTProxy.d.ts +43 -52
- package/network/connection/TCPMTProxy.js +360 -93
- package/package.json +1 -1
- 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 -1
- package/sessions/StringSession.d.ts +1 -0
- package/sessions/StringSession.js +4 -0
- package/tl/custom/dialog.d.ts +1 -1
- package/tl/custom/dialog.js +2 -1
package/Version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "1.225.
|
|
1
|
+
export declare const version = "1.225.4";
|
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/>
|
|
@@ -115,11 +131,18 @@ export declare class TelegramClient extends TelegramBaseClient {
|
|
|
115
131
|
/**
|
|
116
132
|
* logs the user using a QR code to be scanned.<br/>
|
|
117
133
|
* this function generates the QR code that needs to be scanned by mobile.
|
|
134
|
+
*
|
|
135
|
+
* The flow can be cancelled at any time (e.g. the user closed the page) by
|
|
136
|
+
* passing an `abortSignal`; once aborted it stops polling for tokens and
|
|
137
|
+
* rejects with an `AbortError`.
|
|
118
138
|
* @example
|
|
119
139
|
* '''ts
|
|
120
140
|
* await client.connect();
|
|
121
|
-
* const
|
|
122
|
-
*
|
|
141
|
+
* const controller = new AbortController();
|
|
142
|
+
* // call controller.abort() to cancel the QR login at any time
|
|
143
|
+
* try {
|
|
144
|
+
* const user = await client.signInUserWithQrCode({ apiId, apiHash },
|
|
145
|
+
* {
|
|
123
146
|
* onError: async function(p1: Error) {
|
|
124
147
|
* console.log("error", p1);
|
|
125
148
|
* // true = stop the authentication processes
|
|
@@ -134,14 +157,19 @@ export declare class TelegramClient extends TelegramBaseClient {
|
|
|
134
157
|
* password: async (hint) => {
|
|
135
158
|
* // password if needed
|
|
136
159
|
* return "1111";
|
|
137
|
-
* }
|
|
160
|
+
* },
|
|
161
|
+
* abortSignal: controller.signal,
|
|
138
162
|
* }
|
|
139
|
-
*
|
|
140
|
-
*
|
|
163
|
+
* );
|
|
164
|
+
* console.log("user is", user);
|
|
165
|
+
* } catch (err) {
|
|
166
|
+
* if (err.name === "AbortError") console.log("QR login cancelled");
|
|
167
|
+
* else throw err;
|
|
168
|
+
* }
|
|
141
169
|
*
|
|
142
170
|
* '''
|
|
143
171
|
* @param apiCredentials - credentials to be used.
|
|
144
|
-
* @param authParams - user auth params.
|
|
172
|
+
* @param authParams - user auth params. Pass `abortSignal` to cancel the flow.
|
|
145
173
|
*/
|
|
146
174
|
signInUserWithQrCode(apiCredentials: authMethods.ApiCredentials, authParams: authMethods.QrCodeAuthParams): Promise<Api.TypeUser>;
|
|
147
175
|
/**
|
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/>
|
|
@@ -159,11 +177,18 @@ class TelegramClient extends telegramBaseClient_1.TelegramBaseClient {
|
|
|
159
177
|
/**
|
|
160
178
|
* logs the user using a QR code to be scanned.<br/>
|
|
161
179
|
* this function generates the QR code that needs to be scanned by mobile.
|
|
180
|
+
*
|
|
181
|
+
* The flow can be cancelled at any time (e.g. the user closed the page) by
|
|
182
|
+
* passing an `abortSignal`; once aborted it stops polling for tokens and
|
|
183
|
+
* rejects with an `AbortError`.
|
|
162
184
|
* @example
|
|
163
185
|
* '''ts
|
|
164
186
|
* await client.connect();
|
|
165
|
-
* const
|
|
166
|
-
*
|
|
187
|
+
* const controller = new AbortController();
|
|
188
|
+
* // call controller.abort() to cancel the QR login at any time
|
|
189
|
+
* try {
|
|
190
|
+
* const user = await client.signInUserWithQrCode({ apiId, apiHash },
|
|
191
|
+
* {
|
|
167
192
|
* onError: async function(p1: Error) {
|
|
168
193
|
* console.log("error", p1);
|
|
169
194
|
* // true = stop the authentication processes
|
|
@@ -178,14 +203,19 @@ class TelegramClient extends telegramBaseClient_1.TelegramBaseClient {
|
|
|
178
203
|
* password: async (hint) => {
|
|
179
204
|
* // password if needed
|
|
180
205
|
* return "1111";
|
|
181
|
-
* }
|
|
206
|
+
* },
|
|
207
|
+
* abortSignal: controller.signal,
|
|
182
208
|
* }
|
|
183
|
-
*
|
|
184
|
-
*
|
|
209
|
+
* );
|
|
210
|
+
* console.log("user is", user);
|
|
211
|
+
* } catch (err) {
|
|
212
|
+
* if (err.name === "AbortError") console.log("QR login cancelled");
|
|
213
|
+
* else throw err;
|
|
214
|
+
* }
|
|
185
215
|
*
|
|
186
216
|
* '''
|
|
187
217
|
* @param apiCredentials - credentials to be used.
|
|
188
|
-
* @param authParams - user auth params.
|
|
218
|
+
* @param authParams - user auth params. Pass `abortSignal` to cancel the flow.
|
|
189
219
|
*/
|
|
190
220
|
signInUserWithQrCode(apiCredentials, authParams) {
|
|
191
221
|
return authMethods.signInUserWithQrCode(this, apiCredentials, authParams);
|
|
@@ -14,6 +14,8 @@ export declare class UpdateManager {
|
|
|
14
14
|
private globalPtsTimer?;
|
|
15
15
|
private readonly channels;
|
|
16
16
|
private readonly pendingSeq;
|
|
17
|
+
private readonly recentMessageKeys;
|
|
18
|
+
private readonly recentMessageQueue;
|
|
17
19
|
private fetchingDifference;
|
|
18
20
|
private failTimeoutS;
|
|
19
21
|
private failRetryTimer?;
|
|
@@ -38,6 +40,7 @@ export declare class UpdateManager {
|
|
|
38
40
|
private handleShortMessage;
|
|
39
41
|
private feedUpdate;
|
|
40
42
|
private dispatch;
|
|
43
|
+
private isDuplicateMessage;
|
|
41
44
|
private collectEntities;
|
|
42
45
|
private makeGlobalWaiter;
|
|
43
46
|
private getOrCreateChannel;
|
package/client/UpdateManager.js
CHANGED
|
@@ -43,6 +43,7 @@ const NO_UPDATES_TIMEOUT_MS = 15 * 60 * 1000;
|
|
|
43
43
|
const FAIL_DIFFERENCE_INITIAL_S = 1;
|
|
44
44
|
const FAIL_DIFFERENCE_CAP_S = 64;
|
|
45
45
|
const CHANNEL_DIFFERENCE_LIMIT = 100;
|
|
46
|
+
const RECENT_MESSAGE_BUFFER_SIZE = 1000;
|
|
46
47
|
function isCommonPtsUpdate(update) {
|
|
47
48
|
return (update instanceof tl_1.Api.UpdateNewMessage ||
|
|
48
49
|
update instanceof tl_1.Api.UpdateDeleteMessages ||
|
|
@@ -79,6 +80,8 @@ class UpdateManager {
|
|
|
79
80
|
this.lastUpdateTime = 0;
|
|
80
81
|
this.channels = new Map();
|
|
81
82
|
this.pendingSeq = [];
|
|
83
|
+
this.recentMessageKeys = new Set();
|
|
84
|
+
this.recentMessageQueue = [];
|
|
82
85
|
this.fetchingDifference = false;
|
|
83
86
|
this.failTimeoutS = FAIL_DIFFERENCE_INITIAL_S;
|
|
84
87
|
this.channelFailTimeoutS = new Map();
|
|
@@ -306,9 +309,39 @@ class UpdateManager {
|
|
|
306
309
|
}
|
|
307
310
|
dispatch(update, payload) {
|
|
308
311
|
var _a;
|
|
312
|
+
if (this.isDuplicateMessage(update)) {
|
|
313
|
+
this.client._log.debug("Skip duplicate message update (already dispatched)");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
309
316
|
update._entities = (_a = payload.entities) !== null && _a !== void 0 ? _a : new Map();
|
|
310
317
|
(0, updates_1._dispatchUpdate)(this.client, { update }).catch((e) => this.client._log.error(`Error dispatching update: ${e}`));
|
|
311
318
|
}
|
|
319
|
+
isDuplicateMessage(update) {
|
|
320
|
+
if (!(update instanceof tl_1.Api.UpdateNewMessage) &&
|
|
321
|
+
!(update instanceof tl_1.Api.UpdateNewChannelMessage)) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
const message = update.message;
|
|
325
|
+
if ((message === null || message === void 0 ? void 0 : message.id) == undefined || message.peerId == undefined)
|
|
326
|
+
return false;
|
|
327
|
+
let peerId;
|
|
328
|
+
try {
|
|
329
|
+
peerId = utils.getPeerId(message.peerId);
|
|
330
|
+
}
|
|
331
|
+
catch (_a) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
const key = `${peerId}:${message.id}`;
|
|
335
|
+
if (this.recentMessageKeys.has(key))
|
|
336
|
+
return true;
|
|
337
|
+
this.recentMessageKeys.add(key);
|
|
338
|
+
this.recentMessageQueue.push(key);
|
|
339
|
+
if (this.recentMessageQueue.length > RECENT_MESSAGE_BUFFER_SIZE) {
|
|
340
|
+
const old = this.recentMessageQueue.shift();
|
|
341
|
+
this.recentMessageKeys.delete(old);
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
312
345
|
collectEntities(users, chats) {
|
|
313
346
|
const entities = new Map();
|
|
314
347
|
for (const x of [...users, ...chats]) {
|
package/client/auth.d.ts
CHANGED
|
@@ -86,6 +86,9 @@ export interface QrCodeAuthParams extends UserPasswordAuthParams {
|
|
|
86
86
|
/** when an error happens during auth this function will be called with the error.<br/>
|
|
87
87
|
* if this returns true the auth operation will stop. */
|
|
88
88
|
onError: (err: Error) => Promise<boolean> | void;
|
|
89
|
+
/** an AbortSignal to cancel the QR login flow (e.g. the user closed the page).<br/>
|
|
90
|
+
* when aborted, the flow stops polling and rejects with an `AbortError`. */
|
|
91
|
+
abortSignal?: AbortSignal;
|
|
89
92
|
}
|
|
90
93
|
interface ReturnString {
|
|
91
94
|
(): string;
|
|
@@ -115,6 +118,8 @@ export declare function start(client: TelegramClient, authParams: UserAuthParams
|
|
|
115
118
|
/** @hidden */
|
|
116
119
|
export declare function checkAuthorization(client: TelegramClient): Promise<boolean>;
|
|
117
120
|
/** @hidden */
|
|
121
|
+
export declare function logOut(client: TelegramClient): Promise<boolean>;
|
|
122
|
+
/** @hidden */
|
|
118
123
|
export declare function signInUser(client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams): Promise<Api.TypeUser>;
|
|
119
124
|
/** @hidden */
|
|
120
125
|
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 = "";
|
|
@@ -233,17 +248,23 @@ async function signInUser(client, apiCredentials, authParams) {
|
|
|
233
248
|
await authParams.onError(new Error("Auth failed"));
|
|
234
249
|
return client.signInUser(apiCredentials, authParams);
|
|
235
250
|
}
|
|
251
|
+
function qrAbortError() {
|
|
252
|
+
const err = new Error("QR login aborted");
|
|
253
|
+
err.name = "AbortError";
|
|
254
|
+
return err;
|
|
255
|
+
}
|
|
236
256
|
/** @hidden */
|
|
237
257
|
async function signInUserWithQrCode(client, apiCredentials, authParams) {
|
|
238
|
-
let isScanningComplete = false;
|
|
239
258
|
if (authParams.qrCode == undefined) {
|
|
240
259
|
throw new Error("qrCode callback not defined");
|
|
241
260
|
}
|
|
261
|
+
const { abortSignal } = authParams;
|
|
262
|
+
if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted)
|
|
263
|
+
throw qrAbortError();
|
|
264
|
+
let isScanningComplete = false;
|
|
265
|
+
const stopped = () => isScanningComplete || !!(abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted);
|
|
242
266
|
const inputPromise = (async () => {
|
|
243
|
-
while (
|
|
244
|
-
if (isScanningComplete) {
|
|
245
|
-
break;
|
|
246
|
-
}
|
|
267
|
+
while (!stopped()) {
|
|
247
268
|
const result = await client.invoke(new tl_1.Api.auth.ExportLoginToken({
|
|
248
269
|
apiId: Number(apiCredentials.apiId),
|
|
249
270
|
apiHash: apiCredentials.apiHash,
|
|
@@ -260,21 +281,24 @@ async function signInUserWithQrCode(client, apiCredentials, authParams) {
|
|
|
260
281
|
await (0, Helpers_1.sleep)(QR_CODE_TIMEOUT);
|
|
261
282
|
}
|
|
262
283
|
})();
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
284
|
+
const Raw = require("../events/Raw").Raw;
|
|
285
|
+
const rawEvent = new Raw({});
|
|
286
|
+
const onUpdate = (update) => {
|
|
287
|
+
if (update instanceof tl_1.Api.UpdateLoginToken)
|
|
288
|
+
resolveUpdate();
|
|
289
|
+
};
|
|
290
|
+
let resolveUpdate;
|
|
291
|
+
const updatePromise = new Promise((resolve) => (resolveUpdate = resolve));
|
|
292
|
+
client.addEventHandler(onUpdate, rawEvent);
|
|
293
|
+
const abortPromise = new Promise((_, reject) => abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener("abort", () => reject(qrAbortError()), {
|
|
294
|
+
once: true,
|
|
295
|
+
}));
|
|
270
296
|
try {
|
|
271
|
-
await Promise.race([updatePromise, inputPromise]);
|
|
272
|
-
}
|
|
273
|
-
catch (err) {
|
|
274
|
-
throw err;
|
|
297
|
+
await Promise.race([updatePromise, inputPromise, abortPromise]);
|
|
275
298
|
}
|
|
276
299
|
finally {
|
|
277
300
|
isScanningComplete = true;
|
|
301
|
+
client.removeEventHandler(onUpdate, rawEvent);
|
|
278
302
|
}
|
|
279
303
|
try {
|
|
280
304
|
const result2 = await client.invoke(new tl_1.Api.auth.ExportLoginToken({
|
|
@@ -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
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,13 +113,16 @@ 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) {
|
|
119
123
|
return;
|
|
120
124
|
}
|
|
121
125
|
for (const row of rows) {
|
|
122
|
-
row.push(new Date().getTime().toString());
|
|
123
126
|
this.store.set(this.sessionName + row[0], row);
|
|
124
127
|
}
|
|
125
128
|
}
|
|
@@ -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 "";
|
package/tl/custom/dialog.d.ts
CHANGED
package/tl/custom/dialog.js
CHANGED
|
@@ -7,13 +7,14 @@ const draft_1 = require("./draft");
|
|
|
7
7
|
const Helpers_1 = require("../../Helpers");
|
|
8
8
|
class Dialog {
|
|
9
9
|
constructor(client, dialog, entities, message) {
|
|
10
|
+
var _a;
|
|
10
11
|
this._client = client;
|
|
11
12
|
this.dialog = dialog;
|
|
12
13
|
this.pinned = !!dialog.pinned;
|
|
13
14
|
this.folderId = dialog.folderId;
|
|
14
15
|
this.archived = dialog.folderId != undefined;
|
|
15
16
|
this.message = message;
|
|
16
|
-
this.date = this.message.date;
|
|
17
|
+
this.date = (_a = this.message) === null || _a === void 0 ? void 0 : _a.date;
|
|
17
18
|
this.entity = entities.get((0, Utils_1.getPeerId)(dialog.peer));
|
|
18
19
|
this.inputEntity = (0, Utils_1.getInputPeer)(this.entity);
|
|
19
20
|
if (this.entity) {
|