w-ipproxy 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.eslintrc.js +18 -17
  2. package/.github/workflows/ci-test.yml +3 -3
  3. package/README.md +64 -2
  4. package/babel.config.js +4 -2
  5. package/dist/w-ip-proxy.umd.js +7 -0
  6. package/dist/w-ip-proxy.umd.js.map +1 -0
  7. package/docs/WIpProxy.html +722 -0
  8. package/docs/WIpProxy.mjs.html +554 -0
  9. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  10. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  11. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  12. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  13. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  14. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  15. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  16. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  17. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  18. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  19. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  20. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  21. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  22. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  23. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  24. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  25. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  26. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  27. package/docs/index.html +84 -0
  28. package/docs/scripts/collapse.js +39 -0
  29. package/docs/scripts/commonNav.js +28 -0
  30. package/docs/scripts/linenumber.js +25 -0
  31. package/docs/scripts/nav.js +12 -0
  32. package/docs/scripts/polyfill.js +4 -0
  33. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  34. package/docs/scripts/prettify/lang-css.js +2 -0
  35. package/docs/scripts/prettify/prettify.js +28 -0
  36. package/docs/scripts/search.js +99 -0
  37. package/docs/styles/jsdoc.css +776 -0
  38. package/docs/styles/prettify.css +80 -0
  39. package/g-getByTimer.mjs +24 -0
  40. package/g-getOnce.mjs +11 -0
  41. package/g-srv.mjs +28 -0
  42. package/package.json +11 -9
  43. package/script.txt +17 -0
  44. package/src/WIpProxy.mjs +473 -14
  45. package/src/provideServer.mjs +65 -0
  46. package/test/anonymity.test.mjs +140 -0
  47. package/test/getProxiesOnce.test.mjs +89 -0
  48. package/toolg/cleanFolder.mjs +4 -0
  49. package/toolg/gDistRollup.mjs +29 -0
  50. package/toolg/modifyReadme.mjs +4 -0
  51. package/g.mjs +0 -12
  52. package/src/getHtml.mjs +0 -84
  53. package/src/getProxyListOrg.mjs +0 -66
  54. package/src/parseHtml.mjs +0 -19
  55. package/test/all.test.mjs +0 -10
@@ -0,0 +1,65 @@
1
+ import Hapi from '@hapi/hapi'
2
+ import get from 'lodash-es/get.js'
3
+ import ispint from 'wsemi/src/ispint.mjs'
4
+ import isearr from 'wsemi/src/isearr.mjs'
5
+ import isestr from 'wsemi/src/isestr.mjs'
6
+ import cint from 'wsemi/src/cint.mjs'
7
+
8
+
9
+ function provideServer(fun, opt = {}) {
10
+
11
+ //port
12
+ let port = get(opt, 'port')
13
+ if (!ispint(port)) {
14
+ port = 8080
15
+ }
16
+ port = cint(port)
17
+
18
+ //corsOrigins
19
+ let corsOrigins = get(opt, 'corsOrigins', [])
20
+ if (!isearr(corsOrigins)) {
21
+ corsOrigins = ['*']
22
+ }
23
+
24
+ //apiName
25
+ let apiName = get(opt, 'apiName')
26
+ if (!isestr(apiName)) {
27
+ apiName = 'getProxies'
28
+ }
29
+
30
+ //startServer
31
+ let startServer = async () => {
32
+ let server = Hapi.server({
33
+ // host: 'localhost',
34
+ port,
35
+ routes: {
36
+ cors: {
37
+ origin: corsOrigins, //Access-Control-Allow-Origin
38
+ credentials: false, //Access-Control-Allow-Credentials
39
+ },
40
+ },
41
+ })
42
+
43
+ //route
44
+ server.route({
45
+ method: 'GET',
46
+ path: `/${apiName}`,
47
+ handler: async () => {
48
+ let proxies = await fun()
49
+ return proxies
50
+ },
51
+ })
52
+
53
+ //start
54
+ await server.start()
55
+
56
+ console.log(`Server running at: ${server.info.uri}`)
57
+ }
58
+
59
+ //startServer
60
+ startServer()
61
+
62
+ }
63
+
64
+
65
+ export default provideServer
@@ -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
+ })
@@ -0,0 +1,4 @@
1
+ import cleanFolder from 'w-package-tools/src/cleanFolder.mjs'
2
+
3
+
4
+ cleanFolder('docs')
@@ -0,0 +1,29 @@
1
+ import rollupFiles from 'w-package-tools/src/rollupFiles.mjs'
2
+ import getFiles from 'w-package-tools/src/getFiles.mjs'
3
+ import w from 'wsemi'
4
+
5
+
6
+ let fdSrc = './src'
7
+ let fdTar = './dist'
8
+ let fns = getFiles(fdSrc)
9
+ fns = fns.filter((v) => {
10
+ return w.strleft(v, 1) === 'W'
11
+ })
12
+
13
+ rollupFiles({
14
+ fns,
15
+ fdSrc,
16
+ fdTar,
17
+ nameDistType: 'kebabCase',
18
+ globals: {
19
+ '@hapi/hapi': '@hapi/hapi',
20
+ '@hapi/inert': '@hapi/inert',
21
+ 'https': 'https',
22
+ },
23
+ external: [
24
+ '@hapi/hapi',
25
+ '@hapi/inert',
26
+ 'https',
27
+ ],
28
+ })
29
+
@@ -0,0 +1,4 @@
1
+ import modifyReadme from 'w-package-tools/src/modifyReadme.mjs'
2
+
3
+
4
+ modifyReadme()
package/g.mjs DELETED
@@ -1,12 +0,0 @@
1
- import wip from './src/WIpProxy.mjs'
2
-
3
- wip()
4
- .then((res) => {
5
- console.log(res)
6
- console.log(res.length)
7
- })
8
- .catch((err) => {
9
- console.error(err)
10
- })
11
-
12
- //node --experimental-modules --es-module-specifier-resolution=node g.mjs
package/src/getHtml.mjs DELETED
@@ -1,84 +0,0 @@
1
- import puppeteer from 'puppeteer'
2
-
3
-
4
- async function getHtml(url, selector, muti = false) {
5
-
6
- async function view(url) {
7
-
8
- //new page
9
- let olaunch = {
10
- //timeout: 0,
11
- headless: false,
12
- slowMo: 20,
13
- // arg: [
14
- // //`--window-size=${viewport.width},${viewport.height}`, //無效
15
- // //`--disable-resize-lock`, //無效
16
- // ],
17
- // defaultViewport: { //無效
18
- // x: 0,
19
- // y: 0,
20
- // width: 800,
21
- // height: 600,
22
- // }
23
- }
24
- let browser = await puppeteer.launch(olaunch)
25
- let page = await browser.newPage()
26
-
27
- //show page
28
- await page.goto(url)
29
- let viewport = {
30
- x: 0,
31
- y: 0,
32
- width: 800,
33
- height: 600,
34
- }
35
- await page.setViewport(viewport)
36
-
37
- //wait
38
- let waitsec = 5
39
- await page.waitFor(waitsec * 1000)
40
-
41
- return {
42
- browser,
43
- page,
44
- }
45
- }
46
-
47
- async function close(p) {
48
-
49
- //close
50
- await p.page.close()
51
- await p.browser.close()
52
-
53
- }
54
-
55
- async function g1(p, selector) {
56
- return await p.page.$eval(selector, (node) => {
57
- return node.innerHTML
58
- })
59
- }
60
-
61
- async function gs(p, selector) {
62
- return await p.page.$$eval(selector, (nodes) => {
63
- return nodes.map((node) => {
64
- return node.innerHTML
65
- })
66
- })
67
- }
68
-
69
- let p = await view(url)
70
-
71
- let r
72
- if (muti) {
73
- r = await gs(p, selector)
74
- }
75
- else {
76
- r = await g1(p, selector)
77
- }
78
-
79
- close(p)
80
-
81
- return r
82
- }
83
-
84
- export default getHtml
@@ -1,66 +0,0 @@
1
- // import fs from 'fs'
2
- import get from 'lodash/get'
3
- import sep from 'wsemi/src/sep.mjs'
4
- import pmMap from 'wsemi/src/pmMap.mjs'
5
- import parseHtml from './parseHtml.mjs'
6
- import getHtml from './getHtml.mjs'
7
-
8
-
9
- async function core(url) {
10
- let ps = []
11
-
12
- let selector = 'div.table'
13
- let h = await getHtml(url, selector)
14
- // fs.writeFileSync('temp.html', h)
15
- // let h = fs.readFileSync('tempProxyListOrg.html', 'utf8')
16
-
17
- //parseHtml
18
- let $ = parseHtml(h)
19
- $('ul').map((k, v) => {
20
- let q = $(v)
21
-
22
- let proxy = q.children('li.proxy').html()
23
- proxy = sep(proxy, 'script>')
24
- proxy = get(proxy, 1)
25
- // console.log(k,'proxy',proxy)
26
-
27
- let httpType = q.children('li.https').text()
28
- // console.log(k,'httpType',httpType)
29
-
30
- let country = q.find('span.country').text()
31
- // console.log(k,'country',country)
32
-
33
- let countryName = q.find('span.name').attr('name')
34
- // console.log(k,'countryName',countryName)
35
-
36
- ps.push({
37
- proxy,
38
- httpType,
39
- country,
40
- countryName,
41
- })
42
- })
43
-
44
- return ps
45
- }
46
-
47
- async function getProxyListOrg() {
48
- let psAll = []
49
-
50
- await pmMap([1, 2, 3], async(v) => {
51
- // console.log(v)
52
-
53
- //url
54
- //let url = 'https://proxy-list.org/chinese/index.php?setlang=chinese'
55
- let url = `https://proxy-list.org/chinese/index.php?setlang=chinese&p=${v}`
56
-
57
- //core
58
- let ps = await core(url)
59
- psAll = [...psAll, ...ps]
60
-
61
- })
62
-
63
- return psAll
64
- }
65
-
66
- export default getProxyListOrg
package/src/parseHtml.mjs DELETED
@@ -1,19 +0,0 @@
1
-
2
- import cheerio from 'cheerio'
3
-
4
-
5
- function parseHtml(h) {
6
-
7
- let $setting = {
8
- //ignoreWhitespace: true,
9
- xmlMode: true,
10
- decodeEntities: false,
11
- }
12
-
13
- //$
14
- let $ = cheerio.load(h, $setting)
15
-
16
- return $
17
- }
18
-
19
- export default parseHtml
package/test/all.test.mjs DELETED
@@ -1,10 +0,0 @@
1
- import assert from 'assert'
2
-
3
-
4
- describe('all', function() {
5
-
6
- it('test in localhost', function() {
7
- assert.strict.deepEqual(1, 1)
8
- })
9
-
10
- })