whistle.easy-proxy 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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 EasyProxy Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # whistle.easy-proxy
2
+
3
+ > 基于 whistle 的 GitHub 直连加速插件 — DNS 防污染 + DoH 解析 + SNI 改写 + 智能 IP 选路,一键解决国内 GitHub 访问慢/不稳定问题。
4
+
5
+ [![npm version](https://img.shields.io/npm/v/whistle.easy-proxy.svg)](https://www.npmjs.com/package/whistle.easy-proxy)
6
+ [![license](https://img.shields.io/npm/l/whistle.easy-proxy.svg)](https://github.com/sqlwwx/easy-proxy/blob/master/LICENSE)
7
+ [![node version](https://img.shields.io/node/v/whistle.easy-proxy.svg)](https://nodejs.org/)
8
+
9
+ ## 功能特性
10
+
11
+ - **DNS 防污染**:通过 DoH 解析真实 IP,绕过 DNS 污染
12
+ - **智能 IP 选路**:多候选 IP 自动选择最优线路
13
+ - **SNI 改写**:绕过 SNI 检测,支持默认 SNI 和域名映射
14
+ - **上游代理**:灵活配置域名级别的代理转发
15
+ - **拦截规则**:内置 80+ 条 GitHub / npm / CDN 优化规则
16
+ - **Web 管理面板**:测速、选路、配置修改,所见即所得
17
+ - **跨平台**:支持 macOS / Windows,系统代理一键切换
18
+
19
+ ## 快速开始
20
+
21
+ ### 前置依赖
22
+
23
+ - Node.js >= 22.0.0
24
+
25
+ ### 3 步启动
26
+
27
+ ```bash
28
+ # 1. 全局安装
29
+ npm i -g whistle.easy-proxy
30
+
31
+ # 2. 启动 whistle(自动设系统代理 + 安装根证书)
32
+ easy-proxy start --init
33
+
34
+ # 3. 开启 GitHub 加速
35
+ easy-proxy accel on
36
+ ```
37
+
38
+ 现在访问 `github.com`,流量会自动走最优线路。
39
+
40
+ ### 验证加速效果
41
+
42
+ ```bash
43
+ # 检查加速状态
44
+ easy-proxy accel status
45
+
46
+ # 检测网络延迟
47
+ easy-proxy network delay
48
+
49
+ # 打开 Web 管理面板
50
+ open http://local.whistlejs.com/plugin.easy-proxy/
51
+ ```
52
+
53
+ ## 常用命令
54
+
55
+ | 命令 | 说明 |
56
+ |------|------|
57
+ | `easy-proxy start --init` | 启动 whistle + 自动设代理 + 安装证书 |
58
+ | `easy-proxy stop` | 停止 whistle |
59
+ | `easy-proxy accel on / off` | 开启 / 关闭 GitHub 加速 |
60
+ | `easy-proxy accel status` | 查看加速状态 |
61
+ | `easy-proxy accel dns` | 刷新 DNS 缓存 |
62
+ | `easy-proxy host add <域名> <IP>` | 添加手动 IP 覆盖 |
63
+ | `easy-proxy health` | 综合健康检查 |
64
+ | `easy-proxy status` | 查看整体状态 |
65
+
66
+ **完整命令参考** → [docs/cli.md](docs/cli.md)
67
+
68
+ ## 配置文件
69
+
70
+ 配置文件路径:`~/.easy-proxy/config.json`
71
+
72
+ ```jsonc
73
+ {
74
+ "enabled": true, // 加速总开关
75
+ "doh": {
76
+ "bootstrapIp": "8.8.8.8", // DoH 引导 IP
77
+ "host": "dns.google", // DoH 服务器域名
78
+ "path": "/resolve", // DoH 解析路径
79
+ "port": 443 // DoH 端口
80
+ },
81
+ "ipCacheTtl": 600, // IP 缓存时间(秒),默认 10 分钟
82
+ "sniRewrite": {
83
+ "enabled": false, // SNI 改写开关(默认关闭)
84
+ "defaultSni": null, // 默认 SNI 域名
85
+ "sniMap": {} // 域名 → SNI 映射
86
+ },
87
+ "hosts": [], // 手动覆盖列表
88
+ "upstreamProxies": [], // 上游代理列表
89
+ "proxyBypassDomains": null, // 跳过代理的域名列表(null 表示默认 150+ 条)
90
+ "interceptEnabled": true, // 拦截规则总开关
91
+ "interceptRules": {} // 自定义拦截规则(空对象使用默认 80+ 条)
92
+ }
93
+ ```
94
+
95
+ **配置说明 / 热重载 / 验证规则** → [docs/architecture.md](docs/architecture.md)
96
+
97
+ ## Web 管理面板
98
+
99
+ 启动 whistle 后访问 `http://local.whistlejs.com/plugin.easy-proxy/`,可以:
100
+
101
+ | 功能 | 说明 |
102
+ |------|------|
103
+ | 加速总开关 | 一键开关 GitHub 加速 |
104
+ | 域名列表 | 查看所有 GitHub 域名的解析 IP 和延迟 |
105
+ | 测速 | 并行测试所有候选 IP,一键选用最优 |
106
+ | 手动覆盖 | Web 界面添加/删除域名→IP 覆盖 |
107
+ | SNI 映射 | Web 界面管理 SNI 改写映射 |
108
+ | 刷新 IP | 刷新全部或单个域名的 DNS 解析缓存 |
109
+
110
+ **Web 面板 API 列表** → [docs/cli.md#web-面板-api](docs/cli.md)
111
+
112
+ ## 加速原理
113
+
114
+ ```
115
+ 用户请求 github.com
116
+ ↓
117
+ ┌─ whistle host:// 规则(覆盖 95% 场景)──────────┐
118
+ │ DoH 解析真实 IP(绕过 DNS 污染) │
119
+ │ 映射到降速域名,走国内 CDN 或最优路由 │
120
+ └────────────────────────────────────────────────┘
121
+ ↓(可选)
122
+ ┌─ SNI 改写钩子(覆盖剩余 5% 场景)──────────────┐
123
+ │ 直连真实 IP + 改写 ClientHello SNI │
124
+ └────────────────────────────────────────────────┘
125
+ ↓
126
+ TCP 隧道 → IP 直连 + TLS 握手
127
+ ```
128
+
129
+ **完整原理 / IP 选择策略 / 技术细节** → [docs/architecture.md](docs/architecture.md)
130
+
131
+ ## 故障排查
132
+
133
+ 最常见的 3 个问题:
134
+
135
+ | 问题 | 快速解决 |
136
+ |------|---------|
137
+ | GitHub 无法访问 | `easy-proxy start --init && easy-proxy accel on && easy-proxy proxy on` |
138
+ | 访问速度慢 | `easy-proxy accel dns` 刷新缓存,或在 Web 面板测速选 IP |
139
+ | 证书不受信任 | `easy-proxy cert install` 重新安装根证书 |
140
+
141
+ **完整故障排查** → [docs/troubleshooting.md](docs/troubleshooting.md)
142
+
143
+ ## 与其他 whistle 插件配合
144
+
145
+ `w2` 命令会随 whistle.easy-proxy 一起安装:
146
+
147
+ ```bash
148
+ w2 install whistle.chii # 远程调试(替代 Chrome DevTools)
149
+ w2 install whistle.script # 脚本扩展(自定义请求/响应修改)
150
+ w2 install whistle.vase # Mock 数据服务
151
+ ```
152
+
153
+ ## 贡献
154
+
155
+ 欢迎贡献代码!请参阅 [贡献指南](docs/contributing.md) 了解开发环境设置、代码规范和提交流程。
156
+
157
+ ## License
158
+
159
+ MIT
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const { registerAllCommands } = require('../lib/cli/commands');
5
+
6
+ program
7
+ .name('easy-proxy')
8
+ .description('EasyProxy CLI — GitHub 加速、SNI 改写、Hosts 管理')
9
+ .version('1.0.0');
10
+
11
+ // 注册所有命令
12
+ registerAllCommands(program);
13
+
14
+ program.parse(process.argv);
package/index.js ADDED
@@ -0,0 +1,125 @@
1
+ const { loadConfig } = require('./lib/config');
2
+ const resolver = require('./lib/plugin/resolver');
3
+ const ui = require('./lib/plugin/ui');
4
+ const sni = require('./lib/plugin/sni-agent');
5
+ const ruleEngine = require('./lib/plugin/rule-engine');
6
+ const ruleActions = require('./lib/plugin/rule-actions');
7
+
8
+ function extractHost(fullUrl) {
9
+ if (!fullUrl) return null;
10
+ try {
11
+ let u = fullUrl;
12
+ if (!/^https?:\/\//i.test(u)) u = `http://${u.replace(/^\/\//, '')}`;
13
+ const url = new URL(u);
14
+ return url.hostname;
15
+ } catch (_e) {
16
+ const m = /(?:https?:\/\/)?([^/:]+)/i.exec(fullUrl);
17
+ return m ? m[1] : null;
18
+ }
19
+ }
20
+
21
+ async function handleProxy(req, client, next, _options, config) {
22
+ const oreq = req.originalReq || {};
23
+ const fullUrl = oreq.fullUrl || oreq.url || req.url || '';
24
+ const host = extractHost(fullUrl);
25
+ if (!host) return next();
26
+
27
+ const path = ruleEngine.extractPath(fullUrl);
28
+
29
+ // 1. 拦截规则(高优先级)
30
+ if (config.interceptEnabled !== false) {
31
+ const rules = config.interceptRules || require('./lib/plugin/default-rules');
32
+ const matched = ruleEngine.matchRules(host, path, rules);
33
+ if (matched) {
34
+ const ctx = { host: host, path: path, config: config, resolver: resolver, sni: sni };
35
+ try {
36
+ await ruleActions.applyActions(req, client, next, matched, ctx);
37
+ } catch (_e) {
38
+ // 动作执行失败,放行
39
+ if (typeof next === 'function') return next();
40
+ }
41
+ return;
42
+ }
43
+ }
44
+
45
+ // 2. GitHub 加速:DoH 解析真实 IP,绕过 DNS 污染
46
+ if (!config.enabled) return next();
47
+ if (!ui.isTarget(host)) return next();
48
+
49
+ const ip = resolver.resolve(host);
50
+ if (!ip) return next();
51
+
52
+ // 可选:SNI 改写模式,直连真实 IP,改写 ClientHello SNI
53
+ if (config.sniRewrite?.enabled) {
54
+ const sniVal = config.sniRewrite.sniMap?.[host] || config.sniRewrite.defaultSni || host;
55
+ return sni.forward(req, { domain: host, ip, port: 443, servername: sniVal });
56
+ }
57
+
58
+ // 默认:whistle host:// 映射(推荐方式)
59
+ req.setReqRules(`${host} host://${ip}`);
60
+ req.passThrough();
61
+ }
62
+
63
+ // === Whistle plugin exports ===
64
+
65
+ // UI server hook: management dashboard + REST API
66
+ exports.uiServer = (server, options) => {
67
+ const config = loadConfig(options);
68
+ resolver.setConfig(config);
69
+
70
+ server.on('request', (req, client) => {
71
+ try {
72
+ handleUi(req, client, options, config);
73
+ } catch (_e) {
74
+ client.statusCode = 500;
75
+ client.end('Internal Server Error');
76
+ }
77
+ });
78
+ };
79
+
80
+ function isUiRequest(req) {
81
+ const oreq = req.originalReq || {};
82
+ if (oreq.isUIRequest) return true;
83
+ const url = req.url || '';
84
+ return url.startsWith('/plugin.') || url.startsWith('/api/') || url === '/' || url === '';
85
+ }
86
+
87
+ function handleUi(req, client, options, config) {
88
+ const url = (req.url || '/').split('?')[0];
89
+ if (url.startsWith('/api/')) {
90
+ return ui.handleApi(req, client, options, config);
91
+ }
92
+ client.statusCode = 200;
93
+ client.setHeader('content-type', 'text/html; charset=utf-8');
94
+ client.end(ui.renderDashboard(config));
95
+ }
96
+
97
+ // 使用 init hook 而不是 server hook,避免 whistle 的隧道处理干扰
98
+ // 这是关键修复:exports.server 会导致 whistle 的 HTTPS 隧道代理规则(如 proxy://)失效
99
+ // 而 exports.init 有相同的参数和功能,但不会干扰 HTTPS 代理
100
+ exports.init = (server, options) => {
101
+ const config = loadConfig(options);
102
+ resolver.setConfig(config);
103
+
104
+ server.on('request', (req, client, next) => {
105
+ try {
106
+ if (isUiRequest(req)) return handleUi(req, client, options, config);
107
+ } catch (_e) {
108
+ /* fall through */
109
+ }
110
+ handleProxy(req, client, next, options, config).catch(() => {
111
+ if (typeof next === 'function') return next();
112
+ });
113
+ });
114
+
115
+ server.on('upgrade', (_req, _client, next) => {
116
+ if (typeof next === 'function') next();
117
+ });
118
+ };
119
+
120
+ // Expose for CLI usage
121
+ exports.extractHost = extractHost;
122
+ exports.config = { loadConfig, saveConfig: require('./lib/config').saveConfig };
123
+ exports.resolver = resolver;
124
+ exports.ruleEngine = ruleEngine;
125
+ exports.ruleActions = ruleActions;
@@ -0,0 +1,211 @@
1
+ /**
2
+ * 证书状态检查模块
3
+ * 检查 whistle 根证书是否存在、是否已安装到系统信任库、以及有效期。
4
+ *
5
+ * whistle 证书路径: ~/.WhistleAppData/.whistle/certs/root.crt
6
+ * macOS 信任检查: security find-certificate
7
+ * Windows 信任检查: certutil
8
+ * 有效期解析: 优先用 openssl(macOS 内置),回退到 node-forge
9
+ */
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const os = require('node:os');
14
+ const { execSync } = require('node:child_process');
15
+
16
+ const HOME = os.homedir();
17
+ const IS_MACOS = os.platform() === 'darwin';
18
+ const IS_WINDOWS = os.platform() === 'win32';
19
+
20
+ // whistle 默认证书路径(w2 start 生成的路径)
21
+ const CERT_PATHS = [
22
+ path.join(HOME, '.WhistleAppData', '.whistle', 'certs', 'root.crt'),
23
+ path.join(HOME, '.WhistleAppData', '.whistle', 'certs', 'root.cer'),
24
+ ];
25
+
26
+ /**
27
+ * 查找 whistle 根证书文件
28
+ * @returns {string|null}
29
+ */
30
+ function findCertFile() {
31
+ for (var i = 0; i < CERT_PATHS.length; i++) {
32
+ if (fs.existsSync(CERT_PATHS[i])) {
33
+ return CERT_PATHS[i];
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ /**
40
+ * 用 openssl 解析证书有效期
41
+ * @param {string} certPath
42
+ * @returns {{notBefore: Date, notAfter: Date, subject: string}|null}
43
+ */
44
+ function parseCertWithOpenssl(certPath) {
45
+ try {
46
+ var subject = execSync(`openssl x509 -in "${certPath}" -noout -subject`, {
47
+ encoding: 'utf-8',
48
+ }).trim();
49
+ var dates = execSync(`openssl x509 -in "${certPath}" -noout -dates`, {
50
+ encoding: 'utf-8',
51
+ }).trim();
52
+ var notBeforeMatch = dates.match(/notBefore=(.+)/);
53
+ var notAfterMatch = dates.match(/notAfter=(.+)/);
54
+ if (!notAfterMatch) return null;
55
+ return {
56
+ subject: subject,
57
+ notBefore: notBeforeMatch ? new Date(notBeforeMatch[1]) : null,
58
+ notAfter: new Date(notAfterMatch[1]),
59
+ };
60
+ } catch (_e) {
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * 用 node-forge 解析证书有效期(回退方案)
67
+ * @param {string} certPath
68
+ * @returns {{notBefore: Date, notAfter: Date, subject: string}|null}
69
+ */
70
+ function parseCertWithForge(certPath) {
71
+ try {
72
+ var forge = require('node-forge');
73
+ var pem = fs.readFileSync(certPath, 'utf-8');
74
+ var cert = forge.pki.certificateFromPem(pem);
75
+ var cn = cert.subject.attributes.find((a) => a.name === 'commonName');
76
+ return {
77
+ subject: cn ? cn.value : '',
78
+ notBefore: cert.validity.notBefore,
79
+ notAfter: cert.validity.notAfter,
80
+ };
81
+ } catch (_e) {
82
+ return null;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 检查证书是否已安装到系统信任库
88
+ * @param {string} subject - 证书 CN(用于搜索)
89
+ * @returns {boolean}
90
+ */
91
+ function checkTrusted(subject) {
92
+ if (IS_MACOS) {
93
+ // 检查 login keychain 和 system keychain
94
+ try {
95
+ var keyword = subject ? subject.split('.')[0] : 'whistle';
96
+ execSync(`security find-certificate -c "${keyword}" 2>/dev/null`, { encoding: 'utf-8' });
97
+ return true;
98
+ } catch (_e) {
99
+ // 在 login keychain 中未找到,检查 system keychain
100
+ try {
101
+ execSync(
102
+ 'security find-certificate -c "whistle" /Library/Keychains/System.keychain 2>/dev/null',
103
+ { encoding: 'utf-8' }
104
+ );
105
+ return true;
106
+ } catch (_e2) {
107
+ return false;
108
+ }
109
+ }
110
+ }
111
+ if (IS_WINDOWS) {
112
+ try {
113
+ execSync('certutil -store Root "whistle"', { encoding: 'utf-8', windowsHide: true });
114
+ return true;
115
+ } catch (_e) {
116
+ return false;
117
+ }
118
+ }
119
+ return false;
120
+ }
121
+
122
+ // ANSI 颜色(与 bin CLI 一致)
123
+ var c = {
124
+ reset: '\x1b[0m',
125
+ bold: '\x1b[1m',
126
+ green: '\x1b[32m',
127
+ red: '\x1b[31m',
128
+ blue: '\x1b[34m',
129
+ yellow: '\x1b[33m',
130
+ gray: '\x1b[90m',
131
+ };
132
+
133
+ /**
134
+ * 检查证书状态并输出
135
+ * @param {boolean} [compact] - 紧凑模式(用于 health 命令,不输出标题)
136
+ * @returns {Promise<void>}
137
+ */
138
+ async function checkCertStatus(compact) {
139
+ var certPath = findCertFile();
140
+ if (!certPath) {
141
+ console.log(`${(compact ? ' ' : '') + c.red}✖${c.reset} 证书: 未找到(whistle 可能未启动过)`);
142
+ if (!compact) {
143
+ console.log(`${c.gray} 运行 easy-proxy start 启动 whistle(会自动生成证书)${c.reset}`);
144
+ console.log(`${c.gray} 然后运行 easy-proxy cert install 安装证书到系统信任库${c.reset}`);
145
+ }
146
+ return;
147
+ }
148
+
149
+ // 解析有效期
150
+ var info = parseCertWithOpenssl(certPath) || parseCertWithForge(certPath);
151
+ if (!info) {
152
+ console.log(`${(compact ? ' ' : '') + c.yellow}⚠${c.reset} 证书: 已存在(无法解析有效期)`);
153
+ console.log(`${c.gray} 路径: ${certPath}${c.reset}`);
154
+ return;
155
+ }
156
+
157
+ var now = new Date();
158
+ var daysLeft = Math.floor((info.notAfter.getTime() - now.getTime()) / (24 * 60 * 60 * 1000));
159
+ var trusted = checkTrusted(info.subject);
160
+ var dateStr = info.notAfter.toISOString().slice(0, 10);
161
+
162
+ // 状态判断
163
+ var status, color;
164
+ if (daysLeft <= 0) {
165
+ status = `已过期(超过 ${-daysLeft} 天)`;
166
+ color = c.red;
167
+ } else if (daysLeft <= 30) {
168
+ status = `即将过期(剩余 ${daysLeft} 天)`;
169
+ color = c.yellow;
170
+ } else {
171
+ status = `有效(剩余 ${daysLeft} 天)`;
172
+ color = c.green;
173
+ }
174
+
175
+ var trustedStr = trusted ? `${c.green}✓ 已信任` : `${c.yellow}⚠ 未信任${c.reset}`;
176
+
177
+ if (compact) {
178
+ console.log(
179
+ ' ' +
180
+ color +
181
+ (daysLeft <= 0 ? '✖' : daysLeft <= 30 ? '⚠' : '✓') +
182
+ c.reset +
183
+ ' 证书: ' +
184
+ status +
185
+ c.gray +
186
+ ',过期 ' +
187
+ dateStr +
188
+ c.reset +
189
+ ' — ' +
190
+ trustedStr
191
+ );
192
+ } else {
193
+ console.log(`${c.bold + c.blue}证书状态${c.reset}`);
194
+ console.log(c.gray + '─'.repeat(40) + c.reset);
195
+ console.log(`${c.blue}ℹ${c.reset} 状态: ${color}${status}${c.reset}`);
196
+ console.log(`${c.blue}ℹ${c.reset} 过期时间: ${dateStr}`);
197
+ console.log(`${c.blue}ℹ${c.reset} 信任: ${trustedStr}`);
198
+ console.log(`${c.blue}ℹ${c.reset} 路径: ${c.gray}${certPath}${c.reset}`);
199
+ if (info.subject) {
200
+ console.log(`${c.blue}ℹ${c.reset} 颁发对象: ${c.gray}${info.subject}${c.reset}`);
201
+ }
202
+ console.log(c.gray + '─'.repeat(40) + c.reset);
203
+ if (daysLeft <= 0) {
204
+ console.log(`${c.gray} 运行 easy-proxy cert install 重新安装证书${c.reset}`);
205
+ } else if (!trusted) {
206
+ console.log(`${c.gray} 运行 easy-proxy cert install 安装证书到系统信任库${c.reset}`);
207
+ }
208
+ }
209
+ }
210
+
211
+ module.exports = { checkCertStatus, findCertFile, parseCertWithOpenssl };