sse.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. // client2server - Server-Sent Events broadcaster
  2. //
  3. // Used by the dashboard for real-time event feed. Clients connect to
  4. // /api/events/stream and receive a newline-delimited stream of JSON
  5. // envelopes.
  6. package main
  7. import (
  8. "encoding/json"
  9. "fmt"
  10. "net/http"
  11. "sync"
  12. "time"
  13. )
  14. type sseClient struct {
  15. channel chan []byte
  16. filter sseFilter
  17. lastPing time.Time
  18. }
  19. type sseFilter struct {
  20. RouterID string
  21. EventType string
  22. }
  23. type sseHub struct {
  24. mu sync.RWMutex
  25. clients map[*sseClient]struct{}
  26. }
  27. var hub = &sseHub{
  28. clients: make(map[*sseClient]struct{}),
  29. }
  30. func (h *sseHub) add(c *sseClient) {
  31. h.mu.Lock()
  32. defer h.mu.Unlock()
  33. h.clients[c] = struct{}{}
  34. }
  35. func (h *sseHub) remove(c *sseClient) {
  36. h.mu.Lock()
  37. defer h.mu.Unlock()
  38. delete(h.clients, c)
  39. close(c.channel)
  40. }
  41. func (h *sseHub) broadcast(eventType string, payload any) {
  42. env := map[string]any{
  43. "type": eventType,
  44. "payload": payload,
  45. "timestamp": time.Now().UTC().Format(time.RFC3339Nano),
  46. }
  47. data, err := json.Marshal(env)
  48. if err != nil {
  49. return
  50. }
  51. h.mu.RLock()
  52. defer h.mu.RUnlock()
  53. for c := range h.clients {
  54. if c.filter.RouterID != "" {
  55. if rid, ok := payload.(map[string]any)["router_id"].(string); ok && rid != c.filter.RouterID {
  56. continue
  57. }
  58. }
  59. if c.filter.EventType != "" && c.filter.EventType != eventType {
  60. continue
  61. }
  62. select {
  63. case c.channel <- data:
  64. default:
  65. // drop if client too slow
  66. }
  67. }
  68. }
  69. func handleSSEStream(w http.ResponseWriter, r *http.Request) {
  70. // Auth via Bearer JWT or legacy token
  71. token := r.Header.Get("Authorization")
  72. if t := r.URL.Query().Get("token"); t != "" {
  73. token = "Bearer " + t
  74. }
  75. token = trimBearer(token)
  76. if token == "" {
  77. http.Error(w, "missing token", http.StatusUnauthorized)
  78. return
  79. }
  80. // Accept either a JWT or the legacy shared TOKEN
  81. if _, err := ParseJWT(token); err != nil && token != cfg.Token {
  82. http.Error(w, "invalid token", http.StatusUnauthorized)
  83. return
  84. }
  85. flusher, ok := w.(http.Flusher)
  86. if !ok {
  87. http.Error(w, "streaming not supported", http.StatusInternalServerError)
  88. return
  89. }
  90. w.Header().Set("Content-Type", "text/event-stream")
  91. w.Header().Set("Cache-Control", "no-cache")
  92. w.Header().Set("Connection", "keep-alive")
  93. w.Header().Set("X-Accel-Buffering", "no")
  94. w.WriteHeader(http.StatusOK)
  95. flusher.Flush()
  96. client := &sseClient{
  97. channel: make(chan []byte, 64),
  98. filter: sseFilter{RouterID: r.URL.Query().Get("router_id"), EventType: r.URL.Query().Get("event_type")},
  99. lastPing: time.Now(),
  100. }
  101. hub.add(client)
  102. defer hub.remove(client)
  103. // Initial comment so curl shows connection is live
  104. _, _ = fmt.Fprintf(w, ": connected\n\n")
  105. flusher.Flush()
  106. pingTicker := time.NewTicker(15 * time.Second)
  107. defer pingTicker.Stop()
  108. for {
  109. select {
  110. case <-r.Context().Done():
  111. return
  112. case data := <-client.channel:
  113. if _, err := fmt.Fprintf(w, "data: %s\n\n", data); err != nil {
  114. return
  115. }
  116. flusher.Flush()
  117. case <-pingTicker.C:
  118. if _, err := fmt.Fprintf(w, ": ping\n\n"); err != nil {
  119. return
  120. }
  121. flusher.Flush()
  122. }
  123. }
  124. }
  125. func trimBearer(s string) string {
  126. if len(s) > 7 && s[:7] == "Bearer " {
  127. return s[7:]
  128. }
  129. return s
  130. }