@yufengtadian/freedom-cli 1.13.0 → 1.13.2
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/README.md +96 -14
- package/lib/agents.js +318 -0
- package/lib/build.js +168 -39
- package/lib/cli.js +160 -4
- package/lib/config.js +20 -1
- package/lib/desktop.js +133 -0
- package/lib/dev.js +149 -0
- package/lib/mcp.js +217 -0
- package/lib/release.js +90 -0
- package/lib/security.js +113 -54
- package/lib/shell.js +5 -2
- package/lib/tui.js +37 -1
- package/lib/verify.js +43 -8
- package/package.json +2 -1
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/skill/freedom/SKILL.md +159 -0
- package/templates/desktop/app.html +559 -0
- package/templates/desktop/backend/desktop.mjs +317 -0
- package/templates/desktop/freedom.config.js +29 -0
- package/templates/desktop/icon.ico +0 -0
- package/templates/go/pkg/freedom/anti_debug_windows.go +114 -11
- package/templates/go/pkg/freedom/assets/freedom.js +3 -0
- package/templates/go/pkg/freedom/freedom.go +52 -9
- package/templates/go/pkg/freedom/resources.go +68 -14
- package/templates/go/pkg/freedom/securetemp.go +61 -0
- package/templates/go/pkg/freedom/securetemp_other.go +20 -0
- package/templates/go/pkg/freedom/securetemp_windows.go +25 -0
- package/templates/go/pkg/freedom/security.go +240 -61
- package/templates/go/pkg/freedom/store.go +8 -0
- package/templates/go/pkg/freedom/window_other.go +7 -1
- package/templates/go/pkg/freedom/window_windows.go +185 -17
- package/templates/installer/app.nsi +43 -0
- package/templates/project/freedom.config.js +14 -0
- package/templates/project/freedom.d.ts +77 -0
- package/templates/project-minimal/freedom.config.js +14 -0
- package/templates/project-minimal/freedom.d.ts +77 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Freedom Desktop 后端(零依赖 Node,NDJSON over stdio 进程后端)。
|
|
2
|
+
//
|
|
3
|
+
// 职责:把 freedom CLI 的能力包成界面可调用的方法。所有实际操作都经"子进程调用 CLI"完成,
|
|
4
|
+
// 而不是 require 内部模块——这样界面走的正是用户手册里那条命令路径,CLI 升级即界面升级,
|
|
5
|
+
// 且子进程输出不会污染本进程与壳之间的 NDJSON 协议通道(日志改经 event 帧转发)。
|
|
6
|
+
//
|
|
7
|
+
// 协议:入 {id, method, params:[...]};出 {id, result, error};推送 {event, data}。
|
|
8
|
+
// 日志:每次调用先回 {event:'call', data:{id, label}},再逐行 {event:'log', data:{id, line}}。
|
|
9
|
+
|
|
10
|
+
import readline from 'node:readline';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { spawn } from 'node:child_process';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
function write(msg) {
|
|
20
|
+
process.stdout.write(JSON.stringify(msg) + '\n');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ---- CLI 定位:优先 cli-entry.json(freedom desktop 打包时写入),再退到环境变量 ----
|
|
24
|
+
function cliEntry() {
|
|
25
|
+
const candidates = [];
|
|
26
|
+
try {
|
|
27
|
+
const meta = JSON.parse(fs.readFileSync(path.join(__dirname, 'cli-entry.json'), 'utf8'));
|
|
28
|
+
if (meta && meta.entry) candidates.push(meta.entry);
|
|
29
|
+
} catch { /* 缺文件走后续回退 */ }
|
|
30
|
+
if (process.env.FREEDOM_CLI_ENTRY) candidates.push(process.env.FREEDOM_CLI_ENTRY);
|
|
31
|
+
for (const c of candidates) {
|
|
32
|
+
if (c && fs.existsSync(c)) return c;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 子进程跑 CLI:逐行转发输出,返回 {code, lines}。
|
|
38
|
+
function runCli(args, cwd) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
const entry = cliEntry();
|
|
41
|
+
const id = args.join(' ');
|
|
42
|
+
if (!entry) {
|
|
43
|
+
write({ event: 'log', data: { id, line: '未找到 freedom CLI 入口(cli-entry.json 缺失),请重新运行 freedom desktop。' } });
|
|
44
|
+
return resolve({ code: 1, lines: [] });
|
|
45
|
+
}
|
|
46
|
+
const child = spawn(process.execPath, [entry, ...args], {
|
|
47
|
+
cwd: cwd || os.homedir(),
|
|
48
|
+
env: { ...process.env, FREEDOM_AUTO_UPDATE: '0', NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
49
|
+
shell: false,
|
|
50
|
+
});
|
|
51
|
+
const lines = [];
|
|
52
|
+
let buf = '';
|
|
53
|
+
const onData = (chunk) => {
|
|
54
|
+
buf += String(chunk);
|
|
55
|
+
let i;
|
|
56
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
57
|
+
const line = buf.slice(0, i).replace(/\r$/, '');
|
|
58
|
+
buf = buf.slice(i + 1);
|
|
59
|
+
if (!line) continue;
|
|
60
|
+
lines.push(line);
|
|
61
|
+
write({ event: 'log', data: { id, line } });
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
child.stdout.on('data', onData);
|
|
65
|
+
child.stderr.on('data', onData);
|
|
66
|
+
child.on('exit', (code) => {
|
|
67
|
+
if (buf) { lines.push(buf); write({ event: 'log', data: { id, line: buf } }); }
|
|
68
|
+
resolve({ code: code === null ? -1 : code, lines });
|
|
69
|
+
});
|
|
70
|
+
child.on('error', (err) => {
|
|
71
|
+
write({ event: 'log', data: { id, line: `启动 CLI 失败:${err.message}` } });
|
|
72
|
+
resolve({ code: 1, lines: [] });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- 最近项目状态(存 ~/.freedom/desktop-state.json) ----
|
|
78
|
+
function statePath() {
|
|
79
|
+
return path.join(os.homedir(), '.freedom', 'desktop-state.json');
|
|
80
|
+
}
|
|
81
|
+
function readState() {
|
|
82
|
+
try {
|
|
83
|
+
const s = JSON.parse(fs.readFileSync(statePath(), 'utf8'));
|
|
84
|
+
if (Array.isArray(s.recent)) return s;
|
|
85
|
+
} catch { /* 首次运行无状态文件 */ }
|
|
86
|
+
return { recent: [], current: null };
|
|
87
|
+
}
|
|
88
|
+
function writeState(s) {
|
|
89
|
+
fs.mkdirSync(path.dirname(statePath()), { recursive: true });
|
|
90
|
+
fs.writeFileSync(statePath(), JSON.stringify(s, null, 2), 'utf8');
|
|
91
|
+
}
|
|
92
|
+
function isProject(dir) {
|
|
93
|
+
return !!(dir && fs.existsSync(path.join(dir, 'freedom.config.js')));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let devChild = null;
|
|
97
|
+
function stopDev() {
|
|
98
|
+
if (!devChild) return false;
|
|
99
|
+
const c = devChild;
|
|
100
|
+
devChild = null;
|
|
101
|
+
try {
|
|
102
|
+
if (process.platform === 'win32') spawn('taskkill', ['/pid', String(c.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
103
|
+
else c.kill('SIGTERM');
|
|
104
|
+
} catch { /* 已退出 */ }
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const api = {
|
|
109
|
+
async 'app.info'() {
|
|
110
|
+
const entry = cliEntry();
|
|
111
|
+
const pkg = entry ? (() => { try { return JSON.parse(fs.readFileSync(path.join(path.dirname(entry), '..', 'package.json'), 'utf8')); } catch { return {}; } })() : {};
|
|
112
|
+
return {
|
|
113
|
+
cliEntry: entry,
|
|
114
|
+
cliVersion: pkg.version || null,
|
|
115
|
+
platform: `${process.platform}-${process.arch}`,
|
|
116
|
+
nodeVersion: process.version,
|
|
117
|
+
home: os.homedir(),
|
|
118
|
+
desktopDir: process.env.FREEDOM_DESKTOP_DIR || null,
|
|
119
|
+
hasConfig: !!isProject(process.cwd()),
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async 'project.list'() {
|
|
124
|
+
const s = readState();
|
|
125
|
+
return {
|
|
126
|
+
recent: s.recent.filter(isProject),
|
|
127
|
+
current: isProject(s.current) ? s.current : (s.recent.find(isProject) || null),
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
async 'project.open'([dir]) {
|
|
132
|
+
const abs = path.resolve(String(dir || ''));
|
|
133
|
+
if (!isProject(abs)) throw new Error(`不是 freedom 项目(缺 freedom.config.js):${abs}`);
|
|
134
|
+
const s = readState();
|
|
135
|
+
s.recent = [abs, ...s.recent.filter((p) => p !== abs)].slice(0, 12);
|
|
136
|
+
s.current = abs;
|
|
137
|
+
writeState(s);
|
|
138
|
+
return { current: abs, recent: s.recent };
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
async 'project.forget'([dir]) {
|
|
142
|
+
const abs = path.resolve(String(dir || ''));
|
|
143
|
+
const s = readState();
|
|
144
|
+
s.recent = s.recent.filter((p) => p !== abs);
|
|
145
|
+
if (s.current === abs) s.current = s.recent[0] || null;
|
|
146
|
+
writeState(s);
|
|
147
|
+
return { recent: s.recent, current: s.current };
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async 'project.init'([dir, template]) {
|
|
151
|
+
const target = path.resolve(String(dir || ''));
|
|
152
|
+
const args = ['init', target];
|
|
153
|
+
if (template === 'minimal') args.push('--template', 'minimal');
|
|
154
|
+
const r = await runCli(args, path.dirname(target));
|
|
155
|
+
return { code: r.code };
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
async 'project.build'([opts]) {
|
|
159
|
+
const o = opts || {};
|
|
160
|
+
const dir = path.resolve(String(o.dir || ''));
|
|
161
|
+
if (!isProject(dir)) throw new Error(`不是 freedom 项目:${dir}`);
|
|
162
|
+
const args = ['build'];
|
|
163
|
+
if (o.platform) args.push('--platform', String(o.platform));
|
|
164
|
+
if (o.security) args.push('--security', String(o.security));
|
|
165
|
+
if (o.installer) args.push('--installer');
|
|
166
|
+
if (o.noCache) args.push('--no-cache');
|
|
167
|
+
const r = await runCli(args, dir);
|
|
168
|
+
return { code: r.code };
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
async 'project.verify'([dir]) {
|
|
172
|
+
const target = path.resolve(String(dir || ''));
|
|
173
|
+
const r = await runCli(['verify'], target);
|
|
174
|
+
return { code: r.code };
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
async 'project.dev'([opts]) {
|
|
178
|
+
const o = opts || {};
|
|
179
|
+
const dir = path.resolve(String(o.dir || ''));
|
|
180
|
+
if (!isProject(dir)) throw new Error(`不是 freedom 项目:${dir}`);
|
|
181
|
+
if (devChild) stopDev();
|
|
182
|
+
const entry = cliEntry();
|
|
183
|
+
if (!entry) throw new Error('未找到 freedom CLI 入口');
|
|
184
|
+
const args = ['dev', ...(o.port ? ['--port', String(o.port)] : [])];
|
|
185
|
+
devChild = spawn(process.execPath, [entry, ...args], {
|
|
186
|
+
cwd: dir,
|
|
187
|
+
env: { ...process.env, FREEDOM_AUTO_UPDATE: '0', NO_COLOR: '1' },
|
|
188
|
+
shell: false,
|
|
189
|
+
});
|
|
190
|
+
const id = 'dev';
|
|
191
|
+
let buf = '';
|
|
192
|
+
const onData = (chunk) => {
|
|
193
|
+
buf += String(chunk);
|
|
194
|
+
let i;
|
|
195
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
196
|
+
const line = buf.slice(0, i).replace(/\r$/, '');
|
|
197
|
+
buf = buf.slice(i + 1);
|
|
198
|
+
if (line) write({ event: 'log', data: { id, line } });
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
devChild.stdout.on('data', onData);
|
|
202
|
+
devChild.stderr.on('data', onData);
|
|
203
|
+
devChild.on('exit', (code) => {
|
|
204
|
+
devChild = null;
|
|
205
|
+
write({ event: 'log', data: { id, line: `[dev] 结束(code=${code})` } });
|
|
206
|
+
write({ event: 'dev.exit', data: { code } });
|
|
207
|
+
});
|
|
208
|
+
return { started: true };
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
async 'project.devStop'() {
|
|
212
|
+
return { stopped: stopDev() };
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
async 'project.config'([dir]) {
|
|
216
|
+
const target = path.resolve(String(dir || ''));
|
|
217
|
+
const r = await runCli(['config'], target);
|
|
218
|
+
return { text: r.lines.join('\n'), code: r.code };
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
async 'project.configSet'([dir, key, value]) {
|
|
222
|
+
const target = path.resolve(String(dir || ''));
|
|
223
|
+
const r = await runCli(['config', 'set', String(key), String(value)], target);
|
|
224
|
+
return { code: r.code, text: r.lines.join('\n') };
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async 'shell.list'() {
|
|
228
|
+
const r = await runCli(['shell', 'list'], os.homedir());
|
|
229
|
+
return { text: r.lines.join('\n') };
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
async 'shell.download'([plat]) {
|
|
233
|
+
const r = await runCli(['shell', 'download', String(plat)], os.homedir());
|
|
234
|
+
return { code: r.code };
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async 'release.keygen'([dir]) {
|
|
238
|
+
const r = await runCli(['keygen'], path.resolve(String(dir || '')));
|
|
239
|
+
return { code: r.code, text: r.lines.join('\n') };
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
async 'release.manifest'([opts]) {
|
|
243
|
+
const o = opts || {};
|
|
244
|
+
const args = ['manifest', '--artifact', String(o.artifact || ''), '--url', String(o.url || '')];
|
|
245
|
+
if (o.version) args.push('--version', String(o.version));
|
|
246
|
+
if (o.notes) args.push('--notes', String(o.notes));
|
|
247
|
+
const r = await runCli(args, path.resolve(String(o.dir || '')));
|
|
248
|
+
return { code: r.code, text: r.lines.join('\n') };
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
async 'agents.list'() {
|
|
252
|
+
const r = await runCli(['agents'], os.homedir());
|
|
253
|
+
return { text: r.lines.join('\n') };
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async 'agents.install'([opts]) {
|
|
257
|
+
const o = opts || {};
|
|
258
|
+
const what = o.what === 'mcp' ? 'mcp' : 'skill';
|
|
259
|
+
const args = [what, 'install'];
|
|
260
|
+
if (Array.isArray(o.agents) && o.agents.length) args.push('--agent', o.agents.join(','));
|
|
261
|
+
else args.push('--agent', 'all');
|
|
262
|
+
if (o.dryRun) args.push('--dry-run');
|
|
263
|
+
const r = await runCli(args, os.homedir());
|
|
264
|
+
return { code: r.code };
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
async 'desktop.rebuild'() {
|
|
268
|
+
const r = await runCli(['desktop', '--rebuild'], os.homedir());
|
|
269
|
+
return { code: r.code };
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
async 'docs.open'() {
|
|
273
|
+
const r = await runCli(['tutorial'], os.homedir());
|
|
274
|
+
return { code: r.code };
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
279
|
+
|
|
280
|
+
// stdin EOF 不等于可以立刻退出:在途请求(如经子进程跑 CLI 的调用)还没回帧,
|
|
281
|
+
// 直接 exit 会让调用方永远等不到响应。等在途清零后再收尾。
|
|
282
|
+
let inflight = 0;
|
|
283
|
+
let stdinClosed = false;
|
|
284
|
+
function maybeExit() {
|
|
285
|
+
if (stdinClosed && inflight === 0) {
|
|
286
|
+
stopDev();
|
|
287
|
+
process.exit(0);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
rl.on('close', () => {
|
|
291
|
+
stdinClosed = true;
|
|
292
|
+
maybeExit();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
rl.on('line', async (line) => {
|
|
296
|
+
let req;
|
|
297
|
+
try {
|
|
298
|
+
req = JSON.parse(line);
|
|
299
|
+
} catch {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
inflight++;
|
|
303
|
+
try {
|
|
304
|
+
const fn = api[req.method];
|
|
305
|
+
if (!fn) {
|
|
306
|
+
write({ id: req.id, result: null, error: `unknown method ${req.method}` });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const result = await fn(req.params || []);
|
|
310
|
+
write({ id: req.id, result, error: '' });
|
|
311
|
+
} catch (e) {
|
|
312
|
+
write({ id: req.id, result: null, error: String((e && e.message) || e) });
|
|
313
|
+
} finally {
|
|
314
|
+
inflight--;
|
|
315
|
+
maybeExit();
|
|
316
|
+
}
|
|
317
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Freedom Desktop 应用配置(自举:本项目的界面由 freedom 自己打包成桌面应用)。
|
|
2
|
+
// 由 `freedom desktop` 自动同步到 ~/.freedom/desktop/ 并自动打包,一般无需手改。
|
|
3
|
+
export default {
|
|
4
|
+
name: 'freedom-desktop',
|
|
5
|
+
title: 'Freedom Desktop',
|
|
6
|
+
|
|
7
|
+
width: 1180,
|
|
8
|
+
height: 780,
|
|
9
|
+
minWidth: 920,
|
|
10
|
+
minHeight: 560,
|
|
11
|
+
center: true,
|
|
12
|
+
debug: false,
|
|
13
|
+
|
|
14
|
+
// 静态单文件前端直通:不跑 npm install / vite(自举工具不应依赖网络与构建链)。
|
|
15
|
+
staticHtml: 'app.html',
|
|
16
|
+
|
|
17
|
+
titlebar: 'native',
|
|
18
|
+
security: 'none',
|
|
19
|
+
outDir: 'dist',
|
|
20
|
+
|
|
21
|
+
// 应用图标:构建时注入 exe(Windows 用 rcedit 写 PE 资源),运行时壳层经
|
|
22
|
+
// WM_SETICON 同步到标题栏与任务栏。由 tools/freedomres -ico-out 从
|
|
23
|
+
// assets/freedom-desktop.png 生成(多尺寸阶梯 16/24/32/48/64/128/256)。
|
|
24
|
+
icon: 'icon.ico',
|
|
25
|
+
|
|
26
|
+
// 后端 = Node 进程(零依赖),经 NDJSON/stdio 桥将 freedom CLI 能力暴露给界面。
|
|
27
|
+
// 相对路径以 resources/ 为工作目录解析;CLI 入口由 freedom desktop 写入 cli-entry.json。
|
|
28
|
+
backend: { command: 'node', args: ['backend/desktop.mjs'] },
|
|
29
|
+
};
|
|
Binary file
|
|
@@ -2,9 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
package freedom
|
|
4
4
|
|
|
5
|
-
// 反调试(Windows 实现):high 安全模式下由 Run()
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// 反调试(Windows 实现):high 安全模式下由 Run() 在资源解密前后各调用一次。
|
|
6
|
+
// 命中时静默退出(退出码 77,无提示),防止攻击者在调试器下单步追踪
|
|
7
|
+
// 密钥派生 / 容器解密逻辑,或直接读内存里的明文密钥。
|
|
8
|
+
//
|
|
9
|
+
// 六道独立信号(任一命中即判定被调试),覆盖不同层次的附加手段:
|
|
10
|
+
// 1. IsDebuggerPresent —— 读 PEB.BeingDebugged 的快捷 API(最易被 hook 篡改);
|
|
11
|
+
// 2. CheckRemoteDebuggerPresent —— 内核调试器 / 远程调试器;
|
|
12
|
+
// 3. 直接读 PEB.BeingDebugged —— 不经 API,绕过"只 patch API 返回值"的调试辅助;
|
|
13
|
+
// 4. NtQueryInformationProcess(ProcessDebugPort) —— 调试端口句柄;
|
|
14
|
+
// 5. NtQueryInformationProcess(ProcessDebugObjectHandle) —— 调试对象(无端口也命中);
|
|
15
|
+
// 6. NtQueryInformationProcess(ProcessDebugFlags) —— 标志位反向确认。
|
|
16
|
+
//
|
|
17
|
+
// 未纳入的信号及原因:硬件断点(Dr7)需先 SuspendThread 再 GetThreadContext,
|
|
18
|
+
// 对本线程调用实测返回 ERROR_ACCESS_DENIED(非挂起线程读不到上下文),
|
|
19
|
+
// 为一次启动期检测去挂起自建线程不划算;时间差检测在 CI/低配机误判率高,
|
|
20
|
+
// 误杀正常用户的代价高于其收益。所有信号都遵循"探测失败即视为未命中",
|
|
21
|
+
// 只在确证被调试时退出,杜绝误杀。
|
|
22
|
+
//
|
|
23
|
+
// 诚实边界:这些是抬升成本的对抗,不是不可绕过的墙。绕开者仍需逆向壳二进制、
|
|
24
|
+
// 复现掩码后的主密钥与 PBKDF2 参数,并在无调试器环境下取密钥。
|
|
8
25
|
|
|
9
26
|
import (
|
|
10
27
|
"os"
|
|
@@ -13,25 +30,111 @@ import (
|
|
|
13
30
|
)
|
|
14
31
|
|
|
15
32
|
var (
|
|
16
|
-
kernel32AntiDebug
|
|
33
|
+
kernel32AntiDebug = syscall.NewLazyDLL("kernel32.dll")
|
|
34
|
+
ntdllAntiDebug = syscall.NewLazyDLL("ntdll.dll")
|
|
35
|
+
|
|
17
36
|
procIsDebuggerPresent = kernel32AntiDebug.NewProc("IsDebuggerPresent")
|
|
18
37
|
procCheckRemoteDebuggerPresent = kernel32AntiDebug.NewProc("CheckRemoteDebuggerPresent")
|
|
38
|
+
procNtQueryInformationProcess = ntdllAntiDebug.NewProc("NtQueryInformationProcess")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// 进程信息类别(PROCESSINFOCLASS)与各类别的返回长度,编号与长度均为本机实测校准:
|
|
42
|
+
// ProcessBasicInformation(0) 48 字节(x64,PEB 在偏移 8)
|
|
43
|
+
// ProcessDebugPort(7) 8 字节(HANDLE);len 给 4 会返回 STATUS_INFO_LENGTH_MISMATCH
|
|
44
|
+
// ProcessDebugObjectHandle(30) 8 字节;未被调试时返回 STATUS_PORT_NOT_SET(0xC0000353)
|
|
45
|
+
// ProcessDebugFlags(31) 4 字节(ULONG);未被调试时返回 1
|
|
46
|
+
// 注意 30/31 两号易写成 32/33(那是 LUIDDeviceMapsEnabled / BreakOnTermination),
|
|
47
|
+
// 编号或长度写错不会报错、只会静默失去一路信号,故此处配套测试锁死。
|
|
48
|
+
const (
|
|
49
|
+
processBasicInformation = 0
|
|
50
|
+
processDebugPort = 7
|
|
51
|
+
processDebugObjectHandle = 30
|
|
52
|
+
processDebugFlags = 31
|
|
53
|
+
|
|
54
|
+
basicInfoSize = unsafe.Sizeof(processBasicInfo{}) // 48B(x64)
|
|
55
|
+
debugPortSize = 8
|
|
56
|
+
debugObjectSize = 8
|
|
57
|
+
debugFlagsSize = 4
|
|
58
|
+
|
|
59
|
+
pebBeingDebuggedOff = 2 // PEB+0x02:BYTE BeingDebugged
|
|
19
60
|
)
|
|
20
61
|
|
|
21
|
-
// antiDebugCheck
|
|
62
|
+
// antiDebugCheck 检测调试器;确证被调试则立即结束进程(不显示窗口、不回显原因)。
|
|
22
63
|
func antiDebugCheck() {
|
|
23
|
-
if
|
|
64
|
+
if debuggerPresent() {
|
|
24
65
|
os.Exit(77)
|
|
25
66
|
}
|
|
26
67
|
}
|
|
27
68
|
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
func isBeingDebugged() bool {
|
|
69
|
+
// debuggerPresent 汇总六道信号。lazy DLL 加载失败(Proc.Find 出错)时该信号记为未命中。
|
|
70
|
+
func debuggerPresent() bool {
|
|
31
71
|
if r, _, _ := procIsDebuggerPresent.Call(); r != 0 {
|
|
32
72
|
return true
|
|
33
73
|
}
|
|
34
74
|
var present int32
|
|
35
|
-
procCheckRemoteDebuggerPresent.Call(^uintptr(0), uintptr(unsafe.Pointer(&present)))
|
|
36
|
-
|
|
75
|
+
if r, _, _ := procCheckRemoteDebuggerPresent.Call(^uintptr(0), uintptr(unsafe.Pointer(&present))); r != 0 {
|
|
76
|
+
return present != 0
|
|
77
|
+
}
|
|
78
|
+
if pebBeingDebuggedSet() {
|
|
79
|
+
return true
|
|
80
|
+
}
|
|
81
|
+
if r, ok := ntQueryProcessInfo(processDebugPort, debugPortSize); ok && r != 0 {
|
|
82
|
+
return true // 调试端口非 0
|
|
83
|
+
}
|
|
84
|
+
// 调试对象:未被调试时返回 STATUS_PORT_NOT_SET(查询失败即未命中),
|
|
85
|
+
// 能成功取到句柄说明挂了调试对象。
|
|
86
|
+
if _, ok := ntQueryProcessInfo(processDebugObjectHandle, debugObjectSize); ok {
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
// DebugFlags:1 = 无调试器;0 = 有。
|
|
90
|
+
if r, ok := ntQueryProcessInfo(processDebugFlags, debugFlagsSize); ok && r == 0 {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ntQueryProcessInfo 以 GetCurrentProcess 伪句柄(-1)查询进程信息,size 必须与
|
|
97
|
+
// class 的约定结构体大小一致(见上方常量表),否则系统直接返回长度不匹配。
|
|
98
|
+
// 返回查询成功时的数值;API 不可用、状态非 SUCCESS 时 ok=false(调用方按未命中处理)。
|
|
99
|
+
func ntQueryProcessInfo(class, size uintptr) (uint64, bool) {
|
|
100
|
+
if err := procNtQueryInformationProcess.Find(); err != nil {
|
|
101
|
+
return 0, false
|
|
102
|
+
}
|
|
103
|
+
var buf [8]byte
|
|
104
|
+
r, _, _ := procNtQueryInformationProcess.Call(
|
|
105
|
+
^uintptr(0), class, uintptr(unsafe.Pointer(&buf[0])), size, 0)
|
|
106
|
+
if r != 0 { // 非 STATUS_SUCCESS 一律按未命中,避免误杀
|
|
107
|
+
return 0, false
|
|
108
|
+
}
|
|
109
|
+
if size >= 8 {
|
|
110
|
+
return *(*uint64)(unsafe.Pointer(&buf[0])), true
|
|
111
|
+
}
|
|
112
|
+
return uint64(*(*uint32)(unsafe.Pointer(&buf[0]))), true
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// processBasicInfo 是 PROCESS_BASIC_INFORMATION 的 x64 布局(字段按指针宽度对齐)。
|
|
116
|
+
type processBasicInfo struct {
|
|
117
|
+
exitStatus uintptr
|
|
118
|
+
pebAddress uintptr
|
|
119
|
+
affinityMask uintptr
|
|
120
|
+
basePriority uintptr
|
|
121
|
+
uniqueProcessId uintptr
|
|
122
|
+
inheritedFromUniquePid uintptr
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// pebBeingDebuggedSet 不经 API 直接读自身 PEB.BeingDebugged。
|
|
126
|
+
// 与 IsDebuggerPresent 读同一字段,但绕过对该 API 的 hook / patch。
|
|
127
|
+
// 本进程内解引用自身 PEB 指针,无并发与生命周期问题。
|
|
128
|
+
func pebBeingDebuggedSet() bool {
|
|
129
|
+
var info processBasicInfo
|
|
130
|
+
if err := procNtQueryInformationProcess.Find(); err != nil {
|
|
131
|
+
return false
|
|
132
|
+
}
|
|
133
|
+
r, _, _ := procNtQueryInformationProcess.Call(
|
|
134
|
+
^uintptr(0), processBasicInformation,
|
|
135
|
+
uintptr(unsafe.Pointer(&info)), unsafe.Sizeof(info), 0)
|
|
136
|
+
if r != 0 || info.pebAddress == 0 {
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
return *(*byte)(unsafe.Pointer(info.pebAddress + pebBeingDebuggedOff)) != 0
|
|
37
140
|
}
|
|
@@ -127,6 +127,9 @@
|
|
|
127
127
|
// 聚合信息:title/outerX/outerY/outerWidth/outerHeight/innerWidth/innerHeight/
|
|
128
128
|
// scaleFactor/visible/focused/maximized/minimized/alwaysOnTop/skipTaskbar/frameless/fullscreen。
|
|
129
129
|
getInfo: function () { return windowAction('getInfo'); },
|
|
130
|
+
// appIcon():exe 内嵌图标 → PNG data URL(无边框自绘标题栏用);
|
|
131
|
+
// 未注入图标或非 Windows 平台返回空字符串。
|
|
132
|
+
appIcon: function () { return windowAction('appIcon'); },
|
|
130
133
|
// 显示器列表(经 sys 桥接):[{x,y,width,height,workX,workY,workWidth,workHeight,scaleFactor,isPrimary}]
|
|
131
134
|
monitors: function () { return sysCall('window.monitors', {}); },
|
|
132
135
|
// 窗口视觉效果(Win11):backdrop: auto|none|solid|mica|acrylic;corner: round|roundSmall|square|default;borderColor: 0x00BBGGRR
|
|
@@ -22,6 +22,7 @@ import (
|
|
|
22
22
|
"errors"
|
|
23
23
|
"fmt"
|
|
24
24
|
"os"
|
|
25
|
+
"strings"
|
|
25
26
|
"sync"
|
|
26
27
|
"sync/atomic"
|
|
27
28
|
|
|
@@ -69,6 +70,14 @@ type Config struct {
|
|
|
69
70
|
// HTML 返回要加载到窗口的前端页面内容(内存加载,无本地端口)。
|
|
70
71
|
// 为 nil 时使用框架内置的默认占位页。
|
|
71
72
|
HTML func() (string, error)
|
|
73
|
+
// URL 非空时主窗口直接 Navigate 到该地址(如 `freedom dev` 的 vite 服务器),
|
|
74
|
+
// 优先于 HTML/resources 页面。SDK 经 Init 在每次导航前注入,桥接不失效。
|
|
75
|
+
// 仅放行 http/https(dev/HMR 走 localhost ws,页面源本身不许 file:// 等面)。
|
|
76
|
+
URL string
|
|
77
|
+
// SingleInstance 启用单实例:二次启动把命令行参数转发给主实例后自行退出;
|
|
78
|
+
// 主实例前端收 `app.secondInstance` 事件({args})。Windows 生效,
|
|
79
|
+
// 其他平台为无操作(始终视为唯一实例)。
|
|
80
|
+
SingleInstance bool
|
|
72
81
|
// AppID 是应用标识,决定数据/配置目录名与单实例窗口类标题。
|
|
73
82
|
// 为空时回退可执行文件名(去扩展名)。一经发布不要更改,否则用户数据"丢失"。
|
|
74
83
|
AppID string
|
|
@@ -83,6 +92,11 @@ type Config struct {
|
|
|
83
92
|
Capabilities *Capabilities
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
// pageURLAllowed 校验主窗口 URL 页面源:仅 http/https 放行(file:// 等本地面拒绝)。
|
|
96
|
+
func pageURLAllowed(raw string) bool {
|
|
97
|
+
return strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://")
|
|
98
|
+
}
|
|
99
|
+
|
|
86
100
|
// App 是 Freedom 应用实例。
|
|
87
101
|
type App struct {
|
|
88
102
|
cfg Config
|
|
@@ -93,9 +107,11 @@ type App struct {
|
|
|
93
107
|
onReady func(a *App)
|
|
94
108
|
|
|
95
109
|
// 运行时资源覆盖(resources.go):secure = high 模式(app.bin 解密成功),
|
|
96
|
-
// backendExplicit = 用户经 Bind 显式使用内嵌后端(config.json 的 backend
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
// backendExplicit = 用户经 Bind 显式使用内嵌后端(config.json 的 backend 不得再替换),
|
|
111
|
+
// secureBackendDir = high 模式容器内后端源码解密落地用的私有临时目录(退出即删)。
|
|
112
|
+
secure bool
|
|
113
|
+
backendExplicit bool
|
|
114
|
+
secureBackendDir string
|
|
99
115
|
|
|
100
116
|
// upMu 保护 upPending:CheckUpdate 验签通过的更新条目,install 桥接只认它
|
|
101
117
|
//(前端无法注入未验签的 URL/哈希)。见 updater.go。
|
|
@@ -213,6 +229,11 @@ func (a *App) Unbind(name string) {
|
|
|
213
229
|
|
|
214
230
|
// Run 启动窗口并进入主事件循环,阻塞直到窗口被关闭。
|
|
215
231
|
func (a *App) Run() {
|
|
232
|
+
// high 产物:解密前先跑一次反调试(此时密钥与明文都还没进内存),
|
|
233
|
+
// 解密后再跑一次(兜住"启动后才附加调试器")。非 high 应用不受影响。
|
|
234
|
+
if hasSecureResources() {
|
|
235
|
+
antiDebugCheck()
|
|
236
|
+
}
|
|
216
237
|
// 通用壳:先加载 exe 同目录 resources/ 覆盖窗口与后端配置(CLI build 写入;
|
|
217
238
|
// 缺失则使用编译期/默认配置)。配置存在但非法时打印告警不中断启动。
|
|
218
239
|
if err := a.loadRuntimeConfig(); err != nil {
|
|
@@ -225,13 +246,20 @@ func (a *App) Run() {
|
|
|
225
246
|
}
|
|
226
247
|
fmt.Printf("freedom: warning: %v\n", err)
|
|
227
248
|
}
|
|
228
|
-
// high
|
|
249
|
+
// high 模式的临时后端目录随进程退出清理(defer 早于后端 Close 注册 → 后于其执行)。
|
|
250
|
+
defer a.cleanupSecureBackend()
|
|
251
|
+
// high 模式:解密后复查调试器(见 Run 开头的"解密前一次")。
|
|
229
252
|
if a.secure {
|
|
230
253
|
antiDebugCheck()
|
|
231
254
|
}
|
|
232
255
|
// 平台层回调(单实例转发/热键)经 currentApp 找到本实例推送事件。
|
|
233
256
|
appForEvents.Store(a)
|
|
234
257
|
defer appForEvents.Store(nil)
|
|
258
|
+
// 单实例:二次启动的参数已在 RequestSingleInstance 内转发给主实例,
|
|
259
|
+
// 本进程不建窗直接返回(主实例前端收 app.secondInstance 事件)。
|
|
260
|
+
if a.cfg.SingleInstance && !RequestSingleInstance(a.effectiveAppID()) {
|
|
261
|
+
return
|
|
262
|
+
}
|
|
235
263
|
// M2:标记运行中(NewWindow 的前置条件)并登记主窗口;退出前回收次级窗口。
|
|
236
264
|
a.winMu.Lock()
|
|
237
265
|
a.running = true
|
|
@@ -247,10 +275,19 @@ func (a *App) Run() {
|
|
|
247
275
|
a.running = false
|
|
248
276
|
a.winMu.Unlock()
|
|
249
277
|
}()
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
278
|
+
// URL 模式(dev 服务器/远程页):跳过页面内容解析,稍后 Navigate。
|
|
279
|
+
// scheme 白名单:只放行 http/https,file:// 等本地面一律拒绝。
|
|
280
|
+
if a.cfg.URL != "" && !pageURLAllowed(a.cfg.URL) {
|
|
281
|
+
fmt.Printf("freedom: 拒绝非 http/https 的 URL %q,回退页面源\n", a.cfg.URL)
|
|
282
|
+
a.cfg.URL = ""
|
|
283
|
+
}
|
|
284
|
+
var html string
|
|
285
|
+
if a.cfg.URL == "" {
|
|
286
|
+
var err error
|
|
287
|
+
if html, err = a.resolveHTML(); err != nil {
|
|
288
|
+
fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
254
291
|
}
|
|
255
292
|
|
|
256
293
|
w := webview.New(a.cfg.Debug)
|
|
@@ -288,6 +325,8 @@ func (a *App) Run() {
|
|
|
288
325
|
}
|
|
289
326
|
a.applyCenter()
|
|
290
327
|
a.applyTitleBar()
|
|
328
|
+
// 标题栏 / 任务栏图标同步为 exe 内嵌图标(Windows;其他平台 no-op)。
|
|
329
|
+
a.setWindowIcon()
|
|
291
330
|
// 窗口事件与关闭拦截(Windows 经 WndProc 子类化;其他平台 no-op)。
|
|
292
331
|
a.installWindowEvents()
|
|
293
332
|
// 恢复上次退出的窗口几何(仅 Config.RememberWindowState 时生效)。
|
|
@@ -352,7 +391,11 @@ func (a *App) Run() {
|
|
|
352
391
|
|
|
353
392
|
// 注:onReady 不再在 SetHtml 前触发(H3),改由前端 SDK 在页面
|
|
354
393
|
// DOMContentLoaded 后经 __freedom__ready 回调,保证初始化事件不丢失。
|
|
355
|
-
|
|
394
|
+
if a.cfg.URL != "" {
|
|
395
|
+
w.Navigate(a.cfg.URL) // Init 注入对每次导航生效,URL 模式桥接同样可用
|
|
396
|
+
} else {
|
|
397
|
+
w.SetHtml(html)
|
|
398
|
+
}
|
|
356
399
|
w.Run()
|
|
357
400
|
}
|
|
358
401
|
|