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