//go:build windows package main import ( "fmt" "runtime" "syscall" "unicode/utf16" "unsafe" ) const ( mbOK = 0x00000000 mbOKCancel = 0x00000001 mbIconError = 0x00000010 mbIconQuestion = 0x00000020 mbIconInformation = 0x00000040 mbSetForeground = 0x00010000 mbTopMost = 0x00040000 idOK = 1 ) const ( cfUnicodeText = 13 gmemMoveable = 0x0002 ) const ( wmNull = 0x0000 wmDestroy = 0x0002 wmClose = 0x0010 wmCopyData = 0x004A wmCommand = 0x0111 wmLButtonUp = 0x0202 wmLButtonDblClk = 0x0203 wmRButtonUp = 0x0205 wmContextMenu = 0x007B wmApp = 0x8000 ) const ( trayCallbackMessage = wmApp + 1 showWindowMessage = wmApp + 2 copyDataMagic = 0x44434131 ) const ( trayMenuDeviceInfo = 1001 trayMenuExit = 1002 ) const ( nimAdd = 0x00000000 nimModify = 0x00000001 nimDelete = 0x00000002 ) const ( nifMessage = 0x00000001 nifIcon = 0x00000002 nifTip = 0x00000004 nifInfo = 0x00000010 ) const ( niifInfo = 0x00000001 ) const ( mfString = 0x00000000 mfGrayed = 0x00000001 mfSeparator = 0x00000800 tpmRight = 0x00000002 tpmReturn = 0x00000100 ) const ( errorAlreadyExists = syscall.Errno(183) errorClassAlreadyReg = syscall.Errno(1410) ) const ( windowClassName = "DCAuthHiddenWindow" windowTitle = "DCAuth" mutexName = `Local\DCAuth.OrderSite` authorizationMutexName = `Local\DCAuth.OrderSite.Authorization` ) type point struct { X int32 Y int32 } type message struct { HWnd uintptr Message uint32 WParam uintptr LParam uintptr Time uint32 Pt point Private uint32 } type wndClassEx struct { CbSize uint32 Style uint32 LpfnWndProc uintptr CbClsExtra int32 CbWndExtra int32 HInstance uintptr HIcon uintptr HCursor uintptr HbrBackground uintptr LpszMenuName *uint16 LpszClassName *uint16 HIconSm uintptr } type copyDataStruct struct { DwData uintptr CbData uint32 LpData uintptr } type guid struct { Data1 uint32 Data2 uint16 Data3 uint16 Data4 [8]byte } type notifyIconData struct { CbSize uint32 HWnd uintptr UID uint32 UFlags uint32 UCallbackMessage uint32 HIcon uintptr SzTip [128]uint16 DwState uint32 DwStateMask uint32 SzInfo [256]uint16 UVersion uint32 SzInfoTitle [64]uint16 DwInfoFlags uint32 GuidItem guid HBalloonIcon uintptr } var ( user32DLL = syscall.NewLazyDLL("user32.dll") shell32DLL = syscall.NewLazyDLL("shell32.dll") messageBoxW = user32DLL.NewProc("MessageBoxW") openClip = user32DLL.NewProc("OpenClipboard") closeClip = user32DLL.NewProc("CloseClipboard") emptyClip = user32DLL.NewProc("EmptyClipboard") setClipData = user32DLL.NewProc("SetClipboardData") registerClassExW = user32DLL.NewProc("RegisterClassExW") createWindowExW = user32DLL.NewProc("CreateWindowExW") defWindowProcW = user32DLL.NewProc("DefWindowProcW") getMessageW = user32DLL.NewProc("GetMessageW") translateMessage = user32DLL.NewProc("TranslateMessage") dispatchMessageW = user32DLL.NewProc("DispatchMessageW") postQuitMessage = user32DLL.NewProc("PostQuitMessage") destroyWindow = user32DLL.NewProc("DestroyWindow") findWindowW = user32DLL.NewProc("FindWindowW") sendMessageW = user32DLL.NewProc("SendMessageW") postMessageW = user32DLL.NewProc("PostMessageW") createPopupMenu = user32DLL.NewProc("CreatePopupMenu") appendMenuW = user32DLL.NewProc("AppendMenuW") trackPopupMenu = user32DLL.NewProc("TrackPopupMenu") destroyMenu = user32DLL.NewProc("DestroyMenu") setForegroundWindow = user32DLL.NewProc("SetForegroundWindow") getCursorPos = user32DLL.NewProc("GetCursorPos") loadIconW = user32DLL.NewProc("LoadIconW") shellNotifyIconW = shell32DLL.NewProc("Shell_NotifyIconW") globalAlloc = kernel32DLL.NewProc("GlobalAlloc") globalLock = kernel32DLL.NewProc("GlobalLock") globalUnlock = kernel32DLL.NewProc("GlobalUnlock") createMutexW = kernel32DLL.NewProc("CreateMutexW") getModuleW = kernel32DLL.NewProc("GetModuleHandleW") sleep = kernel32DLL.NewProc("Sleep") ) var ( mainWindow uintptr instanceMutex uintptr trayData notifyIconData trayAdded bool runtimeIdentity identity identityReady bool authorizationActive bool windowProcedure = syscall.NewCallback(dcauthWindowProc) ) func runClient(protocolArg string) { runtime.LockOSThread() firstInstance, err := acquireSingleInstance() if err != nil { showError("DCAuth 启动失败", err.Error()) return } if !firstInstance { if authorizationInProgress() { return } if forwardToRunningInstance(protocolArg) { return } firstInstance, err = acquireSingleInstance() if err != nil || !firstInstance { showError("DCAuth 启动失败", "DCAuth 已在运行但暂时没有响应。请在任务管理器中结束 DCAuth.exe 后重试。") return } } registerProtocol() if err := createHiddenWindow(); err != nil { showError("DCAuth 启动失败", err.Error()) return } if err := addTrayIcon(); err != nil { showError("DCAuth 启动失败", err.Error()) return } runtimeIdentity, err = loadOrCreateIdentity() if err != nil { removeTrayIcon() showError("DCAuth 初始化失败", err.Error()) destroyWindow.Call(mainWindow) mainWindow = 0 return } identityReady = true if protocolArg != "" { handleAuthorization(protocolArg) } else { showTrayBalloon("DCAuth v"+clientVersion+" 已在后台运行", "双击托盘图标可查看版本和设备码。") } runMessageLoop() removeTrayIcon() } func acquireSingleInstance() (bool, error) { namePtr, _ := syscall.UTF16PtrFromString(mutexName) handle, _, callErr := createMutexW.Call(0, 0, uintptr(unsafe.Pointer(namePtr))) runtime.KeepAlive(namePtr) if handle == 0 { return false, fmt.Errorf("无法创建单实例锁:%v", callErr) } if errno, ok := callErr.(syscall.Errno); ok && errno == errorAlreadyExists { _ = syscall.CloseHandle(syscall.Handle(handle)) return false, nil } instanceMutex = handle return true, nil } func authorizationInProgress() bool { handle, acquired, err := acquireNamedMutex(authorizationMutexName) if err != nil || !acquired { if handle != 0 { _ = syscall.CloseHandle(syscall.Handle(handle)) } return err == nil && !acquired } _ = syscall.CloseHandle(syscall.Handle(handle)) return false } func acquireNamedMutex(name string) (uintptr, bool, error) { namePtr, _ := syscall.UTF16PtrFromString(name) handle, _, callErr := createMutexW.Call(0, 0, uintptr(unsafe.Pointer(namePtr))) runtime.KeepAlive(namePtr) if handle == 0 { return 0, false, fmt.Errorf("无法创建互斥锁:%v", callErr) } if errno, ok := callErr.(syscall.Errno); ok && errno == errorAlreadyExists { return handle, false, nil } return handle, true, nil } func forwardToRunningInstance(protocolArg string) bool { for attempt := 0; attempt < 300; attempt++ { if hwnd := findRunningInstanceWindow(); hwnd != 0 { return sendToRunningInstance(hwnd, protocolArg) } sleep.Call(100) } return false } func findRunningInstanceWindow() uintptr { classPtr, _ := syscall.UTF16PtrFromString(windowClassName) titlePtr, _ := syscall.UTF16PtrFromString(windowTitle) hwnd, _, _ := findWindowW.Call( uintptr(unsafe.Pointer(classPtr)), uintptr(unsafe.Pointer(titlePtr)), ) runtime.KeepAlive(classPtr) runtime.KeepAlive(titlePtr) return hwnd } func sendToRunningInstance(hwnd uintptr, protocolArg string) bool { if protocolArg == "" { result, _, _ := postMessageW.Call(hwnd, showWindowMessage, 0, 0) return result != 0 } encoded, _ := syscall.UTF16FromString(protocolArg) data := copyDataStruct{ DwData: copyDataMagic, CbData: uint32(len(encoded) * 2), LpData: uintptr(unsafe.Pointer(&encoded[0])), } result, _, _ := sendMessageW.Call( hwnd, wmCopyData, 0, uintptr(unsafe.Pointer(&data)), ) runtime.KeepAlive(encoded) return result != 0 } func createHiddenWindow() error { instance, _, _ := getModuleW.Call(0) if instance == 0 { return fmt.Errorf("无法读取程序实例句柄") } icon, _, _ := loadIconW.Call(0, 32512) classNamePtr, _ := syscall.UTF16PtrFromString(windowClassName) class := wndClassEx{ CbSize: uint32(unsafe.Sizeof(wndClassEx{})), LpfnWndProc: windowProcedure, HInstance: instance, HIcon: icon, LpszClassName: classNamePtr, HIconSm: icon, } result, _, callErr := registerClassExW.Call(uintptr(unsafe.Pointer(&class))) runtime.KeepAlive(classNamePtr) if result == 0 { if errno, ok := callErr.(syscall.Errno); !ok || errno != errorClassAlreadyReg { return fmt.Errorf("无法注册窗口类:%v", callErr) } } titlePtr, _ := syscall.UTF16PtrFromString(windowTitle) const wsExToolWindow = 0x00000080 hwnd, _, callErr := createWindowExW.Call( wsExToolWindow, uintptr(unsafe.Pointer(classNamePtr)), uintptr(unsafe.Pointer(titlePtr)), 0, 0, 0, 0, 0, 0, 0, instance, 0, ) if hwnd == 0 { return fmt.Errorf("无法创建 DCAuth 后台窗口:%v", callErr) } runtime.KeepAlive(classNamePtr) runtime.KeepAlive(titlePtr) mainWindow = hwnd return nil } func runMessageLoop() { var msg message for { result, _, _ := getMessageW.Call( uintptr(unsafe.Pointer(&msg)), 0, 0, 0, ) if int32(result) <= 0 { return } translateMessage.Call(uintptr(unsafe.Pointer(&msg))) dispatchMessageW.Call(uintptr(unsafe.Pointer(&msg))) } } func dcauthWindowProc(hwnd, msg, wParam, lParam uintptr) uintptr { switch uint32(msg) { case wmCopyData: if !identityReady { return 0 } data := (*copyDataStruct)(unsafe.Pointer(lParam)) if data != nil && data.DwData == copyDataMagic && data.CbData > 0 && data.LpData != 0 { count := int(data.CbData / 2) raw := syscall.UTF16ToString(unsafe.Slice((*uint16)(unsafe.Pointer(data.LpData)), count)) handleAuthorization(raw) return 1 } return 0 case showWindowMessage: if identityReady { showDeviceInfo() } return 0 case trayCallbackMessage: switch uint32(lParam) { case wmLButtonDblClk: if identityReady { showDeviceInfo() } else { showTrayBalloon("DCAuth 正在初始化", "设备信息准备完成后即可查看。") } case wmRButtonUp, wmContextMenu: showTrayMenu(hwnd) } return 0 case wmClose: destroyWindow.Call(hwnd) return 0 case wmDestroy: removeTrayIcon() postQuitMessage.Call(0) return 0 } result, _, _ := defWindowProcW.Call(hwnd, msg, wParam, lParam) return result } func addTrayIcon() error { icon, _, _ := loadIconW.Call(0, 32512) trayData = notifyIconData{ HWnd: mainWindow, UID: 1, UFlags: nifMessage | nifIcon | nifTip, UCallbackMessage: trayCallbackMessage, HIcon: icon, } trayData.CbSize = uint32(unsafe.Sizeof(trayData)) copyUTF16(trayData.SzTip[:], "DCAuth v"+clientVersion+" - 本机设备认证(后台运行)") result, _, _ := shellNotifyIconW.Call( nimAdd, uintptr(unsafe.Pointer(&trayData)), ) if result == 0 { return fmt.Errorf("无法创建系统托盘图标") } trayAdded = true return nil } func removeTrayIcon() { if !trayAdded { return } shellNotifyIconW.Call(nimDelete, uintptr(unsafe.Pointer(&trayData))) trayAdded = false } func showTrayBalloon(title, text string) { if !trayAdded { return } info := trayData info.UFlags |= nifInfo info.UVersion = 10000 info.DwInfoFlags = niifInfo copyUTF16(info.SzInfoTitle[:], title) copyUTF16(info.SzInfo[:], text) shellNotifyIconW.Call(nimModify, uintptr(unsafe.Pointer(&info))) } func showTrayMenu(hwnd uintptr) { menu, _, _ := createPopupMenu.Call() if menu == 0 { return } defer destroyMenu.Call(menu) statusText := mustUTF16("DCAuth v" + clientVersion + " · 后台运行中") deviceText := mustUTF16("查看设备码") exitText := mustUTF16("退出 DCAuth") appendMenuW.Call(menu, mfString|mfGrayed, 0, uintptr(unsafe.Pointer(statusText))) appendMenuW.Call(menu, mfString, trayMenuDeviceInfo, uintptr(unsafe.Pointer(deviceText))) appendMenuW.Call(menu, mfSeparator, 0, 0) appendMenuW.Call(menu, mfString, trayMenuExit, uintptr(unsafe.Pointer(exitText))) runtime.KeepAlive(statusText) runtime.KeepAlive(deviceText) runtime.KeepAlive(exitText) var cursor point if ok, _, _ := getCursorPos.Call(uintptr(unsafe.Pointer(&cursor))); ok == 0 { return } setForegroundWindow.Call(hwnd) command, _, _ := trackPopupMenu.Call( menu, tpmRight|tpmReturn, uintptr(cursor.X), uintptr(cursor.Y), 0, hwnd, 0, ) postMessageW.Call(hwnd, wmNull, 0, 0) switch command { case trayMenuDeviceInfo: if identityReady { showDeviceInfo() } else { showTrayBalloon("DCAuth 正在初始化", "设备信息准备完成后即可查看。") } case trayMenuExit: destroyWindow.Call(hwnd) } } func showDeviceInfo() { text := fmt.Sprintf( "版本:v%s\n\n设备码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码已复制到剪贴板。", clientVersion, runtimeIdentity.deviceID, ) copyToClipboard(runtimeIdentity.deviceID) showInfo("DCAuth v"+clientVersion+" 设备信息", text) } func handleAuthorization(raw string) { if authorizationActive { showTrayBalloon("DCAuth v"+clientVersion, "已有一次登录确认正在进行,请先完成当前窗口。") return } authorizationActive = true defer func() { authorizationActive = false }() authHandle, acquired, err := acquireNamedMutex(authorizationMutexName) if err != nil { showError("DCAuth 验证失败", err.Error()) return } if !acquired { _ = syscall.CloseHandle(syscall.Handle(authHandle)) showTrayBalloon("DCAuth v"+clientVersion, "已有一次登录确认正在进行,请先完成当前窗口。") return } defer syscall.CloseHandle(syscall.Handle(authHandle)) target, err := parseAuthorizeURL(raw) if err != nil { showError("DCAuth 协议无效", err.Error()) return } app := appRegistry[target.App] if !confirm( "DCAuth v"+clientVersion+" · "+app.DisplayName+" 本机登录确认", fmt.Sprintf( "是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n\n点击“确定”允许本次登录,点击“取消”拒绝。", app.DisplayName, clientVersion, runtimeIdentity.deviceID, ), ) { return } if err := submitAuthorization(runtimeIdentity, target); err != nil { showError("本机验证失败", err.Error()) return } showTrayBalloon("DCAuth v"+clientVersion+" 验证成功", "已确认本机设备,请返回浏览器继续登录。") } func showInfo(title, text string) { showMessage(title, text, mbOK|mbIconInformation) } func showError(title, text string) { showMessage(title, text, mbOK|mbIconError) } func confirm(title, text string) bool { return showMessage(title, text, mbOKCancel|mbIconQuestion) == idOK } func showMessage(title, text string, flags uint) int { titlePtr, _ := syscall.UTF16PtrFromString(title) textPtr, _ := syscall.UTF16PtrFromString(text) result, _, _ := messageBoxW.Call( mainWindow, uintptr(unsafe.Pointer(textPtr)), uintptr(unsafe.Pointer(titlePtr)), uintptr(flags|mbSetForeground|mbTopMost), ) runtime.KeepAlive(titlePtr) runtime.KeepAlive(textPtr) return int(result) } func copyUTF16(destination []uint16, text string) { encoded := utf16.Encode([]rune(text)) if len(encoded) >= len(destination) { encoded = encoded[:len(destination)-1] } copy(destination, encoded) destination[len(encoded)] = 0 } func mustUTF16(text string) *uint16 { value, _ := syscall.UTF16PtrFromString(text) return value } func copyToClipboard(text string) { opened, _, _ := openClip.Call(mainWindow) if opened == 0 { return } defer closeClip.Call() emptyClip.Call(mainWindow) encoded := utf16.Encode([]rune(text)) encoded = append(encoded, 0) size := uintptr(len(encoded) * 2) handle, _, _ := globalAlloc.Call(gmemMoveable, size) if handle == 0 { return } pointer, _, _ := globalLock.Call(handle) if pointer == 0 { return } target := unsafe.Slice((*uint16)(unsafe.Pointer(pointer)), len(encoded)) copy(target, encoded) globalUnlock.Call(handle) setClipData.Call(cfUnicodeText, handle) }