| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147 |
- // client2server - Server-Sent Events broadcaster
- //
- // Used by the dashboard for real-time event feed. Clients connect to
- // /api/events/stream and receive a newline-delimited stream of JSON
- // envelopes.
- package main
- import (
- "encoding/json"
- "fmt"
- "net/http"
- "sync"
- "time"
- )
- type sseClient struct {
- channel chan []byte
- filter sseFilter
- lastPing time.Time
- }
- type sseFilter struct {
- RouterID string
- EventType string
- }
- type sseHub struct {
- mu sync.RWMutex
- clients map[*sseClient]struct{}
- }
- var hub = &sseHub{
- clients: make(map[*sseClient]struct{}),
- }
- func (h *sseHub) add(c *sseClient) {
- h.mu.Lock()
- defer h.mu.Unlock()
- h.clients[c] = struct{}{}
- }
- func (h *sseHub) remove(c *sseClient) {
- h.mu.Lock()
- defer h.mu.Unlock()
- delete(h.clients, c)
- close(c.channel)
- }
- func (h *sseHub) broadcast(eventType string, payload any) {
- env := map[string]any{
- "type": eventType,
- "payload": payload,
- "timestamp": time.Now().UTC().Format(time.RFC3339Nano),
- }
- data, err := json.Marshal(env)
- if err != nil {
- return
- }
- h.mu.RLock()
- defer h.mu.RUnlock()
- for c := range h.clients {
- if c.filter.RouterID != "" {
- if rid, ok := payload.(map[string]any)["router_id"].(string); ok && rid != c.filter.RouterID {
- continue
- }
- }
- if c.filter.EventType != "" && c.filter.EventType != eventType {
- continue
- }
- select {
- case c.channel <- data:
- default:
- // drop if client too slow
- }
- }
- }
- func handleSSEStream(w http.ResponseWriter, r *http.Request) {
- // Auth via Bearer JWT or legacy token
- token := r.Header.Get("Authorization")
- if t := r.URL.Query().Get("token"); t != "" {
- token = "Bearer " + t
- }
- token = trimBearer(token)
- if token == "" {
- http.Error(w, "missing token", http.StatusUnauthorized)
- return
- }
- // Accept either a JWT or the legacy shared TOKEN
- if _, err := ParseJWT(token); err != nil && token != cfg.Token {
- http.Error(w, "invalid token", http.StatusUnauthorized)
- return
- }
- flusher, ok := w.(http.Flusher)
- if !ok {
- http.Error(w, "streaming not supported", http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "text/event-stream")
- w.Header().Set("Cache-Control", "no-cache")
- w.Header().Set("Connection", "keep-alive")
- w.Header().Set("X-Accel-Buffering", "no")
- w.WriteHeader(http.StatusOK)
- flusher.Flush()
- client := &sseClient{
- channel: make(chan []byte, 64),
- filter: sseFilter{RouterID: r.URL.Query().Get("router_id"), EventType: r.URL.Query().Get("event_type")},
- lastPing: time.Now(),
- }
- hub.add(client)
- defer hub.remove(client)
- // Initial comment so curl shows connection is live
- _, _ = fmt.Fprintf(w, ": connected\n\n")
- flusher.Flush()
- pingTicker := time.NewTicker(15 * time.Second)
- defer pingTicker.Stop()
- for {
- select {
- case <-r.Context().Done():
- return
- case data := <-client.channel:
- if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil {
- return
- }
- flusher.Flush()
- case <-pingTicker.C:
- if _, err := fmt.Fprintf(w, ": ping\n\n"); err != nil {
- return
- }
- flusher.Flush()
- }
- }
- }
- func trimBearer(s string) string {
- if len(s) > 7 && s[:7] == "Bearer " {
- return s[7:]
- }
- return s
- }
|