Files
rikkei_simple_care/client/internal/blocker/blocker_windows.go
2026-07-01 09:49:27 +07:00

291 lines
7.6 KiB
Go

//go:build windows
package blocker
import (
"log"
"os"
"path/filepath"
"strings"
"sync"
"syscall"
"unsafe"
)
var (
user32 = syscall.NewLazyDLL("user32.dll")
procEnumWindows = user32.NewProc("EnumWindows")
procIsWindowVisible = user32.NewProc("IsWindowVisible")
procGetWindowTextW = user32.NewProc("GetWindowTextW")
procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW")
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
procGetWindow = user32.NewProc("GetWindow")
procGetWindowLongW = user32.NewProc("GetWindowLongW")
procGetAncestor = user32.NewProc("GetAncestor")
dwmapi = syscall.NewLazyDLL("dwmapi.dll")
procDwmGetWindowAttribute = dwmapi.NewProc("DwmGetWindowAttribute")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
)
const (
GW_OWNER = 4
WS_EX_TOOLWINDOW = 0x00000080
DWMWA_CLOAKED = 14
)
func isRealGUIWindow(hwnd uintptr) bool {
// 1. Phải đang hiển thị
ret, _, _ := procIsWindowVisible.Call(hwnd)
if ret == 0 {
return false
}
// 2. Không được có chủ sở hữu (phải là cửa sổ chính - top-level)
owner, _, _ := procGetWindow.Call(hwnd, GW_OWNER)
if owner != 0 {
return false
}
// 3. Không phải là tool window (WS_EX_TOOLWINDOW)
gwlExStyle := int32(-20) // GWL_EXSTYLE = -20
style, _, _ := procGetWindowLongW.Call(hwnd, uintptr(gwlExStyle))
if (style & WS_EX_TOOLWINDOW) != 0 {
return false
}
// 4. Không bị cloaked bởi DWM (ví dụ: app UWP bị treo/chạy ngầm, màn hình ảo)
var cloaked uint32
hr, _, _ := procDwmGetWindowAttribute.Call(hwnd, DWMWA_CLOAKED, uintptr(unsafe.Pointer(&cloaked)), 4)
if hr == 0 && cloaked != 0 {
return false
}
return true
}
type WindowInfo struct {
PID uint32
Title string
ProcessName string
}
func getProcessMap() (map[uint32]string, map[uint32]uint32, error) {
snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
if err != nil {
return nil, nil, err
}
defer syscall.CloseHandle(snapshot)
var pe syscall.ProcessEntry32
pe.Size = uint32(unsafe.Sizeof(pe))
err = syscall.Process32First(snapshot, &pe)
if err != nil {
return nil, nil, err
}
pm := make(map[uint32]string)
parents := make(map[uint32]uint32)
for {
name := syscall.UTF16ToString(pe.ExeFile[:])
pm[pe.ProcessID] = name
parents[pe.ProcessID] = pe.ParentProcessID
err = syscall.Process32Next(snapshot, &pe)
if err != nil {
break
}
}
return pm, parents, nil
}
func getWindowText(hwnd uintptr) string {
length, _, _ := procGetWindowTextLengthW.Call(hwnd)
if length == 0 {
return ""
}
buf := make([]uint16, length+1)
procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), length+1)
return syscall.UTF16ToString(buf)
}
var (
enumWindowsMutex sync.Mutex
enumWindowsList []WindowInfo
enumProcessMap map[uint32]string
enumParentMap map[uint32]uint32
)
var enumWindowsCallback = syscall.NewCallback(func(hwnd uintptr, lParam uintptr) uintptr {
defer func() {
if r := recover(); r != nil {
log.Printf("[BLOCKER] Callback panic recovered: %v", r)
}
}()
if !isRealGUIWindow(hwnd) {
return 1
}
// Kiểm tra xem chủ sở hữu gốc (root owner) của cửa sổ này có thuộc về app ta hay không
rootHwnd, _, _ := procGetAncestor.Call(hwnd, 3) // GA_ROOTOWNER = 3
if rootHwnd != 0 {
var rootPid uint32
procGetWindowThreadProcessId.Call(rootHwnd, uintptr(unsafe.Pointer(&rootPid)))
if rootPid == uint32(os.Getpid()) {
return 1 // Cửa sổ thuộc về WebView2 / app của ta, bỏ qua không quét
}
}
title := getWindowText(hwnd)
if title == "" {
return 1
}
var pid uint32
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
procName := enumProcessMap[pid]
if procName == "" {
procName = "Unknown"
}
enumWindowsList = append(enumWindowsList, WindowInfo{
PID: pid,
Title: title,
ProcessName: procName,
})
return 1
})
func EnumerateGUIWindows() ([]WindowInfo, map[uint32]uint32, error) {
pMap, parentMap, err := getProcessMap()
if err != nil {
return nil, nil, err
}
enumWindowsMutex.Lock()
defer enumWindowsMutex.Unlock()
enumWindowsList = make([]WindowInfo, 0, 100)
enumProcessMap = pMap
enumParentMap = parentMap
procEnumWindows.Call(enumWindowsCallback, 0)
// Clean up map reference so GC can reclaim it
enumProcessMap = nil
enumParentMap = nil
// Copy to a new slice to return safely
res := make([]WindowInfo, len(enumWindowsList))
copy(res, enumWindowsList)
return res, parentMap, nil
}
var systemAllowed = map[string]bool{
"explorer.exe": true,
"taskmgr.exe": true,
"cmd.exe": true,
"powershell.exe": true,
"conhost.exe": true,
"client.exe": true, // App Wails của ta
"simple_care_v1.0.exe": true,
"wails.exe": true,
"msedgewebview2.exe": true, // WebView2 runtime của Wails
"code.exe": true, // VSCode
"cursor.exe": true, // Cursor
"windsurf.exe": true, // Windsurf
"goland.exe": true, // GoLand
"goland64.exe": true, // GoLand
"idea64.exe": true, // IntelliJ IDEA
"clion64.exe": true, // CLion
"webstorm64.exe": true, // WebStorm
"pycharm64.exe": true, // PyCharm
"rider64.exe": true, // Rider
"studio64.exe": true, // Android Studio
"eclipse.exe": true, // Eclipse
"sublime_text.exe": true, // Sublime Text
"notepad++.exe": true, // Notepad++
"devenv.exe": true, // Visual Studio
"git-bash.exe": true, // Git Bash
"bash.exe": true, // Bash
}
func isDescendant(pid, targetPid uint32, parentMap map[uint32]uint32) bool {
curr := pid
for i := 0; i < 16; i++ {
parent, ok := parentMap[curr]
if !ok || parent == 0 {
return false
}
if parent == targetPid {
return true
}
curr = parent
}
return false
}
func (b *Blocker) checkAndKill() {
b.mu.Lock()
keywords := make([]string, len(b.allowedKeywords))
copy(keywords, b.allowedKeywords)
b.mu.Unlock()
// Nếu không cấu hình keyword thì không chặn gì cả
if len(keywords) == 0 {
return
}
currentExec := ""
if execPath, err := os.Executable(); err == nil {
currentExec = strings.ToLower(filepath.Base(execPath))
}
windows, parentMap, err := EnumerateGUIWindows()
if err != nil {
return
}
myPid := uint32(os.Getpid())
for _, w := range windows {
pNameLower := strings.ToLower(w.ProcessName)
wTitleLower := strings.ToLower(w.Title)
// 1. Luôn cho phép hệ thống/app cốt lõi hoặc chính tiến trình này (bao gồm tiến trình con/cháu, và đổi tên)
isOurApp := w.PID == myPid || isDescendant(w.PID, myPid, parentMap)
if isOurApp || (currentExec != "" && pNameLower == currentExec) || systemAllowed[pNameLower] || strings.Contains(pNameLower, "antigravity") || strings.Contains(wTitleLower, "antigravity") {
continue
}
// 2. Kiểm tra xem có chứa bất kỳ từ khóa nào được cho phép không
allowed := false
for _, kw := range keywords {
if matchesAllowedKeyword(kw, pNameLower, wTitleLower) {
allowed = true
break
}
}
// 3. Nếu không nằm trong whitelist, tắt ứng dụng
if !allowed {
if b.OnBlocked != nil {
b.OnBlocked(w.ProcessName, w.Title)
}
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d, Title: %s)", w.ProcessName, w.PID, w.Title)
h, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, w.PID)
if err == nil {
errTerm := syscall.TerminateProcess(h, 0)
_ = syscall.CloseHandle(h)
if errTerm == nil && b.OnKill != nil {
b.OnKill(w.ProcessName, w.Title)
}
}
}
}
}