Files
sultan/pkg/stream/event.go
T
br ca6cdcca63 software steht so weit.
jetzt build + docker build und testen
2026-10-02 13:25:34 +02:00

82 lines
2.6 KiB
Go

// 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"`
// Signature: Ed25519-Signatur des Servers über alle übrigen Felder
// (SignEvent/VerifyEvent); der Agent verwirft Events ohne gültige Signatur.
Signature string `json:"signature,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
}