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/src/WIpProxy.mjs CHANGED
@@ -1,516 +1,609 @@
1
- import axios from 'axios'
2
- import https from 'https'
3
- import get from 'lodash-es/get.js'
4
- import size from 'lodash-es/size.js'
5
- import each from 'lodash-es/each.js'
6
- import map from 'lodash-es/map.js'
7
- import trim from 'lodash-es/trim.js'
8
- import values from 'lodash-es/values.js'
9
- import evem from 'wsemi/src/evem.mjs'
10
- import sep from 'wsemi/src/sep.mjs'
11
- import isestr from 'wsemi/src/isestr.mjs'
12
- import ispint from 'wsemi/src/ispint.mjs'
13
- import isbol from 'wsemi/src/isbol.mjs'
14
- import isearr from 'wsemi/src/isearr.mjs'
15
- import iseobj from 'wsemi/src/iseobj.mjs'
16
- import cint from 'wsemi/src/cint.mjs'
17
- import waitFun from 'wsemi/src/waitFun.mjs'
18
- import provideServer from './provideServer.mjs'
19
-
20
-
21
- /**
22
- * 抓取代理伺服器
23
- *
24
- * @class
25
- * @param {Object} [opt={}] 輸入設定物件,預設{}
26
- * @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'
27
- * @param {String} [opt.tar='https://httpbin.org/ip'] 輸入檢測目標網址字串,預設'https://httpbin.org/ip'
28
- * @param {Integer} [opt.timeGetProxies=30*1000] 輸入輪循抓取代理伺服器時間間隔整數,單位ms,預設30*1000
29
- * @param {Integer} [opt.timeTestProxies=60*1000] 輸入輪循測試代理伺服器時間間隔整數,單位ms,預設60*1000
30
- * @param {Boolean} [opt.anonymityCheck=true] 輸入是否對代理做匿名性檢測布林值,開啟時會先打https://httpbin.org/ip驗證代理不會將本機真實IP透過X-Forwarded-For等方式洩漏給目標端,預設true
31
- * @param {Integer} [opt.anonymityCheckRepeats=2] 輸入匿名性檢測重複次數整數,用以偵測負載平衡型代理的機率性洩漏,任一次洩漏即判定失敗,預設2
32
- * @param {Boolean} [opt.withServer=false] 輸入是否創建供數據供給伺服器布林值,預設false
33
- * @param {Integer} [opt.serverPort=8080] 輸入創建伺服器所用port整數,預設8080
34
- * @param {String} [opt.serverApiName='getProxies'] 輸入供給數據API名稱字串,預設'getProxies'
35
- * @param {Array} [opt.serverCorsOrigins=['*']] 輸入允許跨域網域陣列,若給予['*']代表允許全部,預設['*']
36
- * @returns {Object} 回傳事件物件,提供函數getProxies,可監聽事件getRawProxies、add、delete、change
37
- * @example
38
- *
39
- * import _ from 'lodash-es'
40
- * import wip from './src/WIpProxy.mjs'
41
- *
42
- * let wo = wip({
43
- * // tar: `https://www.google.com`,
44
- * withServer: true,
45
- * serverPort: 9000,
46
- * serverCorsOrigins: ['*'],
47
- * })
48
- * wo.on('getRawProxies', (prxsRaw) => {
49
- * console.log(`已抓取公開代理共 ${_.size(prxsRaw)} 個...`)
50
- * })
51
- * wo.on('add', (p, proxies) => {
52
- * // console.log('新加入代理', { host: p.host, port: p.port }, _.map(proxies, 'proxy'))
53
- * })
54
- * wo.on('delete', (p, proxies) => {
55
- * // console.log('刪除代理', { host: p.host, port: p.port }, _.map(proxies, 'proxy'))
56
- * })
57
- * wo.on('change', (proxies) => {
58
- * // console.log(`有效代理`, _.map(proxies, 'proxy'))
59
- * console.log(`已檢測有效代理共 ${_.size(proxies)} 個...`)
60
- * })
61
- *
62
- * //browser view: http://localhost:9000/getProxies
63
- *
64
- */
65
- function WIpProxy(opt = {}) {
66
-
67
- //src
68
- let src = get(opt, 'src', '')
69
- if (!isestr(src)) {
70
- src = 'https://api.proxyscrape.com/v2/?request=displayproxies' +
71
- `&protocol=http` +
72
- // `&protocol=https` +
73
- `&timeout=5000` +
74
- // `&country=${join(['tw', 'cn', 'hk', 'jp', 'kr', 'sg', 'my', 'in', 'id', 'th', 'vn'])}` +
75
- // `&ssl=yes` +
76
- `&anonymity=elite` //elite匿名代理, all會使用X-Forwarded-For儲存原本ip導致被識別
77
- }
78
-
79
- //tar
80
- let tar = get(opt, 'tar', '')
81
- if (!isestr(tar)) {
82
- tar = 'https://httpbin.org/ip' //'https://www.google.com'
83
- }
84
-
85
- //timeGetProxies
86
- let timeGetProxies = get(opt, 'timeGetProxies')
87
- if (!ispint(timeGetProxies)) {
88
- timeGetProxies = 30 * 1000 //30s
89
- }
90
- timeGetProxies = cint(timeGetProxies)
91
-
92
- //timeTestProxies
93
- let timeTestProxies = get(opt, 'timeTestProxies')
94
- if (!ispint(timeTestProxies)) {
95
- timeTestProxies = 60 * 1000 //1min
96
- }
97
- timeTestProxies = cint(timeTestProxies)
98
-
99
- //anonymityCheck
100
- let anonymityCheck = get(opt, 'anonymityCheck')
101
- if (!isbol(anonymityCheck)) {
102
- anonymityCheck = true
103
- }
104
-
105
- //anonymityCheckRepeats
106
- let anonymityCheckRepeats = get(opt, 'anonymityCheckRepeats')
107
- if (!ispint(anonymityCheckRepeats)) {
108
- anonymityCheckRepeats = 2
109
- }
110
- anonymityCheckRepeats = cint(anonymityCheckRepeats)
111
-
112
- //withServer
113
- let withServer = get(opt, 'withServer')
114
- if (!isbol(withServer)) {
115
- withServer = false
116
- }
117
-
118
- //serverPort
119
- let serverPort = get(opt, 'serverPort')
120
- if (!ispint(serverPort)) {
121
- serverPort = 8080
122
- }
123
- serverPort = cint(serverPort)
124
-
125
- //serverApiName
126
- let serverApiName = get(opt, 'serverApiName')
127
- if (!isestr(serverApiName)) {
128
- serverApiName = 'getProxies'
129
- }
130
-
131
- //serverCorsOrigins
132
- let serverCorsOrigins = get(opt, 'serverCorsOrigins', [])
133
- if (!isearr(serverCorsOrigins)) {
134
- serverCorsOrigins = ['*']
135
- }
136
-
137
- //prxsRaw, kpPrx
138
- let prxsRaw = []
139
- let kpPrx = {}
140
-
141
- //_normalizeIp: 正規化單一IP字串, 去除[]包裹, ipv4之port, 及ipv4-mapped-ipv6之'::ffff:'前綴
142
- function _normalizeIp(v) {
143
- let s = trim(v)
144
- if (s === '') {
145
- return ''
146
- }
147
- if (s[0] === '[') {
148
- //形如'[::1]:8080'或'[2001:db8::1]'
149
- let k = s.indexOf(']')
150
- if (k > 0) {
151
- s = s.slice(1, k)
152
- }
153
- }
154
- else if (s.split(':').length === 2) {
155
- //僅單一冒號才視為'1.2.3.4:port', 多冒號為ipv6不可截斷
156
- s = s.split(':')[0]
157
- }
158
- //ipv4-mapped-ipv6, 如'::ffff:1.2.3.4', 統一還原為ipv4
159
- let m = s.match(/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i)
160
- if (m) {
161
- s = m[1]
162
- }
163
- return s
164
- }
165
-
166
- //_originHasIp: 檢查origin(可能為'ip1, ip2, ...')逐段正規化後是否含有指定IP
167
- function _originHasIp(origin, ip) {
168
- if (!isestr(ip)) {
169
- return false
170
- }
171
- let vs = map(sep(origin, ','), (v) => _normalizeIp(v))
172
- return vs.indexOf(ip) >= 0
173
- }
174
-
175
- //myRealIp: 本機真實公網IP, 用於匿名性檢測時比對代理egress是否洩漏
176
- let myRealIp = ''
177
- let myRealIpReady = false
178
- let myRealIpPm = null
179
- async function _fetchMyRealIp() {
180
- if (myRealIpReady) {
181
- return
182
- }
183
- if (!myRealIpPm) {
184
- myRealIpPm = (async () => {
185
- try {
186
- let res = await axios.get('https://httpbin.org/ip', { timeout: 10000 })
187
- let origin = get(res, 'data.origin', '')
188
- if (isestr(origin)) {
189
- //origin可能為"1.2.3.4", "1.2.3.4:port", 或"::ffff:1.2.3.4"(部分環境如雲端runner為ipv4-mapped-ipv6), 取首個逗號前再正規化
190
- myRealIp = _normalizeIp(sep(origin, ',')[0])
191
- }
192
- }
193
- catch (err) {
194
- //取不到就算了, 匿名性檢查會自動跳過
195
- }
196
- myRealIpReady = true
197
- })()
198
- }
199
- await myRealIpPm
200
- }
201
-
202
- async function _anonymityCheckCore(host, port) {
203
- if (!anonymityCheck) {
204
- return { ok: true }
205
- }
206
- if (!isestr(myRealIp)) {
207
- return { ok: true } //沒抓到自己IP就無從比對, 放行
208
- }
209
- let agent = new https.Agent({ rejectUnauthorized: false })
210
- for (let i = 0; i < anonymityCheckRepeats; i++) {
211
- try {
212
- let res = await axios.get('https://httpbin.org/ip', {
213
- proxy: { host, port, protocol: 'http' },
214
- httpsAgent: agent,
215
- timeout: 5000,
216
- headers: {
217
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
218
- 'Accept': 'application/json',
219
- },
220
- validateStatus: () => true,
221
- })
222
- if (res.status !== 200) {
223
- return { ok: false, reason: `anonymity status[${res.status}]` }
224
- }
225
- let origin = get(res, 'data.origin', '')
226
- if (!isestr(origin)) {
227
- return { ok: false, reason: 'anonymity non-json' }
228
- }
229
- if (_originHasIp(origin, myRealIp)) {
230
- return { ok: false, reason: `leaks real IP` }
231
- }
232
- }
233
- catch (err) {
234
- return { ok: false, reason: trim(err.message || '') }
235
- }
236
- }
237
- return { ok: true }
238
- }
239
-
240
- async function _getProxiesCore(src) {
241
- let ps = []
242
-
243
- try {
244
-
245
- //res
246
- let res = await axios.get(src)
247
-
248
- //c
249
- let c = res.data
250
- // console.log('c', c)
251
-
252
- //sep
253
- let ss = sep(c, '\n')
254
- // console.log('ss', ss)
255
-
256
- //ps
257
- ps = map(ss, (v) => {
258
- let [host, port] = v.split(':')
259
- port = cint(port)
260
- return {
261
- proxy: v,
262
- host,
263
- port,
264
- }
265
- })
266
- // console.log('ps', ps)
267
-
268
- }
269
- catch (err) {
270
- console.log('_getProxiesCore catch', err.message)
271
- }
272
- // console.log('ps', ps)
273
-
274
- return ps
275
- }
276
-
277
- let b_getProxies = false
278
- async function _getProxies() {
279
- // console.log('call getProxies...')
280
-
281
- let core = async() => {
282
-
283
- //get
284
- prxsRaw = await _getProxiesCore(src)
285
-
286
- //emit
287
- ev.emit('getRawProxies', prxsRaw)
288
-
289
- }
290
-
291
- //core
292
- if (b_getProxies) {
293
- return
294
- }
295
- b_getProxies = true
296
- await core()
297
- .catch(() => {}) //吃掉錯誤
298
- .finally(() => {
299
- b_getProxies = false
300
- })
301
-
302
- }
303
-
304
- async function _testProxyCore(host, port, url) {
305
-
306
- //ret
307
- let ret = (state, msg) => {
308
- return {
309
- state,
310
- msg,
311
- p: {
312
- proxy: `${host}:${port}`,
313
- host,
314
- port,
315
- },
316
- }
317
- }
318
-
319
- //匿名性預檢: 先確認代理不會透過X-Forwarded-For等洩漏本機真實IP
320
- let ac = await _anonymityCheckCore(host, port)
321
- if (!ac.ok) {
322
- return ret('error', ac.reason)
323
- }
324
-
325
- //r
326
- let r = {}
327
- try {
328
-
329
- //agent
330
- let agent = new https.Agent({ rejectUnauthorized: false }) //避免證書錯誤中止
331
-
332
- //get
333
- let res = await axios.get(url, {
334
- proxy: {
335
- host,
336
- port,
337
- protocol: 'http',
338
- },
339
- httpsAgent: agent,
340
- timeout: 5000,
341
- headers: {
342
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
343
- 'Accept': 'application/json',
344
- },
345
- })
346
-
347
- if (res.status === 200) {
348
- r = ret('success', res.data)
349
- }
350
- else {
351
- r = ret('error', `status[${res.status}] error`)
352
- }
353
-
354
- }
355
- catch (err) {
356
- r = ret('error', trim(err.message))
357
- // if (r.msg.indexOf(`ssl3_get_record`) >= 0) {
358
- // r.msg = `ssl3_get_record error`
359
- // }
360
- }
361
- // console.log(r)
362
-
363
- return r
364
- }
365
-
366
- async function _testProxiesCore(prxs) {
367
-
368
- //pms
369
- let pms = map(prxs, (p) => {
370
- let pm = _testProxyCore(p.host, p.port, tar)
371
- return pm
372
- })
373
-
374
- //rrs, allSettled
375
- let rrs = await Promise.allSettled(pms)
376
- // console.log('rrs', rrs)
377
-
378
- //psValid, psInvalid
379
- let psValid = []
380
- let psInvalid = []
381
- each(rrs, (v) => {
382
-
383
- //be
384
- let p = get(v, 'value.p', null)
385
- let be = iseobj(p) && isestr(get(p, 'proxy'))
386
-
387
- //check
388
- if (!be) {
389
- return true //跳出換下一個
390
- }
391
-
392
- //b
393
- let status = get(v, 'status', '')
394
- let b1 = status === 'fulfilled'
395
- let state = get(v, 'value.state', '')
396
- let b2 = state === 'success'
397
- let b = b1 && b2
398
-
399
- //push
400
- if (b) {
401
- psValid.push(p)
402
- }
403
- else {
404
- psInvalid.push(p)
405
- }
406
-
407
- })
408
-
409
- return {
410
- psValid,
411
- psInvalid,
412
- }
413
- }
414
-
415
- let b_testProxies = false
416
- async function _testProxies() {
417
- // console.log('call testProxies...')
418
-
419
- let core = async() => {
420
-
421
- //確保本機真實IP已取得, 供匿名性檢查比對
422
- await _fetchMyRealIp()
423
-
424
- //get
425
- let r = await _testProxiesCore(prxsRaw)
426
-
427
- //emit add
428
- each(r.psValid, (p) => {
429
- kpPrx[p.proxy] = p
430
- ev.emit('add', p, values(kpPrx))
431
- })
432
-
433
- //emit delete
434
- each(r.psInvalid, (p) => {
435
- delete kpPrx[p.proxy]
436
- ev.emit('delete', p, values(kpPrx))
437
- })
438
-
439
- //emit change
440
- ev.emit('change', values(kpPrx))
441
-
442
- }
443
-
444
- //core
445
- if (b_testProxies) {
446
- return
447
- }
448
- b_testProxies = true
449
- await core()
450
- .catch(() => {}) //吃掉錯誤
451
- .finally(() => {
452
- b_testProxies = false
453
- })
454
-
455
- }
456
-
457
- function init() {
458
-
459
- //取得代理清單
460
- if (true) {
461
- _getProxies()
462
- setInterval(() => {
463
- _getProxies()
464
- }, timeGetProxies)
465
- }
466
-
467
- //過濾出有效代理清單, 延遲3s觸發, 給時間抓取
468
- if (true) {
469
- setTimeout(() => {
470
- _testProxies()
471
- setInterval(() => {
472
- _testProxies()
473
- }, timeTestProxies)
474
- }, 3000)
475
- }
476
-
477
- }
478
-
479
- async function getProxies() {
480
- let vs
481
- await waitFun(() => {
482
- vs = values(kpPrx)
483
- return size(vs) > 0
484
- })
485
- return vs
486
- }
487
-
488
- //ev
489
- let ev = evem()
490
-
491
- //withServer
492
- if (withServer) {
493
- provideServer(getProxies, {
494
- port: serverPort,
495
- apiName: serverApiName,
496
- corsOrigins: serverCorsOrigins,
497
- })
498
- }
499
-
500
- async function getProxiesOnce() {
501
- await _getProxies()
502
- await _testProxies()
503
- let ps = await getProxies()
504
- return ps
505
- }
506
-
507
- //save
508
- ev.init = init
509
- ev.getProxies = getProxies
510
- ev.getProxiesOnce = getProxiesOnce
511
-
512
- return ev
513
- }
514
-
515
-
516
- export default WIpProxy
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