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.
- package/README.md +234 -0
- package/check.js +471 -0
- package/package.json +31 -0
- 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
|