mtproto-checker 0.0.1 → 0.1.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 (3) hide show
  1. package/README.md +105 -4
  2. package/check.js +239 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -42,8 +42,36 @@ Optional, if you want the `check-proxies` command available locally:
42
42
  npm link
43
43
  ```
44
44
 
45
+ ### GitHub Packages
46
+
47
+ This project can also be published to GitHub Packages as
48
+ `@tar4s/mtproto-checker`. GitHub's npm registry requires scoped package names,
49
+ so the GitHub Packages workflow applies that scoped name during publishing.
50
+
51
+ To install from GitHub Packages:
52
+
53
+ ```bash
54
+ npm config set @tar4s:registry https://npm.pkg.github.com
55
+ npm install @tar4s/mtproto-checker
56
+ ```
57
+
58
+ Private packages require authentication with a GitHub personal access token that
59
+ has `read:packages`.
60
+
45
61
  ## Quick Start
46
62
 
63
+ Start the HTTP API server:
64
+
65
+ ```bash
66
+ TG_API_ID=12345 \
67
+ TG_API_HASH=abcdef \
68
+ CHECK_AUTH_USER=admin \
69
+ CHECK_AUTH_PASSWORD=secret \
70
+ node check.js
71
+ ```
72
+
73
+ By default it listens on port `3080`. Override it with `PORT`.
74
+
47
75
  Check URLs listed in `urls.txt`:
48
76
 
49
77
  ```bash
@@ -62,11 +90,25 @@ With `npm link`:
62
90
  TG_API_ID=12345 TG_API_HASH=abcdef check-proxies --sources urls.txt
63
91
  ```
64
92
 
93
+ Check one proxy link directly:
94
+
95
+ ```bash
96
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js \
97
+ --proxy 'tg://proxy?server=quackton.life&port=443&secret=7mX8dVOh9cqLULccAVs4ciR5YW5kZXgucnU'
98
+ ```
99
+
65
100
  ## Input Sources
66
101
 
67
102
  You can provide proxies in several ways.
68
103
 
69
- ### 1. Source URL File
104
+ ### 1. Direct Proxy Link
105
+
106
+ ```bash
107
+ TG_API_ID=12345 TG_API_HASH=abcdef node check.js \
108
+ --proxy 'https://t.me/proxy?server=1.2.3.4&port=443&secret=...'
109
+ ```
110
+
111
+ ### 2. Source URL File
70
112
 
71
113
  `urls.txt` contains one remote text-list URL per line:
72
114
 
@@ -81,7 +123,7 @@ Run:
81
123
  TG_API_ID=12345 TG_API_HASH=abcdef node check.js --sources urls.txt
82
124
  ```
83
125
 
84
- ### 2. One Or More Remote URLs
126
+ ### 3. One Or More Remote URLs
85
127
 
86
128
  ```bash
87
129
  TG_API_ID=12345 TG_API_HASH=abcdef node check.js \
@@ -95,13 +137,13 @@ Positional HTTP URLs also work:
95
137
  TG_API_ID=12345 TG_API_HASH=abcdef node check.js https://example.com/proxies.txt
96
138
  ```
97
139
 
98
- ### 3. Local Proxy File
140
+ ### 4. Local Proxy File
99
141
 
100
142
  ```bash
101
143
  TG_API_ID=12345 TG_API_HASH=abcdef node check.js proxies.txt
102
144
  ```
103
145
 
104
- ### 4. stdin
146
+ ### 5. stdin
105
147
 
106
148
  ```bash
107
149
  cat proxies.txt | TG_API_ID=12345 TG_API_HASH=abcdef node check.js
@@ -114,6 +156,7 @@ Input files may contain blank lines and `#` comments.
114
156
  | Option | Default | Description |
115
157
  | --- | ---: | --- |
116
158
  | `--url <url>` | none | Add a remote proxy-list URL. Can be repeated. |
159
+ | `--proxy <link>` | none | Check one `tg://proxy` or `https://t.me/proxy` link directly. |
117
160
  | `--sources <file>` | none | Read remote source URLs from a file, one URL per line. |
118
161
  | `--dc <1-5>` | `2` | Telegram data center ID used for `testProxy`. |
119
162
  | `--timeout <sec>` | `10` | Per-proxy TDLib timeout in seconds. Decimals are allowed. |
@@ -127,6 +170,64 @@ Environment variables:
127
170
  | --- | --- | --- |
128
171
  | `TG_API_ID` | yes | Telegram API ID from `my.telegram.org`. |
129
172
  | `TG_API_HASH` | yes | Telegram API hash from `my.telegram.org`. |
173
+ | `CHECK_AUTH_USER` | HTTP server only | Basic auth username. |
174
+ | `CHECK_AUTH_PASSWORD` | HTTP server only | Basic auth password. |
175
+ | `PORT` | no | HTTP server port. Defaults to `3080`. |
176
+
177
+ ## HTTP API
178
+
179
+ Running `node check.js` without CLI arguments starts the HTTP server. The server
180
+ requires Basic auth and exposes one endpoint:
181
+
182
+ ```http
183
+ POST /check
184
+ Content-Type: application/json
185
+ Authorization: Basic ...
186
+
187
+ { "url": "https://example.com/proxies.txt" }
188
+ ```
189
+
190
+ The `url` field accepts either a remote `http(s)` proxy list or one direct
191
+ `tg://proxy` / `https://t.me/proxy` link.
192
+
193
+ Example:
194
+
195
+ ```bash
196
+ curl -u admin:secret \
197
+ -H 'content-type: application/json' \
198
+ -d '{"url":"https://example.com/proxies.txt"}' \
199
+ http://127.0.0.1:3080/check
200
+ ```
201
+
202
+ Direct proxy link example:
203
+
204
+ ```bash
205
+ curl -u admin:secret \
206
+ -H 'content-type: application/json' \
207
+ -d '{"url":"tg://proxy?server=quackton.life&port=443&secret=7mX8dVOh9cqLULccAVs4ciR5YW5kZXgucnU"}' \
208
+ http://127.0.0.1:3080/check
209
+ ```
210
+
211
+ Response:
212
+
213
+ ```json
214
+ {
215
+ "url": "https://example.com/proxies.txt",
216
+ "count": 1,
217
+ "working": 1,
218
+ "results": [
219
+ {
220
+ "server": "1.2.3.4",
221
+ "port": 443,
222
+ "sni": "example.com",
223
+ "ok": true,
224
+ "ms": 841,
225
+ "error": null,
226
+ "link": "tg://proxy?server=1.2.3.4&port=443&secret=..."
227
+ }
228
+ ]
229
+ }
230
+ ```
130
231
 
131
232
  ## Output
132
233
 
package/check.js CHANGED
@@ -18,15 +18,19 @@
18
18
  * result de-duplicated (by server+port+secret) via a Set.
19
19
  *
20
20
  * CLI usage:
21
+ * TG_API_ID=12345 TG_API_HASH=abcdef CHECK_AUTH_USER=admin CHECK_AUTH_PASSWORD=secret node check.js
22
+ * # starts the HTTP API server on PORT (default 3080)
21
23
  * TG_API_ID=12345 TG_API_HASH=abcdef... node check.js [sources] [options]
22
24
  * # sources: any positional http(s) URL, a local file path, or stdin
23
25
  * node check.js https://raw.githubusercontent.com/u/r/main/list.txt
24
26
  * node check.js --url URL1 --url URL2
27
+ * node check.js --proxy "tg://proxy?server=...&port=443&secret=..."
25
28
  * node check.js --sources urls.txt # file with one URL per line
26
29
  * cat proxies.txt | node check.js
27
30
  *
28
31
  * Options:
29
32
  * --url <url> add a source URL (repeatable)
33
+ * --proxy <link> check one proxy link directly
30
34
  * --sources <file> file containing source URLs (one per line, # comments ok)
31
35
  * --dc <1-5> data center id to test against (default 2)
32
36
  * --timeout <sec> per-proxy TDLib timeout in seconds (default 10)
@@ -43,7 +47,9 @@
43
47
  */
44
48
 
45
49
  const fs = require('fs')
50
+ const http = require('http')
46
51
  const path = require('path')
52
+ const crypto = require('crypto')
47
53
  const tdl = require('tdl')
48
54
  const { getTdjson } = require('prebuilt-tdlib')
49
55
 
@@ -65,10 +71,10 @@ function configureTdlibOnce(state = tdlibConfigState, configure = tdl.configure,
65
71
  /**
66
72
  * Parse argv into an options object, collecting source URLs and/or a file path.
67
73
  * @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}}
74
+ * @returns {{file: string|null, urls: string[], proxy: string|null, sourcesFile: string|null, dc: number, timeout: number, concurrency: number, out: string, iterations: number}}
69
75
  */
70
76
  function parseArgs(argv) {
71
- const opts = { file: null, urls: [], sourcesFile: null, dc: 2, timeout: 10, concurrency: 30, out: 'result', iterations: 1 }
77
+ const opts = { file: null, urls: [], proxy: null, sourcesFile: null, dc: 2, timeout: 10, concurrency: 30, out: 'result', iterations: 1 }
72
78
  for (let i = 0; i < argv.length; i++) {
73
79
  const a = argv[i]
74
80
  if (a === '--dc') opts.dc = parseInt(argv[++i], 10)
@@ -77,6 +83,7 @@ function parseArgs(argv) {
77
83
  else if (a === '--out') opts.out = argv[++i]
78
84
  else if (a === '--iterations') opts.iterations = parseInt(argv[++i], 10)
79
85
  else if (a === '--url') opts.urls.push(argv[++i])
86
+ else if (a === '--proxy') opts.proxy = argv[++i]
80
87
  else if (a === '--sources') opts.sourcesFile = argv[++i]
81
88
  else if (/^https?:\/\//i.test(a)) opts.urls.push(a)
82
89
  else if (!a.startsWith('--')) opts.file = a
@@ -384,6 +391,233 @@ async function checkProxiesFromUrls(urls, opts) {
384
391
  return checkProxies(proxies, opts)
385
392
  }
386
393
 
394
+ async function checkSingleUrl(url, opts) {
395
+ const directProxy = parseLink(url)
396
+ if (directProxy) {
397
+ const checker = opts.checker || checkProxies
398
+ return checker([directProxy], opts)
399
+ }
400
+
401
+ let parsed
402
+ try {
403
+ parsed = new URL(url)
404
+ } catch {
405
+ throw new Error('url must be a proxy link or an http or https URL')
406
+ }
407
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
408
+ throw new Error('url must be a proxy link or an http or https URL')
409
+ }
410
+
411
+ const fetcher = opts.fetcher || fetchText
412
+ const checker = opts.checker || checkProxies
413
+ const text = await fetcher(url)
414
+ const proxies = mergeProxies([text])
415
+ return checker(proxies, opts)
416
+ }
417
+
418
+ async function resolveInputProxies(opts, deps = {}) {
419
+ const readFile = deps.readFile || (file => fs.readFileSync(file, 'utf8'))
420
+ const readInputFn = deps.readInput || readInput
421
+ const loadFromUrls = deps.loadFromUrls || loadProxiesFromUrls
422
+
423
+ if (opts.proxy) {
424
+ const proxy = parseLink(opts.proxy)
425
+ return proxy ? [proxy] : []
426
+ }
427
+
428
+ if (opts.sourcesFile) {
429
+ const text = readFile(opts.sourcesFile)
430
+ for (const rawLine of text.split(/\r?\n/)) {
431
+ const line = rawLine.split('#')[0].trim()
432
+ if (line) opts.urls.push(line)
433
+ }
434
+ }
435
+
436
+ if (opts.urls.length > 0) {
437
+ console.error(`Fetching ${opts.urls.length} source URL(s)...`)
438
+ return loadFromUrls(opts.urls)
439
+ }
440
+
441
+ const input = await readInputFn(opts.file)
442
+ return mergeProxies([input])
443
+ }
444
+
445
+ function toReport(results) {
446
+ return results.map(c => ({
447
+ server: c.proxy ? c.proxy.server : c.server,
448
+ port: c.proxy ? c.proxy.port : c.port,
449
+ sni: c.proxy ? c.proxy.sni : c.sni,
450
+ ok: c.ok,
451
+ ms: c.ms,
452
+ error: c.error,
453
+ link: c.proxy ? c.proxy.raw : c.link
454
+ }))
455
+ }
456
+
457
+ function jsonResponse(res, statusCode, body, headers = {}) {
458
+ const payload = JSON.stringify(body, null, 2)
459
+ res.writeHead(statusCode, {
460
+ 'content-type': 'application/json; charset=utf-8',
461
+ 'content-length': Buffer.byteLength(payload),
462
+ ...headers
463
+ })
464
+ res.end(payload)
465
+ }
466
+
467
+ function safeEqual(a, b) {
468
+ const left = Buffer.from(a)
469
+ const right = Buffer.from(b)
470
+ return left.length === right.length && crypto.timingSafeEqual(left, right)
471
+ }
472
+
473
+ function isAuthorized(req, auth) {
474
+ const header = req.headers.authorization
475
+ if (!header || !header.startsWith('Basic ')) return false
476
+
477
+ let decoded
478
+ try {
479
+ decoded = Buffer.from(header.slice(6), 'base64').toString('utf8')
480
+ } catch {
481
+ return false
482
+ }
483
+
484
+ const separator = decoded.indexOf(':')
485
+ if (separator === -1) return false
486
+ const user = decoded.slice(0, separator)
487
+ const password = decoded.slice(separator + 1)
488
+ return safeEqual(user, auth.user) && safeEqual(password, auth.password)
489
+ }
490
+
491
+ function readJsonBody(req, limitBytes = 1024 * 1024) {
492
+ return new Promise((resolve, reject) => {
493
+ let size = 0
494
+ let raw = ''
495
+ req.setEncoding('utf8')
496
+ req.on('data', chunk => {
497
+ size += Buffer.byteLength(chunk)
498
+ if (size > limitBytes) {
499
+ reject(Object.assign(new Error('Request body too large'), { statusCode: 413 }))
500
+ req.destroy()
501
+ return
502
+ }
503
+ raw += chunk
504
+ })
505
+ req.on('end', () => {
506
+ try {
507
+ resolve(raw ? JSON.parse(raw) : {})
508
+ } catch {
509
+ reject(Object.assign(new Error('Invalid JSON body'), { statusCode: 400 }))
510
+ }
511
+ })
512
+ req.on('error', reject)
513
+ })
514
+ }
515
+
516
+ function logRequest(logger, req, statusCode, started) {
517
+ const ms = Date.now() - started
518
+ const forwarded = req.headers['x-forwarded-for']
519
+ const remote = Array.isArray(forwarded) ? forwarded[0] : forwarded || req.socket.remoteAddress || '-'
520
+ logger(`${req.id} ${remote} ${req.method} ${req.url} ${statusCode} ${ms}ms`)
521
+ }
522
+
523
+ function createServer({ auth, checkUrl, logger = console.error }) {
524
+ let nextRequestId = 0
525
+ const realm = 'Basic realm="mtproto-checker"'
526
+
527
+ return http.createServer(async (req, res) => {
528
+ const started = Date.now()
529
+ req.id = `req-${++nextRequestId}`
530
+ let statusCode = 500
531
+
532
+ try {
533
+ if (!isAuthorized(req, auth)) {
534
+ statusCode = 401
535
+ jsonResponse(res, statusCode, { error: 'Unauthorized' }, { 'www-authenticate': realm })
536
+ return
537
+ }
538
+
539
+ if (req.url !== '/check') {
540
+ statusCode = 404
541
+ jsonResponse(res, statusCode, { error: 'Not found' })
542
+ return
543
+ }
544
+
545
+ if (req.method !== 'POST') {
546
+ statusCode = 405
547
+ jsonResponse(res, statusCode, { error: 'Method not allowed' }, { allow: 'POST' })
548
+ return
549
+ }
550
+
551
+ const body = await readJsonBody(req)
552
+ if (!body || typeof body.url !== 'string' || body.url.trim() === '') {
553
+ statusCode = 400
554
+ jsonResponse(res, statusCode, { error: 'Request body must include url' })
555
+ return
556
+ }
557
+
558
+ const url = body.url.trim()
559
+ let results
560
+ try {
561
+ results = await checkUrl(url)
562
+ } catch (err) {
563
+ statusCode = 502
564
+ jsonResponse(res, statusCode, {
565
+ error: 'Failed to check url',
566
+ detail: err.message || String(err)
567
+ })
568
+ return
569
+ }
570
+ const report = toReport(results)
571
+ statusCode = 200
572
+ jsonResponse(res, statusCode, {
573
+ url,
574
+ count: report.length,
575
+ working: report.filter(item => item.ok).length,
576
+ results: report
577
+ })
578
+ } catch (err) {
579
+ statusCode = err.statusCode || 500
580
+ const message = statusCode === 500 ? 'Internal server error' : err.message
581
+ jsonResponse(res, statusCode, { error: message })
582
+ if (statusCode === 500) logger(`${req.id} error ${err.stack || err.message || err}`)
583
+ } finally {
584
+ logRequest(logger, req, statusCode, started)
585
+ }
586
+ })
587
+ }
588
+
589
+ function shouldStartServer(argv) {
590
+ return argv.length === 0
591
+ }
592
+
593
+ async function startServer(env = process.env) {
594
+ const apiId = parseInt(env.TG_API_ID, 10)
595
+ const apiHash = env.TG_API_HASH
596
+ const user = env.CHECK_AUTH_USER
597
+ const password = env.CHECK_AUTH_PASSWORD
598
+ const port = parseInt(env.PORT || '3080', 10)
599
+
600
+ if (!apiId || !apiHash) throw new Error('Set TG_API_ID and TG_API_HASH (get them at https://my.telegram.org).')
601
+ if (!user || !password) throw new Error('Set CHECK_AUTH_USER and CHECK_AUTH_PASSWORD for HTTP Basic auth.')
602
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('PORT must be a valid TCP port.')
603
+
604
+ const server = createServer({
605
+ auth: { user, password },
606
+ checkUrl: async url => checkSingleUrl(url, { apiId, apiHash })
607
+ })
608
+
609
+ await new Promise((resolve, reject) => {
610
+ server.once('error', reject)
611
+ server.listen(port, () => {
612
+ server.off('error', reject)
613
+ resolve()
614
+ })
615
+ })
616
+
617
+ console.error(`MTProto checker HTTP server listening on :${port}`)
618
+ return server
619
+ }
620
+
387
621
  /**
388
622
  * CLI entry point: resolve sources (URLs / file / stdin), check, and write
389
623
  * `<out>.json` (full report) and `<out>.txt` (working links, fastest first).
@@ -398,23 +632,7 @@ async function main() {
398
632
  process.exit(1)
399
633
  }
400
634
 
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
- }
635
+ const proxies = await resolveInputProxies(opts)
418
636
 
419
637
  if (proxies.length === 0) {
420
638
  console.error('No valid tg://proxy or t.me/proxy links found.')
@@ -463,9 +681,9 @@ async function main() {
463
681
  process.exit(0)
464
682
  }
465
683
 
466
- module.exports = { configureTdlibOnce, parseArgs, checkProxiesFromUrls, loadProxiesFromUrls, checkProxies, runIterativeChecks, mergeProxies, parseLink, normalizeSecret, faketlsSni }
684
+ module.exports = { checkSingleUrl, configureTdlibOnce, createServer, parseArgs, resolveInputProxies, checkProxiesFromUrls, loadProxiesFromUrls, checkProxies, runIterativeChecks, mergeProxies, parseLink, normalizeSecret, faketlsSni, shouldStartServer, startServer }
467
685
 
468
- if (require.main === module) main().catch(err => {
686
+ if (require.main === module) (shouldStartServer(process.argv.slice(2)) ? startServer() : main()).catch(err => {
469
687
  console.error(err)
470
688
  process.exit(1)
471
689
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtproto-checker",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "Check Telegram MTProto proxies via a real TDLib handshake (testProxy), like tdesktop does",
5
5
  "repository": {
6
6
  "type": "git",