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.
@@ -1,40 +0,0 @@
1
- const MAX_ITEMS = 2000;
2
-
3
- /**
4
- * 抓包记录:仅元数据,不含 body 内容
5
- * @typedef {{ id: string, time: number, method: string, url: string, host: string, statusCode?: number, requestHeaders?: object, responseHeaders?: object, requestBodySize?: number, responseBodySize?: number }} CaptureRecord
6
- */
7
-
8
- let idSeq = 0;
9
- function nextId() {
10
- return String(++idSeq);
11
- }
12
-
13
- export function createCaptureStore() {
14
- /** @type {CaptureRecord[]} */
15
- const list = [];
16
-
17
- return {
18
- add(record) {
19
- const id = nextId();
20
- const item = { id, ...record };
21
- list.push(item);
22
- if (list.length > MAX_ITEMS) list.shift();
23
- return id;
24
- },
25
- update(id, updates) {
26
- const i = list.findIndex((r) => r.id === id);
27
- if (i === -1) return;
28
- Object.assign(list[i], updates);
29
- },
30
- getAll() {
31
- return [...list].reverse();
32
- },
33
- getById(id) {
34
- return list.find((r) => r.id === id) || null;
35
- },
36
- clear() {
37
- list.length = 0;
38
- },
39
- };
40
- }
package/src/cli/panel.js DELETED
@@ -1,23 +0,0 @@
1
- import { createPanelServer } from '../panel/server.js';
2
- import { createPacStore } from '../pac/store.js';
3
- import { createCaptureStore } from '../capture/store.js';
4
- import open from 'open';
5
-
6
- const DEFAULT_PANEL_PORT = 3892;
7
-
8
- export async function panelOnly(options) {
9
- const panelPort = Number(options.port) || DEFAULT_PANEL_PORT;
10
- const shouldOpen = options.open !== false;
11
-
12
- const app = await createPanelServer(panelPort, {
13
- proxyPort: 3893,
14
- pacStore: createPacStore(),
15
- captureStore: createCaptureStore(),
16
- });
17
- const panelUrl = `http://127.0.0.1:${panelPort}`;
18
-
19
- console.log(`Web 面板: ${panelUrl}`);
20
- if (shouldOpen) open(panelUrl).catch(() => {});
21
-
22
- process.on('SIGINT', () => process.exit(0));
23
- }
package/src/cli/start.js DELETED
@@ -1,62 +0,0 @@
1
- import { createProxyServer } from '../proxy/server.js';
2
- import { createPanelServer } from '../panel/server.js';
3
- import { createPacStore } from '../pac/store.js';
4
- import { createCaptureStore } from '../capture/store.js';
5
- import open from 'open';
6
-
7
- const DEFAULT_PROXY_PORT = 3893;
8
- const DEFAULT_PANEL_PORT = 3892;
9
-
10
- export async function startProxyAndPanel(options) {
11
- const proxyPort = Number(options.port) || DEFAULT_PROXY_PORT;
12
- const panelPort = Number(options.panelPort) || DEFAULT_PANEL_PORT;
13
- const socks = options.socks;
14
- const serverUrl = String(options.server === true ? (process.env.PROXY_SERVER_URL || '') : (options.server || process.env.PROXY_SERVER_URL || '')).trim() || process.env.PROXY_SERVER_URL || '';
15
- const shouldOpen = options.open !== false;
16
-
17
- if (!options.server && !socks || !serverUrl) {
18
- console.error('请指定上游:使用 -s/--socks <host:port>、--server <url>');
19
- process.exit(1);
20
- }
21
-
22
- let upstream = null;
23
- if (socks) {
24
- const [host, port] = socks.split(':');
25
- if (!host || !port) {
26
- console.error('--socks 格式应为 host:port,如 127.0.0.1:1080');
27
- process.exit(1);
28
- }
29
- upstream = { type: 'socks5', host, port: Number(port) };
30
- } else {
31
- const parsed = new URL(serverUrl);
32
- upstream = { type: 'server', url: serverUrl, parsed };
33
- }
34
-
35
- const pacStore = createPacStore();
36
- const captureStore = createCaptureStore();
37
-
38
- let proxyToken = null;
39
- const panelOpts = { proxyPort, pacStore, captureStore };
40
- if (upstream.type === 'server') {
41
- panelOpts.setProxyToken = (v) => { proxyToken = v || null; };
42
- }
43
-
44
- const panel = await createPanelServer(panelPort, panelOpts);
45
- const panelUrl = `http://127.0.0.1:${panelPort}`;
46
-
47
- const proxyOpts = { captureStore };
48
- if (upstream.type === 'server') {
49
- proxyOpts.getProxyToken = () => proxyToken;
50
- }
51
- const proxy = createProxyServer(proxyPort, upstream, proxyOpts);
52
- proxy.on('listening', () => {
53
- console.log(`HTTP 代理: http://127.0.0.1:${proxyPort}`);
54
- console.log(`Web 面板: ${panelUrl}`);
55
- if (shouldOpen) open(panelUrl).catch(() => {});
56
- });
57
-
58
- process.on('SIGINT', () => {
59
- proxy.close();
60
- process.exit(0);
61
- });
62
- }
package/src/pac/store.js DELETED
@@ -1,186 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
-
5
- /**
6
- * 单条规则:type=host 时用 shExpMatch(host, pattern);url 时用 shExpMatch(url, pattern);default 时匹配所有
7
- * @typedef {{ id: string, type: 'host'|'url'|'default', pattern: string, proxy: string, enabled: boolean, order: number }} PacRule
8
- */
9
-
10
- function getDataDir() {
11
- const base = process.env.PROXY_DATA_DIR || path.join(os.homedir(), '.proxy');
12
- try {
13
- fs.mkdirSync(base, { recursive: true });
14
- } catch {}
15
- return base;
16
- }
17
-
18
- function getRulesPath() {
19
- return path.join(getDataDir(), 'pac-rules.json');
20
- }
21
-
22
- function nextId() {
23
- return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
24
- }
25
-
26
- /** 无规则时返回空数组,PAC 将全部走直连 */
27
- function defaultRules() {
28
- return [];
29
- }
30
-
31
- /**
32
- * 简易 shExpMatch:* 匹配任意字符,? 匹配单字符(与 PAC 行为一致)
33
- */
34
- function shExpMatch(str, pattern) {
35
- const escaped = String(pattern).replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*').replace(/\?/g, '.');
36
- return new RegExp('^' + escaped + '$').test(String(str));
37
- }
38
-
39
- /**
40
- * 根据当前规则计算某请求应返回的代理结果(用于流量列表展示代理/直连)
41
- * 规则未匹配到的一律视为直连(与 PAC 行为一致)
42
- * @param {PacRule[]} rules
43
- * @param {string} host - 请求的 host(可含端口,内部会取 hostname 做 type=host 匹配)
44
- * @param {string} url - 完整 URL(用于 type=url 规则)
45
- * @returns {string} "DIRECT" 或 "PROXY host:port"
46
- */
47
- export function getPacResultForRequest(rules, host, url) {
48
- const sorted = [...(rules || [])].filter((r) => r.enabled).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
49
- const hostStr = String(host || '').trim();
50
- const hostname = hostStr.split(':')[0] || hostStr; // PAC 里 host 不含端口,用 hostname 做 host 规则匹配
51
- const urlStr = String(url || '').trim();
52
- for (const r of sorted) {
53
- const type = (r.type || '').toLowerCase();
54
- const proxyVal = String(r.proxy ?? 'DIRECT').trim();
55
- if (type === 'default') return proxyVal.toUpperCase() === 'DIRECT' ? 'DIRECT' : proxyVal;
56
- if (type === 'host' && r.pattern && shExpMatch(hostname, r.pattern)) return proxyVal.toUpperCase() === 'DIRECT' ? 'DIRECT' : proxyVal;
57
- if (type === 'url' && r.pattern && urlStr && shExpMatch(urlStr, r.pattern)) return proxyVal.toUpperCase() === 'DIRECT' ? 'DIRECT' : proxyVal;
58
- }
59
- return 'DIRECT';
60
- }
61
-
62
- /**
63
- * 从规则列表生成 PAC 脚本内容
64
- * @param {PacRule[]} rules
65
- * @param {number} proxyPort
66
- */
67
- export function rulesToPacContent(rules, proxyPort = 3893) {
68
- const sorted = [...rules].filter((r) => r.enabled).sort((a, b) => a.order - b.order);
69
- const directOnly = 'function FindProxyForURL(url, host) {\n return "DIRECT";\n}\n';
70
- if (sorted.length === 0) return directOnly;
71
- if (sorted.length === 1 && sorted[0].type === 'default' && String(sorted[0].proxy || '').trim().toUpperCase().startsWith('PROXY')) {
72
- return directOnly;
73
- }
74
- const lines = ['function FindProxyForURL(url, host) {'];
75
- for (const r of sorted) {
76
- const type = (r.type || '').toLowerCase();
77
- const proxy = (r.proxy || 'DIRECT').trim().replace(/"/g, '\\"');
78
- const pattern = (r.pattern || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
79
- if (type === 'default') {
80
- lines.push(` return "${proxy}";`);
81
- break;
82
- }
83
- if (type === 'host' && pattern) {
84
- lines.push(` if (shExpMatch(host, "${pattern}")) return "${proxy}";`);
85
- } else if (pattern) {
86
- lines.push(` if (shExpMatch(url, "${pattern}")) return "${proxy}";`);
87
- }
88
- }
89
- lines.push(' return "DIRECT";');
90
- lines.push('}');
91
- return lines.join('\n') + '\n';
92
- }
93
-
94
- /**
95
- * 本地 PAC 规则存储(JSON 文件),并提供生成 PAC 脚本
96
- */
97
- export function createPacStore(proxyPort = 3893) {
98
- let memory = null;
99
-
100
- function load() {
101
- if (memory !== null) return memory;
102
- try {
103
- const p = getRulesPath();
104
- if (fs.existsSync(p)) {
105
- const raw = fs.readFileSync(p, 'utf8');
106
- memory = JSON.parse(raw);
107
- if (!Array.isArray(memory)) memory = defaultRules();
108
- } else {
109
- memory = defaultRules();
110
- }
111
- } catch {
112
- memory = defaultRules();
113
- }
114
- return memory;
115
- }
116
-
117
- function save(rules) {
118
- try {
119
- fs.writeFileSync(getRulesPath(), JSON.stringify(rules, null, 2), 'utf8');
120
- } catch (e) {
121
- console.error('PAC 规则写入失败:', e.message);
122
- }
123
- }
124
-
125
- return {
126
- getRules() {
127
- return load();
128
- },
129
- setRules(rules) {
130
- memory = Array.isArray(rules) ? rules : defaultRules();
131
- save(memory);
132
- return memory;
133
- },
134
- addRule(rule) {
135
- const rules = load();
136
- const id = rule.id || nextId();
137
- const order = typeof rule.order === 'number' ? rule.order : rules.length;
138
- const newRule = {
139
- id,
140
- type: rule.type || 'host',
141
- pattern: String(rule.pattern ?? '').trim(),
142
- proxy: String(rule.proxy ?? 'DIRECT').trim(),
143
- enabled: rule.enabled !== false,
144
- order,
145
- };
146
- rules.push(newRule);
147
- save(rules);
148
- return newRule;
149
- },
150
- updateRule(id, updates) {
151
- const rules = load();
152
- const i = rules.findIndex((r) => r.id === id);
153
- if (i === -1) return null;
154
- if (updates.type !== undefined) rules[i].type = updates.type;
155
- if (updates.pattern !== undefined) rules[i].pattern = String(updates.pattern).trim();
156
- if (updates.proxy !== undefined) rules[i].proxy = String(updates.proxy).trim();
157
- if (updates.enabled !== undefined) rules[i].enabled = !!updates.enabled;
158
- if (updates.order !== undefined) rules[i].order = Number(updates.order);
159
- save(rules);
160
- return rules[i];
161
- },
162
- deleteRule(id) {
163
- const rules = load();
164
- const i = rules.findIndex((r) => r.id === id);
165
- if (i === -1) return false;
166
- rules.splice(i, 1);
167
- save(rules);
168
- return true;
169
- },
170
- get(proxyPortForPac = proxyPort) {
171
- return rulesToPacContent(load(), proxyPortForPac);
172
- },
173
- set(content) {
174
- if (Array.isArray(content)) {
175
- memory = content;
176
- save(memory);
177
- return this.get();
178
- }
179
- if (typeof content === 'string') {
180
- memory = defaultRules();
181
- save(memory);
182
- }
183
- return this.get();
184
- },
185
- };
186
- }
@@ -1,145 +0,0 @@
1
- import express from 'express';
2
- import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { applySystemProxy, clearSystemProxy, getSystemProxyStatus } from '../sysproxy/index.js';
5
- import { getPacResultForRequest } from '../pac/store.js';
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
-
9
- /**
10
- * 创建 Web 管理面板服务(静态页 + 后续 API)
11
- * @param {number} port - 面板端口,用于生成 PAC URL
12
- * @param {{ proxyPort?: number, pacStore?: object, captureStore?: object }} opts
13
- * @returns {Promise<import('express').Express>}
14
- */
15
- export async function createPanelServer(port, opts = {}) {
16
- const app = express();
17
- app.use((req, res, next) => {
18
- res.setHeader('Access-Control-Allow-Origin', '*');
19
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
20
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
21
- if (req.method === 'OPTIONS') return res.sendStatus(204);
22
- next();
23
- });
24
- app.use(express.json());
25
- app.set('panelPort', port);
26
- app.set('proxyPort', opts.proxyPort ?? 3893);
27
- const pacStore = opts.pacStore || null;
28
- const captureStore = opts.captureStore || null;
29
-
30
- const publicDir = path.join(__dirname, '..', '..', 'public');
31
- app.use(express.static(publicDir));
32
-
33
- app.get('/api/health', (req, res) => {
34
- res.json({ ok: true, service: 'panel' });
35
- });
36
-
37
- app.get('/api/config', (req, res) => {
38
- const serverUrl = (process.env.PROXY_SERVER_URL || '').trim();
39
- const proxyPort = app.get('proxyPort') ?? 3893;
40
- const panelPort = app.get('panelPort') ?? 3892;
41
- res.json({
42
- serverUrl,
43
- proxyAddress: `127.0.0.1:${proxyPort}`,
44
- pacUrl: `http://127.0.0.1:${panelPort}/pac.js`,
45
- });
46
- });
47
-
48
- /** 前端同步登录 token(上游为 server 时由代理带给远程);上游为 SOCKS5 时无操作,避免 404 */
49
- const setProxyToken = opts.setProxyToken;
50
- app.post('/api/upstream/token', (req, res) => {
51
- if (typeof setProxyToken === 'function') {
52
- const token = req.body?.token != null ? String(req.body.token).trim() : '';
53
- setProxyToken(token || null);
54
- }
55
- res.json({ ok: true });
56
- });
57
-
58
- app.post('/api/sysproxy/apply', (req, res) => {
59
- const mode = req.body?.mode === 'global' ? 'global' : 'pac';
60
- const panelPort = app.get('panelPort');
61
- const proxyPort = app.get('proxyPort') ?? 3893;
62
- const result =
63
- mode === 'global'
64
- ? applySystemProxy({ mode: 'global', proxyHost: '127.0.0.1', proxyPort })
65
- : applySystemProxy({ mode: 'pac', pacUrl: `http://127.0.0.1:${panelPort}/pac.js?t=${Date.now()}` });
66
- res.json(result);
67
- });
68
-
69
- app.post('/api/sysproxy/clear', (req, res) => {
70
- const result = clearSystemProxy();
71
- res.json(result);
72
- });
73
-
74
- app.get('/api/sysproxy/status', (req, res) => {
75
- const status = getSystemProxyStatus();
76
- res.json(status);
77
- });
78
-
79
- if (pacStore) {
80
- app.get('/api/pac', (req, res) => {
81
- res.json({ rules: pacStore.getRules() });
82
- });
83
- app.put('/api/pac', (req, res) => {
84
- const rules = req.body?.rules;
85
- if (Array.isArray(rules)) pacStore.setRules(rules);
86
- res.json({ ok: true });
87
- });
88
- app.get('/api/pac/rules', (req, res) => {
89
- res.json({ rules: pacStore.getRules() });
90
- });
91
- app.post('/api/pac/rules', (req, res) => {
92
- const rule = pacStore.addRule(req.body || {});
93
- res.status(201).json(rule);
94
- });
95
- app.put('/api/pac/rules/:id', (req, res) => {
96
- const updated = pacStore.updateRule(req.params.id, req.body || {});
97
- if (!updated) return res.status(404).json({ error: 'not found' });
98
- res.json(updated);
99
- });
100
- app.delete('/api/pac/rules/:id', (req, res) => {
101
- const ok = pacStore.deleteRule(req.params.id);
102
- if (!ok) return res.status(404).json({ error: 'not found' });
103
- res.json({ ok: true });
104
- });
105
- }
106
-
107
- app.get('/pac.js', (req, res) => {
108
- const proxyPort = app.get('proxyPort');
109
- const content = pacStore ? pacStore.get(proxyPort) : `function FindProxyForURL(url, host) { return "PROXY 127.0.0.1:${proxyPort}"; }`;
110
- res.set('Content-Type', 'application/x-ns-proxy-autoconfig');
111
- res.set('Cache-Control', 'no-store, no-cache, must-revalidate');
112
- res.set('Pragma', 'no-cache');
113
- res.send(content);
114
- });
115
-
116
- if (captureStore) {
117
- app.get('/api/capture', (req, res) => {
118
- const list = captureStore.getAll();
119
- const rules = pacStore ? pacStore.getRules() : [];
120
- const enriched = list.map((item) => {
121
- const result = pacStore ? getPacResultForRequest(rules, item.host || '', item.url || '') : 'PROXY';
122
- const s = String(result || '').trim().toUpperCase();
123
- const proxyDecision = s.startsWith('PROXY') ? '代理' : '直连';
124
- return { ...item, proxyDecision };
125
- });
126
- res.json({ list: enriched });
127
- });
128
- app.get('/api/capture/:id', (req, res) => {
129
- const r = captureStore.getById(req.params.id);
130
- if (!r) return res.status(404).json({ error: 'not found' });
131
- const result = pacStore ? getPacResultForRequest(pacStore.getRules(), r.host || '', r.url || '') : 'PROXY';
132
- const s = String(result || '').trim().toUpperCase();
133
- const proxyDecision = s.startsWith('PROXY') ? '代理' : '直连';
134
- res.json({ ...r, proxyDecision });
135
- });
136
- app.delete('/api/capture', (req, res) => {
137
- captureStore.clear();
138
- res.json({ ok: true });
139
- });
140
- }
141
-
142
- return new Promise((resolve) => {
143
- app.listen(port, () => resolve(app));
144
- });
145
- }