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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 上游代理配置编辑器解析模块
|
|
3
|
+
* 纯函数:parse / format / validate / buildEditContent
|
|
4
|
+
*
|
|
5
|
+
* 文本格式约定:
|
|
6
|
+
* - 每个代理占一块,第1行=名称,第2行=URL,第3行起=域名列表
|
|
7
|
+
* - 多个代理之间用空行分隔
|
|
8
|
+
* - 以 # 开头的行是注释(整行),会被忽略
|
|
9
|
+
* - 支持 \r\n 和 \n 换行
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ---- 工具函数 ----
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 域名去重(不区分大小写,保留首次出现的原写形式)
|
|
16
|
+
* @param {string[]} domains
|
|
17
|
+
* @returns {string[]}
|
|
18
|
+
*/
|
|
19
|
+
function dedupeDomains(domains) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const result = [];
|
|
22
|
+
for (const d of domains) {
|
|
23
|
+
const key = d.toLowerCase();
|
|
24
|
+
if (!seen.has(key)) {
|
|
25
|
+
seen.add(key);
|
|
26
|
+
result.push(d);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 将文本分割为「原始行号 + 内容」的数组,同时处理 \r\n 和注释过滤
|
|
34
|
+
* 保留原始行号用于错误提示
|
|
35
|
+
* @param {string} text
|
|
36
|
+
* @returns {Array<{lineNo: number, content: string}>}
|
|
37
|
+
*/
|
|
38
|
+
function splitLinesWithNumbers(text) {
|
|
39
|
+
const rawLines = text.replace(/\r/g, '').split('\n');
|
|
40
|
+
const result = [];
|
|
41
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
42
|
+
const line = rawLines[i].trim();
|
|
43
|
+
// 过滤整行注释
|
|
44
|
+
if (line.startsWith('#')) continue;
|
|
45
|
+
result.push({ lineNo: i + 1, content: line });
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- 核心函数 ----
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 解析文本格式的上游代理配置
|
|
54
|
+
* @param {string} text - 编辑器中的文本内容
|
|
55
|
+
* @returns {Array<{name: string, url: string, domains: string[]}>}
|
|
56
|
+
* @throws {Error} 格式错误时抛出,错误信息包含具体位置和原因
|
|
57
|
+
*/
|
|
58
|
+
function parse(text) {
|
|
59
|
+
if (!text || !text.trim()) return [];
|
|
60
|
+
|
|
61
|
+
const lines = splitLinesWithNumbers(text);
|
|
62
|
+
const proxies = [];
|
|
63
|
+
let i = 0;
|
|
64
|
+
|
|
65
|
+
while (i < lines.length) {
|
|
66
|
+
// 跳过连续空行
|
|
67
|
+
if (lines[i].content === '') {
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 第 1 行:代理名称
|
|
73
|
+
const nameLine = lines[i];
|
|
74
|
+
const name = nameLine.content;
|
|
75
|
+
if (!name) {
|
|
76
|
+
throw new Error(`第 ${nameLine.lineNo} 行: 代理名称不能为空`);
|
|
77
|
+
}
|
|
78
|
+
i++;
|
|
79
|
+
|
|
80
|
+
// 第 2 行:代理 URL
|
|
81
|
+
if (i >= lines.length || lines[i].content === '') {
|
|
82
|
+
throw new Error(`第 ${nameLine.lineNo} 行: 代理 "${name}" 缺少 URL`);
|
|
83
|
+
}
|
|
84
|
+
const urlLine = lines[i];
|
|
85
|
+
const url = urlLine.content;
|
|
86
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
87
|
+
throw new Error(`第 ${urlLine.lineNo} 行: 代理 URL 必须以 http:// 或 https:// 开头`);
|
|
88
|
+
}
|
|
89
|
+
i++;
|
|
90
|
+
|
|
91
|
+
// 第 3 行起:域名列表(直到空行或结尾)
|
|
92
|
+
const domains = [];
|
|
93
|
+
while (i < lines.length && lines[i].content !== '') {
|
|
94
|
+
const domain = lines[i].content;
|
|
95
|
+
domains.push(domain);
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
proxies.push({
|
|
100
|
+
name,
|
|
101
|
+
url,
|
|
102
|
+
domains: dedupeDomains(domains),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 名称唯一性校验(不区分大小写)
|
|
107
|
+
const nameSet = new Set();
|
|
108
|
+
for (const p of proxies) {
|
|
109
|
+
const key = p.name.toLowerCase();
|
|
110
|
+
if (nameSet.has(key)) {
|
|
111
|
+
throw new Error(`代理名称重复: "${p.name}"(名称不区分大小写)`);
|
|
112
|
+
}
|
|
113
|
+
nameSet.add(key);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return proxies;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 将代理数组格式化为文本(不含注释头部)
|
|
121
|
+
* @param {Array<{name: string, url: string, domains: string[]}>} proxies
|
|
122
|
+
* @returns {string}
|
|
123
|
+
*/
|
|
124
|
+
function format(proxies) {
|
|
125
|
+
if (!proxies || proxies.length === 0) return '';
|
|
126
|
+
|
|
127
|
+
const blocks = proxies.map((p) => {
|
|
128
|
+
const lines = [p.name, p.url];
|
|
129
|
+
if (p.domains && p.domains.length > 0) {
|
|
130
|
+
lines.push(...p.domains);
|
|
131
|
+
}
|
|
132
|
+
return lines.join('\n');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return blocks.join('\n\n');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 校验代理数组合法性
|
|
140
|
+
* @param {Array<{name: string, url: string, domains: string[]}>} proxies
|
|
141
|
+
* @returns {Array} 返回校验后的 proxies(域名已去重)
|
|
142
|
+
* @throws {Error} 校验失败时抛出
|
|
143
|
+
*/
|
|
144
|
+
function validate(proxies) {
|
|
145
|
+
if (!Array.isArray(proxies)) {
|
|
146
|
+
throw new Error('代理配置必须是数组');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const nameSet = new Set();
|
|
150
|
+
|
|
151
|
+
return proxies.map((proxy, idx) => {
|
|
152
|
+
const position = `第 ${idx + 1} 个代理`;
|
|
153
|
+
|
|
154
|
+
// 名称校验
|
|
155
|
+
const name = (proxy.name || '').trim();
|
|
156
|
+
if (!name) {
|
|
157
|
+
throw new Error(`${position}: 代理名称不能为空`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// URL 校验
|
|
161
|
+
const url = (proxy.url || '').trim();
|
|
162
|
+
if (!url) {
|
|
163
|
+
throw new Error(`${position}: 代理 URL 不能为空`);
|
|
164
|
+
}
|
|
165
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
166
|
+
throw new Error(`${position}: 代理 URL 必须以 http:// 或 https:// 开头`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 名称唯一性(不区分大小写)
|
|
170
|
+
const key = name.toLowerCase();
|
|
171
|
+
if (nameSet.has(key)) {
|
|
172
|
+
throw new Error(`代理名称重复: "${name}"(名称不区分大小写)`);
|
|
173
|
+
}
|
|
174
|
+
nameSet.add(key);
|
|
175
|
+
|
|
176
|
+
// 域名 trim + 去重
|
|
177
|
+
const domains = dedupeDomains(
|
|
178
|
+
(proxy.domains || []).map((d) => (d || '').trim()).filter(Boolean),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
return { name, url, domains };
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 构建编辑器内容(带注释头部 + 当前配置)
|
|
187
|
+
* @param {Array<{name: string, url: string, domains: string[]}>} proxies
|
|
188
|
+
* @returns {string}
|
|
189
|
+
*/
|
|
190
|
+
function buildEditContent(proxies) {
|
|
191
|
+
const header = [
|
|
192
|
+
'# EasyProxy 上游代理配置',
|
|
193
|
+
'# 格式说明:',
|
|
194
|
+
'# - 每个代理占一块,第一行是代理名称,第二行是代理地址,第三行起是域名列表',
|
|
195
|
+
'# - 多个代理之间用空行分隔',
|
|
196
|
+
'# - 以 # 开头的行是注释,会被忽略',
|
|
197
|
+
'# 示例:',
|
|
198
|
+
'# corp-proxy',
|
|
199
|
+
'# http://10.0.0.1:8080',
|
|
200
|
+
'# github.com',
|
|
201
|
+
'# google.com',
|
|
202
|
+
'#',
|
|
203
|
+
'# home-proxy',
|
|
204
|
+
'# http://192.168.1.100:3128',
|
|
205
|
+
'# internal.company.com',
|
|
206
|
+
].join('\n');
|
|
207
|
+
|
|
208
|
+
const body = format(proxies);
|
|
209
|
+
if (!body) return header + '\n';
|
|
210
|
+
return header + '\n\n' + body;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
parse,
|
|
215
|
+
format,
|
|
216
|
+
validate,
|
|
217
|
+
buildEditContent,
|
|
218
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const _fs = require('node:fs');
|
|
2
|
+
const _path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { loadConfig, saveConfig } = require('../config');
|
|
5
|
+
|
|
6
|
+
const RULESET_NAME = 'upstream-proxies';
|
|
7
|
+
|
|
8
|
+
function loadProxies() {
|
|
9
|
+
try {
|
|
10
|
+
const config = loadConfig(null);
|
|
11
|
+
return Array.isArray(config.upstreamProxies) ? config.upstreamProxies : [];
|
|
12
|
+
} catch (e) {
|
|
13
|
+
console.warn('读取上游代理配置失败:', e.message);
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function saveProxies(proxies) {
|
|
19
|
+
try {
|
|
20
|
+
const config = loadConfig(null);
|
|
21
|
+
config.upstreamProxies = proxies;
|
|
22
|
+
saveConfig(null, config);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.warn('保存上游代理配置失败:', e.message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 将代理 URL 转换为 whistle 代理格式
|
|
30
|
+
* 使用 proxy:// 协议格式,支持 HTTP 和 HTTPS CONNECT 隧道
|
|
31
|
+
*/
|
|
32
|
+
function toProxyProtocol(url) {
|
|
33
|
+
return url.replace(/^https?:\/\//, 'proxy://');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 根据配置生成 whistle 规则文本
|
|
38
|
+
*/
|
|
39
|
+
function generateRules(proxies) {
|
|
40
|
+
const lines = ['# EasyProxy 上游代理规则(自动生成,请勿手动编辑)'];
|
|
41
|
+
let hasRules = false;
|
|
42
|
+
proxies.forEach((proxy) => {
|
|
43
|
+
const proxyUrl = toProxyProtocol(proxy.url);
|
|
44
|
+
proxy.domains.forEach((domain) => {
|
|
45
|
+
lines.push(`${domain} ${proxyUrl}`);
|
|
46
|
+
hasRules = true;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
if (!hasRules) {
|
|
50
|
+
lines.push('# (暂无规则 — 使用 easy-proxy proxy upstream edit 添加)');
|
|
51
|
+
}
|
|
52
|
+
return lines.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 同步上游代理规则到 whistle
|
|
57
|
+
* 需要 whistle 运行中,通过 rule-api 写入规则集到系统规则分组
|
|
58
|
+
*/
|
|
59
|
+
async function syncToWhistle(ruleApi, proxies) {
|
|
60
|
+
const list = proxies || loadProxies();
|
|
61
|
+
const rules = generateRules(list);
|
|
62
|
+
await ruleApi.createGroup(ruleApi.SYSTEM_GROUP);
|
|
63
|
+
await ruleApi.addRuleset(RULESET_NAME, rules, ruleApi.SYSTEM_GROUP);
|
|
64
|
+
await ruleApi.selectRuleset(RULESET_NAME, rules);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
RULESET_NAME,
|
|
69
|
+
loadProxies,
|
|
70
|
+
saveProxies,
|
|
71
|
+
toProxyProtocol,
|
|
72
|
+
generateRules,
|
|
73
|
+
syncToWhistle,
|
|
74
|
+
};
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const os = require('node:os');
|
|
4
|
+
|
|
5
|
+
const { debug } = require('./debug');
|
|
6
|
+
|
|
7
|
+
const HOME = os.homedir();
|
|
8
|
+
const CONFIG_DIR = path.join(HOME, '.easy-proxy');
|
|
9
|
+
const CONFIG_FILE = 'config.json';
|
|
10
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, CONFIG_FILE);
|
|
11
|
+
|
|
12
|
+
const defaultRules = require('./plugin/default-rules');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONFIG = {
|
|
15
|
+
enabled: true,
|
|
16
|
+
interceptEnabled: true,
|
|
17
|
+
interceptRules: defaultRules,
|
|
18
|
+
doh: { bootstrapIp: '8.8.8.8', host: 'dns.google', path: '/resolve', port: 443 },
|
|
19
|
+
ipCacheTtl: 600,
|
|
20
|
+
sniRewrite: { enabled: false, defaultSni: null, sniMap: {} },
|
|
21
|
+
hosts: [],
|
|
22
|
+
upstreamProxies: [],
|
|
23
|
+
proxyBypassDomains: null,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function ensureDir() {
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
29
|
+
} catch (e) {
|
|
30
|
+
debug('ensureDir failed:', e.message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 读取配置文件。文件是唯一数据源,whistle storage 仅做同步备份。
|
|
36
|
+
*/
|
|
37
|
+
function loadConfig(options) {
|
|
38
|
+
ensureDir();
|
|
39
|
+
|
|
40
|
+
let fileConfig = null;
|
|
41
|
+
try {
|
|
42
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
43
|
+
fileConfig = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
debug('loadConfig: parse config file failed:', e.message);
|
|
47
|
+
fileConfig = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (fileConfig) {
|
|
51
|
+
// 有文件 → 同步到 whistle storage(备份)
|
|
52
|
+
try {
|
|
53
|
+
if (options?.storage) {
|
|
54
|
+
options.storage.setProperty('config', JSON.stringify(fileConfig));
|
|
55
|
+
}
|
|
56
|
+
} catch (e) {
|
|
57
|
+
debug('loadConfig: sync to whistle storage failed:', e.message);
|
|
58
|
+
}
|
|
59
|
+
const merged = deepMerge(JSON.parse(JSON.stringify(DEFAULT_CONFIG)), fileConfig);
|
|
60
|
+
const errors = validateConfig(merged);
|
|
61
|
+
if (errors.length > 0) {
|
|
62
|
+
console.warn('配置验证警告:', errors.join(', '));
|
|
63
|
+
}
|
|
64
|
+
return merged;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 文件不存在 → 尝试从 whistle storage 恢复
|
|
68
|
+
let stored = null;
|
|
69
|
+
if (options?.storage) {
|
|
70
|
+
try {
|
|
71
|
+
const raw = options.storage.getProperty('config');
|
|
72
|
+
if (raw) stored = JSON.parse(raw);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
debug('loadConfig: read from whistle storage failed:', e.message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (stored) {
|
|
79
|
+
try {
|
|
80
|
+
ensureDir();
|
|
81
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(stored, null, 2));
|
|
82
|
+
} catch (e) {
|
|
83
|
+
debug('loadConfig: restore config from whistle storage to file failed:', e.message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return deepMerge(JSON.parse(JSON.stringify(DEFAULT_CONFIG)), stored || {});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function saveConfig(options, config) {
|
|
91
|
+
ensureDir();
|
|
92
|
+
const text = JSON.stringify(config, null, 2);
|
|
93
|
+
try {
|
|
94
|
+
if (options?.storage) {
|
|
95
|
+
options.storage.setProperty('config', text);
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
debug('saveConfig: sync to whistle storage failed:', e.message);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
fs.writeFileSync(CONFIG_PATH, text);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
debug('saveConfig: write config file failed:', e.message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function deepMerge(base, over) {
|
|
108
|
+
if (Array.isArray(base)) return Array.isArray(over) ? over : base;
|
|
109
|
+
if (base && typeof base === 'object') {
|
|
110
|
+
const out = Object.assign({}, base);
|
|
111
|
+
Object.keys(over || {}).forEach((k) => {
|
|
112
|
+
out[k] = k in out ? deepMerge(out[k], over[k]) : over[k];
|
|
113
|
+
});
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
return over === undefined ? base : over;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateConfig(config) {
|
|
120
|
+
const errors = [];
|
|
121
|
+
|
|
122
|
+
if (typeof config.enabled !== 'boolean') {
|
|
123
|
+
errors.push('enabled 必须是布尔值');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (config.doh) {
|
|
127
|
+
if (typeof config.doh !== 'object') {
|
|
128
|
+
errors.push('doh 必须是对象');
|
|
129
|
+
} else {
|
|
130
|
+
if (config.doh.bootstrapIp && typeof config.doh.bootstrapIp !== 'string') {
|
|
131
|
+
errors.push('doh.bootstrapIp 必须是字符串');
|
|
132
|
+
}
|
|
133
|
+
if (config.doh.host && typeof config.doh.host !== 'string') {
|
|
134
|
+
errors.push('doh.host 必须是字符串');
|
|
135
|
+
}
|
|
136
|
+
if (config.doh.path && typeof config.doh.path !== 'string') {
|
|
137
|
+
errors.push('doh.path 必须是字符串');
|
|
138
|
+
}
|
|
139
|
+
if (config.doh.port && typeof config.doh.port !== 'number') {
|
|
140
|
+
errors.push('doh.port 必须是数字');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (config.ipCacheTtl && typeof config.ipCacheTtl !== 'number') {
|
|
146
|
+
errors.push('ipCacheTtl 必须是数字');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (config.sniRewrite) {
|
|
150
|
+
if (typeof config.sniRewrite !== 'object') {
|
|
151
|
+
errors.push('sniRewrite 必须是对象');
|
|
152
|
+
} else {
|
|
153
|
+
if (config.sniRewrite.enabled && typeof config.sniRewrite.enabled !== 'boolean') {
|
|
154
|
+
errors.push('sniRewrite.enabled 必须是布尔值');
|
|
155
|
+
}
|
|
156
|
+
if (config.sniRewrite.defaultSni && typeof config.sniRewrite.defaultSni !== 'string') {
|
|
157
|
+
errors.push('sniRewrite.defaultSni 必须是字符串');
|
|
158
|
+
}
|
|
159
|
+
if (config.sniRewrite.sniMap && typeof config.sniRewrite.sniMap !== 'object') {
|
|
160
|
+
errors.push('sniRewrite.sniMap 必须是对象');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (config.hosts && !Array.isArray(config.hosts)) {
|
|
166
|
+
errors.push('hosts 必须是数组');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (config.upstreamProxies && !Array.isArray(config.upstreamProxies)) {
|
|
170
|
+
errors.push('upstreamProxies 必须是数组');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (config.proxyBypassDomains && !Array.isArray(config.proxyBypassDomains)) {
|
|
174
|
+
errors.push('proxyBypassDomains 必须是数组');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (config.interceptEnabled !== undefined && typeof config.interceptEnabled !== 'boolean') {
|
|
178
|
+
errors.push('interceptEnabled 必须是布尔值');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (config.interceptRules !== undefined && config.interceptRules !== null) {
|
|
182
|
+
if (typeof config.interceptRules !== 'object' || Array.isArray(config.interceptRules)) {
|
|
183
|
+
errors.push('interceptRules 必须是对象');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return errors;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 从 rules-file.js 重新导出规则集操作
|
|
191
|
+
const { listRulesets, getRuleset, saveRuleset, deleteRuleset } = require('./cli/rules-file');
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
DEFAULT_CONFIG,
|
|
195
|
+
CONFIG_PATH,
|
|
196
|
+
loadConfig,
|
|
197
|
+
saveConfig,
|
|
198
|
+
deepMerge,
|
|
199
|
+
validateConfig,
|
|
200
|
+
listRulesets,
|
|
201
|
+
getRuleset,
|
|
202
|
+
saveRuleset,
|
|
203
|
+
deleteRuleset,
|
|
204
|
+
};
|
package/lib/debug.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 简单的 debug 日志工具
|
|
3
|
+
* 通过环境变量 EASY_PROXY_DEBUG=1 开启
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEBUG_ENABLED = process.env.EASY_PROXY_DEBUG === '1';
|
|
7
|
+
|
|
8
|
+
function debug(...args) {
|
|
9
|
+
if (DEBUG_ENABLED) {
|
|
10
|
+
console.error('[DEBUG]', ...args);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
debug,
|
|
16
|
+
DEBUG_ENABLED,
|
|
17
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the primary LAN IPv4 address.
|
|
6
|
+
*/
|
|
7
|
+
function getLocalIp() {
|
|
8
|
+
const interfaces = os.networkInterfaces();
|
|
9
|
+
for (const name of Object.keys(interfaces)) {
|
|
10
|
+
const addrs = interfaces[name];
|
|
11
|
+
if (!addrs) continue;
|
|
12
|
+
for (const addr of addrs) {
|
|
13
|
+
if (addr.family === 'IPv4' && !addr.internal) {
|
|
14
|
+
return addr.address;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return '127.0.0.1';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check network latency in ms.
|
|
23
|
+
* @param {number} [port] - proxy port, omit for direct connection
|
|
24
|
+
*/
|
|
25
|
+
function checkDelay(port) {
|
|
26
|
+
const CHECK_HOST = 'www.msftconnecttest.com';
|
|
27
|
+
const CHECK_PATH = '/connecttest.txt';
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const startTime = Date.now();
|
|
31
|
+
const options = {
|
|
32
|
+
path: CHECK_PATH,
|
|
33
|
+
headers: { Host: CHECK_HOST },
|
|
34
|
+
port: 80,
|
|
35
|
+
host: CHECK_HOST,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (port) {
|
|
39
|
+
options.port = port;
|
|
40
|
+
options.host = '127.0.0.1';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const req = http.get(options, (res) => {
|
|
44
|
+
if (res.statusCode === 200) {
|
|
45
|
+
res.resume();
|
|
46
|
+
resolve(Date.now() - startTime);
|
|
47
|
+
} else {
|
|
48
|
+
res.resume();
|
|
49
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
req.on('error', reject);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { getLocalIp, checkDelay };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub-related domains that EasyProxy accelerates.
|
|
3
|
+
*
|
|
4
|
+
* `domains` supports two forms:
|
|
5
|
+
* - exact host: "github.com"
|
|
6
|
+
* - wildcard: "*.githubusercontent.com" (matches single-label subdomain)
|
|
7
|
+
*
|
|
8
|
+
* `curated` is a FALLBACK IP list used only when DoH resolution fails.
|
|
9
|
+
* These values should be refreshed periodically (GitHub/Fastly rotate IP ranges).
|
|
10
|
+
* The plugin prefers live DoH resolution at runtime.
|
|
11
|
+
*/
|
|
12
|
+
module.exports = {
|
|
13
|
+
domains: [
|
|
14
|
+
'github.com',
|
|
15
|
+
'api.github.com',
|
|
16
|
+
'gist.github.com',
|
|
17
|
+
'raw.githubusercontent.com',
|
|
18
|
+
'gist.githubusercontent.com',
|
|
19
|
+
'cloud.githubusercontent.com',
|
|
20
|
+
'camo.githubusercontent.com',
|
|
21
|
+
'avatars.githubusercontent.com',
|
|
22
|
+
'avatars0.githubusercontent.com',
|
|
23
|
+
'avatars1.githubusercontent.com',
|
|
24
|
+
'avatars2.githubusercontent.com',
|
|
25
|
+
'avatars3.githubusercontent.com',
|
|
26
|
+
'user-images.githubusercontent.com',
|
|
27
|
+
'media.githubusercontent.com',
|
|
28
|
+
'objects.githubusercontent.com',
|
|
29
|
+
'codeload.github.com',
|
|
30
|
+
'github.githubassets.com',
|
|
31
|
+
'assets-cdn.github.com',
|
|
32
|
+
'collector.github.com',
|
|
33
|
+
'desktop.githubusercontent.com',
|
|
34
|
+
'vignette.githubusercontent.com',
|
|
35
|
+
'pipelines.actions.githubusercontent.com',
|
|
36
|
+
'*.github.io',
|
|
37
|
+
],
|
|
38
|
+
|
|
39
|
+
curated: {
|
|
40
|
+
'github.com': ['20.205.243.166', '140.82.121.4'],
|
|
41
|
+
'api.github.com': ['20.205.243.166', '140.82.121.5'],
|
|
42
|
+
'raw.githubusercontent.com': ['185.199.108.133', '185.199.109.133'],
|
|
43
|
+
'objects.githubusercontent.com': ['185.199.108.133', '185.199.109.133'],
|
|
44
|
+
'avatars.githubusercontent.com': ['185.199.108.133', '185.199.109.133'],
|
|
45
|
+
'github.githubassets.com': ['185.199.108.153', '185.199.109.153'],
|
|
46
|
+
'codeload.github.com': ['20.205.243.166', '140.82.121.10'],
|
|
47
|
+
},
|
|
48
|
+
};
|