Files
sessiongurad/internal/windowsx/windows_windows.go
jbergner 7972ed7e38
All checks were successful
release-tag / release-image (push) Successful in 2m5s
release-main / release-images (push) Successful in 5m5s
Update mit Guacamole-Extension
2026-08-22 15:19:17 +02:00

437 lines
13 KiB
Go

//go:build windows
package windowsx
import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"time"
"unsafe"
"github.com/example/sessionguard/internal/model"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
var (
wtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll")
procWTSEnumerateSessionsW = wtsapi32.NewProc("WTSEnumerateSessionsW")
procWTSEnumerateProcessesW = wtsapi32.NewProc("WTSEnumerateProcessesW")
procWTSFreeMemory = wtsapi32.NewProc("WTSFreeMemory")
procWTSQuerySessionInformationW = wtsapi32.NewProc("WTSQuerySessionInformationW")
procWTSQueryUserToken = wtsapi32.NewProc("WTSQueryUserToken")
procWTSLogoffSession = wtsapi32.NewProc("WTSLogoffSession")
procWTSDisconnectSession = wtsapi32.NewProc("WTSDisconnectSession")
procWTSSendMessageW = wtsapi32.NewProc("WTSSendMessageW")
userenv = windows.NewLazySystemDLL("userenv.dll")
procDeleteProfileW = userenv.NewProc("DeleteProfileW")
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procGetTickCount64 = kernel32.NewProc("GetTickCount64")
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
procExpandEnvironmentStringsW = kernel32.NewProc("ExpandEnvironmentStringsW")
procGetSystemTimes = kernel32.NewProc("GetSystemTimes")
procGetDiskFreeSpaceExW = kernel32.NewProc("GetDiskFreeSpaceExW")
psapi = windows.NewLazySystemDLL("psapi.dll")
procGetProcessMemoryInfo = psapi.NewProc("GetProcessMemoryInfo")
)
type wtsSessionInfo struct {
SessionID uint32
WinStationName *uint16
State uint32
}
type wtsProcessInfo struct {
SessionID uint32
ProcessID uint32
ProcessName *uint16
UserSID *windows.SID
}
type wtsClientAddress struct {
AddressFamily uint32
Address [20]byte
}
type wtsInfoExLevel1 struct {
SessionID uint32
SessionState uint32
SessionFlags int32
WinStationName [33]uint16
UserName [21]uint16
DomainName [18]uint16
LogonTime int64
ConnectTime int64
DisconnectTime int64
LastInputTime int64
CurrentTime int64
IncomingBytes uint32
OutgoingBytes uint32
IncomingFrames uint32
OutgoingFrames uint32
}
const (
wtsUserName = 5
wtsWinStationName = 6
wtsDomainName = 7
wtsClientName = 10
wtsClientAddressClass = 14
wtsSessionInfoEx = 25
)
var stateNames = map[uint32]string{0: "Active", 1: "Connected", 2: "ConnectQuery", 3: "Shadow", 4: "Disconnected", 5: "Idle", 6: "Listen", 7: "Reset", 8: "Down", 9: "Init"}
func Sessions() ([]model.Session, error) {
var buf uintptr
var count uint32
r1, _, e := procWTSEnumerateSessionsW.Call(0, 0, 1, uintptr(unsafe.Pointer(&buf)), uintptr(unsafe.Pointer(&count)))
if r1 == 0 {
return nil, fmt.Errorf("WTSEnumerateSessionsW: %w", e)
}
defer procWTSFreeMemory.Call(buf)
rows := unsafe.Slice((*wtsSessionInfo)(unsafe.Pointer(buf)), int(count))
out := make([]model.Session, 0, len(rows))
for _, row := range rows {
s := model.Session{ID: row.SessionID, State: stateNames[row.State]}
if s.State == "" {
s.State = fmt.Sprintf("State%d", row.State)
}
if row.WinStationName != nil {
s.StationName = windows.UTF16PtrToString(row.WinStationName)
}
s.User, _ = queryString(row.SessionID, wtsUserName)
s.Domain, _ = queryString(row.SessionID, wtsDomainName)
s.ClientName, _ = queryString(row.SessionID, wtsClientName)
if s.StationName == "" {
s.StationName, _ = queryString(row.SessionID, wtsWinStationName)
}
if ca, err := queryClientAddress(row.SessionID); err == nil {
s.ClientAddress = ca
}
if ex, err := querySessionInfoEx(row.SessionID); err == nil {
s.LogonAt = fileTimePtr(ex.LogonTime)
s.ConnectAt = fileTimePtr(ex.ConnectTime)
s.LastInputAt = fileTimePtr(ex.LastInputTime)
if s.State == "Disconnected" {
s.DisconnectedSince = fileTimePtr(ex.DisconnectTime)
}
if s.LastInputAt != nil {
idle := time.Since(*s.LastInputAt)
if idle > 0 {
s.IdleSeconds = int64(idle / time.Second)
}
}
}
if s.User != "" {
var token windows.Token
r, _, _ := procWTSQueryUserToken.Call(uintptr(row.SessionID), uintptr(unsafe.Pointer(&token)))
if r != 0 {
if tu, err := token.GetTokenUser(); err == nil && tu.User.Sid != nil {
s.SID = tu.User.Sid.String()
}
_ = token.Close()
}
}
out = append(out, s)
}
return out, nil
}
func queryString(sessionID uint32, class uintptr) (string, error) {
var p uintptr
var bytes uint32
r1, _, e := procWTSQuerySessionInformationW.Call(0, uintptr(sessionID), class, uintptr(unsafe.Pointer(&p)), uintptr(unsafe.Pointer(&bytes)))
if r1 == 0 {
return "", e
}
defer procWTSFreeMemory.Call(p)
if p == 0 || bytes < 2 {
return "", nil
}
return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(p))), nil
}
func queryClientAddress(sessionID uint32) (string, error) {
var p uintptr
var bytes uint32
r, _, e := procWTSQuerySessionInformationW.Call(0, uintptr(sessionID), wtsClientAddressClass, uintptr(unsafe.Pointer(&p)), uintptr(unsafe.Pointer(&bytes)))
if r == 0 {
return "", e
}
defer procWTSFreeMemory.Call(p)
if p == 0 || bytes < uint32(unsafe.Sizeof(wtsClientAddress{})) {
return "", nil
}
a := (*wtsClientAddress)(unsafe.Pointer(p))
if a.AddressFamily == 2 {
return fmt.Sprintf("%d.%d.%d.%d", a.Address[2], a.Address[3], a.Address[4], a.Address[5]), nil
}
return "", nil
}
func querySessionInfoEx(sessionID uint32) (wtsInfoExLevel1, error) {
var out wtsInfoExLevel1
var p uintptr
var bytes uint32
r, _, e := procWTSQuerySessionInformationW.Call(0, uintptr(sessionID), wtsSessionInfoEx, uintptr(unsafe.Pointer(&p)), uintptr(unsafe.Pointer(&bytes)))
if r == 0 {
return out, e
}
defer procWTSFreeMemory.Call(p)
if p == 0 || bytes < 8+uint32(unsafe.Sizeof(out)) {
return out, fmt.Errorf("WTSSessionInfoEx buffer too small")
}
level := *(*uint32)(unsafe.Pointer(p))
if level != 1 {
return out, fmt.Errorf("unsupported WTSSessionInfoEx level %d", level)
}
out = *(*wtsInfoExLevel1)(unsafe.Pointer(p + 8))
return out, nil
}
func fileTimePtr(v int64) *time.Time {
if v <= 0 {
return nil
}
const unixDelta = 116444736000000000
ns := (v - unixDelta) * 100
if ns <= 0 {
return nil
}
t := time.Unix(0, ns).UTC()
return &t
}
type memoryStatusEx struct {
Length uint32
MemoryLoad uint32
TotalPhys uint64
AvailPhys uint64
TotalPageFile uint64
AvailPageFile uint64
TotalVirtual uint64
AvailVirtual uint64
AvailExtendedVirtual uint64
}
type filetime struct {
LowDateTime uint32
HighDateTime uint32
}
var cpuMu sync.Mutex
var prevIdle, prevKernel, prevUser uint64
func ft64(f filetime) uint64 { return uint64(f.HighDateTime)<<32 | uint64(f.LowDateTime) }
func cpuPercent() float64 {
var idle, kernel, user filetime
r, _, _ := procGetSystemTimes.Call(uintptr(unsafe.Pointer(&idle)), uintptr(unsafe.Pointer(&kernel)), uintptr(unsafe.Pointer(&user)))
if r == 0 {
return 0
}
i, k, u := ft64(idle), ft64(kernel), ft64(user)
cpuMu.Lock()
defer cpuMu.Unlock()
pi, pk, pu := prevIdle, prevKernel, prevUser
prevIdle, prevKernel, prevUser = i, k, u
if pk == 0 {
return 0
}
total := (k - pk) + (u - pu)
if total == 0 {
return 0
}
busy := total - (i - pi)
return float64(busy) * 100 / float64(total)
}
func Server() (model.ServerInfo, error) {
host, _ := os.Hostname()
m := memoryStatusEx{Length: uint32(unsafe.Sizeof(memoryStatusEx{}))}
r, _, e := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&m)))
if r == 0 {
return model.ServerInfo{}, fmt.Errorf("GlobalMemoryStatusEx: %w", e)
}
ticks, _, _ := procGetTickCount64.Call()
info := model.ServerInfo{Hostname: host, OS: "Windows", UptimeSeconds: uint64(ticks) / 1000, MemoryTotal: m.TotalPhys, MemoryAvailable: m.AvailPhys, CPUPercent: cpuPercent()}
drive := os.Getenv("SystemDrive")
if drive == "" {
drive = `C:`
}
root, err := windows.UTF16PtrFromString(drive + `\`)
if err == nil {
var avail, total, free uint64
if rr, _, _ := procGetDiskFreeSpaceExW.Call(uintptr(unsafe.Pointer(root)), uintptr(unsafe.Pointer(&avail)), uintptr(unsafe.Pointer(&total)), uintptr(unsafe.Pointer(&free))); rr != 0 {
info.DiskTotal = total
info.DiskFree = free
}
}
if k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE); err == nil {
defer k.Close()
if v, _, err := k.GetStringValue("ProductName"); err == nil {
info.OS = v
}
if v, _, err := k.GetStringValue("DisplayVersion"); err == nil {
info.Version = v
}
if v, _, err := k.GetStringValue("CurrentBuildNumber"); err == nil {
info.Build = v
}
}
return info, nil
}
func Processes() ([]model.ProcessInfo, error) {
var buf uintptr
var count uint32
r, _, e := procWTSEnumerateProcessesW.Call(0, 0, 1, uintptr(unsafe.Pointer(&buf)), uintptr(unsafe.Pointer(&count)))
if r == 0 {
return nil, fmt.Errorf("WTSEnumerateProcessesW: %w", e)
}
defer procWTSFreeMemory.Call(buf)
rows := unsafe.Slice((*wtsProcessInfo)(unsafe.Pointer(buf)), int(count))
out := make([]model.ProcessInfo, 0, len(rows))
for _, row := range rows {
p := model.ProcessInfo{PID: row.ProcessID, SessionID: row.SessionID}
if row.ProcessName != nil {
p.Name = windows.UTF16PtrToString(row.ProcessName)
}
if row.UserSID != nil {
p.UserSID = row.UserSID.String()
}
p.MemoryBytes = processMemory(row.ProcessID)
out = append(out, p)
}
return out, nil
}
type processMemoryCounters struct {
CB uint32
PageFaultCount uint32
PeakWorkingSetSize uintptr
WorkingSetSize uintptr
QuotaPeakPagedPoolUsage uintptr
QuotaPagedPoolUsage uintptr
QuotaPeakNonPagedPoolUsage uintptr
QuotaNonPagedPoolUsage uintptr
PagefileUsage uintptr
PeakPagefileUsage uintptr
}
func processMemory(pid uint32) uint64 {
h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.PROCESS_VM_READ, false, pid)
if err != nil {
return 0
}
defer windows.CloseHandle(h)
m := processMemoryCounters{CB: uint32(unsafe.Sizeof(processMemoryCounters{}))}
r, _, _ := procGetProcessMemoryInfo.Call(uintptr(h), uintptr(unsafe.Pointer(&m)), uintptr(m.CB))
if r == 0 {
return 0
}
return uint64(m.WorkingSetSize)
}
func ProfilePath(sid string) (string, error) {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+sid, registry.QUERY_VALUE)
if err != nil {
return "", err
}
defer k.Close()
p, _, err := k.GetStringValue("ProfileImagePath")
if err != nil {
return "", err
}
return expandEnv(p), nil
}
func expandEnv(s string) string {
in, err := windows.UTF16PtrFromString(s)
if err != nil {
return s
}
n, _, _ := procExpandEnvironmentStringsW.Call(uintptr(unsafe.Pointer(in)), 0, 0)
if n == 0 {
return s
}
buf := make([]uint16, n)
n2, _, _ := procExpandEnvironmentStringsW.Call(uintptr(unsafe.Pointer(in)), uintptr(unsafe.Pointer(&buf[0])), uintptr(n))
if n2 == 0 || n2 > n {
return s
}
return windows.UTF16ToString(buf)
}
func DeleteProfile(sid string) error {
p, err := windows.UTF16PtrFromString(sid)
if err != nil {
return err
}
r, _, e := procDeleteProfileW.Call(uintptr(unsafe.Pointer(p)), 0, 0)
if r == 0 {
if e == syscall.Errno(0) {
return fmt.Errorf("DeleteProfileW failed")
}
return fmt.Errorf("DeleteProfileW(%s): %w", sid, e)
}
return nil
}
func LogoffSession(id uint32) error {
return boolCallSession(procWTSLogoffSession, "WTSLogoffSession", id)
}
func DisconnectSession(id uint32) error {
return boolCallSession(procWTSDisconnectSession, "WTSDisconnectSession", id)
}
func boolCallSession(p *windows.LazyProc, name string, id uint32) error {
r, _, e := p.Call(0, uintptr(id), 0)
if r == 0 {
if e == syscall.Errno(0) {
return fmt.Errorf("%s(%d) failed", name, id)
}
return fmt.Errorf("%s(%d): %w", name, id, e)
}
return nil
}
func SendMessage(sessionID uint32, title, message string) error {
if strings.TrimSpace(title) == "" {
title = "SessionGuard"
}
t, err := windows.UTF16FromString(title)
if err != nil {
return err
}
m, err := windows.UTF16FromString(message)
if err != nil {
return err
}
var response uint32
r, _, e := procWTSSendMessageW.Call(0, uintptr(sessionID), uintptr(unsafe.Pointer(&t[0])), uintptr((len(t)-1)*2), uintptr(unsafe.Pointer(&m[0])), uintptr((len(m)-1)*2), 0, 60, uintptr(unsafe.Pointer(&response)), 0)
if r == 0 {
if e == syscall.Errno(0) {
return fmt.Errorf("WTSSendMessageW(%d) failed", sessionID)
}
return fmt.Errorf("WTSSendMessageW(%d): %w", sessionID, e)
}
return nil
}
func TerminateProcess(pid uint32) error {
h, err := windows.OpenProcess(windows.PROCESS_TERMINATE, false, pid)
if err != nil {
return err
}
defer windows.CloseHandle(h)
return windows.TerminateProcess(h, 1)
}
func RestartServer(reason string) error {
if strings.TrimSpace(reason) == "" {
reason = "SessionGuard maintenance restart"
}
return exec.Command("shutdown.exe", "/r", "/t", "0", "/d", "p:4:1", "/c", reason).Run()
}
func MachineID() (string, error) {
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Cryptography`, registry.QUERY_VALUE)
if err != nil {
return "", err
}
defer k.Close()
v, _, err := k.GetStringValue("MachineGuid")
return strings.TrimSpace(v), err
}