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 +21 -0
- package/README.md +159 -0
- package/bin/easy-proxy.js +14 -0
- package/index.js +125 -0
- package/lib/cert-check.js +211 -0
- package/lib/cli/bypass-domains.js +187 -0
- package/lib/cli/commands/accel.js +184 -0
- package/lib/cli/commands/cert.js +35 -0
- package/lib/cli/commands/config-cmd.js +50 -0
- package/lib/cli/commands/health.js +53 -0
- package/lib/cli/commands/host.js +79 -0
- package/lib/cli/commands/index.js +31 -0
- package/lib/cli/commands/intercept.js +222 -0
- package/lib/cli/commands/network.js +32 -0
- package/lib/cli/commands/proxy.js +279 -0
- package/lib/cli/commands/rule.js +352 -0
- package/lib/cli/commands/start.js +98 -0
- package/lib/cli/commands/status.js +50 -0
- package/lib/cli/helpers.js +137 -0
- package/lib/cli/proxy-config.js +41 -0
- package/lib/cli/rule-api.js +230 -0
- package/lib/cli/rule-editor.js +362 -0
- package/lib/cli/rules-file.js +121 -0
- package/lib/cli/upstream-editor.js +218 -0
- package/lib/cli/upstream-proxy.js +74 -0
- package/lib/config.js +204 -0
- package/lib/debug.js +17 -0
- package/lib/network-utils.js +56 -0
- package/lib/plugin/default-hosts.js +48 -0
- package/lib/plugin/default-rules.js +668 -0
- package/lib/plugin/resolver.js +228 -0
- package/lib/plugin/rule-actions.js +344 -0
- package/lib/plugin/rule-engine.js +232 -0
- package/lib/plugin/rule-generator.js +100 -0
- package/lib/plugin/sni-agent.js +54 -0
- package/lib/plugin/ui.js +430 -0
- package/lib/proxy-check.js +67 -0
- package/package.json +64 -0
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
|
+
[](https://www.npmjs.com/package/whistle.easy-proxy)
|
|
6
|
+
[](https://github.com/sqlwwx/easy-proxy/blob/master/LICENSE)
|
|
7
|
+
[](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 };
|