328 lines
9.6 KiB
Go
328 lines
9.6 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"`
|
|
Params map[string]interface{} `json:"params,omitempty"` // Backend -> Agent
|
|
Data interface{} `json:"data,omitempty"` // Agent -> Backend (Response)
|
|
}
|
|
|
|
type Handler struct {
|
|
wingetPath string
|
|
}
|
|
|
|
func NewHandler() *Handler {
|
|
return &Handler{}
|
|
}
|
|
|
|
// winget gibt den Pfad zu winget.exe zurueck (cached)
|
|
func (h *Handler) winget() string {
|
|
if h.wingetPath != "" {
|
|
return h.wingetPath
|
|
}
|
|
// Direkt im PATH?
|
|
if path, err := exec.LookPath("winget"); err == nil {
|
|
h.wingetPath = path
|
|
return path
|
|
}
|
|
// WindowsApps durchsuchen
|
|
script := `Get-ChildItem "C:\Program Files\WindowsApps" -Filter winget.exe -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName`
|
|
cmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command", script)
|
|
out, err := cmd.Output()
|
|
if err == nil {
|
|
path := strings.TrimSpace(string(out))
|
|
if path != "" {
|
|
h.wingetPath = path
|
|
log.Printf("winget gefunden: %s", path)
|
|
return path
|
|
}
|
|
}
|
|
log.Printf("winget nicht gefunden")
|
|
return ""
|
|
}
|
|
|
|
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.Params
|
|
if data == nil {
|
|
data, _ = msg.Data.(map[string]interface{})
|
|
}
|
|
command, _ := data["command"].(string)
|
|
if command == "" {
|
|
resp.Status = "error"
|
|
resp.Error = "Kein Befehl angegeben"
|
|
return resp
|
|
}
|
|
|
|
// winget durch vollen Pfad ersetzen falls noetig
|
|
if strings.Contains(command, "winget") {
|
|
if wp := h.winget(); wp != "" {
|
|
command = strings.ReplaceAll(command, "winget ", fmt.Sprintf(`& "%s" `, wp))
|
|
}
|
|
}
|
|
|
|
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}
|
|
wp := h.winget()
|
|
if wp == "" {
|
|
resp.Status = "error"; resp.Error = "winget nicht gefunden"; return resp
|
|
}
|
|
out, err := runWithTimeout(fmt.Sprintf(`& "%s" list --accept-source-agreements 2>&1`, wp), 60)
|
|
if err != nil && out == "" {
|
|
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.Params
|
|
if data == nil { data, _ = msg.Data.(map[string]interface{}) }
|
|
pkg, _ := data["package"].(string)
|
|
if pkg == "" {
|
|
resp.Status = "error"; resp.Error = "Kein Paket angegeben"; return resp
|
|
}
|
|
wp := h.winget()
|
|
if wp == "" {
|
|
resp.Status = "error"; resp.Error = "winget nicht gefunden"; return resp
|
|
}
|
|
cmd := fmt.Sprintf(`& "%s" install --id %s --silent --accept-package-agreements --accept-source-agreements 2>&1`, wp, 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.Params
|
|
if data == nil { data, _ = msg.Data.(map[string]interface{}) }
|
|
pkg, _ := data["package"].(string)
|
|
if pkg == "" {
|
|
resp.Status = "error"; resp.Error = "Kein Paket angegeben"; return resp
|
|
}
|
|
wp := h.winget()
|
|
if wp == "" {
|
|
resp.Status = "error"; resp.Error = "winget nicht gefunden"; return resp
|
|
}
|
|
cmd := fmt.Sprintf(`& "%s" upgrade --id %s --silent --accept-package-agreements --accept-source-agreements 2>&1`, wp, 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}
|
|
wp := h.winget()
|
|
if wp == "" {
|
|
resp.Status = "error"; resp.Error = "winget nicht gefunden"; return resp
|
|
}
|
|
out, err := runWithTimeout(fmt.Sprintf(`& "%s" upgrade --all --silent --accept-package-agreements --accept-source-agreements 2>&1`, wp), 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("powershell", "-NoProfile", "-NonInteractive", "-Command", command)
|
|
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()
|
|
}
|