@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wwkit/cft",
3
- "version": "1.0.3",
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.3"
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": "pnpm version patch && pnpm publish"
49
+ "release": "node ../../packages/shared/bin/release-pkg.js"
50
50
  }
51
51
  }
package/src/config.js CHANGED
@@ -30,6 +30,10 @@ export function getRegistry() {
30
30
  return getConfig().registry
31
31
  }
32
32
 
33
+ export function getProxy() {
34
+ return getConfig().proxy || ''
35
+ }
36
+
33
37
  export function getLocation() {
34
38
  return INSTALL_LOCATION
35
39
  }
package/src/config.json5 CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
2
  version: "141.0.7390.54",
3
- registry: "https://npmmirror.com/mirrors/chrome-for-testing",
3
+ registry: "http://deepbluenet.com/mirrors/chrome-for-testing",
4
+ proxy: "",
4
5
  }
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
- protocol.get(url, (response) => {
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
- }).on('error', reject)
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') return 'linux64'
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
+ }