@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.
- package/lib/build.js +1 -1
- package/lib/security.js +1 -1
- package/package.json +5 -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/templates/go/go.mod +1 -7
- package/templates/go/go.sum +2 -2
- package/templates/go/main.go +22 -8
- package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
- package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
- package/templates/go/pkg/freedom/assets/freedom.js +200 -5
- package/templates/go/pkg/freedom/assets_embed.go +11 -13
- package/templates/go/pkg/freedom/authenticode_other.go +11 -0
- package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
- package/templates/go/pkg/freedom/backend_proc.go +291 -53
- package/templates/go/pkg/freedom/bridge.go +13 -2
- package/templates/go/pkg/freedom/capability.go +77 -0
- package/templates/go/pkg/freedom/center_other.go +3 -0
- package/templates/go/pkg/freedom/center_windows.go +45 -35
- package/templates/go/pkg/freedom/dispatch.go +60 -0
- package/templates/go/pkg/freedom/events_windows.go +319 -0
- package/templates/go/pkg/freedom/freedom.go +193 -99
- package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
- package/templates/go/pkg/freedom/osver_other.go +15 -0
- package/templates/go/pkg/freedom/osver_windows.go +121 -0
- package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
- package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
- package/templates/go/pkg/freedom/security.go +19 -12
- package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
- package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
- package/templates/go/pkg/freedom/spawn_other.go +11 -0
- package/templates/go/pkg/freedom/spawn_windows.go +23 -0
- package/templates/go/pkg/freedom/store.go +458 -0
- package/templates/go/pkg/freedom/syscap_linux.go +278 -0
- package/templates/go/pkg/freedom/syscap_other.go +15 -3
- package/templates/go/pkg/freedom/syscap_windows.go +228 -36
- package/templates/go/pkg/freedom/sysint_common.go +70 -0
- package/templates/go/pkg/freedom/sysint_windows.go +426 -0
- package/templates/go/pkg/freedom/tray_common.go +12 -0
- package/templates/go/pkg/freedom/tray_linux.go +302 -0
- package/templates/go/pkg/freedom/tray_other.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +69 -39
- package/templates/go/pkg/freedom/updater.go +372 -0
- package/templates/go/pkg/freedom/window_mgr.go +385 -0
- package/templates/go/pkg/freedom/window_other.go +32 -66
- package/templates/go/pkg/freedom/window_windows.go +397 -254
- package/templates/go/build_all.txt +0 -0
- package/templates/go/build_err.txt +0 -0
- package/templates/go/gen_config.go +0 -22
- package/templates/go/pkg/freedom/center_windows_test.go +0 -32
- package/templates/go/pkg/freedom/security_test.go +0 -153
- package/templates/go/temp_vet_test/main.go +0 -40
- package/templates/go/test_err.txt +0 -0
- package/templates/go/vet_err.txt +0 -1
- package/templates/go/vet_err2.txt +0 -2
- package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
- package/templates/go/webview_go/CHANGELOG.md +0 -15
- package/templates/go/webview_go/LICENSE +0 -22
- package/templates/go/webview_go/README.md +0 -50
- package/templates/go/webview_go/examples/basic/main.go +0 -12
- package/templates/go/webview_go/examples/bind/main.go +0 -38
- package/templates/go/webview_go/glue.c +0 -36
- package/templates/go/webview_go/go.mod +0 -3
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
- package/templates/go/webview_go/libs/webview/LICENSE +0 -22
- package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
- package/templates/go/webview_go/libs/webview/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/version.txt +0 -1
- package/templates/go/webview_go/webview.cc +0 -1
- package/templates/go/webview_go/webview.go +0 -481
- 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
|
|
11
|
-
// GUI
|
|
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
|
-
//
|
|
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
|
-
|
|
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)
|
|
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 —
|
|
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
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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
|
-
//
|
|
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
|
|
107
|
-
// salt
|
|
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
|
|
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
|
|
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)) {}
|