pac-proxy-cli 1.0.3 → 1.1.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/dist/assets/index-CweQSJse.js +28 -0
- package/dist/assets/index-H0IGlOgE.css +1 -0
- package/dist/index.html +2 -5
- package/lib/index.js +4 -2
- package/lib/local-store.js +39 -2
- package/lib/server.js +62 -1
- package/lib/sslocal-manager.js +146 -0
- package/package.json +7 -2
- package/dist/assets/index-DD-_gVYC.js +0 -27
- package/dist/assets/index-IXkmSGx6.css +0 -1
package/lib/local-store.js
CHANGED
|
@@ -10,12 +10,21 @@ const DEFAULT_CONFIG = {
|
|
|
10
10
|
proxy: {
|
|
11
11
|
enabled: false,
|
|
12
12
|
mode: 'pac',
|
|
13
|
-
upstream: '',
|
|
13
|
+
upstream: 'socks5://127.0.0.1:1080',
|
|
14
14
|
httpPort: 5175,
|
|
15
15
|
httpsPort: 5176,
|
|
16
16
|
applySystemProxy: true,
|
|
17
17
|
},
|
|
18
18
|
pacRules: [],
|
|
19
|
+
sslocal: {
|
|
20
|
+
enabled: false,
|
|
21
|
+
server: '',
|
|
22
|
+
serverPort: 8388,
|
|
23
|
+
localPort: 1080,
|
|
24
|
+
password: '',
|
|
25
|
+
method: 'aes-256-gcm',
|
|
26
|
+
timeout: 300,
|
|
27
|
+
},
|
|
19
28
|
};
|
|
20
29
|
|
|
21
30
|
function getStoreDir() {
|
|
@@ -51,6 +60,7 @@ export function loadConfig() {
|
|
|
51
60
|
mode,
|
|
52
61
|
proxy: { ...DEFAULT_CONFIG.proxy, ...(data.proxy || {}) },
|
|
53
62
|
pacRules: Array.isArray(data.pacRules) ? data.pacRules : DEFAULT_CONFIG.pacRules,
|
|
63
|
+
sslocal: { ...DEFAULT_CONFIG.sslocal, ...(data.sslocal || {}) },
|
|
54
64
|
};
|
|
55
65
|
} catch {
|
|
56
66
|
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
@@ -92,7 +102,14 @@ export function getPacRules() {
|
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
export function setPacRules(rules) {
|
|
95
|
-
const
|
|
105
|
+
const raw = Array.isArray(rules) ? rules : (rules?.rules ?? []);
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
const list = raw.filter((r) => {
|
|
108
|
+
const key = (r.pattern || '').trim();
|
|
109
|
+
if (!key || seen.has(key)) return false;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
96
113
|
const config = loadConfig();
|
|
97
114
|
if (config.mode === 'remote') {
|
|
98
115
|
saveRemoteConfig({ pacRules: list });
|
|
@@ -108,6 +125,9 @@ export function getProxyConfig() {
|
|
|
108
125
|
const proxy = { ...DEFAULT_CONFIG.proxy, ...config.proxy };
|
|
109
126
|
proxy.enabled = proxy.enabled === true || proxy.enabled === 'true';
|
|
110
127
|
proxy.applySystemProxy = proxy.applySystemProxy !== false && proxy.applySystemProxy !== 'false';
|
|
128
|
+
if (!proxy.upstream || !proxy.upstream.trim()) {
|
|
129
|
+
proxy.upstream = DEFAULT_CONFIG.proxy.upstream;
|
|
130
|
+
}
|
|
111
131
|
return proxy;
|
|
112
132
|
}
|
|
113
133
|
|
|
@@ -120,3 +140,20 @@ export function setProxyConfig(proxy) {
|
|
|
120
140
|
saveConfig(config);
|
|
121
141
|
return config.proxy;
|
|
122
142
|
}
|
|
143
|
+
|
|
144
|
+
export function getSslocalConfig() {
|
|
145
|
+
const config = loadConfig();
|
|
146
|
+
return { ...DEFAULT_CONFIG.sslocal, ...(config.sslocal || {}) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function setSslocalConfig(sslocal) {
|
|
150
|
+
const config = loadConfig();
|
|
151
|
+
config.sslocal = { ...DEFAULT_CONFIG.sslocal, ...sslocal };
|
|
152
|
+
config.sslocal.enabled = config.sslocal.enabled === true || config.sslocal.enabled === 'true';
|
|
153
|
+
saveConfig(config);
|
|
154
|
+
return config.sslocal;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function getSslocalConfigPath() {
|
|
158
|
+
return path.join(getStoreDir(), 'sslocal-config.json');
|
|
159
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import dotenv from 'dotenv';
|
|
|
5
5
|
import express from 'express';
|
|
6
6
|
import cors from 'cors';
|
|
7
7
|
import { createServer } from 'http';
|
|
8
|
-
import { loadConfig, saveConfig, getPacRules, setPacRules, getProxyConfig, setProxyConfig } from './local-store.js';
|
|
8
|
+
import { loadConfig, saveConfig, getPacRules, setPacRules, getProxyConfig, setProxyConfig, getSslocalConfig, setSslocalConfig } from './local-store.js';
|
|
9
9
|
import { getRecords as getTrafficRecords } from './traffic-log.js';
|
|
10
10
|
import { getCaptures } from './capture-log.js';
|
|
11
11
|
import { getPort } from 'get-port-please';
|
|
@@ -13,6 +13,7 @@ import { startProxyServer, stopProxyServer, getProxyStatus } from './proxy-serve
|
|
|
13
13
|
import { startMitmProxyServer, stopMitmProxyServer, getMitmProxyStatus, getCaCertPath, hasCaCert } from './mitm-proxy-server.js';
|
|
14
14
|
import { setSystemProxy, clearSystemProxy } from './system-proxy.js';
|
|
15
15
|
import { generatePacJs } from './pac-file.js';
|
|
16
|
+
import { startSslocal, stopSslocal, getSslocalStatus } from './sslocal-manager.js';
|
|
16
17
|
|
|
17
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
19
|
const envDir = path.resolve(__dirname, '..');
|
|
@@ -25,6 +26,19 @@ let controlPanelPort = null;
|
|
|
25
26
|
|
|
26
27
|
async function applyProxyService(proxyOverride) {
|
|
27
28
|
const proxy = proxyOverride ?? getProxyConfig();
|
|
29
|
+
const sslocalCfg = getSslocalConfig();
|
|
30
|
+
|
|
31
|
+
// 若 sslocal 启用,先确保进程运行,并覆盖 upstream
|
|
32
|
+
if (sslocalCfg.enabled && sslocalCfg.server) {
|
|
33
|
+
try {
|
|
34
|
+
const status = getSslocalStatus();
|
|
35
|
+
if (!status.running) await startSslocal(sslocalCfg);
|
|
36
|
+
proxy.upstream = `socks5://127.0.0.1:${sslocalCfg.localPort || 1080}`;
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error('sslocal 启动失败:', e.message);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
const isMitm = (proxy.mode || '') === 'mitm';
|
|
29
43
|
const prevStatus = isMitm ? getMitmProxyStatus() : getProxyStatus();
|
|
30
44
|
const wasProxyRunning = prevStatus.running === true;
|
|
@@ -226,9 +240,56 @@ export async function startServer(port) {
|
|
|
226
240
|
res.sendFile(path.join(distPath, 'index.html'));
|
|
227
241
|
});
|
|
228
242
|
|
|
243
|
+
// ---------- sslocal API ----------
|
|
244
|
+
app.get('/api/local/sslocal', (req, res) => {
|
|
245
|
+
try { res.json(getSslocalConfig()); }
|
|
246
|
+
catch (e) { res.status(500).json({ error: String(e.message) }); }
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
app.put('/api/local/sslocal', async (req, res) => {
|
|
250
|
+
try {
|
|
251
|
+
const cfg = setSslocalConfig(req.body);
|
|
252
|
+
// 若已启用且进程未运行则启动;若禁用则停止
|
|
253
|
+
if (cfg.enabled && cfg.server) {
|
|
254
|
+
const status = getSslocalStatus();
|
|
255
|
+
if (!status.running) await startSslocal(cfg);
|
|
256
|
+
} else if (!cfg.enabled) {
|
|
257
|
+
await stopSslocal();
|
|
258
|
+
}
|
|
259
|
+
res.json(cfg);
|
|
260
|
+
} catch (e) { res.status(500).json({ error: String(e.message) }); }
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
app.get('/api/local/sslocal-status', (req, res) => {
|
|
264
|
+
try { res.json(getSslocalStatus()); }
|
|
265
|
+
catch (e) { res.status(500).json({ running: false, pid: null, error: String(e.message), logs: [] }); }
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
app.post('/api/local/sslocal/start', async (req, res) => {
|
|
269
|
+
try {
|
|
270
|
+
const cfg = getSslocalConfig();
|
|
271
|
+
if (!cfg.server) return res.status(400).json({ error: '请先配置服务器地址' });
|
|
272
|
+
await startSslocal(cfg);
|
|
273
|
+
res.json(getSslocalStatus());
|
|
274
|
+
} catch (e) { res.status(500).json({ error: String(e.message) }); }
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
app.post('/api/local/sslocal/stop', async (req, res) => {
|
|
278
|
+
try {
|
|
279
|
+
await stopSslocal();
|
|
280
|
+
res.json({ ok: true });
|
|
281
|
+
} catch (e) { res.status(500).json({ error: String(e.message) }); }
|
|
282
|
+
});
|
|
283
|
+
|
|
229
284
|
const actualPort = await getPort({ port, portRange: [port, port + 100] });
|
|
230
285
|
const server = createServer(app);
|
|
231
286
|
|
|
287
|
+
// 进程退出时清理 sslocal 子进程
|
|
288
|
+
const cleanup = () => { stopSslocal().catch(() => {}); };
|
|
289
|
+
process.once('exit', cleanup);
|
|
290
|
+
process.once('SIGINT', () => { cleanup(); process.exit(0); });
|
|
291
|
+
process.once('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
292
|
+
|
|
232
293
|
server.listen(actualPort, '127.0.0.1', async () => {
|
|
233
294
|
controlPanelPort = actualPort;
|
|
234
295
|
console.log(`控制台已启动: http://127.0.0.1:${actualPort}`);
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import net from 'net';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { getSslocalConfigPath } from './local-store.js';
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
|
|
10
|
+
const LOG_MAX = 200;
|
|
11
|
+
|
|
12
|
+
const PKG_MAP = {
|
|
13
|
+
'win32-x64': 'pac-proxy-sslocal-win32-x64',
|
|
14
|
+
'darwin-x64': 'pac-proxy-sslocal-darwin-x64',
|
|
15
|
+
'linux-x64': 'pac-proxy-sslocal-linux-x64',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let _proc = null;
|
|
19
|
+
let _logs = [];
|
|
20
|
+
let _lastError = null;
|
|
21
|
+
|
|
22
|
+
function getBinaryPath() {
|
|
23
|
+
const key = `${process.platform}-${process.arch}`;
|
|
24
|
+
const pkgName = PKG_MAP[key];
|
|
25
|
+
if (pkgName) {
|
|
26
|
+
try {
|
|
27
|
+
const pkgDir = path.dirname(require.resolve(`${pkgName}/package.json`));
|
|
28
|
+
const bin = process.platform === 'win32' ? 'sslocal.exe' : 'sslocal';
|
|
29
|
+
const p = path.join(pkgDir, bin);
|
|
30
|
+
if (fs.existsSync(p)) return p;
|
|
31
|
+
} catch (_) {}
|
|
32
|
+
}
|
|
33
|
+
throw new Error(
|
|
34
|
+
`当前平台 ${process.platform}/${process.arch} 没有可用的 sslocal 二进制。\n` +
|
|
35
|
+
`请手动安装:npm install ${PKG_MAP[key] ?? '对应平台包'}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ensureExecutable(binPath) {
|
|
40
|
+
if (process.platform !== 'win32') {
|
|
41
|
+
try { fs.chmodSync(binPath, 0o755); } catch (_) {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function pushLog(line) {
|
|
46
|
+
_logs.push(line);
|
|
47
|
+
if (_logs.length > LOG_MAX) _logs.shift();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function waitForPort(port, timeoutMs = 4000) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const deadline = Date.now() + timeoutMs;
|
|
53
|
+
function attempt() {
|
|
54
|
+
const sock = new net.Socket();
|
|
55
|
+
sock.setTimeout(300);
|
|
56
|
+
sock.connect(port, '127.0.0.1', () => {
|
|
57
|
+
sock.destroy();
|
|
58
|
+
resolve(true);
|
|
59
|
+
});
|
|
60
|
+
sock.on('error', () => {
|
|
61
|
+
sock.destroy();
|
|
62
|
+
if (Date.now() < deadline) setTimeout(attempt, 200);
|
|
63
|
+
else resolve(false);
|
|
64
|
+
});
|
|
65
|
+
sock.on('timeout', () => {
|
|
66
|
+
sock.destroy();
|
|
67
|
+
if (Date.now() < deadline) setTimeout(attempt, 200);
|
|
68
|
+
else resolve(false);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
attempt();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function startSslocal(cfg) {
|
|
76
|
+
if (_proc) await stopSslocal();
|
|
77
|
+
|
|
78
|
+
const binPath = getBinaryPath();
|
|
79
|
+
if (!fs.existsSync(binPath)) {
|
|
80
|
+
throw new Error(`sslocal 二进制不存在: ${binPath}(当前平台: ${process.platform})`);
|
|
81
|
+
}
|
|
82
|
+
ensureExecutable(binPath);
|
|
83
|
+
|
|
84
|
+
// 写入临时 config 文件
|
|
85
|
+
const configPath = getSslocalConfigPath();
|
|
86
|
+
const sslocalCfg = {
|
|
87
|
+
server: cfg.server,
|
|
88
|
+
server_port: Number(cfg.serverPort) || 8388,
|
|
89
|
+
local_address: '127.0.0.1',
|
|
90
|
+
local_port: Number(cfg.localPort) || 1080,
|
|
91
|
+
password: cfg.password,
|
|
92
|
+
method: cfg.method || 'aes-256-gcm',
|
|
93
|
+
timeout: Number(cfg.timeout) || 300,
|
|
94
|
+
};
|
|
95
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
96
|
+
fs.writeFileSync(configPath, JSON.stringify(sslocalCfg, null, 2), 'utf-8');
|
|
97
|
+
|
|
98
|
+
_logs = [];
|
|
99
|
+
_lastError = null;
|
|
100
|
+
|
|
101
|
+
_proc = spawn(binPath, ['-c', configPath], {
|
|
102
|
+
detached: false,
|
|
103
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
_proc.stdout.on('data', (d) => {
|
|
107
|
+
String(d).split('\n').filter(Boolean).forEach(pushLog);
|
|
108
|
+
});
|
|
109
|
+
_proc.stderr.on('data', (d) => {
|
|
110
|
+
String(d).split('\n').filter(Boolean).forEach(pushLog);
|
|
111
|
+
});
|
|
112
|
+
_proc.on('exit', (code, signal) => {
|
|
113
|
+
_lastError = code !== 0 ? `进程退出 code=${code} signal=${signal}` : null;
|
|
114
|
+
_proc = null;
|
|
115
|
+
});
|
|
116
|
+
_proc.on('error', (e) => {
|
|
117
|
+
_lastError = e.message;
|
|
118
|
+
_proc = null;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const localPort = Number(cfg.localPort) || 1080;
|
|
122
|
+
const ready = await waitForPort(localPort);
|
|
123
|
+
if (!ready) {
|
|
124
|
+
const recentLogs = _logs.slice(-10).join('\n');
|
|
125
|
+
throw new Error(`sslocal 启动超时,端口 ${localPort} 未就绪。\n${recentLogs}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function stopSslocal() {
|
|
130
|
+
if (!_proc) return;
|
|
131
|
+
try {
|
|
132
|
+
_proc.kill('SIGTERM');
|
|
133
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
134
|
+
if (_proc) _proc.kill('SIGKILL');
|
|
135
|
+
} catch (_) {}
|
|
136
|
+
_proc = null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getSslocalStatus() {
|
|
140
|
+
return {
|
|
141
|
+
running: _proc !== null && !_proc.killed,
|
|
142
|
+
pid: _proc?.pid ?? null,
|
|
143
|
+
error: _lastError,
|
|
144
|
+
logs: [..._logs],
|
|
145
|
+
};
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pac-proxy-cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "node pac proxy and web control panel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"dev": "node lib/index.js serve --port 5174",
|
|
18
18
|
"build": "vite build",
|
|
19
|
-
"prepublishOnly": "
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"optionalDependencies": {
|
|
22
|
+
"pac-proxy-sslocal-win32-x64": "workspace:*",
|
|
23
|
+
"pac-proxy-sslocal-darwin-x64": "workspace:*",
|
|
24
|
+
"pac-proxy-sslocal-linux-x64": "workspace:*"
|
|
20
25
|
},
|
|
21
26
|
"dependencies": {
|
|
22
27
|
"commander": "^12.0.0",
|