// Package stream implementiert den langlebigen Kanal vom Sultan-Server zum // Agenten als Server-Sent Events (SSE) über eine ausgehende HTTP-Verbindung // des Agenten. // // Der Server (Hub) hält pro Knoten einen offenen Stream, schickt Events sofort // und sendet in Ruhephasen alle 45s die SSE-Kommentarzeile ":\n\n" als // Keepalive, damit NAT-/Conntrack-Einträge nicht verfallen. Der Agent (Client) // liest den Stream, erkennt einen toten Server, wenn zu lange gar nichts // ankommt (auch kein Keepalive), und verbindet sich mit exponentiellem // Backoff neu. // // Die Gegenrichtung (Agent -> Server, z. B. service_state) ist nicht Teil // dieses Streams; sie läuft über eigene Requests. package stream import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "time" ) // EventType ist der Typ eines Events gemäß Spec Abschnitt 4.2. type EventType string const ( EventServiceState EventType = "service_state" EventLeaseRenew EventType = "lease_renew" EventNodeCommand EventType = "node_command" EventError EventType = "error" ) // ErrInvalidEvent meldet ein Event, das sich nicht senden oder lesen lässt. var ErrInvalidEvent = errors.New("stream: ungültiges Event") // Event ist die Nachricht auf dem Stream. type Event struct { ID string `json:"event_id"` Timestamp int64 `json:"timestamp"` Type EventType `json:"type"` NodeID string `json:"node_id"` Payload json.RawMessage `json:"payload,omitempty"` } // NewEvent erzeugt ein Event mit neuer ID, aktuellem Zeitstempel und der als // JSON serialisierten payload (nil ergibt keine Payload). NodeID setzt der Hub // beim Senden. func NewEvent(t EventType, payload any) (Event, error) { if t == "" { return Event{}, fmt.Errorf("%w: Typ fehlt", ErrInvalidEvent) } ev := Event{ID: newEventID(), Timestamp: time.Now().Unix(), Type: t} if payload != nil { b, err := json.Marshal(payload) if err != nil { return Event{}, fmt.Errorf("%w: %v", ErrInvalidEvent, err) } ev.Payload = b } return ev, nil } func newEventID() string { var b [16]byte rand.Read(b[:]) return "evt_" + hex.EncodeToString(b[:]) } // encode erzeugt den SSE-Rahmen für ev. func encode(ev Event) ([]byte, error) { data, err := json.Marshal(ev) if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidEvent, err) } return fmt.Appendf(nil, "id: %s\nevent: %s\ndata: %s\n\n", ev.ID, ev.Type, data), nil }