@yufengtadian/freedom-cli 1.13.1 → 1.13.3
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 +40 -0
- package/README.md +119 -15
- package/lib/agents.js +448 -0
- package/lib/build.js +189 -45
- package/lib/cli.js +258 -9
- 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 +170 -63
- package/lib/toolchain.js +339 -0
- package/lib/tui.js +37 -1
- package/lib/utils.js +51 -0
- package/lib/verify.js +48 -12
- package/lib/webkit.js +45 -0
- package/package.json +3 -2
- 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 +327 -0
- package/templates/desktop/freedom.config.js +32 -0
- package/templates/desktop/icon.ico +0 -0
- package/templates/go/go.mod +3 -0
- package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +238 -11
- package/templates/go/pkg/freedom/assets/freedom.js +3 -0
- package/templates/go/pkg/freedom/assets_embed.go +1 -0
- package/templates/go/pkg/freedom/backend_proc.go +5 -5
- package/templates/go/pkg/freedom/events_windows.go +5 -5
- package/templates/go/pkg/freedom/freedom.go +64 -10
- package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
- package/templates/go/pkg/freedom/resources.go +119 -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 +293 -64
- package/templates/go/pkg/freedom/shutdown.go +55 -0
- package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
- package/templates/go/pkg/freedom/store.go +11 -3
- package/templates/go/pkg/freedom/syscap_windows.go +33 -33
- package/templates/go/pkg/freedom/sysint_windows.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +18 -18
- package/templates/go/pkg/freedom/updater.go +3 -3
- package/templates/go/pkg/freedom/window_other.go +7 -1
- package/templates/go/pkg/freedom/window_windows.go +185 -17
- package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
- package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
- package/templates/go/third_party/webview_go/LICENSE +22 -0
- package/templates/go/third_party/webview_go/README.md +50 -0
- package/templates/go/third_party/webview_go/glue.c +36 -0
- package/templates/go/third_party/webview_go/go.mod +3 -0
- package/templates/go/third_party/webview_go/go.sum +0 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
- package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
- package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
- package/templates/go/third_party/webview_go/webview.cc +1 -0
- package/templates/go/third_party/webview_go/webview.go +332 -0
- 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
|
@@ -40,19 +40,34 @@ type runtimeBackend struct {
|
|
|
40
40
|
Args []string `json:"args"`
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// runtimeUpdater 描述 config.json 中的自动更新配置(对应 updater.go 的 UpdateConfig;
|
|
44
|
+
// 公钥为 base64 std 的 ed25519 32 字节公钥,私钥属发布方资产——签名由 freedom CLI 生成)。
|
|
45
|
+
type runtimeUpdater struct {
|
|
46
|
+
ManifestURL string `json:"manifestURL"`
|
|
47
|
+
PublicKey string `json:"publicKey"`
|
|
48
|
+
RequireSignature *bool `json:"requireSignature"`
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
// runtimeConfigFile 描述 resources/config.json 的磁盘结构。
|
|
44
52
|
// 由 freedom CLI 在 build 阶段根据 freedom.config.js 生成;Name 仅作来源标记不应用。
|
|
45
53
|
type runtimeConfigFile struct {
|
|
46
|
-
Name
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
Name string `json:"name"`
|
|
55
|
+
Version string `json:"version"`
|
|
56
|
+
Title string `json:"title"`
|
|
57
|
+
TitleBar string `json:"titlebar"`
|
|
58
|
+
Width int `json:"width"`
|
|
59
|
+
Height int `json:"height"`
|
|
60
|
+
MinWidth int `json:"minWidth"`
|
|
61
|
+
MinHeight int `json:"minHeight"`
|
|
62
|
+
Center *bool `json:"center"`
|
|
63
|
+
Debug *bool `json:"debug"`
|
|
64
|
+
Backend *runtimeBackend `json:"backend"`
|
|
65
|
+
URL string `json:"url"`
|
|
66
|
+
SingleInstance *bool `json:"singleInstance"`
|
|
67
|
+
Updater *runtimeUpdater `json:"updater"`
|
|
68
|
+
// Capabilities 收口前端可调用面。通用壳里编译不进应用作者的 Config 字面量,
|
|
69
|
+
// 没有这条透传则 freedom.config.js 写 capabilities 会被静默忽略(= 全开)。
|
|
70
|
+
Capabilities *Capabilities `json:"capabilities"`
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
// resourcesDirOverride 是单测缝:非空时替代 exe 同目录的 resources 定位。
|
|
@@ -82,10 +97,19 @@ func (a *App) loadRuntimeConfig() error {
|
|
|
82
97
|
if err := json.Unmarshal([]byte(p.Config), &rc); err != nil {
|
|
83
98
|
return fmt.Errorf("parse encrypted config: %w", err)
|
|
84
99
|
}
|
|
100
|
+
a.secure = true
|
|
101
|
+
// high 模式的后端源码在容器内(磁盘无 resources/backend 明文):解密到进程私有
|
|
102
|
+
// 临时目录供子进程执行,Run 退出时删除。
|
|
103
|
+
if len(p.Backend) > 0 {
|
|
104
|
+
bdir, err := materializeSecureBackend(p.Backend)
|
|
105
|
+
if err != nil {
|
|
106
|
+
return secureFatal(fmt.Errorf("后端源码解密落地失败:%w", err))
|
|
107
|
+
}
|
|
108
|
+
a.secureBackendDir = bdir
|
|
109
|
+
}
|
|
85
110
|
a.applyRuntimeConfig(&rc)
|
|
86
111
|
// high 模式强制关闭 WebView 开发者工具,防止前端源码经 devtools 直接查看。
|
|
87
112
|
a.cfg.Debug = false
|
|
88
|
-
a.secure = true
|
|
89
113
|
return nil
|
|
90
114
|
}
|
|
91
115
|
return a.loadRuntimeConfigPlain()
|
|
@@ -115,6 +139,15 @@ func (a *App) loadRuntimeConfigPlain() error {
|
|
|
115
139
|
|
|
116
140
|
// applyRuntimeConfig 把解析后的运行时配置覆盖到应用配置(明文与加密路径共用)。
|
|
117
141
|
func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) {
|
|
142
|
+
// 应用版本:通用壳编译期注入的 Version 是「壳」的版本,不是「这个应用」的版本。
|
|
143
|
+
// 自更新比较必须用后者,否则要么永远报 uncomparable(壳未打戳时是 "dev"),
|
|
144
|
+
// 要么拿壳的版本号跟应用的清单比——换来的仍是同一个壳,更新永远收敛不了。
|
|
145
|
+
if rc.Version != "" {
|
|
146
|
+
a.runtimeVersion = rc.Version
|
|
147
|
+
}
|
|
148
|
+
if rc.Capabilities != nil && a.cfg.Capabilities == nil {
|
|
149
|
+
a.cfg.Capabilities = rc.Capabilities
|
|
150
|
+
}
|
|
118
151
|
// 窗口标题:config.json 的 title 覆盖编译期默认("Freedom App")。
|
|
119
152
|
if rc.Title != "" {
|
|
120
153
|
a.cfg.Title = rc.Title
|
|
@@ -143,14 +176,29 @@ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) {
|
|
|
143
176
|
if rc.Debug != nil {
|
|
144
177
|
a.cfg.Debug = *rc.Debug
|
|
145
178
|
}
|
|
179
|
+
if rc.URL != "" {
|
|
180
|
+
a.cfg.URL = rc.URL
|
|
181
|
+
}
|
|
182
|
+
if rc.SingleInstance != nil {
|
|
183
|
+
a.cfg.SingleInstance = *rc.SingleInstance
|
|
184
|
+
}
|
|
185
|
+
// 自动更新:manifestURL 与 publicKey 缺一即视为未配置(半截配置宁可不启用,
|
|
186
|
+
// 也不让壳带着空公钥去更新)。
|
|
187
|
+
if rc.Updater != nil && rc.Updater.ManifestURL != "" && rc.Updater.PublicKey != "" {
|
|
188
|
+
uc := &UpdateConfig{ManifestURL: rc.Updater.ManifestURL, PublicKey: rc.Updater.PublicKey}
|
|
189
|
+
if rc.Updater.RequireSignature != nil {
|
|
190
|
+
uc.RequireSignature = *rc.Updater.RequireSignature
|
|
191
|
+
}
|
|
192
|
+
a.cfg.Update = uc
|
|
193
|
+
}
|
|
146
194
|
// 外部后端进程配置:仅在壳未显式绑定后端时生效(用户 Bind 过内嵌方法 =
|
|
147
195
|
// 显式内嵌后端,config.json 不得静默替换,否则其方法全部失效)。
|
|
148
|
-
//
|
|
149
|
-
//
|
|
196
|
+
// 工作目录取 backendWorkDir():明文模式 = resources 目录(相对路径 backend/main.mjs
|
|
197
|
+
// 由其解析);high 模式 = 容器解出的私有临时目录(resources 下没有明文后端)。
|
|
150
198
|
if rc.Backend != nil && rc.Backend.Command != "" && !a.backendExplicit {
|
|
151
199
|
args := append([]string{rc.Backend.Command}, rc.Backend.Args...)
|
|
152
200
|
pb := NewProcBackend(args...)
|
|
153
|
-
if dir, err :=
|
|
201
|
+
if dir, err := a.backendWorkDir(); err == nil && dir != "" {
|
|
154
202
|
pb.SetDir(dir)
|
|
155
203
|
}
|
|
156
204
|
a.cfg.Backend = pb
|
|
@@ -158,6 +206,63 @@ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) {
|
|
|
158
206
|
}
|
|
159
207
|
}
|
|
160
208
|
|
|
209
|
+
// appVersion 返回「本应用」的版本:优先用 config.json / app.bin 容器里声明的值,
|
|
210
|
+
// 回落编译期 Version(examples 自建壳走 ldflags 注入)。通用壳的 Version 是壳自己的
|
|
211
|
+
// 版本域,拿它跟应用发布清单比是两个数在比——见 updater.go 的 CheckUpdate。
|
|
212
|
+
func (a *App) appVersion() string {
|
|
213
|
+
if a != nil && a.runtimeVersion != "" {
|
|
214
|
+
return a.runtimeVersion
|
|
215
|
+
}
|
|
216
|
+
return Version
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// backendWorkDir 返回后端进程的工作目录。
|
|
220
|
+
func (a *App) backendWorkDir() (string, error) {
|
|
221
|
+
if a.secureBackendDir != "" {
|
|
222
|
+
return a.secureBackendDir, nil
|
|
223
|
+
}
|
|
224
|
+
return resourcesDir()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// installSecureShutdown 注册信号退出通道(Run 调用)。
|
|
228
|
+
//
|
|
229
|
+
// 清扫顺序刻意是「先后端、后目录」:后端子进程的工作目录就是 high 模式解出的那个明文临时
|
|
230
|
+
// 目录,它还活着时 Windows 会因文件被占用让 RemoveAll 静默失败——明文源码继续留在 %TEMP%,
|
|
231
|
+
// 而信号路径紧接着就 os.Exit,下次启动的 GC 也帮不上(PID 仍存活)。ProcBackend.Close 自带
|
|
232
|
+
// closed 幂等与 3s 等待上界,与 Run 正常退出的 defer 双跑无害。
|
|
233
|
+
// 非 high 模式下 cleanupSecureBackend 是 no-op,但后端该停照样得停,故不再判安全档。
|
|
234
|
+
func (a *App) installSecureShutdown() {
|
|
235
|
+
secureCleanup = func() {
|
|
236
|
+
a.closeBackendForShutdown()
|
|
237
|
+
a.cleanupSecureBackend()
|
|
238
|
+
}
|
|
239
|
+
installShutdownCleanup()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// closeBackendForShutdown 停后端子进程;未配置后端(nil)时为空操作。
|
|
243
|
+
func (a *App) closeBackendForShutdown() {
|
|
244
|
+
if a.backend == nil {
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
if err := a.backend.Close(); err != nil {
|
|
248
|
+
fmt.Fprintf(os.Stderr, "freedom: shutdown backend close: %v\n", err)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// cleanupSecureBackend 删除 high 模式解出的临时后端目录(Run 退出时调用)。
|
|
253
|
+
// 进程被强杀时目录会残留在系统临时区——内容仍可被下一次启动的壳重新解出,
|
|
254
|
+
// 且明文源码本就来自内存,故不引入跨进程清扫。
|
|
255
|
+
func (a *App) cleanupSecureBackend() {
|
|
256
|
+
if a.secureBackendDir == "" {
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
// 删不掉必须在退出前喊出来:这是「磁盘不留明文」承诺当场失守的时刻,静默等于隐瞒。
|
|
260
|
+
if err := os.RemoveAll(a.secureBackendDir); err != nil {
|
|
261
|
+
fmt.Fprintf(os.Stderr, "freedom: 明文后端临时目录未清除(后端源码可能仍可读取):%s: %v\n", a.secureBackendDir, err)
|
|
262
|
+
}
|
|
263
|
+
a.secureBackendDir = ""
|
|
264
|
+
}
|
|
265
|
+
|
|
161
266
|
// loadRuntimeHTML 返回前端页面内容(high 模式:解密 app.bin 内的 html;
|
|
162
267
|
// 否则读取 exe 同目录 resources/index.html)。文件不存在时返回空串。
|
|
163
268
|
func loadRuntimeHTML() (string, error) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
// high 模式后端源码临时目录的启动期回收。
|
|
4
|
+
//
|
|
5
|
+
// 正常退出由 App.cleanupSecureBackend 删除;进程崩溃或被 taskkill /F 强杀时
|
|
6
|
+
// defer 不会执行,解密的明文源码就留在临时目录里(同用户的逆向者可直接读走)。
|
|
7
|
+
// 这里在每次物化前扫一遍临时目录,回收"同应用且属主进程已不在"的历史目录。
|
|
8
|
+
//
|
|
9
|
+
// 目录名格式:freedom-<app>-<pid>-<MkdirTemp 随机后缀>。按应用名精确匹配,
|
|
10
|
+
// 因此只可能删到同一应用自己的残留目录;判据失败(PID 解析不出、进程仍活着)
|
|
11
|
+
// 一律保留,宁留残留不误删。
|
|
12
|
+
|
|
13
|
+
import (
|
|
14
|
+
"os"
|
|
15
|
+
"path/filepath"
|
|
16
|
+
"strconv"
|
|
17
|
+
"strings"
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
// secureTempDirName 生成物化目录名前缀(含应用标识与当前 PID)。
|
|
21
|
+
func secureTempDirName(app string) string {
|
|
22
|
+
return "freedom-" + app + "-" + strconv.Itoa(os.Getpid()) + "-"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// gcStaleSecureBackendDirs 删除 dir 下属于 appName、且属主进程已退出的物化目录。
|
|
26
|
+
func gcStaleSecureBackendDirs(dir, appName string) {
|
|
27
|
+
entries, err := os.ReadDir(dir)
|
|
28
|
+
if err != nil {
|
|
29
|
+
return // 临时目录不可读:跳过回收,不影响本次启动
|
|
30
|
+
}
|
|
31
|
+
prefix := "freedom-" + appName + "-"
|
|
32
|
+
for _, e := range entries {
|
|
33
|
+
if !e.IsDir() {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
pid, ok := parseSecureTempName(e.Name(), prefix)
|
|
37
|
+
if !ok || backendProcessAlive(pid) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
_ = os.RemoveAll(filepath.Join(dir, e.Name()))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// parseSecureTempName 从 "freedom-<app>-<pid>-<rand>" 取出 pid。
|
|
45
|
+
// 要求 rand 段存在(我们自建的目录必然带 MkdirTemp 后缀),据此排除
|
|
46
|
+
// 手工命名的同前缀目录。
|
|
47
|
+
func parseSecureTempName(name, prefix string) (int, bool) {
|
|
48
|
+
rest, ok := strings.CutPrefix(name, prefix)
|
|
49
|
+
if !ok {
|
|
50
|
+
return 0, false
|
|
51
|
+
}
|
|
52
|
+
pidStr, randStr, ok := strings.Cut(rest, "-")
|
|
53
|
+
if !ok || randStr == "" {
|
|
54
|
+
return 0, false
|
|
55
|
+
}
|
|
56
|
+
pid, err := strconv.Atoi(pidStr)
|
|
57
|
+
if err != nil || pid <= 0 {
|
|
58
|
+
return 0, false
|
|
59
|
+
}
|
|
60
|
+
return pid, true
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"errors"
|
|
7
|
+
"os"
|
|
8
|
+
"syscall"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// backendProcessAlive 用 signal 0 探测进程存在性:
|
|
12
|
+
// 成功=存活;EPERM=存活但非本人所有(保守判存活);ESRCH 等=已退出。
|
|
13
|
+
func backendProcessAlive(pid int) bool {
|
|
14
|
+
p, err := os.FindProcess(pid)
|
|
15
|
+
if err != nil {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
err = p.Signal(syscall.Signal(0))
|
|
19
|
+
return err == nil || errors.Is(err, syscall.EPERM)
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
import "syscall"
|
|
6
|
+
|
|
7
|
+
var (
|
|
8
|
+
kernel32SecureTemp = syscall.NewLazyDLL("kernel32.dll")
|
|
9
|
+
procOpenProcess = kernel32SecureTemp.NewProc("OpenProcess")
|
|
10
|
+
procCloseHandle = kernel32SecureTemp.NewProc("CloseHandle")
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
// processQueryLimitedInformation 是最低权限的进程存在性探测(不需要任何特权,
|
|
14
|
+
// 对已退出但句柄未回收的进程仍返回真,属安全方向的保守判定)。
|
|
15
|
+
const processQueryLimitedInformation = 0x1000
|
|
16
|
+
|
|
17
|
+
// backendProcessAlive 判断 PID 是否仍存活;OpenProcess 失败(含权限不足)即判为不存活。
|
|
18
|
+
func backendProcessAlive(pid int) bool {
|
|
19
|
+
h, _, _ := procOpenProcess.Call(processQueryLimitedInformation, 0, uintptr(pid))
|
|
20
|
+
if h == 0 {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
procCloseHandle.Call(h)
|
|
24
|
+
return true
|
|
25
|
+
}
|