mediamtx/internal/playback/server.go

146 lines
3.5 KiB
Go

// Package playback contains the playback server.
package playback
import (
"errors"
"net"
"net/http"
"sync"
"time"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
"github.com/bluenviron/mediamtx/internal/restrictnetwork"
"github.com/gin-gonic/gin"
)
var errNoSegmentsFound = errors.New("no recording segments found")
type serverAuthManager interface {
Authenticate(req *auth.Request) error
}
// Server is the playback server.
type Server struct {
Address string
Encryption bool
ServerKey string
ServerCert string
AllowOrigin string
TrustedProxies conf.IPNetworks
ReadTimeout conf.StringDuration
PathConfs map[string]*conf.Path
AuthManager serverAuthManager
Parent logger.Writer
httpServer *httpp.WrappedServer
mutex sync.RWMutex
}
// Initialize initializes Server.
func (s *Server) Initialize() error {
router := gin.New()
router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.NoRoute(s.middlewareOrigin)
group := router.Group("/", s.middlewareOrigin)
group.GET("/list", s.onList)
group.GET("/get", s.onGet)
network, address := restrictnetwork.Restrict("tcp", s.Address)
s.httpServer = &httpp.WrappedServer{
Network: network,
Address: address,
ReadTimeout: time.Duration(s.ReadTimeout),
Encryption: s.Encryption,
ServerCert: s.ServerCert,
ServerKey: s.ServerKey,
Handler: router,
Parent: s,
}
err := s.httpServer.Initialize()
if err != nil {
return err
}
s.Log(logger.Info, "listener opened on "+address)
return nil
}
// Close closes Server.
func (s *Server) Close() {
s.Log(logger.Info, "listener is closing")
s.httpServer.Close()
}
// Log implements logger.Writer.
func (s *Server) Log(level logger.Level, format string, args ...interface{}) {
s.Parent.Log(level, "[playback] "+format, args...)
}
// ReloadPathConfs is called by core.Core.
func (s *Server) ReloadPathConfs(pathConfs map[string]*conf.Path) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.PathConfs = pathConfs
}
func (s *Server) writeError(ctx *gin.Context, status int, err error) {
// show error in logs
s.Log(logger.Error, err.Error())
// add error to response
ctx.String(status, err.Error())
}
func (s *Server) safeFindPathConf(name string) (*conf.Path, error) {
s.mutex.RLock()
defer s.mutex.RUnlock()
_, pathConf, _, err := conf.FindPathConf(s.PathConfs, name)
return pathConf, err
}
func (s *Server) middlewareOrigin(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin)
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
}
func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := s.AuthManager.Authenticate(&auth.Request{
User: user,
Pass: pass,
Query: ctx.Request.URL.RawQuery,
IP: net.ParseIP(ctx.ClientIP()),
Action: conf.AuthActionPlayback,
Path: pathName,
})
if err != nil {
if !hasCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return false
}
var terr auth.Error
errors.As(err, &terr)
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return false
}
return true
}