// Package server wires HTTP/WS routes and the UDP listener to the rest of // the application. It owns the Fiber app and the autocert setup. package server import ( "context" "crypto/subtle" "encoding/base64" "log/slog" "net" "os" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/gofiber/websocket/v2" "git.destefano.cloud/texasmade/RocketLeagueBot-Renderer/internal/hub" "git.destefano.cloud/texasmade/RocketLeagueBot-Renderer/internal/recording" "git.destefano.cloud/texasmade/RocketLeagueBot-Renderer/internal/stats" ) // AuthMiddleware returns a Fiber handler enforcing HTTP Basic Auth with the // given password. WebSocket upgrades skip auth because browsers cannot // reliably supply Authorization headers on WS; the page itself is gated. func AuthMiddleware(password string) fiber.Handler { expected := "Basic " + base64.StdEncoding.EncodeToString([]byte(":"+password)) expectedBytes := []byte(expected) return func(c *fiber.Ctx) error { if websocket.IsWebSocketUpgrade(c) { return c.Next() } deny := func() error { c.Set("WWW-Authenticate", `Basic realm="Restricted"`) return c.Status(fiber.StatusUnauthorized).SendString("Unauthorized") } auth := c.Get("Authorization") if auth == "" { return deny() } if subtle.ConstantTimeCompare([]byte(auth), expectedBytes) == 1 { return c.Next() } const scheme = "Basic " if len(auth) < len(scheme) || auth[:len(scheme)] != scheme { return deny() } decoded, err := base64.StdEncoding.DecodeString(auth[len(scheme):]) if err != nil { decoded, err = base64.RawStdEncoding.DecodeString(auth[len(scheme):]) if err != nil { return deny() } } creds := strings.SplitN(string(decoded), ":", 2) if len(creds) != 2 { return deny() } if subtle.ConstantTimeCompare([]byte(creds[1]), []byte(password)) != 1 { return deny() } return c.Next() } } // ListenUDP reads UDP packets and submits them to the recorder and hub. // It exits on ctx cancellation. func ListenUDP(ctx context.Context, addr string, h *hub.Hub, rec *recording.Recorder) { conn, err := net.ListenPacket("udp", addr) if err != nil { slog.Error("udp listen failed", "err", err) os.Exit(1) } slog.Info("udp listening", "addr", addr) go func() { <-ctx.Done() _ = conn.Close() }() buf := make([]byte, 65536) for { n, _, err := conn.ReadFrom(buf) if err != nil { if ctx.Err() != nil { return } slog.Warn("udp read error", "err", err) continue } stats.Global.UDPPackets.Add(1) stats.Global.UDPBytes.Add(int64(n)) stats.Global.LastUDPPacketUnix.Store(time.Now().UnixNano()) data := buf[:n] rec.Submit(data) if h.Count() > 0 { cp := make([]byte, n) copy(cp, data) h.Broadcast(cp) } } }