diff --git a/main.go b/main.go index 76f6a17..05e00a1 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "os/user" "runtime" "sort" + "strconv" "strings" "sync" "time" @@ -82,20 +83,158 @@ type InstalledApp struct { } type DeviceInfo struct { - InstanceID string `json:"instance_id"` - Class string `json:"class"` - ClassGUID string `json:"class_guid"` - FriendlyName string `json:"friendly_name"` - Manufacturer string `json:"manufacturer"` - Status string `json:"status"` - Present bool `json:"present"` + InstanceID string `json:"instance_id"` + Class string `json:"class"` + ClassGUID string `json:"class_guid"` + FriendlyName string `json:"friendly_name"` + Manufacturer string `json:"manufacturer"` + Status string `json:"status"` + Present bool `json:"present"` } +type Notification struct { + ID int64 `json:"id"` + CreatedAt time.Time `json:"created_at"` + Title string `json:"title"` + Message string `json:"message"` + TargetUser string `json:"target_user"` // "" = Broadcast an alle User +} + +var ( + notifyMu sync.Mutex + notifySeq int64 + notifyRing []Notification +) + +// maximale Anzahl vorgehaltener Nachrichten (FIFO-Ring) +const maxNotifications = 1000 + var ( mu sync.RWMutex lastCPUUsage float64 ) +func addNotification(n Notification) Notification { + notifyMu.Lock() + defer notifyMu.Unlock() + + notifySeq++ + n.ID = notifySeq + if n.CreatedAt.IsZero() { + n.CreatedAt = time.Now() + } + + notifyRing = append(notifyRing, n) + if len(notifyRing) > maxNotifications { + // älteste abschneiden + notifyRing = notifyRing[len(notifyRing)-maxNotifications:] + } + return n +} + +// userName: z.B. "DOMAIN\\user" oder "user" +// sinceID: letzte gesehene ID des Agents +func getNotificationsForUser(userName string, sinceID int64) []Notification { + notifyMu.Lock() + defer notifyMu.Unlock() + + userNameLower := strings.ToLower(strings.TrimSpace(userName)) + var res []Notification + for _, n := range notifyRing { + if n.ID <= sinceID { + continue + } + // Broadcast + if strings.TrimSpace(n.TargetUser) == "" { + res = append(res, n) + continue + } + // Ziel-User matchen (case-insensitive) + if strings.EqualFold(n.TargetUser, userName) || + strings.EqualFold(n.TargetUser, userNameLower) { + res = append(res, n) + } + } + return res +} + +type NotifyRequest struct { + Title string `json:"title"` + Message string `json:"message"` + TargetUser string `json:"target_user"` // optional, "" = an alle +} + +func notifyFromServerHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + // optional: Authentifizierung per Token + token := os.Getenv("NOTIFY_TOKEN") + if token != "" && r.Header.Get("X-Notify-Token") != token { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + var req NotifyRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "bad request: "+err.Error(), http.StatusBadRequest) + return + } + req.Title = strings.TrimSpace(req.Title) + req.Message = strings.TrimSpace(req.Message) + req.TargetUser = strings.TrimSpace(req.TargetUser) + + if req.Message == "" { + http.Error(w, "message must not be empty", http.StatusBadRequest) + return + } + if req.Title == "" { + req.Title = "Benachrichtigung" + } + + n := addNotification(Notification{ + Title: req.Title, + Message: req.Message, + TargetUser: req.TargetUser, + }) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(n) +} + +func notificationsForAgentHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + userName := strings.TrimSpace(r.URL.Query().Get("user")) + if userName == "" { + http.Error(w, "missing 'user' query parameter", http.StatusBadRequest) + return + } + + sinceStr := r.URL.Query().Get("since_id") + var sinceID int64 + if sinceStr != "" { + if v, err := strconv.ParseInt(sinceStr, 10, 64); err == nil { + sinceID = v + } + } + + // optional: Zugriff auf localhost beschränken + host, _, _ := net.SplitHostPort(r.RemoteAddr) + if host != "127.0.0.1" && host != "::1" { + http.Error(w, "forbidden", http.StatusForbidden) + return + } + + notifs := getNotificationsForUser(userName, sinceID) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(notifs) +} + func getPnpDevices() ([]DeviceInfo, error) { // PowerShell: aktuelle PnP-Geräte, inkl. ClassGuid cmd := exec.Command( @@ -779,7 +918,10 @@ func runHTTP(ctx context.Context) error { mux.HandleFunc("/", indexHandler) mux.HandleFunc("/api/apps", appsHandler) mux.HandleFunc("/api/summary", apiHandler) - mux.HandleFunc("/api/devices", devicesHandler) + mux.HandleFunc("/api/devices", devicesHandler) + + mux.HandleFunc("/api/notify", notifyFromServerHandler) + mux.HandleFunc("/api/notifications", notificationsForAgentHandler) addr := ":24000"