pac-proxy-cli 1.1.13 → 1.2.0
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/README.md +15 -0
- package/dist/assets/index-CFoGlW0m.css +1 -0
- package/dist/assets/index-DN6Yf8E7.js +28 -0
- package/dist/index.html +2 -2
- package/lib/config-cli.js +709 -0
- package/lib/index.js +70 -0
- package/lib/local-store.js +3 -3
- package/lib/pac-file.js +3 -2
- package/lib/pac-match.js +7 -5
- package/lib/proxy-server.js +12 -0
- package/lib/sslocal-manager.js +51 -14
- package/package.json +4 -2
- package/dist/assets/index-CmIDvBkA.js +0 -28
- package/dist/assets/index-QepWtMiX.css +0 -1
package/lib/index.js
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
5
|
import { startServer } from './server.js';
|
|
6
|
+
import {
|
|
7
|
+
runConfigPrint,
|
|
8
|
+
runConfigInteractive,
|
|
9
|
+
runConfigInit,
|
|
10
|
+
runPacImportFile,
|
|
11
|
+
runPacRulesInteractive,
|
|
12
|
+
runConfigProxyModeInteractive,
|
|
13
|
+
} from './config-cli.js';
|
|
6
14
|
|
|
7
15
|
const require = createRequire(import.meta.url);
|
|
8
16
|
|
|
@@ -22,4 +30,66 @@ program
|
|
|
22
30
|
await startServer(port);
|
|
23
31
|
});
|
|
24
32
|
|
|
33
|
+
const configCmd = program
|
|
34
|
+
.command('config')
|
|
35
|
+
.description('查看或交互编辑本地配置(与 Web 控制台共用数据目录)')
|
|
36
|
+
.option('--print', '打印当前配置 JSON(敏感字段脱敏),无需 TTY');
|
|
37
|
+
|
|
38
|
+
configCmd.action(async (opts) => {
|
|
39
|
+
if (opts.print) {
|
|
40
|
+
runConfigPrint();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!process.stdin.isTTY) {
|
|
44
|
+
console.error('非交互终端无法启动问答。请使用: pac-proxy config --print');
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await runConfigInteractive();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
configCmd
|
|
52
|
+
.command('init')
|
|
53
|
+
.description('首次配置向导(交互)')
|
|
54
|
+
.action(async () => {
|
|
55
|
+
if (!process.stdin.isTTY) {
|
|
56
|
+
console.error('非交互终端无法使用向导。请使用: pac-proxy config --print');
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await runConfigInit();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
configCmd
|
|
64
|
+
.command('rules')
|
|
65
|
+
.description('交互式增删改查 PAC 规则(代理 / 直连 / 拦截)')
|
|
66
|
+
.action(async () => {
|
|
67
|
+
if (!process.stdin.isTTY) {
|
|
68
|
+
console.error('非交互终端无法使用。请使用: pac-proxy config --print');
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
await runPacRulesInteractive();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
configCmd
|
|
76
|
+
.command('proxy-mode')
|
|
77
|
+
.description('交互式选择代理模式(直连 / 系统代理 / PAC / 抓包)')
|
|
78
|
+
.action(async () => {
|
|
79
|
+
if (!process.stdin.isTTY) {
|
|
80
|
+
console.error('非交互终端无法使用。请使用: pac-proxy config --print');
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
await runConfigProxyModeInteractive();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
configCmd
|
|
88
|
+
.command('import')
|
|
89
|
+
.description('从 JSON 文件合并导入 PAC 规则(与 Web 导入逻辑一致)')
|
|
90
|
+
.argument('<file>', '规则 JSON 文件路径')
|
|
91
|
+
.action(async (file) => {
|
|
92
|
+
await runPacImportFile(file);
|
|
93
|
+
});
|
|
94
|
+
|
|
25
95
|
program.parse();
|
package/lib/local-store.js
CHANGED
|
@@ -27,15 +27,15 @@ const DEFAULT_CONFIG = {
|
|
|
27
27
|
},
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
-
function getStoreDir() {
|
|
30
|
+
export function getStoreDir() {
|
|
31
31
|
return process.env.PAC_PROXY_HOME || path.join(os.homedir(), '.pac-proxy');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function getStorePath() {
|
|
34
|
+
export function getStorePath() {
|
|
35
35
|
return path.join(getStoreDir(), 'config.json');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
function getRemoteConfigPath() {
|
|
38
|
+
export function getRemoteConfigPath() {
|
|
39
39
|
return path.join(getStoreDir(), 'remote_config.json');
|
|
40
40
|
}
|
|
41
41
|
|
package/lib/pac-file.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 生成 PAC 文件内容,供浏览器/系统“自动代理配置”使用
|
|
3
|
-
* 规则与 pac-match 一致:按优先级匹配,命中则 proxy/direct
|
|
3
|
+
* 规则与 pac-match 一致:按优先级匹配,命中则 proxy/direct/block,未命中直连(block 与 proxy 均走本机代理以便本地拒绝)
|
|
4
4
|
*/
|
|
5
5
|
import { getPacRules, getProxyConfig } from './local-store.js';
|
|
6
6
|
|
|
@@ -41,7 +41,8 @@ export function generatePacJs(proxyHost, proxyPort) {
|
|
|
41
41
|
const condUrl = patternToPacCondition('url', r.pattern);
|
|
42
42
|
const condHost = patternToPacCondition('host', r.pattern);
|
|
43
43
|
const cond = `${condUrl} || ${condHost}`;
|
|
44
|
-
const
|
|
44
|
+
const viaProxy = r.action === 'proxy' || r.action === 'block';
|
|
45
|
+
const ret = viaProxy ? `return "${proxyStr}";` : 'return "DIRECT";';
|
|
45
46
|
lines.push(` if (${cond}) { ${ret} }`);
|
|
46
47
|
}
|
|
47
48
|
const body = lines.join('\n');
|
package/lib/pac-match.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PAC 规则匹配:按优先级排序,命中则返回对应动作(proxy/direct)
|
|
2
|
+
* PAC 规则匹配:按优先级排序,命中则返回对应动作(proxy/direct/block)
|
|
3
3
|
* pattern 支持 * 通配符,匹配请求的 URL 或 host
|
|
4
4
|
*/
|
|
5
5
|
export function patternToRegex(pattern) {
|
|
@@ -24,11 +24,11 @@ export function matchPattern(url, pattern) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* 根据 PAC 规则决定该 URL
|
|
28
|
-
* PAC 模式下:匹配到规则则按规则执行(proxy/direct),未匹配到的一律直连。
|
|
27
|
+
* 根据 PAC 规则决定该 URL 走代理、直连或拦截。
|
|
28
|
+
* PAC 模式下:匹配到规则则按规则执行(proxy/direct/block),未匹配到的一律直连。
|
|
29
29
|
* @param {string} url - 请求的完整 URL(如 http://example.com/path)或 CONNECT 时为 host:port
|
|
30
30
|
* @param {Array<{pattern:string, action:string, priority:number}>} rules
|
|
31
|
-
* @returns {'proxy'|'direct'}
|
|
31
|
+
* @returns {'proxy'|'direct'|'block'}
|
|
32
32
|
*/
|
|
33
33
|
export function getPacAction(url, rules) {
|
|
34
34
|
if (!url || !Array.isArray(rules) || rules.length === 0) {
|
|
@@ -37,7 +37,9 @@ export function getPacAction(url, rules) {
|
|
|
37
37
|
const sorted = [...rules].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
|
|
38
38
|
for (const r of sorted) {
|
|
39
39
|
if (matchPattern(url, r.pattern)) {
|
|
40
|
-
|
|
40
|
+
if (r.action === 'proxy') return 'proxy';
|
|
41
|
+
if (r.action === 'block') return 'block';
|
|
42
|
+
return 'direct';
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
return 'direct';
|
package/lib/proxy-server.js
CHANGED
|
@@ -103,6 +103,12 @@ export function createProxyServer() {
|
|
|
103
103
|
}
|
|
104
104
|
pushRecord({ type: targetUrl.startsWith('https') ? 'https' : 'http', method: req.method, url: targetUrl, action });
|
|
105
105
|
|
|
106
|
+
if (action === 'block') {
|
|
107
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
108
|
+
res.end('Blocked by PAC rule');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
106
112
|
const protocol = targetUrl.startsWith('https') ? https : http;
|
|
107
113
|
const opts = { method: req.method, headers: req.headers };
|
|
108
114
|
|
|
@@ -156,6 +162,12 @@ export function createProxyServer() {
|
|
|
156
162
|
}
|
|
157
163
|
pushRecord({ type: 'https', method: 'CONNECT', url: req.url, action });
|
|
158
164
|
|
|
165
|
+
if (action === 'block') {
|
|
166
|
+
clientSocket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
167
|
+
clientSocket.end();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
159
171
|
const onTargetSocket = (targetSocket) => {
|
|
160
172
|
targetSocketRef = targetSocket;
|
|
161
173
|
targetSocket.on('error', () => clientSocket.destroy());
|
package/lib/sslocal-manager.js
CHANGED
|
@@ -78,27 +78,64 @@ function waitForPort(port, timeoutMs = 4000) {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
const SSLOCAL_CIPHER_FALLBACK = [
|
|
82
|
+
'aes-256-gcm',
|
|
83
|
+
'aes-128-gcm',
|
|
84
|
+
'chacha20-ietf-poly1305',
|
|
85
|
+
'2022-blake3-aes-256-gcm',
|
|
86
|
+
'2022-blake3-aes-128-gcm',
|
|
87
|
+
'2022-blake3-chacha20-poly1305',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** 协议/传输类枚举,勿当作 encrypt-method 的 possible values */
|
|
91
|
+
const PROTOCOL_LIKE = /^(socks4?|socks5|http|https|tcp|udp|tls|ws|wss|quic|local|remote)$/i;
|
|
92
|
+
|
|
93
|
+
function splitPossibleValuesBlock(inner) {
|
|
94
|
+
return inner.split(',').map((s) => s.trim()).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 从 --help 文本中挑出「加密方法」枚举,避免误用首个 [possible values:](常为协议名) */
|
|
98
|
+
function parseCipherListFromHelp(output) {
|
|
99
|
+
const lines = output.split(/\r?\n/);
|
|
100
|
+
for (const line of lines) {
|
|
101
|
+
if (!/(encrypt|cipher|method).*method|--encrypt|-m[,<\s]/i.test(line)) continue;
|
|
102
|
+
const m = line.match(/\[possible values:\s*([^\]]+)\]/i);
|
|
103
|
+
if (m) {
|
|
104
|
+
const items = splitPossibleValuesBlock(m[1]);
|
|
105
|
+
if (items.length && !items.some((s) => PROTOCOL_LIKE.test(s))) {
|
|
106
|
+
return items;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const re = /\[possible values:\s*([^\]]+)\]/gi;
|
|
112
|
+
let block;
|
|
113
|
+
while ((block = re.exec(output)) !== null) {
|
|
114
|
+
const items = splitPossibleValuesBlock(block[1]);
|
|
115
|
+
if (items.length < 2) continue;
|
|
116
|
+
if (items.some((s) => PROTOCOL_LIKE.test(s))) continue;
|
|
117
|
+
const cipherLike = items.filter(
|
|
118
|
+
(s) =>
|
|
119
|
+
/(gcm|poly1305|blake3|chacha|aes-\d+|rc4|bf-cfb|table|none|plain|sm4|2022-)/i.test(s)
|
|
120
|
+
);
|
|
121
|
+
if (cipherLike.length >= Math.min(items.length, 3) || (items.length >= 4 && cipherLike.length >= 2)) {
|
|
122
|
+
return items;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
81
129
|
export function getSslocalCiphers() {
|
|
82
130
|
try {
|
|
83
131
|
const binPath = getBinaryPath();
|
|
84
132
|
ensureExecutable(binPath);
|
|
85
133
|
const result = spawnSync(binPath, ['--help'], { encoding: 'utf-8', timeout: 3000 });
|
|
86
134
|
const output = (result.stdout || '') + (result.stderr || '');
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (match) {
|
|
90
|
-
return match[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
91
|
-
}
|
|
135
|
+
const parsed = parseCipherListFromHelp(output);
|
|
136
|
+
if (parsed?.length) return parsed;
|
|
92
137
|
} catch (_) {}
|
|
93
|
-
|
|
94
|
-
return [
|
|
95
|
-
'aes-256-gcm',
|
|
96
|
-
'aes-128-gcm',
|
|
97
|
-
'chacha20-ietf-poly1305',
|
|
98
|
-
'2022-blake3-aes-256-gcm',
|
|
99
|
-
'2022-blake3-aes-128-gcm',
|
|
100
|
-
'2022-blake3-chacha20-poly1305',
|
|
101
|
-
];
|
|
138
|
+
return [...SSLOCAL_CIPHER_FALLBACK];
|
|
102
139
|
}
|
|
103
140
|
|
|
104
141
|
export async function startSslocal(cfg) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pac-proxy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "node pac proxy and web control panel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"dev": "node lib/index.js serve --port 5174",
|
|
19
19
|
"build": "vite build",
|
|
20
|
-
"prepublishOnly": "npm run build"
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "node --test test/config-cli.test.js"
|
|
21
22
|
},
|
|
22
23
|
"optionalDependencies": {
|
|
23
24
|
"pac-proxy-sslocal-win32-x64": "1.24.1",
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
"pac-proxy-sslocal-linux-arm64": "1.24.0"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
31
|
+
"@inquirer/prompts": "^8.3.2",
|
|
30
32
|
"commander": "^12.0.0",
|
|
31
33
|
"cors": "^2.8.5",
|
|
32
34
|
"dotenv": "^16.4.5",
|