w-ipproxy 1.0.17 → 1.0.19

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.
@@ -45,522 +45,615 @@
45
45
 
46
46
  <section>
47
47
  <article>
48
- <pre class="prettyprint source linenums"><code>import axios from 'axios'
49
- import https from 'https'
50
- import get from 'lodash-es/get.js'
51
- import size from 'lodash-es/size.js'
52
- import each from 'lodash-es/each.js'
53
- import map from 'lodash-es/map.js'
54
- import trim from 'lodash-es/trim.js'
55
- import values from 'lodash-es/values.js'
56
- import evem from 'wsemi/src/evem.mjs'
57
- import sep from 'wsemi/src/sep.mjs'
58
- import isestr from 'wsemi/src/isestr.mjs'
59
- import ispint from 'wsemi/src/ispint.mjs'
60
- import isbol from 'wsemi/src/isbol.mjs'
61
- import isearr from 'wsemi/src/isearr.mjs'
62
- import iseobj from 'wsemi/src/iseobj.mjs'
63
- import cint from 'wsemi/src/cint.mjs'
64
- import waitFun from 'wsemi/src/waitFun.mjs'
65
- import provideServer from './provideServer.mjs'
66
-
67
-
68
- /**
69
- * 抓取代理伺服器
70
- *
71
- * @class
72
- * @param {Object} [opt={}] 輸入設定物件,預設{}
73
- * @param {String} [opt.src='https://api.proxyscrape.com/v2/?request=displayproxies&amp;protocol=http&amp;timeout=5000&amp;anonymity=all'] 輸入取得代理伺服器API字串,預設'https://api.proxyscrape.com/v2/?request=displayproxies&amp;protocol=http&amp;timeout=5000&amp;anonymity=all'
74
- * @param {String} [opt.tar='https://httpbin.org/ip'] 輸入檢測目標網址字串,預設'https://httpbin.org/ip'
75
- * @param {Integer} [opt.timeGetProxies=30*1000] 輸入輪循抓取代理伺服器時間間隔整數,單位ms,預設30*1000
76
- * @param {Integer} [opt.timeTestProxies=60*1000] 輸入輪循測試代理伺服器時間間隔整數,單位ms,預設60*1000
77
- * @param {Boolean} [opt.anonymityCheck=true] 輸入是否對代理做匿名性檢測布林值,開啟時會先打https://httpbin.org/ip驗證代理不會將本機真實IP透過X-Forwarded-For等方式洩漏給目標端,預設true
78
- * @param {Integer} [opt.anonymityCheckRepeats=2] 輸入匿名性檢測重複次數整數,用以偵測負載平衡型代理的機率性洩漏,任一次洩漏即判定失敗,預設2
79
- * @param {Boolean} [opt.withServer=false] 輸入是否創建供數據供給伺服器布林值,預設false
80
- * @param {Integer} [opt.serverPort=8080] 輸入創建伺服器所用port整數,預設8080
81
- * @param {String} [opt.serverApiName='getProxies'] 輸入供給數據API名稱字串,預設'getProxies'
82
- * @param {Array} [opt.serverCorsOrigins=['*']] 輸入允許跨域網域陣列,若給予['*']代表允許全部,預設['*']
83
- * @returns {Object} 回傳事件物件,提供函數getProxies,可監聽事件getRawProxies、add、delete、change
84
- * @example
85
- *
86
- * import _ from 'lodash-es'
87
- * import wip from './src/WIpProxy.mjs'
88
- *
89
- * let wo = wip({
90
- * // tar: `https://www.google.com`,
91
- * withServer: true,
92
- * serverPort: 9000,
93
- * serverCorsOrigins: ['*'],
94
- * })
95
- * wo.on('getRawProxies', (prxsRaw) => {
96
- * console.log(`已抓取公開代理共 ${_.size(prxsRaw)} 個...`)
97
- * })
98
- * wo.on('add', (p, proxies) => {
99
- * // console.log('新加入代理', { host: p.host, port: p.port }, _.map(proxies, 'proxy'))
100
- * })
101
- * wo.on('delete', (p, proxies) => {
102
- * // console.log('刪除代理', { host: p.host, port: p.port }, _.map(proxies, 'proxy'))
103
- * })
104
- * wo.on('change', (proxies) => {
105
- * // console.log(`有效代理`, _.map(proxies, 'proxy'))
106
- * console.log(`已檢測有效代理共 ${_.size(proxies)} 個...`)
107
- * })
108
- *
109
- * //browser view: http://localhost:9000/getProxies
110
- *
111
- */
112
- function WIpProxy(opt = {}) {
113
-
114
- //src
115
- let src = get(opt, 'src', '')
116
- if (!isestr(src)) {
117
- src = 'https://api.proxyscrape.com/v2/?request=displayproxies' +
118
- `&amp;protocol=http` +
119
- // `&amp;protocol=https` +
120
- `&amp;timeout=5000` +
121
- // `&amp;country=${join(['tw', 'cn', 'hk', 'jp', 'kr', 'sg', 'my', 'in', 'id', 'th', 'vn'])}` +
122
- // `&amp;ssl=yes` +
123
- `&amp;anonymity=elite` //elite匿名代理, all會使用X-Forwarded-For儲存原本ip導致被識別
124
- }
125
-
126
- //tar
127
- let tar = get(opt, 'tar', '')
128
- if (!isestr(tar)) {
129
- tar = 'https://httpbin.org/ip' //'https://www.google.com'
130
- }
131
-
132
- //timeGetProxies
133
- let timeGetProxies = get(opt, 'timeGetProxies')
134
- if (!ispint(timeGetProxies)) {
135
- timeGetProxies = 30 * 1000 //30s
136
- }
137
- timeGetProxies = cint(timeGetProxies)
138
-
139
- //timeTestProxies
140
- let timeTestProxies = get(opt, 'timeTestProxies')
141
- if (!ispint(timeTestProxies)) {
142
- timeTestProxies = 60 * 1000 //1min
143
- }
144
- timeTestProxies = cint(timeTestProxies)
145
-
146
- //anonymityCheck
147
- let anonymityCheck = get(opt, 'anonymityCheck')
148
- if (!isbol(anonymityCheck)) {
149
- anonymityCheck = true
150
- }
151
-
152
- //anonymityCheckRepeats
153
- let anonymityCheckRepeats = get(opt, 'anonymityCheckRepeats')
154
- if (!ispint(anonymityCheckRepeats)) {
155
- anonymityCheckRepeats = 2
156
- }
157
- anonymityCheckRepeats = cint(anonymityCheckRepeats)
158
-
159
- //withServer
160
- let withServer = get(opt, 'withServer')
161
- if (!isbol(withServer)) {
162
- withServer = false
163
- }
164
-
165
- //serverPort
166
- let serverPort = get(opt, 'serverPort')
167
- if (!ispint(serverPort)) {
168
- serverPort = 8080
169
- }
170
- serverPort = cint(serverPort)
171
-
172
- //serverApiName
173
- let serverApiName = get(opt, 'serverApiName')
174
- if (!isestr(serverApiName)) {
175
- serverApiName = 'getProxies'
176
- }
177
-
178
- //serverCorsOrigins
179
- let serverCorsOrigins = get(opt, 'serverCorsOrigins', [])
180
- if (!isearr(serverCorsOrigins)) {
181
- serverCorsOrigins = ['*']
182
- }
183
-
184
- //prxsRaw, kpPrx
185
- let prxsRaw = []
186
- let kpPrx = {}
187
-
188
- //_normalizeIp: 正規化單一IP字串, 去除[]包裹, ipv4之port, 及ipv4-mapped-ipv6之'::ffff:'前綴
189
- function _normalizeIp(v) {
190
- let s = trim(v)
191
- if (s === '') {
192
- return ''
193
- }
194
- if (s[0] === '[') {
195
- //形如'[::1]:8080'或'[2001:db8::1]'
196
- let k = s.indexOf(']')
197
- if (k > 0) {
198
- s = s.slice(1, k)
199
- }
200
- }
201
- else if (s.split(':').length === 2) {
202
- //僅單一冒號才視為'1.2.3.4:port', 多冒號為ipv6不可截斷
203
- s = s.split(':')[0]
204
- }
205
- //ipv4-mapped-ipv6, 如'::ffff:1.2.3.4', 統一還原為ipv4
206
- let m = s.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i)
207
- if (m) {
208
- s = m[1]
209
- }
210
- return s
211
- }
212
-
213
- //_originHasIp: 檢查origin(可能為'ip1, ip2, ...')逐段正規化後是否含有指定IP
214
- function _originHasIp(origin, ip) {
215
- if (!isestr(ip)) {
216
- return false
217
- }
218
- let vs = map(sep(origin, ','), (v) => _normalizeIp(v))
219
- return vs.indexOf(ip) >= 0
220
- }
221
-
222
- //myRealIp: 本機真實公網IP, 用於匿名性檢測時比對代理egress是否洩漏
223
- let myRealIp = ''
224
- let myRealIpReady = false
225
- let myRealIpPm = null
226
- async function _fetchMyRealIp() {
227
- if (myRealIpReady) {
228
- return
229
- }
230
- if (!myRealIpPm) {
231
- myRealIpPm = (async () => {
232
- try {
233
- let res = await axios.get('https://httpbin.org/ip', { timeout: 10000 })
234
- let origin = get(res, 'data.origin', '')
235
- if (isestr(origin)) {
236
- //origin可能為"1.2.3.4", "1.2.3.4:port", 或"::ffff:1.2.3.4"(部分環境如雲端runner為ipv4-mapped-ipv6), 取首個逗號前再正規化
237
- myRealIp = _normalizeIp(sep(origin, ',')[0])
238
- }
239
- }
240
- catch (err) {
241
- //取不到就算了, 匿名性檢查會自動跳過
242
- }
243
- myRealIpReady = true
244
- })()
245
- }
246
- await myRealIpPm
247
- }
248
-
249
- async function _anonymityCheckCore(host, port) {
250
- if (!anonymityCheck) {
251
- return { ok: true }
252
- }
253
- if (!isestr(myRealIp)) {
254
- return { ok: true } //沒抓到自己IP就無從比對, 放行
255
- }
256
- let agent = new https.Agent({ rejectUnauthorized: false })
257
- for (let i = 0; i &lt; anonymityCheckRepeats; i++) {
258
- try {
259
- let res = await axios.get('https://httpbin.org/ip', {
260
- proxy: { host, port, protocol: 'http' },
261
- httpsAgent: agent,
262
- timeout: 5000,
263
- headers: {
264
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
265
- 'Accept': 'application/json',
266
- },
267
- validateStatus: () => true,
268
- })
269
- if (res.status !== 200) {
270
- return { ok: false, reason: `anonymity status[${res.status}]` }
271
- }
272
- let origin = get(res, 'data.origin', '')
273
- if (!isestr(origin)) {
274
- return { ok: false, reason: 'anonymity non-json' }
275
- }
276
- if (_originHasIp(origin, myRealIp)) {
277
- return { ok: false, reason: `leaks real IP` }
278
- }
279
- }
280
- catch (err) {
281
- return { ok: false, reason: trim(err.message || '') }
282
- }
283
- }
284
- return { ok: true }
285
- }
286
-
287
- async function _getProxiesCore(src) {
288
- let ps = []
289
-
290
- try {
291
-
292
- //res
293
- let res = await axios.get(src)
294
-
295
- //c
296
- let c = res.data
297
- // console.log('c', c)
298
-
299
- //sep
300
- let ss = sep(c, '\n')
301
- // console.log('ss', ss)
302
-
303
- //ps
304
- ps = map(ss, (v) => {
305
- let [host, port] = v.split(':')
306
- port = cint(port)
307
- return {
308
- proxy: v,
309
- host,
310
- port,
311
- }
312
- })
313
- // console.log('ps', ps)
314
-
315
- }
316
- catch (err) {
317
- console.log('_getProxiesCore catch', err.message)
318
- }
319
- // console.log('ps', ps)
320
-
321
- return ps
322
- }
323
-
324
- let b_getProxies = false
325
- async function _getProxies() {
326
- // console.log('call getProxies...')
327
-
328
- let core = async() => {
329
-
330
- //get
331
- prxsRaw = await _getProxiesCore(src)
332
-
333
- //emit
334
- ev.emit('getRawProxies', prxsRaw)
335
-
336
- }
337
-
338
- //core
339
- if (b_getProxies) {
340
- return
341
- }
342
- b_getProxies = true
343
- await core()
344
- .catch(() => {}) //吃掉錯誤
345
- .finally(() => {
346
- b_getProxies = false
347
- })
348
-
349
- }
350
-
351
- async function _testProxyCore(host, port, url) {
352
-
353
- //ret
354
- let ret = (state, msg) => {
355
- return {
356
- state,
357
- msg,
358
- p: {
359
- proxy: `${host}:${port}`,
360
- host,
361
- port,
362
- },
363
- }
364
- }
365
-
366
- //匿名性預檢: 先確認代理不會透過X-Forwarded-For等洩漏本機真實IP
367
- let ac = await _anonymityCheckCore(host, port)
368
- if (!ac.ok) {
369
- return ret('error', ac.reason)
370
- }
371
-
372
- //r
373
- let r = {}
374
- try {
375
-
376
- //agent
377
- let agent = new https.Agent({ rejectUnauthorized: false }) //避免證書錯誤中止
378
-
379
- //get
380
- let res = await axios.get(url, {
381
- proxy: {
382
- host,
383
- port,
384
- protocol: 'http',
385
- },
386
- httpsAgent: agent,
387
- timeout: 5000,
388
- headers: {
389
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
390
- 'Accept': 'application/json',
391
- },
392
- })
393
-
394
- if (res.status === 200) {
395
- r = ret('success', res.data)
396
- }
397
- else {
398
- r = ret('error', `status[${res.status}] error`)
399
- }
400
-
401
- }
402
- catch (err) {
403
- r = ret('error', trim(err.message))
404
- // if (r.msg.indexOf(`ssl3_get_record`) >= 0) {
405
- // r.msg = `ssl3_get_record error`
406
- // }
407
- }
408
- // console.log(r)
409
-
410
- return r
411
- }
412
-
413
- async function _testProxiesCore(prxs) {
414
-
415
- //pms
416
- let pms = map(prxs, (p) => {
417
- let pm = _testProxyCore(p.host, p.port, tar)
418
- return pm
419
- })
420
-
421
- //rrs, allSettled
422
- let rrs = await Promise.allSettled(pms)
423
- // console.log('rrs', rrs)
424
-
425
- //psValid, psInvalid
426
- let psValid = []
427
- let psInvalid = []
428
- each(rrs, (v) => {
429
-
430
- //be
431
- let p = get(v, 'value.p', null)
432
- let be = iseobj(p) &amp;&amp; isestr(get(p, 'proxy'))
433
-
434
- //check
435
- if (!be) {
436
- return true //跳出換下一個
437
- }
438
-
439
- //b
440
- let status = get(v, 'status', '')
441
- let b1 = status === 'fulfilled'
442
- let state = get(v, 'value.state', '')
443
- let b2 = state === 'success'
444
- let b = b1 &amp;&amp; b2
445
-
446
- //push
447
- if (b) {
448
- psValid.push(p)
449
- }
450
- else {
451
- psInvalid.push(p)
452
- }
453
-
454
- })
455
-
456
- return {
457
- psValid,
458
- psInvalid,
459
- }
460
- }
461
-
462
- let b_testProxies = false
463
- async function _testProxies() {
464
- // console.log('call testProxies...')
465
-
466
- let core = async() => {
467
-
468
- //確保本機真實IP已取得, 供匿名性檢查比對
469
- await _fetchMyRealIp()
470
-
471
- //get
472
- let r = await _testProxiesCore(prxsRaw)
473
-
474
- //emit add
475
- each(r.psValid, (p) => {
476
- kpPrx[p.proxy] = p
477
- ev.emit('add', p, values(kpPrx))
478
- })
479
-
480
- //emit delete
481
- each(r.psInvalid, (p) => {
482
- delete kpPrx[p.proxy]
483
- ev.emit('delete', p, values(kpPrx))
484
- })
485
-
486
- //emit change
487
- ev.emit('change', values(kpPrx))
488
-
489
- }
490
-
491
- //core
492
- if (b_testProxies) {
493
- return
494
- }
495
- b_testProxies = true
496
- await core()
497
- .catch(() => {}) //吃掉錯誤
498
- .finally(() => {
499
- b_testProxies = false
500
- })
501
-
502
- }
503
-
504
- function init() {
505
-
506
- //取得代理清單
507
- if (true) {
508
- _getProxies()
509
- setInterval(() => {
510
- _getProxies()
511
- }, timeGetProxies)
512
- }
513
-
514
- //過濾出有效代理清單, 延遲3s觸發, 給時間抓取
515
- if (true) {
516
- setTimeout(() => {
517
- _testProxies()
518
- setInterval(() => {
519
- _testProxies()
520
- }, timeTestProxies)
521
- }, 3000)
522
- }
523
-
524
- }
525
-
526
- async function getProxies() {
527
- let vs
528
- await waitFun(() => {
529
- vs = values(kpPrx)
530
- return size(vs) > 0
531
- })
532
- return vs
533
- }
534
-
535
- //ev
536
- let ev = evem()
537
-
538
- //withServer
539
- if (withServer) {
540
- provideServer(getProxies, {
541
- port: serverPort,
542
- apiName: serverApiName,
543
- corsOrigins: serverCorsOrigins,
544
- })
545
- }
546
-
547
- async function getProxiesOnce() {
548
- await _getProxies()
549
- await _testProxies()
550
- let ps = await getProxies()
551
- return ps
552
- }
553
-
554
- //save
555
- ev.init = init
556
- ev.getProxies = getProxies
557
- ev.getProxiesOnce = getProxiesOnce
558
-
559
- return ev
560
- }
561
-
562
-
563
- export default WIpProxy
48
+ <pre class="prettyprint source linenums"><code>import axios from 'axios'
49
+ import https from 'https'
50
+ import http from 'http'
51
+ import net from 'net'
52
+ import get from 'lodash-es/get.js'
53
+ import size from 'lodash-es/size.js'
54
+ import each from 'lodash-es/each.js'
55
+ import map from 'lodash-es/map.js'
56
+ import trim from 'lodash-es/trim.js'
57
+ import values from 'lodash-es/values.js'
58
+ import evem from 'wsemi/src/evem.mjs'
59
+ import sep from 'wsemi/src/sep.mjs'
60
+ import isestr from 'wsemi/src/isestr.mjs'
61
+ import ispint from 'wsemi/src/ispint.mjs'
62
+ import isbol from 'wsemi/src/isbol.mjs'
63
+ import isearr from 'wsemi/src/isearr.mjs'
64
+ import iseobj from 'wsemi/src/iseobj.mjs'
65
+ import cint from 'wsemi/src/cint.mjs'
66
+ import cstr from 'wsemi/src/cstr.mjs'
67
+ import waitFun from 'wsemi/src/waitFun.mjs'
68
+ import provideServer from './provideServer.mjs'
69
+
70
+
71
+ //_reqTimeout: axios 1.20+ 對經proxy之請求其內建timeout不生效(實測連不上之代理會卡到系統TCP connect timeout約21s而非設定值, 直連則正常), 大量無效代理逐一卡死會使整體檢測逾時; 故經proxy之請求一律附加AbortSignal.timeout強制中斷, 另保留timeout供直連情形與未來相容
72
+ function _reqTimeout(ms) {
73
+ return {
74
+ timeout: ms,
75
+ signal: AbortSignal.timeout(ms),
76
+ }
77
+ }
78
+
79
+
80
+ /**
81
+ * 抓取代理伺服器
82
+ *
83
+ * @class
84
+ * @param {Object} [opt={}] 輸入設定物件,預設{}
85
+ * @param {String} [opt.src='https://api.proxyscrape.com/v2/?request=displayproxies&amp;protocol=http&amp;timeout=5000&amp;anonymity=all'] 輸入取得代理伺服器API字串,預設'https://api.proxyscrape.com/v2/?request=displayproxies&amp;protocol=http&amp;timeout=5000&amp;anonymity=all'
86
+ * @param {String} [opt.tar='https://httpbin.org/ip'] 輸入檢測目標網址字串,預設'https://httpbin.org/ip'
87
+ * @param {Integer} [opt.timeGetProxies=30*1000] 輸入輪循抓取代理伺服器時間間隔整數,單位ms,預設30*1000
88
+ * @param {Integer} [opt.timeTestProxies=60*1000] 輸入輪循測試代理伺服器時間間隔整數,單位ms,預設60*1000
89
+ * @param {Boolean} [opt.anonymityCheck=true] 輸入是否對代理做匿名性檢測布林值,開啟時會先查詢本機真實公網IP(依序嘗試httpbin.org、ipify、checkip.amazonaws.com、icanhazip.com),再透過代理打https://httpbin.org/get驗證目標端所見之origin與X-Forwarded-For、X-Real-IP、Forwarded等標頭皆不含本機真實IP,且不帶Via、Proxy-Connection等暴露代理存在之標頭;若本機真實IP查詢全部失敗則視為無法驗證,該回合所有代理皆不放行(fail-closed),預設true
90
+ * @param {Integer} [opt.anonymityCheckRepeats=5] 輸入匿名性檢測重複次數整數,各次以獨立連線送出並於時間上分散(間隔anonymityCheckInterval),用以偵測負載平衡型代理呈時間窗之機率性洩漏,任一次洩漏即判定失敗,預設5
91
+ * @param {Integer} [opt.anonymityCheckInterval=2000] 輸入匿名性檢測各次之間隔時間整數,單位ms,預設2000
92
+ * @param {Boolean} [opt.withServer=false] 輸入是否創建供數據供給伺服器布林值,預設false
93
+ * @param {Integer} [opt.serverPort=8080] 輸入創建伺服器所用port整數,預設8080
94
+ * @param {String} [opt.serverApiName='getProxies'] 輸入供給數據API名稱字串,預設'getProxies'
95
+ * @param {Array} [opt.serverCorsOrigins=['*']] 輸入允許跨域網域陣列,若給予['*']代表允許全部,預設['*']
96
+ * @returns {Object} 回傳事件物件,提供函數getProxies(等待至有有效代理)與getProxiesOnce(抓取並檢測一回合後回傳當前有效代理,可能為空陣列),可監聽事件getRawProxies、add、delete、change
97
+ * @example
98
+ *
99
+ * import _ from 'lodash-es'
100
+ * import wip from './src/WIpProxy.mjs'
101
+ *
102
+ * let wo = wip({
103
+ * // tar: `https://www.google.com`,
104
+ * withServer: true,
105
+ * serverPort: 9000,
106
+ * serverCorsOrigins: ['*'],
107
+ * })
108
+ * wo.on('getRawProxies', (prxsRaw) => {
109
+ * console.log(`已抓取公開代理共 ${_.size(prxsRaw)} 個...`)
110
+ * })
111
+ * wo.on('add', (p, proxies) => {
112
+ * // console.log('新加入代理', { host: p.host, port: p.port }, _.map(proxies, 'proxy'))
113
+ * })
114
+ * wo.on('delete', (p, proxies) => {
115
+ * // console.log('刪除代理', { host: p.host, port: p.port }, _.map(proxies, 'proxy'))
116
+ * })
117
+ * wo.on('change', (proxies) => {
118
+ * // console.log(`有效代理`, _.map(proxies, 'proxy'))
119
+ * console.log(`已檢測有效代理共 ${_.size(proxies)} 個...`)
120
+ * })
121
+ *
122
+ * //browser view: http://localhost:9000/getProxies
123
+ *
124
+ */
125
+ function WIpProxy(opt = {}) {
126
+
127
+ //src
128
+ let src = get(opt, 'src', '')
129
+ if (!isestr(src)) {
130
+ src = 'https://api.proxyscrape.com/v2/?request=displayproxies' +
131
+ `&amp;protocol=http` +
132
+ // `&amp;protocol=https` +
133
+ `&amp;timeout=5000` +
134
+ // `&amp;country=${join(['tw', 'cn', 'hk', 'jp', 'kr', 'sg', 'my', 'in', 'id', 'th', 'vn'])}` +
135
+ // `&amp;ssl=yes` +
136
+ `&amp;anonymity=elite` //elite匿名代理, all會使用X-Forwarded-For儲存原本ip導致被識別
137
+ }
138
+
139
+ //tar
140
+ let tar = get(opt, 'tar', '')
141
+ if (!isestr(tar)) {
142
+ tar = 'https://httpbin.org/ip' //'https://www.google.com'
143
+ }
144
+
145
+ //timeGetProxies
146
+ let timeGetProxies = get(opt, 'timeGetProxies')
147
+ if (!ispint(timeGetProxies)) {
148
+ timeGetProxies = 30 * 1000 //30s
149
+ }
150
+ timeGetProxies = cint(timeGetProxies)
151
+
152
+ //timeTestProxies
153
+ let timeTestProxies = get(opt, 'timeTestProxies')
154
+ if (!ispint(timeTestProxies)) {
155
+ timeTestProxies = 60 * 1000 //1min
156
+ }
157
+ timeTestProxies = cint(timeTestProxies)
158
+
159
+ //anonymityCheck
160
+ let anonymityCheck = get(opt, 'anonymityCheck')
161
+ if (!isbol(anonymityCheck)) {
162
+ anonymityCheck = true
163
+ }
164
+
165
+ //anonymityCheckRepeats
166
+ let anonymityCheckRepeats = get(opt, 'anonymityCheckRepeats')
167
+ if (!ispint(anonymityCheckRepeats)) {
168
+ anonymityCheckRepeats = 5
169
+ }
170
+ anonymityCheckRepeats = cint(anonymityCheckRepeats)
171
+
172
+ //anonymityCheckInterval
173
+ let anonymityCheckInterval = get(opt, 'anonymityCheckInterval')
174
+ if (!ispint(anonymityCheckInterval)) {
175
+ anonymityCheckInterval = 2000 //2s
176
+ }
177
+ anonymityCheckInterval = cint(anonymityCheckInterval)
178
+
179
+ //withServer
180
+ let withServer = get(opt, 'withServer')
181
+ if (!isbol(withServer)) {
182
+ withServer = false
183
+ }
184
+
185
+ //serverPort
186
+ let serverPort = get(opt, 'serverPort')
187
+ if (!ispint(serverPort)) {
188
+ serverPort = 8080
189
+ }
190
+ serverPort = cint(serverPort)
191
+
192
+ //serverApiName
193
+ let serverApiName = get(opt, 'serverApiName')
194
+ if (!isestr(serverApiName)) {
195
+ serverApiName = 'getProxies'
196
+ }
197
+
198
+ //serverCorsOrigins
199
+ let serverCorsOrigins = get(opt, 'serverCorsOrigins', [])
200
+ if (!isearr(serverCorsOrigins)) {
201
+ serverCorsOrigins = ['*']
202
+ }
203
+
204
+ //prxsRaw, kpPrx
205
+ let prxsRaw = []
206
+ let kpPrx = {}
207
+
208
+ //_normalizeIp: 正規化單一IP字串, 去除[]包裹, ipv4之port, 及ipv4-mapped-ipv6之'::ffff:'前綴
209
+ function _normalizeIp(v) {
210
+ let s = trim(v)
211
+ if (s === '') {
212
+ return ''
213
+ }
214
+ if (s[0] === '[') {
215
+ //形如'[::1]:8080'或'[2001:db8::1]'
216
+ let k = s.indexOf(']')
217
+ if (k > 0) {
218
+ s = s.slice(1, k)
219
+ }
220
+ }
221
+ else if (s.split(':').length === 2) {
222
+ //僅單一冒號才視為'1.2.3.4:port', 多冒號為ipv6不可截斷
223
+ s = s.split(':')[0]
224
+ }
225
+ //ipv4-mapped-ipv6, 如'::ffff:1.2.3.4'或無括號帶port之'::ffff:1.2.3.4:5678', 統一還原為ipv4
226
+ let m = s.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})(?::\d+)?$/i)
227
+ if (m) {
228
+ s = m[1]
229
+ }
230
+ return s
231
+ }
232
+
233
+ //_originHasIp: 檢查origin或標頭值(如'ip1, ip2', 'for=1.2.3.4;proto=https', '"[::1]:443"')內是否含有指定IP, 以IP可用字元之連續段切token後逐一正規化精確比對
234
+ function _originHasIp(origin, ip) {
235
+ if (!isestr(ip) || !isestr(origin)) {
236
+ return false
237
+ }
238
+ let vs = map(origin.split(/[^0-9a-fA-F:.[\]]+/), (v) => _normalizeIp(v))
239
+ return vs.indexOf(ip) >= 0
240
+ }
241
+
242
+ //myRealIpSrcs: 查詢本機真實公網IP之來源, 依序嘗試, 任一成功即止, 避免單一服務(httpbin.org)逾時或限流時匿名性檢測失去比對基準
243
+ let myRealIpSrcs = [
244
+ { url: 'https://httpbin.org/ip', parse: (data) => get(data, 'origin', '') },
245
+ { url: 'https://api.ipify.org?format=json', parse: (data) => get(data, 'ip', '') },
246
+ { url: 'https://checkip.amazonaws.com', parse: (data) => data },
247
+ { url: 'https://icanhazip.com', parse: (data) => data },
248
+ ]
249
+
250
+ //myRealIp: 本機真實公網IP, 用於匿名性檢測時比對代理egress是否洩漏
251
+ let myRealIp = ''
252
+ let myRealIpReady = false
253
+ let myRealIpPm = null
254
+ async function _fetchMyRealIp() {
255
+ if (myRealIpReady) {
256
+ return
257
+ }
258
+ if (!myRealIpPm) {
259
+ myRealIpPm = (async () => {
260
+ //最多兩輪, 每輪依序嘗試各來源
261
+ for (let round = 0; round &lt; 2 &amp;&amp; !isestr(myRealIp); round++) {
262
+ for (let k = 0; k &lt; myRealIpSrcs.length; k++) {
263
+ let { url, parse } = myRealIpSrcs[k]
264
+ try {
265
+ let res = await axios.get(url, { timeout: 10000, responseType: 'text', transformResponse: [(v) => v] })
266
+ let raw = trim(get(res, 'data', ''))
267
+ let data = raw
268
+ if (raw[0] === '{') {
269
+ data = JSON.parse(raw)
270
+ }
271
+ let origin = parse(data)
272
+ if (isestr(origin)) {
273
+ //origin可能為"1.2.3.4", "1.2.3.4:port", 或"::ffff:1.2.3.4"(部分環境如雲端runner為ipv4-mapped-ipv6), 取首個逗號前再正規化
274
+ let ip = _normalizeIp(sep(origin, ',')[0])
275
+ //須為合法IP才採用: 若來源回200但內容非IP(如被劫持或回HTML), 誤採為真實IP會使比對永遠不中而無聲放行全部代理
276
+ if (net.isIP(ip) !== 0) {
277
+ myRealIp = ip
278
+ }
279
+ }
280
+ }
281
+ catch (err) {
282
+ //換下一個來源
283
+ }
284
+ if (isestr(myRealIp)) {
285
+ break
286
+ }
287
+ }
288
+ }
289
+ if (anonymityCheck &amp;&amp; !isestr(myRealIp)) {
290
+ console.log('_fetchMyRealIp: unable to determine real ip, all proxies are rejected until it is available (anonymityCheck=true)')
291
+ }
292
+ myRealIpReady = true
293
+ })()
294
+ }
295
+ await myRealIpPm
296
+ }
297
+
298
+ //LEAK_IP_HEADERS: 目標端若見到這些標頭且值含本機真實IP即為洩漏
299
+ let LEAK_IP_HEADERS = [
300
+ 'x-forwarded-for', 'x-real-ip', 'x-client-ip', 'client-ip',
301
+ 'true-client-ip', 'cf-connecting-ip', 'forwarded',
302
+ 'x-originating-ip', 'x-cluster-client-ip',
303
+ ]
304
+
305
+ //LEAK_PROXY_HEADERS: 目標端若見到這些標頭即代表代理暴露自身存在, 非elite
306
+ let LEAK_PROXY_HEADERS = [
307
+ 'via', 'proxy-connection', 'x-proxy-id',
308
+ ]
309
+
310
+ async function _anonymityCheckCore(host, port) {
311
+ if (!anonymityCheck) {
312
+ return { ok: true }
313
+ }
314
+ if (!isestr(myRealIp)) {
315
+ //沒抓到自己IP就無從驗證匿名性, 不放行未經驗證之代理(fail-closed)
316
+ return { ok: false, reason: 'anonymity unverifiable: real ip unknown' }
317
+ }
318
+ //單次檢測: 使用/get一次取得origin與目標端所見標頭, 同時檢查IP洩漏與代理暴露
319
+ let checkOnce = async () => {
320
+ try {
321
+ let res = await axios.get('https://httpbin.org/get', {
322
+ proxy: { host, port, protocol: 'http' },
323
+ httpAgent: new http.Agent({ keepAlive: false }), //每次獨立連線, 避免keep-alive重用同一條連線而黏在負載平衡代理之同一後端, 使重複檢測失去獨立性
324
+ httpsAgent: new https.Agent({ rejectUnauthorized: false, keepAlive: false }),
325
+ ..._reqTimeout(5000),
326
+ headers: {
327
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
328
+ 'Accept': 'application/json',
329
+ },
330
+ validateStatus: () => true,
331
+ })
332
+ if (res.status !== 200) {
333
+ return { ok: false, reason: `anonymity status[${res.status}]` }
334
+ }
335
+ let origin = get(res, 'data.origin', '')
336
+ let headers = get(res, 'data.headers', null)
337
+ if (!isestr(origin) || !iseobj(headers)) {
338
+ return { ok: false, reason: 'anonymity non-json' }
339
+ }
340
+ if (_originHasIp(origin, myRealIp)) {
341
+ return { ok: false, reason: `leaks real IP` }
342
+ }
343
+ let hs = {}
344
+ each(headers, (v, k) => {
345
+ hs[cstr(k).toLowerCase()] = cstr(v)
346
+ })
347
+ for (let k of LEAK_IP_HEADERS) {
348
+ if (k in hs &amp;&amp; _originHasIp(hs[k], myRealIp)) {
349
+ return { ok: false, reason: `leaks real IP via ${k}` }
350
+ }
351
+ }
352
+ for (let k of LEAK_PROXY_HEADERS) {
353
+ if (k in hs) {
354
+ return { ok: false, reason: `exposes proxy via ${k}` }
355
+ }
356
+ }
357
+ return { ok: true }
358
+ }
359
+ catch (err) {
360
+ return { ok: false, reason: trim(err.message || '') }
361
+ }
362
+ }
363
+
364
+ //重複檢測於時間上分散: 負載平衡型代理之洩漏常呈數秒級時間窗(實測67.203.23.79:8081約9成時間洩漏, 僅1~2s短暫正常窗), 同一瞬間之多次取樣會一起落入正常窗而整組放行, 故各次相隔anonymityCheckInterval送出且各用獨立連線
365
+ //採順序執行而非並發: _testProxiesCore對全部代理同時檢測, 若每個代理再並發多次取樣, 總連線數暴增(實測百餘代理×4次會使本機/CI大量請求逾時, 有效代理歸零), 順序執行使單一代理同時僅一個連線, 總並發維持在代理數量級
366
+ //任一次失敗(洩漏或連線失敗)即判定失敗並提前結束
367
+ for (let i = 0; i &lt; anonymityCheckRepeats; i++) {
368
+ if (i > 0) {
369
+ await new Promise((resolve) => setTimeout(resolve, anonymityCheckInterval))
370
+ }
371
+ let r = await checkOnce()
372
+ if (!r.ok) {
373
+ return r
374
+ }
375
+ }
376
+ return { ok: true }
377
+ }
378
+
379
+ async function _getProxiesCore(src) {
380
+ let ps = []
381
+
382
+ try {
383
+
384
+ //res, 直連proxyscrape, timeout生效; 設上限避免上游無回應時卡住整個getProxiesOnce
385
+ let res = await axios.get(src, { timeout: 15000 })
386
+
387
+ //c
388
+ let c = res.data
389
+ // console.log('c', c)
390
+
391
+ //sep
392
+ let ss = sep(c, '\n')
393
+ // console.log('ss', ss)
394
+
395
+ //ps
396
+ ps = map(ss, (v) => {
397
+ let [host, port] = v.split(':')
398
+ port = cint(port)
399
+ return {
400
+ proxy: v,
401
+ host,
402
+ port,
403
+ }
404
+ })
405
+ // console.log('ps', ps)
406
+
407
+ }
408
+ catch (err) {
409
+ console.log('_getProxiesCore catch', err.message)
410
+ }
411
+ // console.log('ps', ps)
412
+
413
+ return ps
414
+ }
415
+
416
+ let pm_getProxies = null
417
+ async function _getProxies() {
418
+ // console.log('call getProxies...')
419
+
420
+ let core = async() => {
421
+
422
+ //get
423
+ prxsRaw = await _getProxiesCore(src)
424
+
425
+ //emit
426
+ ev.emit('getRawProxies', prxsRaw)
427
+
428
+ }
429
+
430
+ //core, 若已有進行中之回合則共用同一promise, 讓呼叫端可等待其完成
431
+ if (pm_getProxies) {
432
+ return pm_getProxies
433
+ }
434
+ pm_getProxies = core()
435
+ .catch(() => {}) //吃掉錯誤
436
+ .finally(() => {
437
+ pm_getProxies = null
438
+ })
439
+ return pm_getProxies
440
+
441
+ }
442
+
443
+ async function _testProxyCore(host, port, url) {
444
+
445
+ //ret
446
+ let ret = (state, msg) => {
447
+ return {
448
+ state,
449
+ msg,
450
+ p: {
451
+ proxy: `${host}:${port}`,
452
+ host,
453
+ port,
454
+ },
455
+ }
456
+ }
457
+
458
+ //匿名性預檢: 先確認代理不會透過X-Forwarded-For等洩漏本機真實IP
459
+ let ac = await _anonymityCheckCore(host, port)
460
+ if (!ac.ok) {
461
+ return ret('error', ac.reason)
462
+ }
463
+
464
+ //r
465
+ let r = {}
466
+ try {
467
+
468
+ //agent
469
+ let agent = new https.Agent({ rejectUnauthorized: false }) //避免證書錯誤中止
470
+
471
+ //get
472
+ let res = await axios.get(url, {
473
+ proxy: {
474
+ host,
475
+ port,
476
+ protocol: 'http',
477
+ },
478
+ httpsAgent: agent,
479
+ ..._reqTimeout(5000),
480
+ headers: {
481
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
482
+ 'Accept': 'application/json',
483
+ },
484
+ })
485
+
486
+ if (res.status === 200) {
487
+ r = ret('success', res.data)
488
+ }
489
+ else {
490
+ r = ret('error', `status[${res.status}] error`)
491
+ }
492
+
493
+ }
494
+ catch (err) {
495
+ r = ret('error', trim(err.message))
496
+ // if (r.msg.indexOf(`ssl3_get_record`) >= 0) {
497
+ // r.msg = `ssl3_get_record error`
498
+ // }
499
+ }
500
+ // console.log(r)
501
+
502
+ return r
503
+ }
504
+
505
+ async function _testProxiesCore(prxs) {
506
+
507
+ //pms
508
+ let pms = map(prxs, (p) => {
509
+ let pm = _testProxyCore(p.host, p.port, tar)
510
+ return pm
511
+ })
512
+
513
+ //rrs, allSettled
514
+ let rrs = await Promise.allSettled(pms)
515
+ // console.log('rrs', rrs)
516
+
517
+ //psValid, psInvalid
518
+ let psValid = []
519
+ let psInvalid = []
520
+ each(rrs, (v) => {
521
+
522
+ //be
523
+ let p = get(v, 'value.p', null)
524
+ let be = iseobj(p) &amp;&amp; isestr(get(p, 'proxy'))
525
+
526
+ //check
527
+ if (!be) {
528
+ return true //跳出換下一個
529
+ }
530
+
531
+ //b
532
+ let status = get(v, 'status', '')
533
+ let b1 = status === 'fulfilled'
534
+ let state = get(v, 'value.state', '')
535
+ let b2 = state === 'success'
536
+ let b = b1 &amp;&amp; b2
537
+
538
+ //push
539
+ if (b) {
540
+ psValid.push(p)
541
+ }
542
+ else {
543
+ psInvalid.push(p)
544
+ }
545
+
546
+ })
547
+
548
+ return {
549
+ psValid,
550
+ psInvalid,
551
+ }
552
+ }
553
+
554
+ let pm_testProxies = null
555
+ async function _testProxies() {
556
+ // console.log('call testProxies...')
557
+
558
+ let core = async() => {
559
+
560
+ //確保本機真實IP已取得, 供匿名性檢查比對
561
+ await _fetchMyRealIp()
562
+
563
+ //get
564
+ let r = await _testProxiesCore(prxsRaw)
565
+
566
+ //emit add
567
+ each(r.psValid, (p) => {
568
+ kpPrx[p.proxy] = p
569
+ ev.emit('add', p, values(kpPrx))
570
+ })
571
+
572
+ //emit delete
573
+ each(r.psInvalid, (p) => {
574
+ delete kpPrx[p.proxy]
575
+ ev.emit('delete', p, values(kpPrx))
576
+ })
577
+
578
+ //emit change
579
+ ev.emit('change', values(kpPrx))
580
+
581
+ }
582
+
583
+ //core, 若已有進行中之回合則共用同一promise, 讓呼叫端可等待其完成
584
+ if (pm_testProxies) {
585
+ return pm_testProxies
586
+ }
587
+ pm_testProxies = core()
588
+ .catch(() => {}) //吃掉錯誤
589
+ .finally(() => {
590
+ pm_testProxies = null
591
+ })
592
+ return pm_testProxies
593
+
594
+ }
595
+
596
+ function init() {
597
+
598
+ //取得代理清單
599
+ if (true) {
600
+ _getProxies()
601
+ setInterval(() => {
602
+ _getProxies()
603
+ }, timeGetProxies)
604
+ }
605
+
606
+ //過濾出有效代理清單, 延遲3s觸發, 給時間抓取
607
+ if (true) {
608
+ setTimeout(() => {
609
+ _testProxies()
610
+ setInterval(() => {
611
+ _testProxies()
612
+ }, timeTestProxies)
613
+ }, 3000)
614
+ }
615
+
616
+ }
617
+
618
+ async function getProxies() {
619
+ let vs
620
+ await waitFun(() => {
621
+ vs = values(kpPrx)
622
+ return size(vs) > 0
623
+ })
624
+ return vs
625
+ }
626
+
627
+ //ev
628
+ let ev = evem()
629
+
630
+ //withServer
631
+ if (withServer) {
632
+ provideServer(getProxies, {
633
+ port: serverPort,
634
+ apiName: serverApiName,
635
+ corsOrigins: serverCorsOrigins,
636
+ })
637
+ }
638
+
639
+ async function getProxiesOnce() {
640
+ await _getProxies()
641
+ await _testProxies()
642
+ //本回合已完整結束, 直接回傳當前結果(可能為空陣列), 不可再用getProxies等待, 否則無有效代理時會等到waitFun逾時(約200s)才reject
643
+ let ps = values(kpPrx)
644
+ return ps
645
+ }
646
+
647
+ //save
648
+ ev.init = init
649
+ ev.getProxies = getProxies
650
+ ev.getProxiesOnce = getProxiesOnce
651
+
652
+ return ev
653
+ }
654
+
655
+
656
+ export default WIpProxy
564
657
  </code></pre>
565
658
  </article>
566
659
  </section>
@@ -575,7 +668,7 @@ export default WIpProxy
575
668
  <br class="clear">
576
669
 
577
670
  <footer>
578
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Tue Sep 01 2026 22:38:17 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
671
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Sep 05 2026 21:24:32 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
579
672
  </footer>
580
673
 
581
674
  <script>prettyPrint();</script>