rmm2/agent-windows/ws/handler.go
cynfo3000 8116b71b48 feat: Windows Agent v1.0.0
- Windows-Dienst (golang.org/x/sys/windows/svc)
- Collector: CPU (GetSystemTimes), RAM (GlobalMemoryStatusEx), Disks (GetDiskFreeSpaceEx)
- WS-Client: gleiche Verbindungslogik wie Linux/BSD Agent
- Commands: exec, winget list/install/upgrade/upgrade-all, Windows Updates check/install, reboot
- install.ps1: automatische Service-Installation mit Parametern
- make agent-windows VERSION=x.x.x
2026-03-10 01:22:19 +01:00

278 lines
7.8 KiB
Go

package ws
import (
"encoding/json"
"fmt"
"log"
"os/exec"
"strings"
"time"
)
type Message struct {
Type string `json:"type"`
ID string `json:"id,omitempty"`
Command string `json:"command,omitempty"`
Status string `json:"status,omitempty"`
Error string `json:"error,omitempty"`
Data interface{} `json:"data,omitempty"`
}
type Handler struct{}
func NewHandler() *Handler {
return &Handler{}
}
func (h *Handler) Handle(raw []byte) []byte {
var msg Message
if err := json.Unmarshal(raw, &msg); err != nil {
log.Printf("WS Parse-Fehler: %v", err)
return nil
}
if msg.Type != "command" {
return nil
}
var resp Message
switch msg.Command {
case "exec":
resp = h.handleExec(msg)
case "winget_list":
resp = h.handleWingetList(msg)
case "winget_install":
resp = h.handleWingetInstall(msg)
case "winget_upgrade":
resp = h.handleWingetUpgrade(msg)
case "winget_upgrade_all":
resp = h.handleWingetUpgradeAll(msg)
case "windows_updates_check":
resp = h.handleWinUpdatesCheck(msg)
case "windows_updates_install":
resp = h.handleWinUpdatesInstall(msg)
case "reboot":
resp = h.handleReboot(msg)
default:
resp = Message{Type: "response", ID: msg.ID, Status: "error",
Error: fmt.Sprintf("Unbekanntes Command: %s", msg.Command)}
}
out, _ := json.Marshal(resp)
return out
}
// handleExec führt einen beliebigen Befehl aus (PowerShell oder cmd)
func (h *Handler) handleExec(msg Message) Message {
resp := Message{Type: "response", ID: msg.ID}
data, _ := msg.Data.(map[string]interface{})
command, _ := data["command"].(string)
if command == "" {
resp.Status = "error"
resp.Error = "Kein Befehl angegeben"
return resp
}
timeoutSecs := 60
if t, ok := data["timeout"].(float64); ok && t > 0 {
timeoutSecs = int(t)
}
out, err := runWithTimeout(command, timeoutSecs)
if err != nil {
resp.Status = "error"
resp.Error = err.Error()
resp.Data = map[string]interface{}{"output": out}
} else {
resp.Status = "ok"
resp.Data = map[string]interface{}{"output": out}
}
return resp
}
// handleWingetList listet installierte Software auf
func (h *Handler) handleWingetList(msg Message) Message {
resp := Message{Type: "response", ID: msg.ID}
out, err := runPS("winget list --accept-source-agreements 2>&1")
if err != nil {
resp.Status = "error"
resp.Error = err.Error()
return resp
}
resp.Status = "ok"
resp.Data = map[string]interface{}{"output": out}
return resp
}
// handleWingetInstall installiert ein Paket
func (h *Handler) handleWingetInstall(msg Message) Message {
resp := Message{Type: "response", ID: msg.ID}
data, _ := msg.Data.(map[string]interface{})
pkg, _ := data["package"].(string)
if pkg == "" {
resp.Status = "error"
resp.Error = "Kein Paket angegeben"
return resp
}
cmd := fmt.Sprintf("winget install --id %s --silent --accept-package-agreements --accept-source-agreements 2>&1", sanitize(pkg))
out, err := runWithTimeout(cmd, 300)
if err != nil {
resp.Status = "error"
resp.Error = err.Error()
resp.Data = map[string]interface{}{"output": out}
} else {
resp.Status = "ok"
resp.Data = map[string]interface{}{"output": out}
}
return resp
}
// handleWingetUpgrade aktualisiert ein Paket
func (h *Handler) handleWingetUpgrade(msg Message) Message {
resp := Message{Type: "response", ID: msg.ID}
data, _ := msg.Data.(map[string]interface{})
pkg, _ := data["package"].(string)
if pkg == "" {
resp.Status = "error"
resp.Error = "Kein Paket angegeben"
return resp
}
cmd := fmt.Sprintf("winget upgrade --id %s --silent --accept-package-agreements --accept-source-agreements 2>&1", sanitize(pkg))
out, err := runWithTimeout(cmd, 300)
if err != nil {
resp.Status = "error"
resp.Error = err.Error()
} else {
resp.Status = "ok"
resp.Data = map[string]interface{}{"output": out}
}
return resp
}
// handleWingetUpgradeAll aktualisiert alle Pakete
func (h *Handler) handleWingetUpgradeAll(msg Message) Message {
resp := Message{Type: "response", ID: msg.ID}
out, err := runWithTimeout("winget upgrade --all --silent --accept-package-agreements --accept-source-agreements 2>&1", 600)
if err != nil {
resp.Status = "error"
resp.Error = err.Error()
} else {
resp.Status = "ok"
resp.Data = map[string]interface{}{"output": out}
}
return resp
}
// handleWinUpdatesCheck prüft verfügbare Windows-Updates
func (h *Handler) handleWinUpdatesCheck(msg Message) Message {
resp := Message{Type: "response", ID: msg.ID}
script := `
$updateSession = New-Object -ComObject Microsoft.Update.Session
$searcher = $updateSession.CreateUpdateSearcher()
$result = $searcher.Search("IsInstalled=0 and Type='Software'")
$updates = @()
foreach ($u in $result.Updates) {
$updates += [PSCustomObject]@{
Title = $u.Title
KB = ($u.KBArticleIDs | Select-Object -First 1)
Size = $u.MaxDownloadSize
}
}
$updates | ConvertTo-Json -Compress`
out, err := runPSScript(script, 120)
if err != nil {
resp.Status = "error"
resp.Error = err.Error()
return resp
}
resp.Status = "ok"
resp.Data = map[string]interface{}{"raw": out}
return resp
}
// handleWinUpdatesInstall installiert alle ausstehenden Updates
func (h *Handler) handleWinUpdatesInstall(msg Message) Message {
resp := Message{Type: "response", ID: msg.ID}
script := `
$updateSession = New-Object -ComObject Microsoft.Update.Session
$searcher = $updateSession.CreateUpdateSearcher()
$result = $searcher.Search("IsInstalled=0 and Type='Software'")
if ($result.Updates.Count -eq 0) { Write-Output "Keine Updates verfuegbar"; exit 0 }
$downloader = $updateSession.CreateUpdateDownloader()
$downloader.Updates = $result.Updates
$downloader.Download()
$installer = $updateSession.CreateUpdateInstaller()
$installer.Updates = $result.Updates
$installResult = $installer.Install()
Write-Output "Installiert: $($result.Updates.Count) Updates, ResultCode: $($installResult.ResultCode)"`
out, err := runPSScript(script, 1800) // 30 Min Timeout
if err != nil {
resp.Status = "error"
resp.Error = err.Error()
resp.Data = map[string]interface{}{"output": out}
} else {
resp.Status = "ok"
resp.Data = map[string]interface{}{"output": out, "reboot_required": strings.Contains(out, "ResultCode: 2")}
}
return resp
}
func (h *Handler) handleReboot(msg Message) Message {
go func() {
time.Sleep(3 * time.Second)
exec.Command("shutdown", "/r", "/t", "10", "/c", "RMM-gesteuert").Run()
}()
return Message{Type: "response", ID: msg.ID, Status: "ok",
Data: map[string]interface{}{"message": "Neustart in 10 Sekunden"}}
}
// Hilfsfunktionen
func runWithTimeout(command string, timeoutSecs int) (string, error) {
cmd := exec.Command("cmd", "/C", command)
cmd.SysProcAttr = nil
out, err := withTimeout(cmd, time.Duration(timeoutSecs)*time.Second)
return out, err
}
func runPS(command string) (string, error) {
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", command)
b, err := cmd.CombinedOutput()
return strings.TrimSpace(string(b)), err
}
func runPSScript(script string, timeoutSecs int) (string, error) {
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", script)
out, err := withTimeout(cmd, time.Duration(timeoutSecs)*time.Second)
return out, err
}
func withTimeout(cmd *exec.Cmd, timeout time.Duration) (string, error) {
done := make(chan struct{})
var out []byte
var err error
go func() {
out, err = cmd.CombinedOutput()
close(done)
}()
select {
case <-done:
return strings.TrimSpace(string(out)), err
case <-time.After(timeout):
cmd.Process.Kill()
return "", fmt.Errorf("Timeout nach %v", timeout)
}
}
func sanitize(s string) string {
// Einfache Bereinigung — nur alphanumerisch, Punkt, Bindestrich
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == '.' || r == '-' || r == '_' {
b.WriteRune(r)
}
}
return b.String()
}