w-ipproxy 1.0.2 → 1.0.4
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/.github/workflows/ci-test.yml +2 -2
- package/dist/w-ipproxy.umd.js +7 -0
- package/dist/w-ipproxy.umd.js.map +1 -0
- package/docs/WIpProxy.html +82 -2
- package/docs/WIpProxy.mjs.html +91 -2
- package/docs/index.html +1 -1
- package/package.json +5 -5
- package/src/WIpProxy.mjs +90 -1
- package/test/anonymity.test.mjs +140 -0
- package/test/getProxiesOnce.test.mjs +89 -0
- package/toolg/gDistRollup.mjs +4 -7
- package/dist/w-ip-proxy.umd.js +0 -7
- package/dist/w-ip-proxy.umd.js.map +0 -1
- package/test/all.test.mjs +0 -10
package/docs/WIpProxy.mjs.html
CHANGED
|
@@ -74,6 +74,8 @@ import provideServer from './provideServer.mjs'
|
|
|
74
74
|
* @param {String} [opt.tar='https://httpbin.org/ip'] 輸入檢測目標網址字串,預設'https://httpbin.org/ip'
|
|
75
75
|
* @param {Integer} [opt.timeGetProxies=30*1000] 輸入輪循抓取代理伺服器時間間隔整數,單位ms,預設30*1000
|
|
76
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
|
|
77
79
|
* @param {Boolean} [opt.withServer=false] 輸入是否創建供數據供給伺服器布林值,預設false
|
|
78
80
|
* @param {Integer} [opt.serverPort=8080] 輸入創建伺服器所用port整數,預設8080
|
|
79
81
|
* @param {String} [opt.serverApiName='getProxies'] 輸入供給數據API名稱字串,預設'getProxies'
|
|
@@ -118,7 +120,7 @@ function WIpProxy(opt = {}) {
|
|
|
118
120
|
`&timeout=5000` +
|
|
119
121
|
// `&country=${join(['tw', 'cn', 'hk', 'jp', 'kr', 'sg', 'my', 'in', 'id', 'th', 'vn'])}` +
|
|
120
122
|
// `&ssl=yes` +
|
|
121
|
-
`&anonymity=all
|
|
123
|
+
`&anonymity=elite` //elite匿名代理, all會使用X-Forwarded-For儲存原本ip導致被識別
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
//tar
|
|
@@ -141,6 +143,19 @@ function WIpProxy(opt = {}) {
|
|
|
141
143
|
}
|
|
142
144
|
timeTestProxies = cint(timeTestProxies)
|
|
143
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
|
+
|
|
144
159
|
//withServer
|
|
145
160
|
let withServer = get(opt, 'withServer')
|
|
146
161
|
if (!isbol(withServer)) {
|
|
@@ -170,6 +185,71 @@ function WIpProxy(opt = {}) {
|
|
|
170
185
|
let prxsRaw = []
|
|
171
186
|
let kpPrx = {}
|
|
172
187
|
|
|
188
|
+
//myRealIp: 本機真實公網IP, 用於匿名性檢測時比對代理egress是否洩漏
|
|
189
|
+
let myRealIp = ''
|
|
190
|
+
let myRealIpReady = false
|
|
191
|
+
let myRealIpPm = null
|
|
192
|
+
async function _fetchMyRealIp() {
|
|
193
|
+
if (myRealIpReady) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
if (!myRealIpPm) {
|
|
197
|
+
myRealIpPm = (async () => {
|
|
198
|
+
try {
|
|
199
|
+
let res = await axios.get('https://httpbin.org/ip', { timeout: 10000 })
|
|
200
|
+
let origin = get(res, 'data.origin', '')
|
|
201
|
+
if (isestr(origin)) {
|
|
202
|
+
//origin可能為"1.2.3.4"或"1.2.3.4:port", 取首個逗號前且去port
|
|
203
|
+
myRealIp = origin.split(',')[0].trim().split(':')[0]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
//取不到就算了, 匿名性檢查會自動跳過
|
|
208
|
+
}
|
|
209
|
+
myRealIpReady = true
|
|
210
|
+
})()
|
|
211
|
+
}
|
|
212
|
+
await myRealIpPm
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function _anonymityCheckCore(host, port) {
|
|
216
|
+
if (!anonymityCheck) {
|
|
217
|
+
return { ok: true }
|
|
218
|
+
}
|
|
219
|
+
if (!isestr(myRealIp)) {
|
|
220
|
+
return { ok: true } //沒抓到自己IP就無從比對, 放行
|
|
221
|
+
}
|
|
222
|
+
let agent = new https.Agent({ rejectUnauthorized: false })
|
|
223
|
+
for (let i = 0; i < anonymityCheckRepeats; i++) {
|
|
224
|
+
try {
|
|
225
|
+
let res = await axios.get('https://httpbin.org/ip', {
|
|
226
|
+
proxy: { host, port, protocol: 'http' },
|
|
227
|
+
httpsAgent: agent,
|
|
228
|
+
timeout: 5000,
|
|
229
|
+
headers: {
|
|
230
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
|
|
231
|
+
'Accept': 'application/json',
|
|
232
|
+
},
|
|
233
|
+
validateStatus: () => true,
|
|
234
|
+
})
|
|
235
|
+
if (res.status !== 200) {
|
|
236
|
+
return { ok: false, reason: `anonymity status[${res.status}]` }
|
|
237
|
+
}
|
|
238
|
+
let origin = get(res, 'data.origin', '')
|
|
239
|
+
if (!isestr(origin)) {
|
|
240
|
+
return { ok: false, reason: 'anonymity non-json' }
|
|
241
|
+
}
|
|
242
|
+
if (origin.indexOf(myRealIp) >= 0) {
|
|
243
|
+
return { ok: false, reason: `leaks real IP` }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return { ok: false, reason: trim(err.message || '') }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return { ok: true }
|
|
251
|
+
}
|
|
252
|
+
|
|
173
253
|
async function _getProxiesCore(src) {
|
|
174
254
|
let ps = []
|
|
175
255
|
|
|
@@ -249,6 +329,12 @@ function WIpProxy(opt = {}) {
|
|
|
249
329
|
}
|
|
250
330
|
}
|
|
251
331
|
|
|
332
|
+
//匿名性預檢: 先確認代理不會透過X-Forwarded-For等洩漏本機真實IP
|
|
333
|
+
let ac = await _anonymityCheckCore(host, port)
|
|
334
|
+
if (!ac.ok) {
|
|
335
|
+
return ret('error', ac.reason)
|
|
336
|
+
}
|
|
337
|
+
|
|
252
338
|
//r
|
|
253
339
|
let r = {}
|
|
254
340
|
try {
|
|
@@ -345,6 +431,9 @@ function WIpProxy(opt = {}) {
|
|
|
345
431
|
|
|
346
432
|
let core = async() => {
|
|
347
433
|
|
|
434
|
+
//確保本機真實IP已取得, 供匿名性檢查比對
|
|
435
|
+
await _fetchMyRealIp()
|
|
436
|
+
|
|
348
437
|
//get
|
|
349
438
|
let r = await _testProxiesCore(prxsRaw)
|
|
350
439
|
|
|
@@ -452,7 +541,7 @@ export default WIpProxy
|
|
|
452
541
|
<br class="clear">
|
|
453
542
|
|
|
454
543
|
<footer>
|
|
455
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.
|
|
544
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Apr 17 2026 13:33:04 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
456
545
|
</footer>
|
|
457
546
|
|
|
458
547
|
<script>prettyPrint();</script>
|
package/docs/index.html
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<br class="clear">
|
|
72
72
|
|
|
73
73
|
<footer>
|
|
74
|
-
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.
|
|
74
|
+
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Apr 17 2026 13:33:04 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
|
|
75
75
|
</footer>
|
|
76
76
|
|
|
77
77
|
<script>prettyPrint();</script>
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "w-ipproxy",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"main": "dist/w-ipproxy.umd.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@hapi/hapi": "^21.4.
|
|
6
|
+
"@hapi/hapi": "^21.4.8",
|
|
7
7
|
"axios": "^1.13.6",
|
|
8
8
|
"cheerio": "^1.2.0",
|
|
9
|
-
"lodash-es": "^4.
|
|
10
|
-
"wsemi": "^1.8.
|
|
9
|
+
"lodash-es": "^4.18.1",
|
|
10
|
+
"wsemi": "^1.8.47"
|
|
11
11
|
},
|
|
12
12
|
"devDependencies": {
|
|
13
|
-
"w-package-tools": "^1.1.
|
|
13
|
+
"w-package-tools": "^1.1.5"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
16
|
"test": "mocha --parallel --timeout 60000",
|
package/src/WIpProxy.mjs
CHANGED
|
@@ -27,6 +27,8 @@ import provideServer from './provideServer.mjs'
|
|
|
27
27
|
* @param {String} [opt.tar='https://httpbin.org/ip'] 輸入檢測目標網址字串,預設'https://httpbin.org/ip'
|
|
28
28
|
* @param {Integer} [opt.timeGetProxies=30*1000] 輸入輪循抓取代理伺服器時間間隔整數,單位ms,預設30*1000
|
|
29
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
|
|
30
32
|
* @param {Boolean} [opt.withServer=false] 輸入是否創建供數據供給伺服器布林值,預設false
|
|
31
33
|
* @param {Integer} [opt.serverPort=8080] 輸入創建伺服器所用port整數,預設8080
|
|
32
34
|
* @param {String} [opt.serverApiName='getProxies'] 輸入供給數據API名稱字串,預設'getProxies'
|
|
@@ -71,7 +73,7 @@ function WIpProxy(opt = {}) {
|
|
|
71
73
|
`&timeout=5000` +
|
|
72
74
|
// `&country=${join(['tw', 'cn', 'hk', 'jp', 'kr', 'sg', 'my', 'in', 'id', 'th', 'vn'])}` +
|
|
73
75
|
// `&ssl=yes` +
|
|
74
|
-
`&anonymity=all
|
|
76
|
+
`&anonymity=elite` //elite匿名代理, all會使用X-Forwarded-For儲存原本ip導致被識別
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
//tar
|
|
@@ -94,6 +96,19 @@ function WIpProxy(opt = {}) {
|
|
|
94
96
|
}
|
|
95
97
|
timeTestProxies = cint(timeTestProxies)
|
|
96
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
|
+
|
|
97
112
|
//withServer
|
|
98
113
|
let withServer = get(opt, 'withServer')
|
|
99
114
|
if (!isbol(withServer)) {
|
|
@@ -123,6 +138,71 @@ function WIpProxy(opt = {}) {
|
|
|
123
138
|
let prxsRaw = []
|
|
124
139
|
let kpPrx = {}
|
|
125
140
|
|
|
141
|
+
//myRealIp: 本機真實公網IP, 用於匿名性檢測時比對代理egress是否洩漏
|
|
142
|
+
let myRealIp = ''
|
|
143
|
+
let myRealIpReady = false
|
|
144
|
+
let myRealIpPm = null
|
|
145
|
+
async function _fetchMyRealIp() {
|
|
146
|
+
if (myRealIpReady) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
if (!myRealIpPm) {
|
|
150
|
+
myRealIpPm = (async () => {
|
|
151
|
+
try {
|
|
152
|
+
let res = await axios.get('https://httpbin.org/ip', { timeout: 10000 })
|
|
153
|
+
let origin = get(res, 'data.origin', '')
|
|
154
|
+
if (isestr(origin)) {
|
|
155
|
+
//origin可能為"1.2.3.4"或"1.2.3.4:port", 取首個逗號前且去port
|
|
156
|
+
myRealIp = origin.split(',')[0].trim().split(':')[0]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
//取不到就算了, 匿名性檢查會自動跳過
|
|
161
|
+
}
|
|
162
|
+
myRealIpReady = true
|
|
163
|
+
})()
|
|
164
|
+
}
|
|
165
|
+
await myRealIpPm
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function _anonymityCheckCore(host, port) {
|
|
169
|
+
if (!anonymityCheck) {
|
|
170
|
+
return { ok: true }
|
|
171
|
+
}
|
|
172
|
+
if (!isestr(myRealIp)) {
|
|
173
|
+
return { ok: true } //沒抓到自己IP就無從比對, 放行
|
|
174
|
+
}
|
|
175
|
+
let agent = new https.Agent({ rejectUnauthorized: false })
|
|
176
|
+
for (let i = 0; i < anonymityCheckRepeats; i++) {
|
|
177
|
+
try {
|
|
178
|
+
let res = await axios.get('https://httpbin.org/ip', {
|
|
179
|
+
proxy: { host, port, protocol: 'http' },
|
|
180
|
+
httpsAgent: agent,
|
|
181
|
+
timeout: 5000,
|
|
182
|
+
headers: {
|
|
183
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
|
|
184
|
+
'Accept': 'application/json',
|
|
185
|
+
},
|
|
186
|
+
validateStatus: () => true,
|
|
187
|
+
})
|
|
188
|
+
if (res.status !== 200) {
|
|
189
|
+
return { ok: false, reason: `anonymity status[${res.status}]` }
|
|
190
|
+
}
|
|
191
|
+
let origin = get(res, 'data.origin', '')
|
|
192
|
+
if (!isestr(origin)) {
|
|
193
|
+
return { ok: false, reason: 'anonymity non-json' }
|
|
194
|
+
}
|
|
195
|
+
if (origin.indexOf(myRealIp) >= 0) {
|
|
196
|
+
return { ok: false, reason: `leaks real IP` }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
return { ok: false, reason: trim(err.message || '') }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { ok: true }
|
|
204
|
+
}
|
|
205
|
+
|
|
126
206
|
async function _getProxiesCore(src) {
|
|
127
207
|
let ps = []
|
|
128
208
|
|
|
@@ -202,6 +282,12 @@ function WIpProxy(opt = {}) {
|
|
|
202
282
|
}
|
|
203
283
|
}
|
|
204
284
|
|
|
285
|
+
//匿名性預檢: 先確認代理不會透過X-Forwarded-For等洩漏本機真實IP
|
|
286
|
+
let ac = await _anonymityCheckCore(host, port)
|
|
287
|
+
if (!ac.ok) {
|
|
288
|
+
return ret('error', ac.reason)
|
|
289
|
+
}
|
|
290
|
+
|
|
205
291
|
//r
|
|
206
292
|
let r = {}
|
|
207
293
|
try {
|
|
@@ -298,6 +384,9 @@ function WIpProxy(opt = {}) {
|
|
|
298
384
|
|
|
299
385
|
let core = async() => {
|
|
300
386
|
|
|
387
|
+
//確保本機真實IP已取得, 供匿名性檢查比對
|
|
388
|
+
await _fetchMyRealIp()
|
|
389
|
+
|
|
301
390
|
//get
|
|
302
391
|
let r = await _testProxiesCore(prxsRaw)
|
|
303
392
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
import https from 'https'
|
|
4
|
+
import wip from '../src/WIpProxy.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
describe('anonymity (elite + anonymityCheck)', function() {
|
|
8
|
+
this.timeout(240 * 1000) //4min, 因為getProxiesOnce加上獨立leak驗證本身就耗時
|
|
9
|
+
|
|
10
|
+
let myIp = ''
|
|
11
|
+
let ps = []
|
|
12
|
+
|
|
13
|
+
before(async function() {
|
|
14
|
+
|
|
15
|
+
//取本機真實公網IP
|
|
16
|
+
try {
|
|
17
|
+
let r = await axios.get('https://httpbin.org/ip', { timeout: 10000 })
|
|
18
|
+
myIp = r.data?.origin || ''
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
this.skip()
|
|
22
|
+
}
|
|
23
|
+
if (!myIp) {
|
|
24
|
+
this.skip()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//預設設定已是anonymity=elite且啟用anonymityCheck, 走完整過濾路徑
|
|
28
|
+
let wo = wip({})
|
|
29
|
+
ps = await wo.getProxiesOnce()
|
|
30
|
+
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('no returned proxy should leak real IP via httpbin origin', async function() {
|
|
34
|
+
if (ps.length === 0) {
|
|
35
|
+
this.skip()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let agent = new https.Agent({ rejectUnauthorized: false })
|
|
39
|
+
let sample = ps.slice(0, 30)
|
|
40
|
+
let pms = sample.map(async (p) => {
|
|
41
|
+
try {
|
|
42
|
+
let res = await axios.get('https://httpbin.org/ip', {
|
|
43
|
+
proxy: { host: p.host, port: p.port, protocol: 'http' },
|
|
44
|
+
httpsAgent: agent,
|
|
45
|
+
timeout: 8000,
|
|
46
|
+
headers: {
|
|
47
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
|
|
48
|
+
'Accept': 'application/json',
|
|
49
|
+
},
|
|
50
|
+
validateStatus: () => true,
|
|
51
|
+
})
|
|
52
|
+
if (res.status !== 200) {
|
|
53
|
+
return { proxy: p.proxy, ok: true, reason: 'non-200, ignored' }
|
|
54
|
+
}
|
|
55
|
+
let origin = res.data?.origin
|
|
56
|
+
if (typeof origin !== 'string') {
|
|
57
|
+
return { proxy: p.proxy, ok: true, reason: 'non-json, ignored' }
|
|
58
|
+
}
|
|
59
|
+
//回傳origin含本機真實IP即代表該代理洩漏
|
|
60
|
+
let leaks = origin.indexOf(myIp) >= 0
|
|
61
|
+
return { proxy: p.proxy, ok: !leaks, origin }
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
//網路錯誤不算洩漏, 直接放行
|
|
65
|
+
return { proxy: p.proxy, ok: true, reason: err.message }
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
let rrs = await Promise.all(pms)
|
|
69
|
+
let leaked = rrs.filter((r) => !r.ok)
|
|
70
|
+
|
|
71
|
+
assert.strictEqual(
|
|
72
|
+
leaked.length, 0,
|
|
73
|
+
`expected 0 proxies to leak real IP, got ${leaked.length}: ` +
|
|
74
|
+
leaked.slice(0, 5).map((r) => `${r.proxy} (origin=${r.origin})`).join('; ')
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('no returned proxy should carry X-Forwarded-For / Via when viewed from target', async function() {
|
|
79
|
+
if (ps.length === 0) {
|
|
80
|
+
this.skip()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let LEAK_IP_HEADERS = [
|
|
84
|
+
'x-forwarded-for', 'x-real-ip', 'x-client-ip', 'client-ip',
|
|
85
|
+
'true-client-ip', 'cf-connecting-ip', 'forwarded',
|
|
86
|
+
'x-originating-ip', 'x-cluster-client-ip',
|
|
87
|
+
]
|
|
88
|
+
let LEAK_PROXY_HEADERS = [
|
|
89
|
+
'via', 'proxy-connection', 'x-proxy-id',
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
let agent = new https.Agent({ rejectUnauthorized: false })
|
|
93
|
+
let sample = ps.slice(0, 30)
|
|
94
|
+
let pms = sample.map(async (p) => {
|
|
95
|
+
try {
|
|
96
|
+
let res = await axios.get('https://httpbin.org/headers', {
|
|
97
|
+
proxy: { host: p.host, port: p.port, protocol: 'http' },
|
|
98
|
+
httpsAgent: agent,
|
|
99
|
+
timeout: 8000,
|
|
100
|
+
headers: {
|
|
101
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
|
|
102
|
+
'Accept': 'application/json',
|
|
103
|
+
},
|
|
104
|
+
validateStatus: () => true,
|
|
105
|
+
})
|
|
106
|
+
if (res.status !== 200 || typeof res.data?.headers !== 'object') {
|
|
107
|
+
return { proxy: p.proxy, ok: true, reason: 'ignored' }
|
|
108
|
+
}
|
|
109
|
+
let hs = {}
|
|
110
|
+
for (let k in res.data.headers) {
|
|
111
|
+
hs[k.toLowerCase()] = String(res.data.headers[k])
|
|
112
|
+
}
|
|
113
|
+
let bad = []
|
|
114
|
+
for (let k of LEAK_IP_HEADERS) {
|
|
115
|
+
if (k in hs && hs[k].indexOf(myIp) >= 0) {
|
|
116
|
+
bad.push(`${k}=${hs[k]}`)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (let k of LEAK_PROXY_HEADERS) {
|
|
120
|
+
if (k in hs) {
|
|
121
|
+
bad.push(`${k}=${hs[k]}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { proxy: p.proxy, ok: bad.length === 0, bad }
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return { proxy: p.proxy, ok: true, reason: err.message }
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
let rrs = await Promise.all(pms)
|
|
131
|
+
let leaked = rrs.filter((r) => !r.ok)
|
|
132
|
+
|
|
133
|
+
assert.strictEqual(
|
|
134
|
+
leaked.length, 0,
|
|
135
|
+
`expected 0 proxies with leaky headers, got ${leaked.length}: ` +
|
|
136
|
+
leaked.slice(0, 5).map((r) => `${r.proxy} (${r.bad.join(',')})`).join('; ')
|
|
137
|
+
)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
import axios from 'axios'
|
|
3
|
+
import https from 'https'
|
|
4
|
+
import wip from '../src/WIpProxy.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
describe('getProxiesOnce', function() {
|
|
8
|
+
this.timeout(180 * 1000) //3min, 因為需要抓清單+檢測+獨立驗證
|
|
9
|
+
|
|
10
|
+
let myIp = ''
|
|
11
|
+
let ps = []
|
|
12
|
+
|
|
13
|
+
before(async function() {
|
|
14
|
+
|
|
15
|
+
//取本機真實公網IP (不走代理), 作為後續洩漏比對基準
|
|
16
|
+
try {
|
|
17
|
+
let r = await axios.get('https://httpbin.org/ip', { timeout: 10000 })
|
|
18
|
+
myIp = r.data?.origin || ''
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
//網路異常則整組test跳過
|
|
22
|
+
this.skip()
|
|
23
|
+
}
|
|
24
|
+
if (!myIp) {
|
|
25
|
+
this.skip()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//取代理清單 (預設anonymity=elite + 匿名性檢查)
|
|
29
|
+
let wo = wip({})
|
|
30
|
+
ps = await wo.getProxiesOnce()
|
|
31
|
+
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should return a non-empty array of proxy objects', function() {
|
|
35
|
+
//若上游proxyscrape當下無代理供應, 放行以免CI誤報 (非本專案bug)
|
|
36
|
+
if (ps.length === 0) {
|
|
37
|
+
this.skip()
|
|
38
|
+
}
|
|
39
|
+
assert.ok(Array.isArray(ps))
|
|
40
|
+
for (let p of ps) {
|
|
41
|
+
assert.strictEqual(typeof p.host, 'string')
|
|
42
|
+
assert.ok(p.host.length > 0)
|
|
43
|
+
assert.strictEqual(typeof p.port, 'number')
|
|
44
|
+
assert.ok(p.port > 0)
|
|
45
|
+
assert.strictEqual(p.proxy, `${p.host}:${p.port}`)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('majority of returned proxies should be independently usable against httpbin', async function() {
|
|
50
|
+
if (ps.length === 0) {
|
|
51
|
+
this.skip()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//獨立再打一次httpbin.org/ip, 不倚賴內建的檢測結果
|
|
55
|
+
let agent = new https.Agent({ rejectUnauthorized: false })
|
|
56
|
+
let sample = ps.slice(0, 30) //取前30個避免長時間阻塞CI
|
|
57
|
+
let pms = sample.map(async (p) => {
|
|
58
|
+
try {
|
|
59
|
+
let res = await axios.get('https://httpbin.org/ip', {
|
|
60
|
+
proxy: { host: p.host, port: p.port, protocol: 'http' },
|
|
61
|
+
httpsAgent: agent,
|
|
62
|
+
timeout: 8000,
|
|
63
|
+
headers: {
|
|
64
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/122.0.0.0 Safari/537.36',
|
|
65
|
+
'Accept': 'application/json',
|
|
66
|
+
},
|
|
67
|
+
validateStatus: () => true,
|
|
68
|
+
})
|
|
69
|
+
return res.status === 200
|
|
70
|
+
&& typeof res.data === 'object'
|
|
71
|
+
&& res.data !== null
|
|
72
|
+
&& typeof res.data.origin === 'string'
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
let rrs = await Promise.all(pms)
|
|
79
|
+
let okCount = rrs.filter((v) => v).length
|
|
80
|
+
let ratio = okCount / rrs.length
|
|
81
|
+
|
|
82
|
+
//要求至少50%仍可用, 免疫於個別代理剛好在測試間掛掉的flakiness
|
|
83
|
+
assert.ok(
|
|
84
|
+
ratio >= 0.5,
|
|
85
|
+
`independent reachability ratio too low: ${okCount}/${rrs.length} (${(ratio * 100).toFixed(1)}%)`
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
})
|
package/toolg/gDistRollup.mjs
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import rollupFiles from 'w-package-tools/src/rollupFiles.mjs'
|
|
2
2
|
import getFiles from 'w-package-tools/src/getFiles.mjs'
|
|
3
|
-
import w from 'wsemi'
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
let fdSrc = './src'
|
|
7
6
|
let fdTar = './dist'
|
|
8
|
-
|
|
9
|
-
fns = fns.filter((v) => {
|
|
10
|
-
return w.strleft(v, 1) === 'W'
|
|
11
|
-
})
|
|
7
|
+
|
|
12
8
|
|
|
13
9
|
rollupFiles({
|
|
14
|
-
fns,
|
|
10
|
+
fns: 'WIpProxy.mjs',
|
|
15
11
|
fdSrc,
|
|
16
12
|
fdTar,
|
|
17
|
-
|
|
13
|
+
hookNameDist: () => 'w-ipproxy',
|
|
14
|
+
// nameDistType: 'kebabCase', //直接由hookNameDist給予
|
|
18
15
|
globals: {
|
|
19
16
|
'@hapi/hapi': '@hapi/hapi',
|
|
20
17
|
'@hapi/inert': '@hapi/inert',
|