pac-proxy-cli 0.1.6 → 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,264 @@
1
+ /**
2
+ * 跨平台设置/清除系统代理(macOS、Windows、Linux)
3
+ * 需在用户环境下运行;Linux 仅支持 GNOME(gsettings)
4
+ * 含参数校验、安全转义、逐项检查与兜底容错
5
+ */
6
+ import { execSync } from 'child_process';
7
+
8
+ const isWindows = process.platform === 'win32';
9
+ const isMac = process.platform === 'darwin';
10
+ const isLinux = process.platform === 'linux';
11
+
12
+ const EXEC_TIMEOUT = 12000;
13
+
14
+ function exec(cmd, opts = {}) {
15
+ try {
16
+ const out = execSync(cmd, {
17
+ encoding: 'utf8',
18
+ stdio: opts.stdio ?? 'pipe',
19
+ timeout: opts.timeout ?? EXEC_TIMEOUT,
20
+ windowsHide: isWindows,
21
+ ...opts,
22
+ });
23
+ return opts.returnOutput ? (out || '').trim() : true;
24
+ } catch (e) {
25
+ if (opts.throw) throw e;
26
+ return opts.returnOutput ? '' : false;
27
+ }
28
+ }
29
+
30
+ /** 校验并规范化 host:仅允许 hostname 或 IP,防止注入 */
31
+ function normalizeHost(host) {
32
+ const h = String(host || '127.0.0.1').trim();
33
+ if (!h || h.length > 253) return null;
34
+ if (/[\s\r\n\t'"\\;|&$`<>]/.test(h)) return null;
35
+ return h;
36
+ }
37
+
38
+ /** 校验端口 1-65535 */
39
+ function normalizePort(port) {
40
+ const p = Number(port);
41
+ if (!Number.isInteger(p) || p < 1 || p > 65535) return 5175;
42
+ return p;
43
+ }
44
+
45
+ /** 校验 PAC URL:允许 http(s) URL,防止注入 */
46
+ function normalizePacUrl(url) {
47
+ const u = String(url || '').trim();
48
+ if (!u) return '';
49
+ if (!/^https?:\/\//i.test(u) || u.length > 2048) return '';
50
+ if (/[\r\n\t'"\\]/.test(u)) return '';
51
+ return u;
52
+ }
53
+
54
+ /** Windows: 转义注册表 REG_SZ 的值(用于 reg add /d "value") */
55
+ function escapeRegSzValue(str) {
56
+ return String(str)
57
+ .replace(/\\/g, '\\\\')
58
+ .replace(/"/g, '\\"');
59
+ }
60
+
61
+ /** Linux: 转义单引号 shell 字面('...' 内的 ' 变为 '\'') */
62
+ function escapeShellSingleQuoted(str) {
63
+ return String(str).replace(/'/g, "'\\''");
64
+ }
65
+
66
+ // ---------- macOS ----------
67
+ const MAC_NETWORKSETUP = '/usr/sbin/networksetup';
68
+
69
+ function execSudo(cmd) {
70
+ return exec(`sudo -n ${cmd}`, { stdio: 'pipe', throw: false });
71
+ }
72
+
73
+ function getMacNetworkServices() {
74
+ const out = exec(`${MAC_NETWORKSETUP} -listallnetworkservices`, { returnOutput: true });
75
+ if (!out) return ['Wi-Fi'];
76
+ const lines = out.split('\n').map((s) => s.trim()).filter(Boolean);
77
+ const services = [];
78
+ const skip = 'An asterisk (*) denotes that a network service is disabled';
79
+ for (const line of lines) {
80
+ if (line.startsWith('*') || line === skip) continue;
81
+ const name = line.replace(/^\*\s*/, '').trim();
82
+ if (!name) continue;
83
+ const enabledOut = exec(`${MAC_NETWORKSETUP} -getnetworkserviceenabled "${name}"`, { returnOutput: true });
84
+ if ((String(enabledOut)).toLowerCase().includes('enabled')) services.push(name);
85
+ }
86
+ return services.length ? services : ['Wi-Fi'];
87
+ }
88
+
89
+ // ---------- Windows ----------
90
+ const WIN_REG_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
91
+
92
+ function winRegAdd(valueName, type, data) {
93
+ const escaped = type === 'REG_SZ' ? escapeRegSzValue(data) : String(data);
94
+ const part = type === 'REG_DWORD' ? ` /d ${escaped}` : ` /d "${escaped}"`;
95
+ return exec(`reg add "${WIN_REG_KEY}" /v ${valueName} /t ${type} ${part} /f`, { throw: false });
96
+ }
97
+
98
+ function winRegDelete(valueName) {
99
+ return exec(`reg delete "${WIN_REG_KEY}" /v ${valueName} /f`, { throw: false });
100
+ }
101
+
102
+ // ---------- Linux ----------
103
+ function linuxGsettingsAvailable() {
104
+ const out = exec('gsettings list-schemas', { returnOutput: true, throw: false });
105
+ return out && out.includes('org.gnome.system.proxy');
106
+ }
107
+
108
+ function linuxGset(schema, key, value) {
109
+ const val = typeof value === 'number' ? value : `'${escapeShellSingleQuoted(String(value))}'`;
110
+ return exec(`gsettings set ${schema} ${key} ${val}`, { throw: false });
111
+ }
112
+
113
+ /**
114
+ * 设置系统代理
115
+ * @param {string} host - 如 '127.0.0.1'
116
+ * @param {number} port - 如 5175
117
+ * @param {{ mode?: 'global'|'pac', pacUrl?: string }} [options]
118
+ * @returns {{ ok: boolean, error?: string }}
119
+ */
120
+ export function setSystemProxy(host, port, options = {}) {
121
+ try {
122
+ const h = normalizeHost(host);
123
+ const p = normalizePort(port);
124
+ const mode = options.mode === 'global' ? 'global' : 'pac';
125
+ const pacUrl = normalizePacUrl(options.pacUrl || '');
126
+
127
+ if (h === null) {
128
+ return { ok: false, error: '代理地址无效或包含非法字符' };
129
+ }
130
+
131
+ if (isMac) {
132
+ const services = getMacNetworkServices();
133
+ const run = (cmd) => exec(`${MAC_NETWORKSETUP} ${cmd}`) || execSudo(`${MAC_NETWORKSETUP} ${cmd}`);
134
+ let anyOk = false;
135
+ for (const service of services) {
136
+ try {
137
+ if (mode === 'pac' && pacUrl) {
138
+ const setUrl = run(`-setautoproxyurl "${service}" "${pacUrl}"`);
139
+ const setOn = run(`-setautoproxystate "${service}" on`);
140
+ const offWeb = run(`-setwebproxystate "${service}" off`) && run(`-setsecurewebproxystate "${service}" off`);
141
+ if (setUrl && setOn && offWeb) anyOk = true;
142
+ } else {
143
+ const setAddr = run(`-setwebproxy "${service}" ${h} ${p}`) && run(`-setsecurewebproxy "${service}" ${h} ${p}`);
144
+ const setOn = run(`-setwebproxystate "${service}" on`) && run(`-setsecurewebproxystate "${service}" on`);
145
+ const offAuto = run(`-setautoproxystate "${service}" off`);
146
+ if (setAddr && setOn && offAuto) anyOk = true;
147
+ }
148
+ } catch (_) {
149
+ // 单个服务失败继续尝试其他
150
+ }
151
+ }
152
+ if (!anyOk) {
153
+ return {
154
+ ok: false,
155
+ error: 'macOS 修改系统代理需要管理员权限。请运行: sudo visudo -f /etc/sudoers.d/pac-proxy ,添加: ' + (process.env.USER || '你的用户名') + ' ALL=(ALL) NOPASSWD: ' + MAC_NETWORKSETUP + ' ,保存后重试。或手动在 系统设置 → 网络 → 代理 中设置。',
156
+ };
157
+ }
158
+ return { ok: true };
159
+ }
160
+
161
+ if (isWindows) {
162
+ if (mode === 'pac' && pacUrl) {
163
+ winRegAdd('ProxyEnable', 'REG_DWORD', 0);
164
+ winRegAdd('ProxyServer', 'REG_SZ', '');
165
+ const autoOk = winRegAdd('AutoConfigURL', 'REG_SZ', pacUrl);
166
+ if (!autoOk) {
167
+ return { ok: false, error: '注册表 AutoConfigURL 写入失败,请以当前用户重试或手动在 设置 → 网络和 Internet → 代理 中配置 PAC 地址。' };
168
+ }
169
+ return { ok: true };
170
+ }
171
+ winRegDelete('AutoConfigURL');
172
+ const enableOk = winRegAdd('ProxyEnable', 'REG_DWORD', 1);
173
+ const serverOk = winRegAdd('ProxyServer', 'REG_SZ', `${h}:${p}`);
174
+ if (!enableOk || !serverOk) {
175
+ return { ok: false, error: '注册表代理设置写入失败,请以当前用户重试或手动在 设置 → 网络和 Internet → 代理 中配置。' };
176
+ }
177
+ return { ok: true };
178
+ }
179
+
180
+ if (isLinux) {
181
+ if (!linuxGsettingsAvailable()) {
182
+ return { ok: false, error: '未检测到 GNOME 桌面环境(gsettings)。当前仅支持 GNOME;若使用 KDE/XFCE 等请手动在系统设置中配置代理。' };
183
+ }
184
+ if (mode === 'pac' && pacUrl) {
185
+ const modeOk = linuxGset('org.gnome.system.proxy', 'mode', 'auto');
186
+ if (!modeOk) return { ok: false, error: 'gsettings 设置 mode 失败,请确保在图形会话中运行(DISPLAY/DBUS 已设置)。' };
187
+ linuxGset('org.gnome.system.proxy', 'autoconfig-url', pacUrl);
188
+ return { ok: true };
189
+ }
190
+ const manualOk = linuxGset('org.gnome.system.proxy', 'mode', 'manual');
191
+ if (!manualOk) return { ok: false, error: 'gsettings 设置 mode 失败,请确保在图形会话中运行(DISPLAY/DBUS 已设置)。' };
192
+ linuxGset('org.gnome.system.proxy', 'autoconfig-url', '');
193
+ linuxGset('org.gnome.system.proxy.http', 'host', h);
194
+ linuxGset('org.gnome.system.proxy.http', 'port', p);
195
+ linuxGset('org.gnome.system.proxy.https', 'host', h);
196
+ linuxGset('org.gnome.system.proxy.https', 'port', p);
197
+ return { ok: true };
198
+ }
199
+
200
+ return { ok: false, error: '不支持的平台: ' + process.platform };
201
+ } catch (e) {
202
+ return { ok: false, error: (e && e.message) ? e.message : '设置系统代理时发生未知错误' };
203
+ }
204
+ }
205
+
206
+ /**
207
+ * 清除系统代理(同时清除手动代理与 PAC)
208
+ * @returns {{ ok: boolean, error?: string }}
209
+ */
210
+ export function clearSystemProxy() {
211
+ try {
212
+ if (isMac) {
213
+ const services = getMacNetworkServices();
214
+ const run = (cmd) => exec(`${MAC_NETWORKSETUP} ${cmd}`) || execSudo(`${MAC_NETWORKSETUP} ${cmd}`);
215
+ let anyOk = false;
216
+ for (const service of services) {
217
+ try {
218
+ const offWeb = run(`-setwebproxystate "${service}" off`) && run(`-setsecurewebproxystate "${service}" off`);
219
+ const offAuto = run(`-setautoproxystate "${service}" off`);
220
+ const clearUrl = run(`-setautoproxyurl "${service}" ""`);
221
+ if (offWeb && offAuto && clearUrl) anyOk = true;
222
+ } catch (_) {}
223
+ }
224
+ if (!anyOk) {
225
+ return { ok: false, error: '关闭系统代理可能需要管理员权限(配置 sudo 免密后重试),或手动在 系统设置 → 网络 → 代理 中关闭。' };
226
+ }
227
+ return { ok: true };
228
+ }
229
+
230
+ if (isWindows) {
231
+ const enableOk = winRegAdd('ProxyEnable', 'REG_DWORD', 0);
232
+ winRegDelete('AutoConfigURL');
233
+ winRegAdd('ProxyServer', 'REG_SZ', '');
234
+ if (!enableOk) {
235
+ return { ok: false, error: '注册表 ProxyEnable 清除失败,请手动在 设置 → 网络和 Internet → 代理 中关闭代理。' };
236
+ }
237
+ return { ok: true };
238
+ }
239
+
240
+ if (isLinux) {
241
+ if (!linuxGsettingsAvailable()) {
242
+ return { ok: false, error: '未检测到 GNOME(gsettings),无法自动清除。请手动在系统设置中关闭代理。' };
243
+ }
244
+ linuxGset('org.gnome.system.proxy', 'autoconfig-url', '');
245
+ linuxGset('org.gnome.system.proxy.http', 'host', '');
246
+ linuxGset('org.gnome.system.proxy.http', 'port', 0);
247
+ linuxGset('org.gnome.system.proxy.https', 'host', '');
248
+ linuxGset('org.gnome.system.proxy.https', 'port', 0);
249
+ const ok = linuxGset('org.gnome.system.proxy', 'mode', 'none');
250
+ return ok ? { ok: true } : { ok: false, error: 'gsettings 设置 mode 为 none 失败,请手动在系统设置中关闭代理。' };
251
+ }
252
+
253
+ return { ok: false, error: '不支持的平台: ' + process.platform };
254
+ } catch (e) {
255
+ return { ok: false, error: (e && e.message) ? e.message : '清除系统代理时发生未知错误' };
256
+ }
257
+ }
258
+
259
+ export function getPlatform() {
260
+ if (isMac) return 'darwin';
261
+ if (isWindows) return 'win32';
262
+ if (isLinux) return 'linux';
263
+ return process.platform;
264
+ }
@@ -0,0 +1,14 @@
1
+ const MAX_RECORDS = 500;
2
+ const records = [];
3
+
4
+ export function pushRecord(entry) {
5
+ records.unshift({
6
+ time: Date.now(),
7
+ ...entry,
8
+ });
9
+ if (records.length > MAX_RECORDS) records.length = MAX_RECORDS;
10
+ }
11
+
12
+ export function getRecords() {
13
+ return [...records];
14
+ }
package/package.json CHANGED
@@ -1,28 +1,50 @@
1
1
  {
2
2
  "name": "pac-proxy-cli",
3
- "version": "0.1.6",
4
- "description": "HTTP 代理与隧道 CLI + Web 管理面板",
3
+ "version": "1.0.0",
4
+ "description": "node pac proxy and web control panel",
5
5
  "type": "module",
6
+ "main": "lib/index.js",
7
+ "bin": {
8
+ "pac-proxy": "bin/cli.js"
9
+ },
6
10
  "files": [
7
11
  "bin",
8
- "src",
9
- "public",
10
- "README.md",
12
+ "lib",
13
+ "dist",
11
14
  ".env"
12
15
  ],
13
- "bin": {
14
- "pac-proxy": "./bin/proxy.js"
15
- },
16
16
  "scripts": {
17
- "start": "node bin/proxy.js start",
18
- "panel": "node bin/proxy.js panel"
17
+ "dev": "node lib/index.js serve --port 5174",
18
+ "build": "vite build",
19
+ "prepublishOnly": "pnpm run build"
19
20
  },
20
21
  "dependencies": {
21
22
  "commander": "^12.0.0",
22
- "dotenv": "^16.6.1",
23
+ "cors": "^2.8.5",
24
+ "dotenv": "^16.4.5",
23
25
  "express": "^4.21.0",
24
- "open": "^10.0.0",
25
- "proxy": "^2.2.0",
26
- "socks": "^2.8.0"
27
- }
26
+ "get-port-please": "^3.0.0",
27
+ "http-mitm-proxy": "^1.1.0",
28
+ "https-proxy-agent": "^7.0.0",
29
+ "socks": "^2.8.0",
30
+ "socks-proxy-agent": "^8.0.0",
31
+ "vue-router": "^4.2.0"
32
+ },
33
+ "devDependencies": {
34
+ "@vitejs/plugin-vue": "^5.0.0",
35
+ "vite": "^5.4.0",
36
+ "vue": "^3.4.0",
37
+ "vue-router": "^4.2.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "keywords": [
43
+ "proxy",
44
+ "pac",
45
+ "cli",
46
+ "socks5",
47
+ "http-proxy"
48
+ ],
49
+ "license": "MIT"
28
50
  }
package/bin/proxy.js DELETED
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import 'dotenv/config';
4
- import { program } from 'commander';
5
- import { startProxyAndPanel } from '../src/cli/start.js';
6
- import { panelOnly } from '../src/cli/panel.js';
7
-
8
- program
9
- .name('proxy')
10
- .description('HTTP 代理与隧道 - CLI')
11
- .version('0.1.0');
12
-
13
- program
14
- .command('start')
15
- .description('启动 HTTP 代理 + Web 管理面板(上游为本地 SOCKS5 或远程 Server)')
16
- .option('-p, --port <number>', 'HTTP 代理端口', '3893')
17
- .option('--panel-port <number>', 'Web 面板端口', '3892')
18
- .option('-s, --socks <host:port>', '上游 SOCKS5 地址,如 127.0.0.1:1080(与远程服务器二选一)')
19
- .option('--server [url]', '上游远程代理服务器地址;可为空,为空时使用默认的上游远程服务器地址')
20
- .option('--no-open', '不自动打开浏览器')
21
- .action(startProxyAndPanel);
22
-
23
- program
24
- .command('panel')
25
- .description('仅启动 Web 管理面板(不启动代理)')
26
- .option('-p, --port <number>', '面板端口', '3892')
27
- .option('--no-open', '不自动打开浏览器')
28
- .action(panelOnly);
29
-
30
- program.parse();