mtproto-checker 0.0.1

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.
Files changed (4) hide show
  1. package/README.md +234 -0
  2. package/check.js +471 -0
  3. package/package.json +31 -0
  4. package/urls.txt +4 -0
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # MTProto Checker โšก
2
+
3
+ Fast Telegram MTProto proxy checker powered by TDLib. It does a real `testProxy`
4
+ handshake through every proxy, so a green result means the proxy is much more
5
+ likely to work in Telegram than with a plain TCP/TLS port check.
6
+
7
+ ## What It Does
8
+
9
+ - โœ… Parses `tg://proxy` and `https://t.me/proxy` links
10
+ - ๐ŸŒ Loads proxy lists from remote URLs, local files, or `stdin`
11
+ - ๐Ÿงน Removes duplicates by `server:port:secret`
12
+ - ๐Ÿ” Supports hex and base64url MTProto secrets
13
+ - ๐Ÿ•ต๏ธ Extracts Fake-TLS SNI from `ee...` secrets and supports padded `dd...` secrets
14
+ - ๐Ÿš€ Checks proxies concurrently via TDLib `testProxy`
15
+ - ๐Ÿ’ช Re-checks survivors with `--iterations` to find the most stable proxies
16
+ - ๐Ÿ“„ Writes both a full JSON report and a ready-to-use TXT list
17
+
18
+ ## Requirements
19
+
20
+ - Node.js 18+ recommended
21
+ - npm
22
+ - Telegram API credentials:
23
+ - `TG_API_ID`
24
+ - `TG_API_HASH`
25
+
26
+ Get API credentials from [my.telegram.org](https://my.telegram.org).
27
+
28
+ > No Telegram login or phone number is required. The credentials are only used
29
+ > to initialize TDLib before running `testProxy`.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ git clone git@github.com:Tar4s/mtproto-checker.git
35
+ cd mtproto-checker
36
+ npm install
37
+ ```
38
+
39
+ Optional, if you want the `check-proxies` command available locally:
40
+
41
+ ```bash
42
+ npm link
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ Check URLs listed in `urls.txt`:
48
+
49
+ ```bash
50
+ TG_API_ID=12345 TG_API_HASH=abcdef npm start -- --sources urls.txt
51
+ ```
52
+
53
+ Equivalent direct Node.js run:
54
+
55
+ ```bash
56
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js --sources urls.txt
57
+ ```
58
+
59
+ With `npm link`:
60
+
61
+ ```bash
62
+ TG_API_ID=12345 TG_API_HASH=abcdef check-proxies --sources urls.txt
63
+ ```
64
+
65
+ ## Input Sources
66
+
67
+ You can provide proxies in several ways.
68
+
69
+ ### 1. Source URL File
70
+
71
+ `urls.txt` contains one remote text-list URL per line:
72
+
73
+ ```txt
74
+ https://example.com/proxies.txt
75
+ https://example.com/more-proxies.txt
76
+ ```
77
+
78
+ Run:
79
+
80
+ ```bash
81
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js --sources urls.txt
82
+ ```
83
+
84
+ ### 2. One Or More Remote URLs
85
+
86
+ ```bash
87
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js \
88
+ --url https://example.com/proxies.txt \
89
+ --url https://example.com/more-proxies.txt
90
+ ```
91
+
92
+ Positional HTTP URLs also work:
93
+
94
+ ```bash
95
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js https://example.com/proxies.txt
96
+ ```
97
+
98
+ ### 3. Local Proxy File
99
+
100
+ ```bash
101
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js proxies.txt
102
+ ```
103
+
104
+ ### 4. stdin
105
+
106
+ ```bash
107
+ cat proxies.txt | TG_API_ID=12345 TG_API_HASH=abcdef node check.js
108
+ ```
109
+
110
+ Input files may contain blank lines and `#` comments.
111
+
112
+ ## CLI Options
113
+
114
+ | Option | Default | Description |
115
+ | --- | ---: | --- |
116
+ | `--url <url>` | none | Add a remote proxy-list URL. Can be repeated. |
117
+ | `--sources <file>` | none | Read remote source URLs from a file, one URL per line. |
118
+ | `--dc <1-5>` | `2` | Telegram data center ID used for `testProxy`. |
119
+ | `--timeout <sec>` | `10` | Per-proxy TDLib timeout in seconds. Decimals are allowed. |
120
+ | `--concurrency <n>` | `30` | Number of proxies checked in parallel. Lower values can produce steadier latency numbers. |
121
+ | `--iterations <num>` | `1` | Number of check rounds. Each next round checks only proxies that passed the previous one. |
122
+ | `--out <prefix>` | `result` | Output file prefix. Writes `<prefix>.json` and `<prefix>.txt`. |
123
+
124
+ Environment variables:
125
+
126
+ | Variable | Required | Description |
127
+ | --- | --- | --- |
128
+ | `TG_API_ID` | yes | Telegram API ID from `my.telegram.org`. |
129
+ | `TG_API_HASH` | yes | Telegram API hash from `my.telegram.org`. |
130
+
131
+ ## Output
132
+
133
+ By default the checker writes:
134
+
135
+ - `result.json` โ€” full report for the final completed round
136
+ - `result.txt` โ€” working proxy links from the final completed round, fastest first
137
+
138
+ Example JSON item:
139
+
140
+ ```json
141
+ {
142
+ "server": "1.2.3.4",
143
+ "port": 443,
144
+ "sni": "example.com",
145
+ "ok": true,
146
+ "ms": 841,
147
+ "error": null,
148
+ "link": "tg://proxy?server=1.2.3.4&port=443&secret=..."
149
+ }
150
+ ```
151
+
152
+ Use a custom prefix:
153
+
154
+ ```bash
155
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js --sources urls.txt --out fresh
156
+ ```
157
+
158
+ This creates `fresh.json` and `fresh.txt`.
159
+
160
+ ## Practical Examples
161
+
162
+ Fast broad scan:
163
+
164
+ ```bash
165
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js --sources urls.txt --concurrency 80 --timeout 7
166
+ ```
167
+
168
+ More conservative latency check:
169
+
170
+ ```bash
171
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js --sources urls.txt --concurrency 10 --timeout 15
172
+ ```
173
+
174
+ Find the most stable proxies across several rounds:
175
+
176
+ ```bash
177
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js --sources urls.txt --iterations 3 --out stable
178
+ ```
179
+
180
+ Test against another Telegram DC:
181
+
182
+ ```bash
183
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js --sources urls.txt --dc 4
184
+ ```
185
+
186
+ Check a pasted list:
187
+
188
+ ```bash
189
+ pbpaste | TG_API_ID=12345 TG_API_HASH=abcdef node check.js --out pasted
190
+ ```
191
+
192
+ ## Programmatic Usage
193
+
194
+ ```js
195
+ const { checkProxiesFromUrls } = require('./check')
196
+
197
+ const results = await checkProxiesFromUrls(
198
+ ['https://example.com/proxies.txt'],
199
+ {
200
+ apiId: Number(process.env.TG_API_ID),
201
+ apiHash: process.env.TG_API_HASH,
202
+ dc: 2,
203
+ timeout: 10,
204
+ concurrency: 30
205
+ }
206
+ )
207
+
208
+ console.log(results)
209
+ ```
210
+
211
+ Exported helpers:
212
+
213
+ - `checkProxiesFromUrls(urls, opts)`
214
+ - `loadProxiesFromUrls(urls)`
215
+ - `checkProxies(proxies, opts)`
216
+ - `mergeProxies(texts)`
217
+ - `parseLink(line)`
218
+ - `normalizeSecret(secret)`
219
+ - `faketlsSni(hexSecret)`
220
+
221
+ ## Notes & Troubleshooting
222
+
223
+ - If you see `Set TG_API_ID and TG_API_HASH`, export both credentials or prefix
224
+ the command with them.
225
+ - If remote URLs fail on old Node.js versions, upgrade to Node.js 18+.
226
+ - If checks are noisy, reduce `--concurrency`.
227
+ - If you want only resilient proxies, increase `--iterations`; `result.txt`
228
+ will contain proxies that survived the final round.
229
+ - Only MTProto proxy links are checked. `tg://socks` links are ignored.
230
+ - Temporary TDLib files are created in `.proxy-checker-td/` and removed after the run.
231
+
232
+ ## License
233
+
234
+ No license file is currently included.
package/check.js ADDED
@@ -0,0 +1,471 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ /**
5
+ * MTProto proxy health checker built on TDLib (via the `tdl` Node binding).
6
+ *
7
+ * Unlike a TCP/TLS ping, this performs a real MTProto handshake to a Telegram
8
+ * data center *through* each proxy using TDLib's `testProxy` method โ€” the same
9
+ * protocol path the official clients (tdesktop) use. A "ok" here therefore
10
+ * means the proxy will actually work in the app, not just that the port is open
11
+ * or that some web server is answering the Fake-TLS camouflage.
12
+ *
13
+ * Sources of proxies (any combination):
14
+ * - one or more remote URLs (e.g. raw GitHub files), fetched and merged;
15
+ * - a local file (one link per line);
16
+ * - stdin.
17
+ * In all cases links are parsed, blank/`#`-comment lines dropped, and the
18
+ * result de-duplicated (by server+port+secret) via a Set.
19
+ *
20
+ * CLI usage:
21
+ * TG_API_ID=12345 TG_API_HASH=abcdef... node check.js [sources] [options]
22
+ * # sources: any positional http(s) URL, a local file path, or stdin
23
+ * node check.js https://raw.githubusercontent.com/u/r/main/list.txt
24
+ * node check.js --url URL1 --url URL2
25
+ * node check.js --sources urls.txt # file with one URL per line
26
+ * cat proxies.txt | node check.js
27
+ *
28
+ * Options:
29
+ * --url <url> add a source URL (repeatable)
30
+ * --sources <file> file containing source URLs (one per line, # comments ok)
31
+ * --dc <1-5> data center id to test against (default 2)
32
+ * --timeout <sec> per-proxy TDLib timeout in seconds (default 10)
33
+ * --concurrency <n> parallel checks (default 30; lower = more accurate ms)
34
+ * --out <prefix> output file prefix (default "result")
35
+ * --iterations <num> repeat checks, keeping only proxies that passed the previous round (default 1)
36
+ *
37
+ * Module usage:
38
+ * const { checkProxiesFromUrls } = require('./check')
39
+ * const results = await checkProxiesFromUrls([url1, url2], { apiId, apiHash })
40
+ *
41
+ * Requirements: Node.js v18+ (for global fetch), `npm i tdl prebuilt-tdlib`,
42
+ * and api_id/api_hash from https://my.telegram.org (no login is performed).
43
+ */
44
+
45
+ const fs = require('fs')
46
+ const path = require('path')
47
+ const tdl = require('tdl')
48
+ const { getTdjson } = require('prebuilt-tdlib')
49
+
50
+ const tdlibConfigState = { configured: false }
51
+
52
+ /**
53
+ * Configure TDLib once per process. The `tdl` package rejects configure calls
54
+ * after the first client has been initialized.
55
+ * @param {{configured: boolean}} state - mutable configuration state
56
+ * @param {(opts: object) => void} configure - tdl.configure-compatible function
57
+ * @param {() => unknown} tdjsonFactory - returns tdjson binding
58
+ */
59
+ function configureTdlibOnce(state = tdlibConfigState, configure = tdl.configure, tdjsonFactory = getTdjson) {
60
+ if (state.configured) return
61
+ configure({ tdjson: tdjsonFactory(), verbosityLevel: 0 })
62
+ state.configured = true
63
+ }
64
+
65
+ /**
66
+ * Parse argv into an options object, collecting source URLs and/or a file path.
67
+ * @param {string[]} argv - process.argv.slice(2)
68
+ * @returns {{file: string|null, urls: string[], sourcesFile: string|null, dc: number, timeout: number, concurrency: number, out: string, iterations: number}}
69
+ */
70
+ function parseArgs(argv) {
71
+ const opts = { file: null, urls: [], sourcesFile: null, dc: 2, timeout: 10, concurrency: 30, out: 'result', iterations: 1 }
72
+ for (let i = 0; i < argv.length; i++) {
73
+ const a = argv[i]
74
+ if (a === '--dc') opts.dc = parseInt(argv[++i], 10)
75
+ else if (a === '--timeout') opts.timeout = parseFloat(argv[++i])
76
+ else if (a === '--concurrency') opts.concurrency = parseInt(argv[++i], 10)
77
+ else if (a === '--out') opts.out = argv[++i]
78
+ else if (a === '--iterations') opts.iterations = parseInt(argv[++i], 10)
79
+ else if (a === '--url') opts.urls.push(argv[++i])
80
+ else if (a === '--sources') opts.sourcesFile = argv[++i]
81
+ else if (/^https?:\/\//i.test(a)) opts.urls.push(a)
82
+ else if (!a.startsWith('--')) opts.file = a
83
+ }
84
+ return opts
85
+ }
86
+
87
+ /**
88
+ * Read raw input from a file path, or from stdin when no path is given.
89
+ * @param {string|null} file - path to the proxy list, or null for stdin
90
+ * @returns {Promise<string>} the raw file contents
91
+ */
92
+ function readInput(file) {
93
+ if (file) return Promise.resolve(fs.readFileSync(file, 'utf8'))
94
+ return new Promise((resolve, reject) => {
95
+ let data = ''
96
+ process.stdin.setEncoding('utf8')
97
+ process.stdin.on('data', chunk => { data += chunk })
98
+ process.stdin.on('end', () => resolve(data))
99
+ process.stdin.on('error', reject)
100
+ })
101
+ }
102
+
103
+ /**
104
+ * Normalize a proxy secret to lowercase hex. Accepts hex (`ee...`, `dd...`,
105
+ * plain) or base64url-encoded secrets and returns the hex form expected by
106
+ * TDLib's proxyTypeMtproto.
107
+ * @param {string} secret - the raw `secret` query parameter
108
+ * @returns {string} lowercase hex secret
109
+ */
110
+ function normalizeSecret(secret) {
111
+ const s = secret.trim()
112
+ if (/^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0) return s.toLowerCase()
113
+ return Buffer.from(s, 'base64url').toString('hex')
114
+ }
115
+
116
+ /**
117
+ * Extract the Fake-TLS camouflage domain (SNI) embedded in an `ee`-prefixed
118
+ * secret: one tag byte + 16-byte key (34 hex chars), then the domain in hex.
119
+ * @param {string} hexSecret - lowercase hex secret
120
+ * @returns {string|null} the SNI domain, or null if not a Fake-TLS secret
121
+ */
122
+ function faketlsSni(hexSecret) {
123
+ if (!hexSecret.startsWith('ee')) return null
124
+ const domainHex = hexSecret.slice(34)
125
+ if (!domainHex) return null
126
+ try {
127
+ return Buffer.from(domainHex, 'hex').toString('utf8')
128
+ } catch {
129
+ return null
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Parse a single tg://proxy or https://t.me/proxy link into proxy fields.
135
+ * @param {string} line - a single, comment-stripped, trimmed line
136
+ * @returns {{raw: string, server: string, port: number, secret: string, sni: string|null}|null}
137
+ * parsed proxy, or null if the line is not a supported MTProto proxy link
138
+ */
139
+ function parseLink(line) {
140
+ const raw = line.trim()
141
+ const qIndex = raw.indexOf('?')
142
+ if (qIndex === -1) return null
143
+ // Only MTProto proxies (tg://proxy / t.me/proxy); tg://socks is skipped.
144
+ if (!/\bproxy\b/i.test(raw.slice(0, qIndex))) return null
145
+ const params = new URLSearchParams(raw.slice(qIndex + 1))
146
+ const server = params.get('server')
147
+ const port = parseInt(params.get('port'), 10)
148
+ const secretRaw = params.get('secret')
149
+ if (!server || !port || !secretRaw) return null
150
+ const secret = normalizeSecret(secretRaw)
151
+ return { raw, server, port, secret, sni: faketlsSni(secret) }
152
+ }
153
+
154
+ /**
155
+ * Parse one proxy-list text blob, appending unique proxies (by
156
+ * server:port:secret) to `out` using the shared `seen` set. `#` starts a
157
+ * comment (whole-line or trailing); blank lines are ignored.
158
+ * @param {string} text - a proxy list
159
+ * @param {Set<string>} seen - shared de-duplication set (canonical keys)
160
+ * @param {Array} out - accumulator for unique proxies
161
+ */
162
+ function collectProxies(text, seen, out) {
163
+ for (const rawLine of text.split(/\r?\n/)) {
164
+ const line = rawLine.split('#')[0].trim()
165
+ if (!line) continue
166
+ const proxy = parseLink(line)
167
+ if (!proxy) continue
168
+ const key = `${proxy.server}:${proxy.port}:${proxy.secret}`
169
+ if (seen.has(key)) continue
170
+ seen.add(key)
171
+ out.push(proxy)
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Merge multiple proxy-list text blobs into one de-duplicated array.
177
+ * @param {string[]} texts - raw proxy lists
178
+ * @returns {Array} unique proxies in first-seen order
179
+ */
180
+ function mergeProxies(texts) {
181
+ const seen = new Set()
182
+ const out = []
183
+ for (const text of texts) collectProxies(text, seen, out)
184
+ return out
185
+ }
186
+
187
+ /**
188
+ * Download a URL and return its body text.
189
+ * @param {string} url - the raw file URL
190
+ * @returns {Promise<string>} the response body
191
+ * @throws on a non-2xx response
192
+ */
193
+ async function fetchText(url) {
194
+ const res = await fetch(url)
195
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
196
+ return res.text()
197
+ }
198
+
199
+ /**
200
+ * Download every source URL concurrently, then merge + de-duplicate all proxy
201
+ * links into one array. Sources that fail to download are logged and skipped.
202
+ * @param {string[]} urls - raw URLs of text files, one proxy link per line
203
+ * @returns {Promise<Array>} unique proxies
204
+ */
205
+ async function loadProxiesFromUrls(urls) {
206
+ const texts = await Promise.all(urls.map(async url => {
207
+ try {
208
+ return await fetchText(url)
209
+ } catch (err) {
210
+ console.error(`Skipping ${url}: ${err.message}`)
211
+ return ''
212
+ }
213
+ }))
214
+ return mergeProxies(texts)
215
+ }
216
+
217
+ /**
218
+ * Drive the pre-authorization TDLib flow manually (without `client.login`):
219
+ * answer `authorizationStateWaitTdlibParameters` with `setTdlibParameters` and
220
+ * resolve once the client is ready to serve `testProxy` requests. We never log
221
+ * in, so it parks at `authorizationStateWaitPhoneNumber`, which is enough.
222
+ * @param {import('tdl').Client} client - the tdl client
223
+ * @param {{apiId: number, apiHash: string, databaseDirectory: string, filesDirectory: string}} cfg
224
+ * @returns {Promise<void>} resolves when parameters are set
225
+ */
226
+ function prepare(client, cfg) {
227
+ return new Promise((resolve, reject) => {
228
+ /**
229
+ * React to authorization-state transitions until the client is ready.
230
+ * @param {{_: string, authorization_state?: {_: string}}} update
231
+ */
232
+ const onUpdate = update => {
233
+ if (update._ !== 'updateAuthorizationState') return
234
+ const state = update.authorization_state._
235
+ if (state === 'authorizationStateWaitTdlibParameters')
236
+ client.invoke({
237
+ _: 'setTdlibParameters',
238
+ api_id: cfg.apiId,
239
+ api_hash: cfg.apiHash,
240
+ database_directory: cfg.databaseDirectory,
241
+ files_directory: cfg.filesDirectory,
242
+ database_encryption_key: '',
243
+ use_file_database: false,
244
+ use_chat_info_database: false,
245
+ use_message_database: false,
246
+ use_secret_chats: false,
247
+ system_language_code: 'en',
248
+ device_model: 'mtproto-proxy-checker',
249
+ system_version: '1.0',
250
+ application_version: '1.0',
251
+ use_test_dc: false
252
+ }).catch(reject)
253
+ else if (state === 'authorizationStateWaitPhoneNumber' || state === 'authorizationStateReady')
254
+ resolve()
255
+ }
256
+ client.on('update', onUpdate)
257
+ })
258
+ }
259
+
260
+ /**
261
+ * Test a single proxy by performing a real MTProto handshake through it.
262
+ * @param {import('tdl').Client} client - the tdl client
263
+ * @param {{server: string, port: number, secret: string}} proxy - parsed proxy
264
+ * @param {number} dcId - data center id (1-5) to test against
265
+ * @param {number} timeoutSec - TDLib-side timeout in seconds
266
+ * @returns {Promise<{ok: boolean, ms: number, error: string|null}>} test result
267
+ */
268
+ async function checkOne(client, proxy, dcId, timeoutSec) {
269
+ const started = Date.now()
270
+ try {
271
+ // TDLib >= 1.8.64 takes a single `proxy` object (proxy$Input); the older
272
+ // flat server/port/type form is silently ignored and yields an empty proxy.
273
+ await client.invoke({
274
+ _: 'testProxy',
275
+ proxy: {
276
+ _: 'proxy',
277
+ server: proxy.server,
278
+ port: proxy.port,
279
+ type: { _: 'proxyTypeMtproto', secret: proxy.secret }
280
+ },
281
+ dc_id: dcId,
282
+ timeout: timeoutSec
283
+ })
284
+ return { ok: true, ms: Date.now() - started, error: null }
285
+ } catch (err) {
286
+ const message = err && err.message ? err.message : String(err)
287
+ return { ok: false, ms: Date.now() - started, error: message }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Run an async worker over items with bounded concurrency, preserving input
293
+ * order in the returned results array.
294
+ * @template T, R
295
+ * @param {T[]} items - input items
296
+ * @param {number} concurrency - maximum parallel workers
297
+ * @param {(item: T, index: number) => Promise<R>} worker - per-item handler
298
+ * @returns {Promise<R[]>} results in the same order as `items`
299
+ */
300
+ async function runPool(items, concurrency, worker) {
301
+ const results = new Array(items.length)
302
+ let next = 0
303
+ /** Pull and process items until the shared queue is drained. */
304
+ async function run() {
305
+ while (next < items.length) {
306
+ const index = next++
307
+ results[index] = await worker(items[index], index)
308
+ }
309
+ }
310
+ const runners = []
311
+ for (let i = 0; i < Math.min(concurrency, items.length); i++) runners.push(run())
312
+ await Promise.all(runners)
313
+ return results
314
+ }
315
+
316
+ /**
317
+ * Test an array of parsed proxies through TDLib and return sorted results
318
+ * (working first, then by latency ascending). Spins up and tears down a
319
+ * throwaway TDLib client; no Telegram login is performed.
320
+ * @param {Array} proxies - parsed proxies (from mergeProxies/loadProxiesFromUrls)
321
+ * @param {{apiId: number, apiHash: string, dc?: number, timeout?: number, concurrency?: number, tdlibDir?: string, onProgress?: (proxy: object, res: object, index: number, total: number) => void}} opts
322
+ * @returns {Promise<Array<{proxy: object, ok: boolean, ms: number, error: string|null}>>}
323
+ */
324
+ async function checkProxies(proxies, opts) {
325
+ if (proxies.length === 0) return []
326
+ const dc = opts.dc ?? 2
327
+ const timeout = opts.timeout ?? 10
328
+ const concurrency = opts.concurrency ?? 30
329
+ const tdlibDir = opts.tdlibDir ?? '.proxy-checker-td'
330
+ const databaseDirectory = path.join(tdlibDir, 'db')
331
+ const filesDirectory = path.join(tdlibDir, 'files')
332
+
333
+ configureTdlibOnce()
334
+ const client = tdl.createClient({ apiId: opts.apiId, apiHash: opts.apiHash, databaseDirectory, filesDirectory })
335
+ client.on('error', err => console.error('TDLib error:', err))
336
+
337
+ try {
338
+ await prepare(client, { apiId: opts.apiId, apiHash: opts.apiHash, databaseDirectory, filesDirectory })
339
+ const checks = await runPool(proxies, concurrency, async (proxy, index) => {
340
+ const res = await checkOne(client, proxy, dc, timeout)
341
+ if (opts.onProgress) opts.onProgress(proxy, res, index, proxies.length)
342
+ return { proxy, ...res }
343
+ })
344
+ return checks.slice().sort((a, b) => {
345
+ if (a.ok !== b.ok) return a.ok ? -1 : 1
346
+ return a.ms - b.ms
347
+ })
348
+ } finally {
349
+ await client.close()
350
+ fs.rmSync(tdlibDir, { recursive: true, force: true })
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Re-check proxies for multiple rounds, carrying only working proxies forward.
356
+ * @param {Array} proxies - parsed proxies to check in the first round
357
+ * @param {number} iterations - number of rounds to run
358
+ * @param {(proxies: Array, iteration: number) => Promise<Array>} checker
359
+ * function that checks one round and returns checkProxies-style results
360
+ * @returns {Promise<Array>} survivors after the final completed round
361
+ */
362
+ async function runIterativeChecks(proxies, iterations, checker) {
363
+ const rounds = Math.max(1, iterations)
364
+ let current = proxies
365
+ let results = []
366
+
367
+ for (let iteration = 1; iteration <= rounds && current.length > 0; iteration++) {
368
+ results = await checker(current, iteration)
369
+ current = results.filter(c => c.ok).map(c => c.proxy)
370
+ }
371
+
372
+ return results
373
+ }
374
+
375
+ /**
376
+ * Download proxy lists from the given URLs, merge + de-duplicate them, and test
377
+ * every unique proxy through TDLib. This is the end-to-end entry point.
378
+ * @param {string[]} urls - raw URLs of text files, one proxy link per line
379
+ * @param {{apiId: number, apiHash: string, dc?: number, timeout?: number, concurrency?: number, tdlibDir?: string, onProgress?: Function}} opts
380
+ * @returns {Promise<Array>} per-proxy results, working first, then by latency
381
+ */
382
+ async function checkProxiesFromUrls(urls, opts) {
383
+ const proxies = await loadProxiesFromUrls(urls)
384
+ return checkProxies(proxies, opts)
385
+ }
386
+
387
+ /**
388
+ * CLI entry point: resolve sources (URLs / file / stdin), check, and write
389
+ * `<out>.json` (full report) and `<out>.txt` (working links, fastest first).
390
+ * @returns {Promise<void>}
391
+ */
392
+ async function main() {
393
+ const opts = parseArgs(process.argv.slice(2))
394
+ const apiId = parseInt(process.env.TG_API_ID, 10)
395
+ const apiHash = process.env.TG_API_HASH
396
+ if (!apiId || !apiHash) {
397
+ console.error('Set TG_API_ID and TG_API_HASH (get them at https://my.telegram.org).')
398
+ process.exit(1)
399
+ }
400
+
401
+ // A --sources file contributes one URL per line (with # comments).
402
+ if (opts.sourcesFile) {
403
+ const text = fs.readFileSync(opts.sourcesFile, 'utf8')
404
+ for (const rawLine of text.split(/\r?\n/)) {
405
+ const line = rawLine.split('#')[0].trim()
406
+ if (line) opts.urls.push(line)
407
+ }
408
+ }
409
+
410
+ let proxies
411
+ if (opts.urls.length > 0) {
412
+ console.error(`Fetching ${opts.urls.length} source URL(s)...`)
413
+ proxies = await loadProxiesFromUrls(opts.urls)
414
+ } else {
415
+ const input = await readInput(opts.file)
416
+ proxies = mergeProxies([input])
417
+ }
418
+
419
+ if (proxies.length === 0) {
420
+ console.error('No valid tg://proxy or t.me/proxy links found.')
421
+ process.exit(1)
422
+ }
423
+
424
+ if (!Number.isInteger(opts.iterations) || opts.iterations < 1) {
425
+ console.error('--iterations must be a positive integer.')
426
+ process.exit(1)
427
+ }
428
+
429
+ console.error(`Checking ${proxies.length} unique proxies (dc=${opts.dc}, timeout=${opts.timeout}s, concurrency=${opts.concurrency}, iterations=${opts.iterations})...\n`)
430
+
431
+ const sorted = await runIterativeChecks(proxies, opts.iterations, (batch, iteration) => {
432
+ if (opts.iterations > 1) console.error(`Iteration ${iteration}/${opts.iterations}: checking ${batch.length} proxy/proxies...\n`)
433
+ return checkProxies(batch, {
434
+ apiId,
435
+ apiHash,
436
+ dc: opts.dc,
437
+ timeout: opts.timeout,
438
+ concurrency: opts.concurrency,
439
+ onProgress: (proxy, res, index, total) => {
440
+ const tag = res.ok ? `ok ${String(res.ms).padStart(5)}ms` : `-- ${res.error}`
441
+ const sni = proxy.sni ? ` [sni: ${proxy.sni}]` : ''
442
+ console.error(`[${String(index + 1).padStart(3)}/${total}] ${tag} ${proxy.server}:${proxy.port}${sni}`)
443
+ }
444
+ })
445
+ })
446
+
447
+ const working = sorted.filter(c => c.ok)
448
+ const report = sorted.map(c => ({
449
+ server: c.proxy.server,
450
+ port: c.proxy.port,
451
+ sni: c.proxy.sni,
452
+ ok: c.ok,
453
+ ms: c.ms,
454
+ error: c.error,
455
+ link: c.proxy.raw
456
+ }))
457
+
458
+ fs.writeFileSync(`${opts.out}.json`, JSON.stringify(report, null, 2))
459
+ fs.writeFileSync(`${opts.out}.txt`, working.map(c => c.proxy.raw).join('\n') + (working.length ? '\n' : ''))
460
+
461
+ console.error(`\nDone: ${working.length}/${proxies.length} working.`)
462
+ console.error(`Wrote ${opts.out}.json and ${opts.out}.txt`)
463
+ process.exit(0)
464
+ }
465
+
466
+ module.exports = { configureTdlibOnce, parseArgs, checkProxiesFromUrls, loadProxiesFromUrls, checkProxies, runIterativeChecks, mergeProxies, parseLink, normalizeSecret, faketlsSni }
467
+
468
+ if (require.main === module) main().catch(err => {
469
+ console.error(err)
470
+ process.exit(1)
471
+ })
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "mtproto-checker",
3
+ "version": "0.0.1",
4
+ "description": "Check Telegram MTProto proxies via a real TDLib handshake (testProxy), like tdesktop does",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Tar4s/mtproto-checker"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/Tar4s/mtproto-checker/issues"
11
+ },
12
+ "homepage": "https://github.com/Tar4s/mtproto-checker#readme",
13
+ "bin": {
14
+ "check-proxies": "check.js"
15
+ },
16
+ "files": [
17
+ "check.js",
18
+ "README.md",
19
+ "urls.txt"
20
+ ],
21
+ "scripts": {
22
+ "start": "node check.js"
23
+ },
24
+ "engines": {
25
+ "node": ">=16"
26
+ },
27
+ "dependencies": {
28
+ "tdl": "^8.0.2",
29
+ "prebuilt-tdlib": "0.1008064.0"
30
+ }
31
+ }
package/urls.txt ADDED
@@ -0,0 +1,4 @@
1
+ https://raw.githubusercontent.com/SoliSpirit/mtproto/refs/heads/master/all_proxies.txt
2
+ https://raw.githubusercontent.com/kort0881/telegram-proxy-collector/refs/heads/main/proxy_all.txt
3
+ https://raw.githubusercontent.com/Surfboardv2ray/TGProto/refs/heads/main/proxies.txt
4
+ https://raw.githubusercontent.com/Therealwh/MTPproxyLIST/refs/heads/main/verified/proxy_all_tme_verified.txt