pac-proxy-cli 0.1.7 → 1.0.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.
@@ -0,0 +1,247 @@
1
+ /**
2
+ * 统一代理服务:根据开关、全局/PAC 模式及 PAC 规则处理 HTTP/HTTPS 代理
3
+ */
4
+ import http from 'http';
5
+ import https from 'https';
6
+ import net from 'net';
7
+ import { URL } from 'url';
8
+ import { SocksProxyAgent } from 'socks-proxy-agent';
9
+ import { HttpsProxyAgent } from 'https-proxy-agent';
10
+ import { SocksClient } from 'socks';
11
+ import { getProxyConfig, getPacRules } from './local-store.js';
12
+ import { getPacAction } from './pac-match.js';
13
+ import { pushRecord } from './traffic-log.js';
14
+
15
+ function createUpstreamAgent(upstream) {
16
+ const u = (upstream || '').trim();
17
+ if (!u) return null;
18
+ if (u.startsWith('socks5://') || u.startsWith('socks4://') || u.startsWith('socks://')) {
19
+ return new SocksProxyAgent(u);
20
+ }
21
+ if (u.startsWith('http://') || u.startsWith('https://')) {
22
+ return new HttpsProxyAgent(u);
23
+ }
24
+ return null;
25
+ }
26
+
27
+ /**
28
+ * 通过 socks 建立到 targetHost:targetPort 的 TCP 连接
29
+ */
30
+ function connectViaSocks(upstream, targetHost, targetPort) {
31
+ const u = new URL(upstream);
32
+ const proxy = {
33
+ host: u.hostname,
34
+ port: parseInt(u.port || '1080', 10),
35
+ type: u.protocol === 'socks4:' ? 4 : 5,
36
+ };
37
+ return SocksClient.createConnection({
38
+ proxy,
39
+ command: 'connect',
40
+ destination: { host: targetHost, port: targetPort },
41
+ }).then((info) => info.socket);
42
+ }
43
+
44
+ /**
45
+ * 通过 HTTP 代理发送 CONNECT,返回与目标建立的 socket
46
+ */
47
+ function connectViaHttpProxy(upstream, targetHost, targetPort) {
48
+ return new Promise((resolve, reject) => {
49
+ const u = new URL(upstream);
50
+ const proxyPort = parseInt(u.port || (u.protocol === 'https:' ? '443' : '80'), 10);
51
+ const socket = net.connect(proxyPort, u.hostname, () => {
52
+ const req = `CONNECT ${targetHost}:${targetPort} HTTP/1.1\r\nHost: ${targetHost}:${targetPort}\r\n\r\n`;
53
+ socket.write(req);
54
+ });
55
+ let buf = '';
56
+ const onData = (chunk) => {
57
+ buf += chunk.toString();
58
+ if (buf.includes('\r\n\r\n')) {
59
+ socket.removeListener('data', onData);
60
+ const code = parseInt(buf.split('\r\n')[0].split(' ')[1], 10);
61
+ if (code >= 200 && code < 300) resolve(socket);
62
+ else reject(new Error(`Proxy CONNECT failed: ${code}`));
63
+ }
64
+ };
65
+ socket.on('data', onData);
66
+ socket.on('error', reject);
67
+ });
68
+ }
69
+
70
+ /**
71
+ * 直连 targetHost:targetPort
72
+ */
73
+ function connectDirect(targetHost, targetPort) {
74
+ return new Promise((resolve, reject) => {
75
+ const socket = net.connect(targetPort, targetHost, () => resolve(socket));
76
+ socket.on('error', reject);
77
+ });
78
+ }
79
+
80
+ export function createProxyServer() {
81
+ const server = http.createServer((req, res) => {
82
+ const config = getProxyConfig();
83
+ const rules = getPacRules();
84
+
85
+ if (!config.enabled) {
86
+ res.writeHead(403, { 'Content-Type': 'text/plain' });
87
+ res.end('Proxy is disabled');
88
+ return;
89
+ }
90
+
91
+ const targetUrl = req.url;
92
+ if (!targetUrl || targetUrl === '/') {
93
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
94
+ res.end('Bad Request');
95
+ return;
96
+ }
97
+
98
+ let action = 'direct';
99
+ if (config.mode === 'global') {
100
+ action = config.upstream ? 'proxy' : 'direct';
101
+ } else {
102
+ action = getPacAction(targetUrl, rules);
103
+ }
104
+ pushRecord({ type: targetUrl.startsWith('https') ? 'https' : 'http', method: req.method, url: targetUrl, action });
105
+
106
+ const protocol = targetUrl.startsWith('https') ? https : http;
107
+ const opts = { method: req.method, headers: req.headers };
108
+
109
+ if (action === 'proxy' && config.upstream) {
110
+ const agent = createUpstreamAgent(config.upstream);
111
+ if (agent) opts.agent = agent;
112
+ }
113
+
114
+ res.on('error', () => proxyReq.destroy());
115
+ const proxyReq = protocol.request(targetUrl, opts, (proxyRes) => {
116
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
117
+ proxyRes.pipe(res);
118
+ proxyRes.on('error', () => res.destroy());
119
+ });
120
+ proxyReq.on('error', (err) => {
121
+ if (!res.writableEnded) {
122
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
123
+ res.end('Proxy Error: ' + err.message);
124
+ }
125
+ });
126
+ req.on('error', () => {
127
+ proxyReq.destroy();
128
+ });
129
+ req.pipe(proxyReq);
130
+ });
131
+
132
+ server.on('connect', (req, clientSocket, head) => {
133
+ const config = getProxyConfig();
134
+ const rules = getPacRules();
135
+
136
+ if (!config.enabled) {
137
+ clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
138
+ clientSocket.end();
139
+ return;
140
+ }
141
+
142
+ let targetSocketRef = null;
143
+ clientSocket.on('error', () => {
144
+ if (targetSocketRef) targetSocketRef.destroy();
145
+ });
146
+
147
+ const [targetHost, targetPortStr] = req.url.split(':');
148
+ const targetPort = parseInt(targetPortStr || '443', 10);
149
+ const urlForPac = req.url;
150
+
151
+ let action = 'direct';
152
+ if (config.mode === 'global') {
153
+ action = config.upstream ? 'proxy' : 'direct';
154
+ } else {
155
+ action = getPacAction(urlForPac, rules);
156
+ }
157
+ pushRecord({ type: 'https', method: 'CONNECT', url: req.url, action });
158
+
159
+ const onTargetSocket = (targetSocket) => {
160
+ targetSocketRef = targetSocket;
161
+ targetSocket.on('error', () => clientSocket.destroy());
162
+ clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
163
+ if (head && head.length) targetSocket.write(head);
164
+ targetSocket.pipe(clientSocket);
165
+ clientSocket.pipe(targetSocket);
166
+ };
167
+
168
+ const onError = (err) => {
169
+ clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
170
+ clientSocket.end();
171
+ };
172
+
173
+ if (action === 'direct') {
174
+ connectDirect(targetHost, targetPort).then(onTargetSocket).catch(onError);
175
+ return;
176
+ }
177
+
178
+ const upstream = (config.upstream || '').trim();
179
+ if (!upstream) {
180
+ connectDirect(targetHost, targetPort).then(onTargetSocket).catch(onError);
181
+ return;
182
+ }
183
+
184
+ if (upstream.startsWith('socks5://') || upstream.startsWith('socks4://') || upstream.startsWith('socks://')) {
185
+ connectViaSocks(upstream, targetHost, targetPort).then(onTargetSocket).catch(onError);
186
+ } else if (upstream.startsWith('http://') || upstream.startsWith('https://')) {
187
+ connectViaHttpProxy(upstream, targetHost, targetPort).then(onTargetSocket).catch(onError);
188
+ } else {
189
+ connectDirect(targetHost, targetPort).then(onTargetSocket).catch(onError);
190
+ }
191
+ });
192
+
193
+ return server;
194
+ }
195
+
196
+ let proxyServerInstance = null;
197
+ let proxyListenPort = null;
198
+
199
+ export async function startProxyServer(port) {
200
+ await stopProxyServer();
201
+ if (port == null || port <= 0) return null;
202
+ try {
203
+ proxyServerInstance = createProxyServer();
204
+ await new Promise((resolve, reject) => {
205
+ proxyServerInstance.once('error', reject);
206
+ proxyServerInstance.listen(port, '127.0.0.1', () => {
207
+ proxyListenPort = port;
208
+ proxyServerInstance.removeListener('error', reject);
209
+ proxyServerInstance.on('error', (err) => console.error('Proxy server error:', err.message));
210
+ console.log(`代理服务已启动: http://127.0.0.1:${port}`);
211
+ resolve();
212
+ });
213
+ });
214
+ } catch (e) {
215
+ console.error('Failed to start proxy server:', e.message);
216
+ proxyServerInstance = null;
217
+ proxyListenPort = null;
218
+ }
219
+ return proxyServerInstance;
220
+ }
221
+
222
+ export function stopProxyServer() {
223
+ if (!proxyServerInstance) return Promise.resolve();
224
+ const server = proxyServerInstance;
225
+ proxyServerInstance = null;
226
+ proxyListenPort = null;
227
+ return new Promise((resolve) => {
228
+ let settled = false;
229
+ const done = () => {
230
+ if (settled) return;
231
+ settled = true;
232
+ resolve();
233
+ };
234
+ server.close(done);
235
+ server.once('error', done);
236
+ setTimeout(done, 3000);
237
+ });
238
+ }
239
+
240
+ export function getProxyServerInstance() {
241
+ return proxyServerInstance;
242
+ }
243
+
244
+ export function getProxyStatus() {
245
+ const running = !!(proxyServerInstance && proxyListenPort);
246
+ return { running, port: running ? proxyListenPort : null };
247
+ }
package/lib/server.js ADDED
@@ -0,0 +1,239 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import dotenv from 'dotenv';
5
+ import express from 'express';
6
+ import cors from 'cors';
7
+ import { createServer } from 'http';
8
+ import { loadConfig, saveConfig, getPacRules, setPacRules, getProxyConfig, setProxyConfig } from './local-store.js';
9
+ import { getRecords as getTrafficRecords } from './traffic-log.js';
10
+ import { getCaptures } from './capture-log.js';
11
+ import { getPort } from 'get-port-please';
12
+ import { startProxyServer, stopProxyServer, getProxyStatus } from './proxy-server.js';
13
+ import { startMitmProxyServer, stopMitmProxyServer, getMitmProxyStatus, getCaCertPath, hasCaCert } from './mitm-proxy-server.js';
14
+ import { setSystemProxy, clearSystemProxy } from './system-proxy.js';
15
+ import { generatePacJs } from './pac-file.js';
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const envDir = path.resolve(__dirname, '..');
19
+ dotenv.config({ path: path.join(envDir, '.env') });
20
+ dotenv.config({ path: path.join(envDir, '.env.local'), override: true });
21
+
22
+ let lastSystemProxyResult = null;
23
+ /** 控制台实际监听端口,用于拼 PAC URL(PAC 模式时系统代理指向此地址的 /proxy.pac) */
24
+ let controlPanelPort = null;
25
+
26
+ async function applyProxyService(proxyOverride) {
27
+ const proxy = proxyOverride ?? getProxyConfig();
28
+ const isMitm = (proxy.mode || '') === 'mitm';
29
+ const prevStatus = isMitm ? getMitmProxyStatus() : getProxyStatus();
30
+ const wasProxyRunning = prevStatus.running === true;
31
+ await stopProxyServer();
32
+ await stopMitmProxyServer();
33
+ const enabled = proxy.enabled === true || proxy.enabled === 'true';
34
+ const port = Number(proxy.httpPort) || 5175;
35
+ lastSystemProxyResult = null;
36
+ if (enabled) {
37
+ if (isMitm) {
38
+ try {
39
+ await startMitmProxyServer(port);
40
+ } catch (e) {
41
+ console.error('抓包代理启动失败:', e.message);
42
+ }
43
+ } else {
44
+ await startProxyServer(port);
45
+ }
46
+ if (proxy.applySystemProxy !== false) {
47
+ const mode = (proxy.mode || 'pac') === 'pac' ? 'pac' : 'global';
48
+ const pacUrl = controlPanelPort && mode === 'pac'
49
+ ? `http://127.0.0.1:${controlPanelPort}/proxy.pac?t=${Date.now()}`
50
+ : undefined;
51
+ const result = setSystemProxy('127.0.0.1', port, { mode, pacUrl });
52
+ lastSystemProxyResult = result;
53
+ if (!result.ok) console.warn('自动设置系统代理失败:', result.error || '未知');
54
+ }
55
+ } else {
56
+ if (proxy.applySystemProxy !== false && wasProxyRunning) {
57
+ const result = clearSystemProxy();
58
+ lastSystemProxyResult = result;
59
+ if (!result.ok) console.warn('自动清除系统代理失败:', result.error || '未知');
60
+ }
61
+ }
62
+ }
63
+
64
+ export async function startServer(port) {
65
+ const app = express();
66
+ app.use(cors());
67
+ app.use(express.json());
68
+
69
+ // 静态资源:优先 dist(生产),否则开发时可用 web/dist
70
+ const distPath = path.resolve(__dirname, '..', 'dist');
71
+ if (distPath) {
72
+ app.use(express.static(distPath, { index: 'index.html' }));
73
+ }
74
+
75
+ // ---------- 本地模式 API ----------
76
+ app.get('/api/local/config', (req, res) => {
77
+ try {
78
+ const config = loadConfig();
79
+ res.json(config);
80
+ } catch (e) {
81
+ res.status(500).json({ error: String(e.message) });
82
+ }
83
+ });
84
+
85
+ app.put('/api/local/config', (req, res) => {
86
+ try {
87
+ const current = loadConfig();
88
+ const body = req.body || {};
89
+ const merged = {
90
+ ...current,
91
+ ...body,
92
+ proxy: { ...current.proxy, ...(body.proxy || {}) },
93
+ pacRules: body.pacRules !== undefined ? body.pacRules : current.pacRules,
94
+ };
95
+ if (body.mode === 'remote' || body.mode === 'local') merged.mode = body.mode;
96
+ const config = saveConfig(merged);
97
+ res.json(config);
98
+ } catch (e) {
99
+ res.status(500).json({ error: String(e.message) });
100
+ }
101
+ });
102
+
103
+ app.get('/api/local/proxy', (req, res) => {
104
+ try {
105
+ const proxy = getProxyConfig();
106
+ res.json(proxy);
107
+ } catch (e) {
108
+ res.status(500).json({ error: String(e.message) });
109
+ }
110
+ });
111
+
112
+ app.put('/api/local/proxy', async (req, res) => {
113
+ try {
114
+ const proxy = setProxyConfig(req.body);
115
+ await applyProxyService(proxy);
116
+ res.json(proxy);
117
+ } catch (e) {
118
+ res.status(500).json({ error: String(e.message) });
119
+ }
120
+ });
121
+
122
+ app.get('/api/local/pac-rules', (req, res) => {
123
+ try {
124
+ const rules = getPacRules();
125
+ res.json(rules);
126
+ } catch (e) {
127
+ res.status(500).json({ error: String(e.message) });
128
+ }
129
+ });
130
+
131
+ app.put('/api/local/pac-rules', (req, res) => {
132
+ try {
133
+ const rules = setPacRules(Array.isArray(req.body) ? req.body : req.body.rules || []);
134
+ const proxy = getProxyConfig();
135
+ if (proxy.enabled && proxy.mode === 'pac' && proxy.applySystemProxy !== false && controlPanelPort) {
136
+ const pacUrl = `http://127.0.0.1:${controlPanelPort}/proxy.pac?t=${Date.now()}`;
137
+ const result = setSystemProxy('127.0.0.1', Number(proxy.httpPort) || 5175, { mode: 'pac', pacUrl });
138
+ if (!result.ok) console.warn('刷新系统 PAC URL 失败:', result.error);
139
+ }
140
+ res.json(rules);
141
+ } catch (e) {
142
+ res.status(500).json({ error: String(e.message) });
143
+ }
144
+ });
145
+
146
+ app.get('/api/local/traffic', (req, res) => {
147
+ try {
148
+ res.json(getTrafficRecords());
149
+ } catch (e) {
150
+ res.status(500).json({ error: String(e.message) });
151
+ }
152
+ });
153
+
154
+ app.get('/api/local/capture', (req, res) => {
155
+ try {
156
+ const { q, method, status, type, sort, order, limit, offset } = req.query;
157
+ const result = getCaptures({
158
+ q,
159
+ method,
160
+ status,
161
+ type,
162
+ sort: sort || 'time',
163
+ order: order === 'asc' ? 'asc' : 'desc',
164
+ limit,
165
+ offset,
166
+ });
167
+ res.json(result);
168
+ } catch (e) {
169
+ res.status(500).json({ error: String(e.message) });
170
+ }
171
+ });
172
+
173
+ app.get('/api/local/proxy-status', (req, res) => {
174
+ try {
175
+ const proxy = getProxyConfig();
176
+ const status = (proxy.mode || '') === 'mitm' ? getMitmProxyStatus() : getProxyStatus();
177
+ if (lastSystemProxyResult) {
178
+ status.systemProxyOk = lastSystemProxyResult.ok;
179
+ status.systemProxyError = lastSystemProxyResult.error || null;
180
+ }
181
+ res.json(status);
182
+ } catch (e) {
183
+ res.status(500).json({ running: false, port: null });
184
+ }
185
+ });
186
+
187
+ app.get('/api/local/ca-cert', (req, res) => {
188
+ try {
189
+ if (!hasCaCert()) {
190
+ res.status(404).set('Content-Type', 'text/plain; charset=utf-8').send('CA 证书尚未生成,请先启用抓包代理并访问任意 HTTPS 网站。');
191
+ return;
192
+ }
193
+ const certPath = getCaCertPath();
194
+ const pem = fs.readFileSync(certPath, 'utf-8');
195
+ res.set('Content-Type', 'application/x-pem-file');
196
+ res.set('Content-Disposition', 'attachment; filename="pac-proxy-ca.crt"');
197
+ res.send(pem);
198
+ } catch (e) {
199
+ res.status(500).set('Content-Type', 'text/plain; charset=utf-8').send('读取 CA 证书失败: ' + e.message);
200
+ }
201
+ });
202
+
203
+ app.get('/api/local/remote-server-url', (req, res) => {
204
+ const url = (process.env.REMOTE_SERVER_URL || '').trim().replace(/\/$/, '');
205
+ res.json({ url });
206
+ });
207
+
208
+ app.get('/proxy.pac', (req, res) => {
209
+ try {
210
+ const proxy = getProxyConfig();
211
+ const status = (proxy.mode || '') === 'mitm' ? getMitmProxyStatus() : getProxyStatus();
212
+ const port = status.port || getProxyConfig().httpPort || 5175;
213
+ const js = generatePacJs('127.0.0.1', port);
214
+ res.set('Content-Type', 'application/x-ns-proxy-autoconfig');
215
+ res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
216
+ res.set('Pragma', 'no-cache');
217
+ res.set('Expires', '0');
218
+ res.send(js);
219
+ } catch (e) {
220
+ res.status(500).set('Content-Type', 'text/plain').send('FindProxyForURL(url,host){return"DIRECT";}');
221
+ }
222
+ });
223
+
224
+ // SPA fallback
225
+ app.get('*', (req, res) => {
226
+ res.sendFile(path.join(distPath, 'index.html'));
227
+ });
228
+
229
+ const actualPort = await getPort({ port, portRange: [port, port + 100] });
230
+ const server = createServer(app);
231
+
232
+ server.listen(actualPort, '127.0.0.1', async () => {
233
+ controlPanelPort = actualPort;
234
+ console.log(`控制台已启动: http://127.0.0.1:${actualPort}`);
235
+ await applyProxyService();
236
+ });
237
+
238
+ return server;
239
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * 将 SOCKS5 代理转为本地 HTTP/HTTPS 代理(供本地模式使用)
3
+ * 用户自行提供 SOCKS5,如 ssh -D 1080 user@host
4
+ */
5
+ import http from 'http';
6
+ import { SocksProxyAgent } from 'socks-proxy-agent';
7
+ import { createServer } from 'http';
8
+
9
+ /**
10
+ * 创建基于 SOCKS5 的 HTTP 代理服务
11
+ * @param {string} socksUrl - 如 socks5://127.0.0.1:1080
12
+ * @param {number} localPort - 本地 HTTP 代理监听端口
13
+ * @returns {http.Server}
14
+ */
15
+ export function createSocksHttpProxy(socksUrl, localPort) {
16
+ const agent = new SocksProxyAgent(socksUrl);
17
+
18
+ const server = createServer((req, res) => {
19
+ const target = req.url;
20
+ if (!target || target === '/') {
21
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
22
+ res.end('Bad Request: no URL');
23
+ return;
24
+ }
25
+
26
+ const opts = {
27
+ method: req.method,
28
+ headers: req.headers,
29
+ agent,
30
+ };
31
+
32
+ const proxyReq = http.request(target, opts, (proxyRes) => {
33
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
34
+ proxyRes.pipe(res);
35
+ });
36
+
37
+ proxyReq.on('error', (err) => {
38
+ res.writeHead(502, { 'Content-Type': 'text/plain' });
39
+ res.end('Proxy Error: ' + err.message);
40
+ });
41
+
42
+ req.pipe(proxyReq);
43
+ });
44
+
45
+ server.listen(localPort, '127.0.0.1');
46
+ return server;
47
+ }