@yufengtadian/freedom-cli 1.12.18 → 1.13.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.
Files changed (79) hide show
  1. package/lib/build.js +1 -1
  2. package/lib/security.js +1 -1
  3. package/package.json +5 -1
  4. package/shell/darwin-arm64/freedom-shell +0 -0
  5. package/shell/linux-x64/freedom-shell +0 -0
  6. package/shell/win-x64/freedom-shell.exe +0 -0
  7. package/templates/go/go.mod +1 -7
  8. package/templates/go/go.sum +2 -2
  9. package/templates/go/main.go +22 -8
  10. package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
  11. package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
  12. package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
  13. package/templates/go/pkg/freedom/assets/freedom.js +200 -5
  14. package/templates/go/pkg/freedom/assets_embed.go +11 -13
  15. package/templates/go/pkg/freedom/authenticode_other.go +11 -0
  16. package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
  17. package/templates/go/pkg/freedom/backend_proc.go +291 -53
  18. package/templates/go/pkg/freedom/bridge.go +13 -2
  19. package/templates/go/pkg/freedom/capability.go +77 -0
  20. package/templates/go/pkg/freedom/center_other.go +3 -0
  21. package/templates/go/pkg/freedom/center_windows.go +45 -35
  22. package/templates/go/pkg/freedom/dispatch.go +60 -0
  23. package/templates/go/pkg/freedom/events_windows.go +319 -0
  24. package/templates/go/pkg/freedom/freedom.go +193 -99
  25. package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
  26. package/templates/go/pkg/freedom/osver_other.go +15 -0
  27. package/templates/go/pkg/freedom/osver_windows.go +121 -0
  28. package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
  29. package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
  30. package/templates/go/pkg/freedom/security.go +19 -12
  31. package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
  32. package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
  33. package/templates/go/pkg/freedom/spawn_other.go +11 -0
  34. package/templates/go/pkg/freedom/spawn_windows.go +23 -0
  35. package/templates/go/pkg/freedom/store.go +458 -0
  36. package/templates/go/pkg/freedom/syscap_linux.go +278 -0
  37. package/templates/go/pkg/freedom/syscap_other.go +15 -3
  38. package/templates/go/pkg/freedom/syscap_windows.go +228 -36
  39. package/templates/go/pkg/freedom/sysint_common.go +70 -0
  40. package/templates/go/pkg/freedom/sysint_windows.go +426 -0
  41. package/templates/go/pkg/freedom/tray_common.go +12 -0
  42. package/templates/go/pkg/freedom/tray_linux.go +302 -0
  43. package/templates/go/pkg/freedom/tray_other.go +2 -2
  44. package/templates/go/pkg/freedom/tray_windows.go +69 -39
  45. package/templates/go/pkg/freedom/updater.go +372 -0
  46. package/templates/go/pkg/freedom/window_mgr.go +385 -0
  47. package/templates/go/pkg/freedom/window_other.go +32 -66
  48. package/templates/go/pkg/freedom/window_windows.go +397 -254
  49. package/templates/go/build_all.txt +0 -0
  50. package/templates/go/build_err.txt +0 -0
  51. package/templates/go/gen_config.go +0 -22
  52. package/templates/go/pkg/freedom/center_windows_test.go +0 -32
  53. package/templates/go/pkg/freedom/security_test.go +0 -153
  54. package/templates/go/temp_vet_test/main.go +0 -40
  55. package/templates/go/test_err.txt +0 -0
  56. package/templates/go/vet_err.txt +0 -1
  57. package/templates/go/vet_err2.txt +0 -2
  58. package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
  59. package/templates/go/webview_go/CHANGELOG.md +0 -15
  60. package/templates/go/webview_go/LICENSE +0 -22
  61. package/templates/go/webview_go/README.md +0 -50
  62. package/templates/go/webview_go/examples/basic/main.go +0 -12
  63. package/templates/go/webview_go/examples/bind/main.go +0 -38
  64. package/templates/go/webview_go/glue.c +0 -36
  65. package/templates/go/webview_go/go.mod +0 -3
  66. package/templates/go/webview_go/go.sum +0 -0
  67. package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
  68. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
  69. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
  70. package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
  71. package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
  72. package/templates/go/webview_go/libs/webview/LICENSE +0 -22
  73. package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
  74. package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
  75. package/templates/go/webview_go/libs/webview/vendor.go +0 -2
  76. package/templates/go/webview_go/libs/webview/version.txt +0 -1
  77. package/templates/go/webview_go/webview.cc +0 -1
  78. package/templates/go/webview_go/webview.go +0 -481
  79. package/templates/go/webview_go/webview_test.go +0 -50
@@ -0,0 +1,257 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ import (
6
+ "fmt"
7
+ "runtime"
8
+ "sync"
9
+ "syscall"
10
+ "time"
11
+ "unsafe"
12
+ )
13
+
14
+ // 辅助消息窗口(W2):独立线程上的隐藏窗口,承载全局热键(WM_HOTKEY)、
15
+ // 单实例参数转发(WM_COPYDATA)等需要消息泵的能力。
16
+ // 与托盘隐藏窗不同:托盘窗口依赖主 UI 线程泵消息且仅在 tray.create 后存在;
17
+ // 本窗口在首次需要时创建、自带 GetMessage 循环,进程存续期间常驻。
18
+
19
+ const (
20
+ wmDestroy = 0x0002
21
+ wmHotkey = 0x0312
22
+ wmCopyData = 0x004A
23
+ )
24
+
25
+ type copyDataStruct struct {
26
+ DwData uintptr
27
+ CbData uint32
28
+ _ uint32 // amd64 对齐
29
+ LpData uintptr
30
+ }
31
+
32
+ // validCopyData 入站 WM_COPYDATA 守卫:魔数、非空、指针有效、负载 ≤64KB。
33
+ func validCopyData(dwData uintptr, cbData uint32, lpData uintptr) bool {
34
+ return dwData == forwardWindowMagic && cbData > 0 && cbData <= 64*1024 && lpData != 0
35
+ }
36
+
37
+ type msgWindow struct {
38
+ mu sync.Mutex
39
+ hwnd uintptr
40
+ ready chan struct{}
41
+ created bool
42
+ hotkeys map[string]uint32 // 前端 id → 热键数值 id
43
+ nextHKID uint32
44
+ onHotkey func(id string)
45
+ onData func(payload []byte)
46
+ }
47
+
48
+ var (
49
+ msgWindowOnce sync.Once
50
+ msgWindowInst *msgWindow
51
+ msgWndProcCB uintptr
52
+ )
53
+
54
+ // ensureMsgWindow 惰性创建消息窗口线程并等待窗口就绪。
55
+ // title 作为窗口标题:单实例模式以 appID 为标题,供 FindWindowW 定位主实例。
56
+ func ensureMsgWindow(title string) (*msgWindow, error) {
57
+ var initErr error
58
+ msgWindowOnce.Do(func() {
59
+ mw := &msgWindow{
60
+ ready: make(chan struct{}),
61
+ hotkeys: map[string]uint32{},
62
+ }
63
+ msgWindowInst = mw
64
+ go mw.runLoop(title)
65
+ select {
66
+ case <-mw.ready:
67
+ case <-time.After(3 * time.Second):
68
+ initErr = fmt.Errorf("freedom: 消息窗口创建超时")
69
+ }
70
+ })
71
+ if initErr != nil {
72
+ return nil, initErr
73
+ }
74
+ if msgWindowInst == nil {
75
+ return nil, fmt.Errorf("freedom: 消息窗口不可用")
76
+ }
77
+ msgWindowInst.mu.Lock()
78
+ created := msgWindowInst.created
79
+ msgWindowInst.mu.Unlock()
80
+ if !created {
81
+ return nil, fmt.Errorf("freedom: 消息窗口不可用")
82
+ }
83
+ return msgWindowInst, nil
84
+ }
85
+
86
+ // setTitle 更新窗口标题(单实例以 appID 为标题供 FindWindowW 定位;
87
+ // 窗口可能先由热键等能力以默认标题创建,此时需纠正)。
88
+ func (mw *msgWindow) setTitle(title string) {
89
+ mw.mu.Lock()
90
+ hwnd := mw.hwnd
91
+ mw.mu.Unlock()
92
+ if hwnd == 0 || title == "" {
93
+ return
94
+ }
95
+ if t, err := syscall.UTF16PtrFromString(title); err == nil {
96
+ procSetWindowText.Call(hwnd, uintptr(unsafe.Pointer(t)))
97
+ }
98
+ }
99
+
100
+ func msgWndProc(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
101
+ mw := msgWindowInst
102
+ switch msg {
103
+ case wmHotkey:
104
+ if mw != nil {
105
+ mw.mu.Lock()
106
+ var key string
107
+ for id, hid := range mw.hotkeys {
108
+ if hid == uint32(wParam) {
109
+ key = id
110
+ break
111
+ }
112
+ }
113
+ cb := mw.onHotkey
114
+ mw.mu.Unlock()
115
+ if cb != nil && key != "" {
116
+ go cb(key)
117
+ }
118
+ }
119
+ return 0
120
+ case wmCopyData:
121
+ if mw != nil && lParam != 0 {
122
+ cds := (*copyDataStruct)(unsafe.Pointer(lParam))
123
+ // 校验魔数与长度:任意进程都可向本窗口投 COPYDATA,
124
+ // 未验 DwData 会把伪造内存当 JSON 解析,未限 CbData 可放大内存占用。
125
+ if validCopyData(cds.DwData, cds.CbData, cds.LpData) {
126
+ buf := make([]byte, cds.CbData)
127
+ copy(buf, unsafe.Slice((*byte)(unsafe.Pointer(cds.LpData)), cds.CbData))
128
+ mw.mu.Lock()
129
+ cb := mw.onData
130
+ mw.mu.Unlock()
131
+ if cb != nil {
132
+ go cb(buf)
133
+ }
134
+ }
135
+ }
136
+ return 0
137
+ case wmDestroy:
138
+ procPostQuitMessage.Call(0)
139
+ }
140
+ r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
141
+ return r
142
+ }
143
+
144
+ // runLoop 在锁定线程上创建隐藏窗口并泵消息(窗口随进程存活,不销毁)。
145
+ func (mw *msgWindow) runLoop(title string) {
146
+ runtime.LockOSThread()
147
+ className, _ := syscall.UTF16PtrFromString(siWndClass)
148
+ windowTitle := className
149
+ if title != "" {
150
+ t, err := syscall.UTF16PtrFromString(title)
151
+ if err == nil {
152
+ windowTitle = t
153
+ }
154
+ }
155
+ if msgWndProcCB == 0 {
156
+ msgWndProcCB = syscall.NewCallback(msgWndProc)
157
+ }
158
+ hInst := uintptr(0)
159
+ if m, _, _ := procGetModuleHandle.Call(0); m != 0 {
160
+ hInst = m
161
+ }
162
+ wc := wndClassExW{
163
+ CbSize: uint32(unsafe.Sizeof(wndClassExW{})),
164
+ LpfnWndProc: msgWndProcCB,
165
+ HInstance: hInst,
166
+ LpszClassName: className,
167
+ }
168
+ procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))) // 重复注册失败可忽略
169
+ hwnd, _, _ := procCreateWindowExW.Call(
170
+ 0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(windowTitle)),
171
+ 0, 0, 0, 0, 0, 0, 0, hInst, 0)
172
+ mw.mu.Lock()
173
+ mw.hwnd = hwnd
174
+ mw.created = hwnd != 0
175
+ mw.mu.Unlock()
176
+ close(mw.ready)
177
+ if hwnd == 0 {
178
+ return
179
+ }
180
+ // 标准消息循环:WM_HOTKEY / WM_COPYDATA 在此派发。
181
+ var msg struct {
182
+ HWnd uintptr
183
+ Message uint32
184
+ WParam uintptr
185
+ LParam uintptr
186
+ Time uint32
187
+ Pt point
188
+ }
189
+ for {
190
+ r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&msg)), 0, 0, 0)
191
+ if int32(r) == 0 || r == ^uintptr(0) { // WM_QUIT 或错误 → 退出
192
+ break
193
+ }
194
+ procTranslateMessage.Call(uintptr(unsafe.Pointer(&msg)))
195
+ procDispatchMessageW.Call(uintptr(unsafe.Pointer(&msg)))
196
+ }
197
+ }
198
+
199
+ // registerHotkey 注册全局热键(win/chord 组合,见 hotkeyKeys 表)。
200
+ func (mw *msgWindow) registerHotkey(id, combo string) error {
201
+ mods, vk, err := parseHotkey(combo)
202
+ if err != nil {
203
+ return err
204
+ }
205
+ mw.mu.Lock()
206
+ defer mw.mu.Unlock()
207
+ if _, dup := mw.hotkeys[id]; dup {
208
+ return fmt.Errorf("freedom: 热键 %q 已注册", id)
209
+ }
210
+ if mw.nextHKID == 0 {
211
+ mw.nextHKID = 0x4651 // "FQ"
212
+ }
213
+ hid := mw.nextHKID
214
+ mw.nextHKID++
215
+ r, _, e := procRegisterHotKey.Call(mw.hwnd, uintptr(hid), uintptr(mods), uintptr(vk))
216
+ if r == 0 {
217
+ return fmt.Errorf("freedom: RegisterHotKey(%s) 失败: %w", combo, e)
218
+ }
219
+ mw.hotkeys[id] = hid
220
+ return nil
221
+ }
222
+
223
+ func (mw *msgWindow) unregisterHotkey(id string) error {
224
+ mw.mu.Lock()
225
+ defer mw.mu.Unlock()
226
+ hid, ok := mw.hotkeys[id]
227
+ if !ok {
228
+ return fmt.Errorf("freedom: 热键 %q 未注册", id)
229
+ }
230
+ procUnregisterHotKey.Call(mw.hwnd, uintptr(hid))
231
+ delete(mw.hotkeys, id)
232
+ return nil
233
+ }
234
+
235
+ func (mw *msgWindow) hotkeyIDs() []string {
236
+ mw.mu.Lock()
237
+ defer mw.mu.Unlock()
238
+ out := make([]string, 0, len(mw.hotkeys))
239
+ for id := range mw.hotkeys {
240
+ out = append(out, id)
241
+ }
242
+ return out
243
+ }
244
+
245
+ // setOnHotkey 注册热键触发回调(WM_HOTKEY → 前端事件)。
246
+ func (mw *msgWindow) setOnHotkey(fn func(id string)) {
247
+ mw.mu.Lock()
248
+ mw.onHotkey = fn
249
+ mw.mu.Unlock()
250
+ }
251
+
252
+ // setOnData 注册 WM_COPYDATA 负载回调(单实例参数转发)。
253
+ func (mw *msgWindow) setOnData(fn func(payload []byte)) {
254
+ mw.mu.Lock()
255
+ mw.onData = fn
256
+ mw.mu.Unlock()
257
+ }
@@ -0,0 +1,15 @@
1
+ //go:build !windows
2
+
3
+ package freedom
4
+
5
+ // osVersionString 在非 Windows 平台返回空串(osInfo.osVersion 缺省,前端可用 platform 字段兜底)。
6
+ func osVersionString() string { return "" }
7
+
8
+ // webview2RuntimeVersion 非 Windows 无 WebView2 Runtime 概念(渲染内核不同),恒为空。
9
+ func webview2RuntimeVersion() string { return "" }
10
+
11
+ // stateOnScreen 非 Windows 无显示器枚举实现,恒为 true(保存路径本身是 no-op)。
12
+ func stateOnScreen(st WindowState) bool { return true }
13
+
14
+ // persistWindowState 非 Windows 平台的窗口几何读取未实现,no-op。
15
+ func (a *App) persistWindowState(hwnd uintptr, dir string) {}
@@ -0,0 +1,121 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ import (
6
+ "strings"
7
+ "syscall"
8
+ "unsafe"
9
+ )
10
+
11
+ // osVersionString 经 ntdll!RtlGetVersion 取真实 Windows 版本
12
+ // (GetVersionExW 受 manifest 兼容性垫片影响会谎报,RtlGetVersion 不受影响)。
13
+ func osVersionString() string {
14
+ ntdll := syscall.NewLazyDLL("ntdll.dll")
15
+ proc := ntdll.NewProc("RtlGetVersion")
16
+ if proc.Find() != nil {
17
+ return ""
18
+ }
19
+ type osVersionInfoExW struct {
20
+ _len uint32
21
+ major, minor uint32
22
+ build, platformID uint32
23
+ csd [128]uint16
24
+ }
25
+ var v osVersionInfoExW
26
+ v._len = uint32(unsafe.Sizeof(v))
27
+ if st, _, _ := proc.Call(uintptr(unsafe.Pointer(&v))); st != 0 { // NT_SUCCESS
28
+ return ""
29
+ }
30
+ return "Windows " + itoa(int(v.major)) + "." + itoa(int(v.minor)) + " build " + itoa(int(v.build))
31
+ }
32
+
33
+ // ---- WebView2 Runtime 探测(M6 运行时引导回显)----
34
+
35
+ // Edge Evergreen 客户端注册表项 GUID;HKCU 用户级安装优先,HKLM 系统级兜底。
36
+ const (
37
+ hkeyLocalMachine = uintptr(0x80000002)
38
+ webview2CUKey = `Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}`
39
+ webview2LMKey = `SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}`
40
+ )
41
+
42
+ // normalizeWebView2Version 过滤占位值:"N/A"(Edge 随 OS 镜像的 MSIX 占位)与空白视为未检出。
43
+ func normalizeWebView2Version(pv string) string {
44
+ pv = strings.TrimSpace(pv)
45
+ if pv == "" || pv == "N/A" {
46
+ return ""
47
+ }
48
+ return pv
49
+ }
50
+
51
+ // webview2RuntimeVersion 返回系统 WebView2 Runtime 版本号("120.0.2210.91" 风格),
52
+ // 未安装/不可探测返回 ""。前端经 os.info.webview2Runtime 预检运行环境(对标
53
+ // Tauri webview_install 的引导前探测——本框架不自动下载 Runtime,只回显)。
54
+ func webview2RuntimeVersion() string {
55
+ for _, q := range []struct {
56
+ base uintptr
57
+ path string
58
+ }{{hkeyCurrentUser, webview2CUKey}, {hkeyLocalMachine, webview2LMKey}} {
59
+ hk, err := regOpen(q.base, q.path, keyRead)
60
+ if err != nil {
61
+ continue
62
+ }
63
+ v, found, err := regQuerySz(hk, "pv")
64
+ procRegCloseKey.Call(hk)
65
+ if err != nil || !found {
66
+ continue
67
+ }
68
+ if s := normalizeWebView2Version(v); s != "" {
69
+ return s
70
+ }
71
+ }
72
+ return ""
73
+ }
74
+
75
+ func itoa(n int) string {
76
+ if n == 0 {
77
+ return "0"
78
+ }
79
+ var b [24]byte
80
+ i := len(b)
81
+ for n > 0 {
82
+ i--
83
+ b[i] = byte('0' + n%10)
84
+ n /= 10
85
+ }
86
+ return string(b[i:])
87
+ }
88
+
89
+ // stateOnScreen 判断保存的坐标是否仍落在某个显示器范围内(含 8px 容差)。
90
+ func stateOnScreen(st WindowState) bool {
91
+ for _, m := range listMonitors() {
92
+ if st.X >= m.X-8 && st.Y >= m.Y-8 &&
93
+ st.X < m.X+m.Width-8 && st.Y < m.Y+m.Height-8 {
94
+ return true
95
+ }
96
+ }
97
+ return false
98
+ }
99
+
100
+ // persistWindowState 把当前窗口几何写入状态文件(store.go 的 RememberWindowState)。
101
+ // 全屏中跳过:存的应是全屏前的几何(进入全屏前的最后一次 ExitSizeMove 已落过盘),
102
+ // 全屏矩形落盘会在下次启动还原成一个占满屏幕的普通窗口。
103
+ func (a *App) persistWindowState(hwnd uintptr, dir string) {
104
+ a.wsMu.Lock()
105
+ defer a.wsMu.Unlock()
106
+ if v, ok := windowRuntimes.Load(hwnd); ok {
107
+ if v.(*windowRuntime).getFullscreen() {
108
+ return
109
+ }
110
+ }
111
+ r, err := getWindowRect(hwnd)
112
+ if err != nil {
113
+ return
114
+ }
115
+ st := WindowState{
116
+ X: int(r.left), Y: int(r.top),
117
+ Width: int(r.right - r.left), Height: int(r.bottom - r.top),
118
+ Maximized: isZoomed(hwnd),
119
+ }
120
+ _ = saveWindowState(dir, st)
121
+ }
@@ -7,8 +7,8 @@ import (
7
7
  "syscall"
8
8
  )
9
9
 
10
- // hideWindow 让后端子进程(即使自身是控制台程序)不弹出 cmd 黑窗。
11
- // GUI 子系统父进程拉起 console 子进程时,Windows 默认会为其新建控制台窗口,
10
+ // hideWindow 让后端子进程(即便是控制台程序)不弹出 cmd 黑窗。
11
+ // GUI 子系统拉起 console 子进程时,Windows 默认会为其新建控制台窗口,
12
12
  // 设置 HideWindow 可使其在后台静默运行。
13
13
  func hideWindow(cmd *exec.Cmd) {
14
14
  cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
@@ -1,14 +1,15 @@
1
1
  package freedom
2
2
 
3
- // 运行时外部资源(resources/)加载逻辑。
3
+ // 运行时外部资源(resources/)加载层——预编译通用壳(cmd/shell)与源码构建应用共用。
4
4
  //
5
- // 通用壳二进制(预编译)本身不嵌入任何应用专属的前端与配置;
6
- // 应用内容来自 exe 同目录的 resources/ 目录:
5
+ // 通用壳二进制本身不嵌入任何应用专属的前端与配置;应用内容来自 exe 同目录的
6
+ // resources/ 目录:
7
7
  // resources/index.html 前端页面(CLI build 写入)
8
8
  // resources/config.json 窗口与后端配置(CLI build 写入)
9
+ // resources/app.bin high 安全模式加密容器(config+html,见 security.go)
9
10
  //
10
- // 外部资源缺失时静默回退:配置使用编译期/默认值,页面使用内置占位页。
11
- // 因此同一份壳二进制可以复用于任意应用,且打包时无需任何语言工具链。
11
+ // 外部资源缺失时静默回退:配置使用编译期/默认值,页面使用 cfg.HTML 或内置占位页。
12
+ // 同一份壳二进制可复用于任意应用,最终用户打包时无需任何语言工具链。
12
13
 
13
14
  import (
14
15
  "encoding/json"
@@ -33,27 +34,6 @@ func secureFatal(err error) error {
33
34
  return &secureFatalError{err: err}
34
35
  }
35
36
 
36
- // loadRuntimeConfig 读取 exe 同目录 resources/config.json,将命中的字段覆盖到应用配置。
37
- // 文件不存在时视为未配置(返回 nil,保持编译期/默认配置不变);
38
- // 文件存在但读取/解析失败时返回具体错误,由调用方打印告警,避免用户手改配置出错时无感知。
39
- func (a *App) loadRuntimeConfig() error {
40
- // high 安全模式:配置与页面封装在加密容器 app.bin 内,优先内存解密加载。
41
- if p, hit, err := loadSecureResources(); err != nil {
42
- return secureFatal(err)
43
- } else if hit {
44
- var rc runtimeConfigFile
45
- if err := json.Unmarshal([]byte(p.Config), &rc); err != nil {
46
- return fmt.Errorf("parse encrypted config: %w", err)
47
- }
48
- a.applyRuntimeConfig(&rc)
49
- // high 模式强制关闭 WebView 开发者工具,防止前端源码经 devtools 直接查看。
50
- a.cfg.Debug = false
51
- a.secure = true
52
- return nil
53
- }
54
- return a.loadRuntimeConfigPlain()
55
- }
56
-
57
37
  // runtimeBackend 描述 config.json 中的后端进程配置(任意语言,经 stdio NDJSON 桥接)。
58
38
  type runtimeBackend struct {
59
39
  Command string `json:"command"`
@@ -61,7 +41,7 @@ type runtimeBackend struct {
61
41
  }
62
42
 
63
43
  // runtimeConfigFile 描述 resources/config.json 的磁盘结构。
64
- // 由 freedom CLI 在 build 阶段根据 freedom.config.js 生成。
44
+ // 由 freedom CLI 在 build 阶段根据 freedom.config.js 生成;Name 仅作来源标记不应用。
65
45
  type runtimeConfigFile struct {
66
46
  Name string `json:"name"`
67
47
  Title string `json:"title"`
@@ -75,8 +55,14 @@ type runtimeConfigFile struct {
75
55
  Backend *runtimeBackend `json:"backend"`
76
56
  }
77
57
 
58
+ // resourcesDirOverride 是单测缝:非空时替代 exe 同目录的 resources 定位。
59
+ var resourcesDirOverride string
60
+
78
61
  // resourcesDir 返回 exe 同目录的 resources 目录绝对路径。
79
62
  func resourcesDir() (string, error) {
63
+ if resourcesDirOverride != "" {
64
+ return resourcesDirOverride, nil
65
+ }
80
66
  exe, err := os.Executable()
81
67
  if err != nil {
82
68
  return "", err
@@ -84,6 +70,27 @@ func resourcesDir() (string, error) {
84
70
  return filepath.Join(filepath.Dir(exe), "resources"), nil
85
71
  }
86
72
 
73
+ // loadRuntimeConfig 读取 exe 同目录 resources/config.json,将命中的字段覆盖到应用配置。
74
+ // 文件不存在时视为未配置(返回 nil,保持编译期/默认配置不变);
75
+ // 文件存在但读取/解析失败时返回具体错误,由调用方打印告警,避免用户手改配置出错时无感知。
76
+ func (a *App) loadRuntimeConfig() error {
77
+ // high 安全模式:配置与页面封装在加密容器 app.bin 内,优先内存解密加载。
78
+ if p, hit, err := loadSecureResources(); err != nil {
79
+ return secureFatal(err)
80
+ } else if hit {
81
+ var rc runtimeConfigFile
82
+ if err := json.Unmarshal([]byte(p.Config), &rc); err != nil {
83
+ return fmt.Errorf("parse encrypted config: %w", err)
84
+ }
85
+ a.applyRuntimeConfig(&rc)
86
+ // high 模式强制关闭 WebView 开发者工具,防止前端源码经 devtools 直接查看。
87
+ a.cfg.Debug = false
88
+ a.secure = true
89
+ return nil
90
+ }
91
+ return a.loadRuntimeConfigPlain()
92
+ }
93
+
87
94
  // loadRuntimeConfigPlain 明文配置路径(非 high 模式):读取 resources/config.json。
88
95
  func (a *App) loadRuntimeConfigPlain() error {
89
96
  dir, err := resourcesDir()
@@ -102,17 +109,13 @@ func (a *App) loadRuntimeConfigPlain() error {
102
109
  if err := json.Unmarshal(data, &rc); err != nil {
103
110
  return fmt.Errorf("parse %s: %w", cfgPath, err)
104
111
  }
105
- if err := a.applyRuntimeConfig(&rc); err != nil {
106
- return err
107
- }
112
+ a.applyRuntimeConfig(&rc)
108
113
  return nil
109
114
  }
110
115
 
111
116
  // applyRuntimeConfig 把解析后的运行时配置覆盖到应用配置(明文与加密路径共用)。
112
- func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) error {
117
+ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) {
113
118
  // 窗口标题:config.json 的 title 覆盖编译期默认("Freedom App")。
114
- // 此前只解析未应用,导致改 freedom.config.js 的 name/title 无效、
115
- // 打包产物窗口标题永远停留在默认值("无法修改程序名字")。
116
119
  if rc.Title != "" {
117
120
  a.cfg.Title = rc.Title
118
121
  }
@@ -140,7 +143,8 @@ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) error {
140
143
  if rc.Debug != nil {
141
144
  a.cfg.Debug = *rc.Debug
142
145
  }
143
- // 外部后端进程配置:仅在壳未显式绑定后端时生效。
146
+ // 外部后端进程配置:仅在壳未显式绑定后端时生效(用户 Bind 过内嵌方法 =
147
+ // 显式内嵌后端,config.json 不得静默替换,否则其方法全部失效)。
144
148
  // 工作目录设为 resources 目录,使相对路径参数(backend/main.mjs)按
145
149
  // resources/ 解析(CLI 会把项目 backend/ 目录分发到 resources/backend/)。
146
150
  if rc.Backend != nil && rc.Backend.Command != "" && !a.backendExplicit {
@@ -152,7 +156,6 @@ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) error {
152
156
  a.cfg.Backend = pb
153
157
  a.backend = pb
154
158
  }
155
- return nil
156
159
  }
157
160
 
158
161
  // loadRuntimeHTML 返回前端页面内容(high 模式:解密 app.bin 内的 html;
@@ -1,17 +1,17 @@
1
1
  package freedom
2
2
 
3
- // security.go — 安全加固:high 模式资源加密与完整性校验。
3
+ // security.go — high 安全模式:资源加密容器(app.bin)与完整性校验。
4
4
  //
5
- // 与 lib/security.js 跨语言同步(改任一侧必须同步另一侧,否则无法解密):
5
+ // 与 freedom-cli lib/security.js 跨语言同步(改任一侧必须同步另一侧,否则 CLI
6
+ // 加密的产物壳无法解密):
6
7
  // - securityMasterKey / securityDeriveSalt / securityPbkdf2Iter / securityKeyLen
7
8
  // - securityMagic / securityIVLen / securityTagLen(app.bin 容器头格式)
8
9
  // - 派生算法:PBKDF2-HMAC-SHA256(RFC 2898),salt = "freedom:derive:v1:" + 应用标识
9
10
  // - 应用标识 = exe 文件名去扩展名(CLI build 时的 name 与运行时 os.Executable() 一致)
10
11
  //
11
- // 加密算法说明:AES-256-CTR + HMAC-SHA256(Encrypt-then-MAC),
12
- // 与 Node crypto 的 aes-256-ctr + createHmac('sha256') 跨语言一致。
13
- // (不采用 GCM:在当前 Windows 环境的 Go 标准库 GCM/GHASH 存在确定性认证失败问题,
14
- // 经最小复现确认与缓存/CPU 指令无关;CTR+HMAC 语义等价且规避该问题。)
12
+ // 加密算法:AES-256-CTR + HMAC-SHA256(Encrypt-then-MAC),与 Node crypto 的
13
+ // aes-256-ctr + createHmac('sha256') 跨语言一致。(不用 GCM:曾在 Windows 环境
14
+ // 出现标准库 GHASH 确定性认证失败,CTR+HMAC 语义等价且规避该问题。)
15
15
  //
16
16
  // high 模式产物布局(CLI build 写入,与明文模式互斥):
17
17
  // resources/app.bin 加密容器:magic(5B FRDM1)+iv(16B)+tag(16B)+ciphertext
@@ -60,7 +60,7 @@ type integrityFile struct {
60
60
 
61
61
  // pbkdf2HMACSHA256 自实现 PBKDF2-HMAC-SHA256(RFC 2898),
62
62
  // 与 Node 的 crypto.pbkdf2Sync(password, salt, iter, keyLen, 'sha256') 结果一致。
63
- // 不引入 golang.org/x/crypto(go.mod 为 go 1.22,标准库未含 PBKDF2)。
63
+ // 标准库至今未含 PBKDF2,且 go.mod 保持零外部依赖,故手写字面实现。
64
64
  func pbkdf2HMACSHA256(password, salt []byte, iter, keyLen int) []byte {
65
65
  hLen := sha256.Size
66
66
  numBlocks := (keyLen + hLen - 1) / hLen
@@ -87,15 +87,22 @@ func pbkdf2HMACSHA256(password, salt []byte, iter, keyLen int) []byte {
87
87
  return dk[:keyLen]
88
88
  }
89
89
 
90
- // appIdentityName 返回应用标识:exe 文件名去扩展名(.exe / .app)。
90
+ // appIdentityName 返回应用标识:exe 文件名去扩展名(.exe / .app,仅去最后一个后缀,
91
+ // 与 lib/security.js appIdentityFor 的正则语义一致)。
91
92
  func appIdentityName(name string) string {
92
93
  s := strings.TrimSuffix(name, ".exe")
93
94
  s = strings.TrimSuffix(s, ".app")
94
95
  return s
95
96
  }
96
97
 
98
+ // appIdentityOverride 是单测缝:非空时替代 exe 文件名推导的应用标识。
99
+ var appIdentityOverride string
100
+
97
101
  // appIdentity 返回运行时应用标识(当前进程 exe 文件名去扩展名)。
98
102
  func appIdentity() (string, error) {
103
+ if appIdentityOverride != "" {
104
+ return appIdentityName(appIdentityOverride), nil
105
+ }
99
106
  exe, err := os.Executable()
100
107
  if err != nil {
101
108
  return "", err
@@ -103,8 +110,8 @@ func appIdentity() (string, error) {
103
110
  return appIdentityName(filepath.Base(exe)), nil
104
111
  }
105
112
 
106
- // deriveSecurityKey 派生 high 模式加密密钥(AES-256-CTR + HMAC-SHA256 共用同一 32B 密钥)。
107
- // salt 引入应用标识,使不同应用密钥不同;PBKDF2 迭代增加暴力破解成本。
113
+ // deriveSecurityKey 派生 high 模式加密密钥(AES-256-CTR 与 HMAC-SHA256 共用同一 32B 密钥)。
114
+ // salt 引入应用标识使不同应用密钥不同;PBKDF2 迭代增加暴力破解成本。
108
115
  func deriveSecurityKey(appName string) []byte {
109
116
  salt := []byte(securityDeriveSalt + ":" + appIdentityName(appName))
110
117
  return pbkdf2HMACSHA256([]byte(securityMasterKey), salt, securityPbkdf2Iter, securityKeyLen)
@@ -129,7 +136,7 @@ func decryptAppBin(appName string, data []byte) (*securePayload, error) {
129
136
 
130
137
  key := deriveSecurityKey(appName)
131
138
 
132
- // 1) 认证:HMAC-SHA256(ct) 前 16 字节,与容器头 tag 恒定时间比对。
139
+ // 1) 认证:HMAC-SHA256(ct) 前 16 字节与容器头 tag 恒定时间比对。
133
140
  mac := hmac.New(sha256.New, key)
134
141
  mac.Write(ct)
135
142
  if !hmac.Equal(tag, mac.Sum(nil)[:securityTagLen]) {
@@ -154,7 +161,7 @@ func decryptAppBin(appName string, data []byte) (*securePayload, error) {
154
161
 
155
162
  // verifyIntegrity 校验 resources/.integrity 清单(存在时)。
156
163
  // 比对 app.bin 与 backend/** 各文件 HMAC-SHA256,防整体替换/篡改。
157
- // 无 .integrity 文件(CLI 旧版产物)时跳过,保持向后兼容。
164
+ // 无 .integrity 文件(旧产物)时跳过,保持向后兼容。
158
165
  func verifyIntegrity(dir, appName string, appBin []byte) error {
159
166
  raw, err := os.ReadFile(filepath.Join(dir, ".integrity"))
160
167
  if err != nil {
@@ -0,0 +1,10 @@
1
+ //go:build !windows
2
+
3
+ package freedom
4
+
5
+ // RequestSingleInstance 在非 Windows 平台为无操作:始终视为唯一实例。
6
+ // TODO(cross-platform): macOS 可用 NSRunningApplication、Linux 可用 D-Bus 单实例总线对齐该能力。
7
+ func RequestSingleInstance(appID string) bool { return true }
8
+
9
+ // OnSecondInstance 在非 Windows 平台不会被触发。
10
+ func OnSecondInstance(fn func(args []string)) {}