@wwkit/cft 1.0.3 → 1.0.5
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/package.json +3 -3
- package/src/config.js +4 -0
- package/src/config.json5 +2 -1
- package/src/index.js +1 -1
- package/src/installer.js +50 -8
- package/src/platform.js +6 -1
- package/src/proxy.js +72 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwkit/cft",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"author": "bluesliu <langcai163@163.com>",
|
|
5
5
|
"description": "Chrome for Testing (CFT) installer — download, extract, chmod, dep-check",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"extract-zip": "^2.0.1",
|
|
39
|
-
"@wwkit/shared": "1.0.
|
|
39
|
+
"@wwkit/shared": "1.0.5"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"jest": "^29.7.0"
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"start": "node bin/index.js",
|
|
47
47
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache tests/unit",
|
|
48
48
|
"test:all": "node --experimental-vm-modules node_modules/jest/bin/jest.js --no-cache tests/unit",
|
|
49
|
-
"release": "
|
|
49
|
+
"release": "node ../../packages/shared/bin/release-pkg.js"
|
|
50
50
|
}
|
|
51
51
|
}
|
package/src/config.js
CHANGED
package/src/config.json5
CHANGED
package/src/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { install, status, clear } from './installer.js'
|
|
2
2
|
export { getOs, isWindows, isMacos, isLinux, VALID_OS_TYPES } from './platform.js'
|
|
3
|
-
export { getConfig, getVersion, getRegistry, getLocation, copyBuiltinConfig, INSTALL_LOCATION, USER_CONFIG_FILE } from './config.js'
|
|
3
|
+
export { getConfig, getVersion, getRegistry, getProxy, getLocation, copyBuiltinConfig, INSTALL_LOCATION, USER_CONFIG_FILE } from './config.js'
|
package/src/installer.js
CHANGED
|
@@ -8,23 +8,67 @@ import extractZip from 'extract-zip'
|
|
|
8
8
|
import { getOs, VALID_OS_TYPES } from './platform.js'
|
|
9
9
|
import { find as findExe } from './exefinder.js'
|
|
10
10
|
import { check as checkDeps, mapToPackages, install as installDeps, isSupported as depsSupported, detectPackageManager } from './depchecker.js'
|
|
11
|
-
import { getVersion, getRegistry, getLocation, copyBuiltinConfig } from './config.js'
|
|
11
|
+
import { getVersion, getRegistry, getProxy, getLocation, copyBuiltinConfig } from './config.js'
|
|
12
|
+
import { parseProxy, openTunnel } from './proxy.js'
|
|
13
|
+
|
|
14
|
+
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 cft-installer'
|
|
12
15
|
|
|
13
16
|
function log(msg) {
|
|
14
17
|
console.log(` ${msg}`)
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
function sendRequest(url, cb) {
|
|
21
|
+
const proxy = parseProxy(getProxy())
|
|
22
|
+
const target = new URL(url)
|
|
23
|
+
const headers = { 'user-agent': USER_AGENT }
|
|
24
|
+
const onError = (e) => cb(e, null)
|
|
25
|
+
|
|
26
|
+
if (!proxy) {
|
|
27
|
+
const proto = target.protocol === 'https:' ? https : http
|
|
28
|
+
const req = proto.get(url, { headers }, (res) => cb(null, res))
|
|
29
|
+
req.on('error', onError)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (proxy.protocol === 'http:' && target.protocol === 'http:') {
|
|
34
|
+
const req = http.get({
|
|
35
|
+
host: proxy.hostname,
|
|
36
|
+
port: Number(proxy.port) || 80,
|
|
37
|
+
path: url,
|
|
38
|
+
headers: { ...headers, host: target.host },
|
|
39
|
+
}, (res) => cb(null, res))
|
|
40
|
+
req.on('error', onError)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
openTunnel(proxy, target).then((socket) => {
|
|
45
|
+
const proto = target.protocol === 'https:' ? https : http
|
|
46
|
+
const req = proto.request({
|
|
47
|
+
method: 'GET',
|
|
48
|
+
hostname: target.hostname,
|
|
49
|
+
port: Number(target.port) || (target.protocol === 'https:' ? 443 : 80),
|
|
50
|
+
path: target.pathname + target.search,
|
|
51
|
+
headers: { ...headers, host: target.host },
|
|
52
|
+
agent: false,
|
|
53
|
+
createConnection: () => socket,
|
|
54
|
+
}, (res) => cb(null, res))
|
|
55
|
+
req.on('error', onError)
|
|
56
|
+
socket.on('error', onError)
|
|
57
|
+
req.end()
|
|
58
|
+
}).catch(onError)
|
|
59
|
+
}
|
|
60
|
+
|
|
17
61
|
function downloadFile(url, destPath, redirects = 0) {
|
|
18
62
|
return new Promise((resolve, reject) => {
|
|
19
63
|
if (redirects > 5) {
|
|
20
64
|
reject(new Error(`Too many redirects: ${url}`))
|
|
21
65
|
return
|
|
22
66
|
}
|
|
23
|
-
const protocol = url.startsWith('https:') ? https : http
|
|
24
67
|
const filename = path.basename(destPath)
|
|
25
68
|
log(`下载 ${filename} ...`)
|
|
26
69
|
|
|
27
|
-
|
|
70
|
+
sendRequest(url, (err, response) => {
|
|
71
|
+
if (err) { reject(err); return }
|
|
28
72
|
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
|
|
29
73
|
const location = response.headers.location
|
|
30
74
|
response.resume()
|
|
@@ -58,7 +102,9 @@ function downloadFile(url, destPath, redirects = 0) {
|
|
|
58
102
|
file.close()
|
|
59
103
|
resolve()
|
|
60
104
|
})
|
|
61
|
-
|
|
105
|
+
file.on('error', reject)
|
|
106
|
+
response.on('error', reject)
|
|
107
|
+
})
|
|
62
108
|
})
|
|
63
109
|
}
|
|
64
110
|
|
|
@@ -172,10 +218,6 @@ export async function install() {
|
|
|
172
218
|
throw new Error(`Unsupported OS: ${osType}`)
|
|
173
219
|
}
|
|
174
220
|
|
|
175
|
-
if (process.platform === 'linux' && process.arch !== 'x64') {
|
|
176
|
-
throw new Error(`Chrome for Testing 仅提供 linux64 (x86_64) 构建,不支持 linux-${process.arch}`)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
221
|
const { installed, info } = checkInstalled(version, location, osType)
|
|
180
222
|
if (installed) {
|
|
181
223
|
log(`CFT ${version} 已安装,跳过`)
|
package/src/platform.js
CHANGED
|
@@ -8,7 +8,12 @@ export function getOs() {
|
|
|
8
8
|
const arch = process.arch
|
|
9
9
|
if (platform === 'win32') return 'win64'
|
|
10
10
|
if (platform === 'darwin') return arch === 'arm64' ? 'mac-arm64' : 'mac-x64'
|
|
11
|
-
if (platform === 'linux')
|
|
11
|
+
if (platform === 'linux') {
|
|
12
|
+
if (arch !== 'x64') {
|
|
13
|
+
throw new Error(`Chrome for Testing 不提供 linux-${arch} 构建(上游仅支持 linux64/x86_64)。请改用系统 Chromium:apt/dnf install chromium chromium-driver`)
|
|
14
|
+
}
|
|
15
|
+
return 'linux64'
|
|
16
|
+
}
|
|
12
17
|
throw new Error(`Unsupported platform: ${platform}`)
|
|
13
18
|
}
|
|
14
19
|
|
package/src/proxy.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import net from 'node:net'
|
|
2
|
+
import http from 'node:http'
|
|
3
|
+
|
|
4
|
+
export function parseProxy(raw) {
|
|
5
|
+
if (!raw) return null
|
|
6
|
+
try {
|
|
7
|
+
const u = new URL(raw)
|
|
8
|
+
if (!u.hostname) return null
|
|
9
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:' && u.protocol !== 'socks5:' && u.protocol !== 'socks:') {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
return u
|
|
13
|
+
} catch {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function socks5Connect(proxy, host, port) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const sock = net.connect(Number(proxy.port) || 1080, proxy.hostname)
|
|
21
|
+
let phase = 'greeting'
|
|
22
|
+
const cleanup = (err) => { sock.removeAllListeners(); sock.destroy(); reject(err) }
|
|
23
|
+
sock.once('error', cleanup)
|
|
24
|
+
sock.once('connect', () => {
|
|
25
|
+
sock.write(Buffer.from([0x05, 0x01, 0x00]))
|
|
26
|
+
})
|
|
27
|
+
sock.on('data', (data) => {
|
|
28
|
+
if (phase === 'greeting') {
|
|
29
|
+
if (data[0] !== 0x05 || data[1] !== 0x00) { cleanup(new Error(`socks5 代理拒绝无认证 (method=${data[1]})`)); return }
|
|
30
|
+
phase = 'connect'
|
|
31
|
+
const hostBuf = Buffer.from(host, 'utf8')
|
|
32
|
+
const req = Buffer.alloc(7 + hostBuf.length)
|
|
33
|
+
req[0] = 0x05; req[1] = 0x01; req[2] = 0x00; req[3] = 0x03
|
|
34
|
+
req[4] = hostBuf.length
|
|
35
|
+
hostBuf.copy(req, 5)
|
|
36
|
+
req.writeUInt16BE(port, 5 + hostBuf.length)
|
|
37
|
+
sock.write(req)
|
|
38
|
+
} else if (phase === 'connect') {
|
|
39
|
+
if (data[0] !== 0x05 || data[1] !== 0x00) { cleanup(new Error(`socks5 连接失败 (code=${data[1]})`)); return }
|
|
40
|
+
sock.removeAllListeners('data')
|
|
41
|
+
sock.removeListener('error', cleanup)
|
|
42
|
+
resolve(sock)
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function httpConnect(proxy, host, port) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const req = http.request({
|
|
51
|
+
method: 'CONNECT',
|
|
52
|
+
host: proxy.hostname,
|
|
53
|
+
port: Number(proxy.port) || 80,
|
|
54
|
+
path: `${host}:${port}`,
|
|
55
|
+
})
|
|
56
|
+
req.once('error', reject)
|
|
57
|
+
req.once('connect', (res, socket) => {
|
|
58
|
+
if (res.statusCode !== 200) { socket.destroy(); reject(new Error(`HTTP 代理 CONNECT 返回 ${res.statusCode}`)); return }
|
|
59
|
+
resolve(socket)
|
|
60
|
+
})
|
|
61
|
+
req.end()
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function openTunnel(proxy, target) {
|
|
66
|
+
const host = target.hostname
|
|
67
|
+
const port = Number(target.port) || (target.protocol === 'https:' ? 443 : 80)
|
|
68
|
+
if (proxy.protocol === 'socks5:' || proxy.protocol === 'socks:') {
|
|
69
|
+
return socks5Connect(proxy, host, port)
|
|
70
|
+
}
|
|
71
|
+
return httpConnect(proxy, host, port)
|
|
72
|
+
}
|