105 lines
2.4 KiB
Go
105 lines
2.4 KiB
Go
// ©AngelaMos | 2026
|
|
// handler.go
|
|
|
|
package ws
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
cdrws "github.com/coder/websocket"
|
|
|
|
"github.com/carterperez-dev/monitor-the-situation/backend/internal/events"
|
|
"github.com/carterperez-dev/monitor-the-situation/backend/internal/middleware"
|
|
)
|
|
|
|
// defaultMaxConnsPerIP is permissive — a single user with React
|
|
// StrictMode in dev (effect runs twice), two tabs, and a brief reconnect
|
|
// race already burns ~6 simultaneous connections from one IP. The hub-
|
|
// level MaxSubscribers (default 5000) is the real DoS guard; this just
|
|
// blocks a single host from running thousands of sockets.
|
|
const defaultMaxConnsPerIP = 25
|
|
|
|
type HandlerConfig struct {
|
|
Hub *Hub
|
|
MaxConnsPerIP int
|
|
TrustedProxyHops int
|
|
}
|
|
|
|
type Handler struct {
|
|
hub *Hub
|
|
maxConnsPerIP int
|
|
trustedProxyHops int
|
|
perIP sync.Map
|
|
}
|
|
|
|
func NewHandler(hub *Hub) *Handler {
|
|
return NewHandlerWithConfig(HandlerConfig{Hub: hub})
|
|
}
|
|
|
|
func NewHandlerWithConfig(cfg HandlerConfig) *Handler {
|
|
if cfg.MaxConnsPerIP <= 0 {
|
|
cfg.MaxConnsPerIP = defaultMaxConnsPerIP
|
|
}
|
|
return &Handler{
|
|
hub: cfg.Hub,
|
|
maxConnsPerIP: cfg.MaxConnsPerIP,
|
|
trustedProxyHops: cfg.TrustedProxyHops,
|
|
}
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
ip := middleware.ClientIP(r, h.trustedProxyHops)
|
|
counter := h.counterFor(ip)
|
|
if counter.Add(1) > int32(h.maxConnsPerIP) {
|
|
counter.Add(-1)
|
|
http.Error(w, "too many connections", http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
defer counter.Add(-1)
|
|
|
|
c, err := cdrws.Accept(w, r, &cdrws.AcceptOptions{
|
|
InsecureSkipVerify: false,
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
topics := parseTopics(r.URL.Query().Get("topics"))
|
|
_ = h.hub.Serve(r.Context(), c, topics)
|
|
}
|
|
|
|
func (h *Handler) counterFor(ip string) *atomic.Int32 {
|
|
if existing, ok := h.perIP.Load(ip); ok {
|
|
if c, ok := existing.(*atomic.Int32); ok {
|
|
return c
|
|
}
|
|
}
|
|
fresh := &atomic.Int32{}
|
|
actual, _ := h.perIP.LoadOrStore(ip, fresh)
|
|
if c, ok := actual.(*atomic.Int32); ok {
|
|
return c
|
|
}
|
|
return fresh
|
|
}
|
|
|
|
func parseTopics(raw string) []events.Topic {
|
|
if raw == "" {
|
|
return events.AllTopics()
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
out := make([]events.Topic, 0, len(parts))
|
|
for _, p := range parts {
|
|
t := events.Topic(strings.TrimSpace(p))
|
|
if t.IsValid() {
|
|
out = append(out, t)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return events.AllTopics()
|
|
}
|
|
return out
|
|
}
|