Files
mediamtx/internal/api/api.go
T
Alessandro RosandGitHub f87d9e659e hls: track sessions (#962) (#5683)
sessions are now tracked through cookies or query parameters.

This provides the ability to inspect sessions through logs, metrics and
API, allows more precise tracking of outbound bytes, decreases load on
external HTTP authentication URLs since they are now called once per
session and not once per request.
2026-04-25 21:10:34 +02:00

288 lines
7.8 KiB
Go

// Package api contains the API server.
package api //nolint:revive
import (
"fmt"
"net"
"net/http"
"reflect"
"sort"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
)
const (
maxInboundConfigSize = 10 * 1024 * 1024
)
func interfaceIsEmpty(i any) bool {
return reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()
}
func sortedKeys(paths map[string]*conf.Path) []string {
ret := make([]string, len(paths))
i := 0
for name := range paths {
ret[i] = name
i++
}
sort.Strings(ret)
return ret
}
func paramName(ctx *gin.Context) (string, bool) {
name := ctx.Param("name")
if len(name) < 2 || name[0] != '/' {
return "", false
}
return name[1:], true
}
type apiAuthManager interface {
Authenticate(req *auth.Request) (string, *auth.Error)
RefreshJWTJWKS()
}
type apiParent interface {
logger.Writer
APIConfigSet(conf *conf.Conf)
}
// API is an API server.
type API struct {
Version string
Started time.Time
Address string
DumpPackets bool
Encryption bool
ServerKey string
ServerCert string
AllowOrigins []string
TrustedProxies conf.IPNetworks
ReadTimeout conf.Duration
WriteTimeout conf.Duration
Conf *conf.Conf
AuthManager apiAuthManager
PathManager defs.APIPathManager
RTSPServer defs.APIRTSPServer
RTSPSServer defs.APIRTSPServer
RTMPServer defs.APIRTMPServer
RTMPSServer defs.APIRTMPServer
HLSServer defs.APIHLSServer
WebRTCServer defs.APIWebRTCServer
SRTServer defs.APISRTServer
Parent apiParent
httpServer *httpp.Server
mutex sync.RWMutex
}
// Initialize initializes API.
func (a *API) Initialize() error {
router := gin.New()
router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck
router.Use(a.middlewarePreflightRequests)
router.Use(a.middlewareAuth)
group := router.Group("/v3")
group.GET("/info", a.onInfo)
group.POST("/auth/jwks/refresh", a.onAuthJwksRefresh)
group.GET("/config/global/get", a.onConfigGlobalGet)
group.PATCH("/config/global/patch", a.onConfigGlobalPatch)
group.GET("/config/pathdefaults/get", a.onConfigPathDefaultsGet)
group.PATCH("/config/pathdefaults/patch", a.onConfigPathDefaultsPatch)
group.GET("/config/paths/list", a.onConfigPathsList)
group.GET("/config/paths/get/*name", a.onConfigPathsGet)
group.POST("/config/paths/add/*name", a.onConfigPathsAdd)
group.PATCH("/config/paths/patch/*name", a.onConfigPathsPatch)
group.POST("/config/paths/replace/*name", a.onConfigPathsReplace)
group.DELETE("/config/paths/delete/*name", a.onConfigPathsDelete)
group.GET("/paths/list", a.onPathsList)
group.GET("/paths/get/*name", a.onPathsGet)
if !interfaceIsEmpty(a.HLSServer) {
group.GET("/hlsmuxers/list", a.onHLSMuxersList)
group.GET("/hlsmuxers/get/*name", a.onHLSMuxersGet)
group.GET("/hlssessions/list", a.onHLSSessionsList)
group.GET("/hlssessions/get/:id", a.onHLSSessionsGet)
group.POST("/hlssessions/kick/:id", a.onHLSSessionsKick)
}
if !interfaceIsEmpty(a.RTSPServer) {
group.GET("/rtspconns/list", a.onRTSPConnsList)
group.GET("/rtspconns/get/:id", a.onRTSPConnsGet)
group.GET("/rtspsessions/list", a.onRTSPSessionsList)
group.GET("/rtspsessions/get/:id", a.onRTSPSessionsGet)
group.POST("/rtspsessions/kick/:id", a.onRTSPSessionsKick)
}
if !interfaceIsEmpty(a.RTSPSServer) {
group.GET("/rtspsconns/list", a.onRTSPSConnsList)
group.GET("/rtspsconns/get/:id", a.onRTSPSConnsGet)
group.GET("/rtspssessions/list", a.onRTSPSSessionsList)
group.GET("/rtspssessions/get/:id", a.onRTSPSSessionsGet)
group.POST("/rtspssessions/kick/:id", a.onRTSPSSessionsKick)
}
if !interfaceIsEmpty(a.RTMPServer) {
group.GET("/rtmpconns/list", a.onRTMPConnsList)
group.GET("/rtmpconns/get/:id", a.onRTMPConnsGet)
group.POST("/rtmpconns/kick/:id", a.onRTMPConnsKick)
}
if !interfaceIsEmpty(a.RTMPSServer) {
group.GET("/rtmpsconns/list", a.onRTMPSConnsList)
group.GET("/rtmpsconns/get/:id", a.onRTMPSConnsGet)
group.POST("/rtmpsconns/kick/:id", a.onRTMPSConnsKick)
}
if !interfaceIsEmpty(a.WebRTCServer) {
group.GET("/webrtcsessions/list", a.onWebRTCSessionsList)
group.GET("/webrtcsessions/get/:id", a.onWebRTCSessionsGet)
group.POST("/webrtcsessions/kick/:id", a.onWebRTCSessionsKick)
}
if !interfaceIsEmpty(a.SRTServer) {
group.GET("/srtconns/list", a.onSRTConnsList)
group.GET("/srtconns/get/:id", a.onSRTConnsGet)
group.POST("/srtconns/kick/:id", a.onSRTConnsKick)
}
group.GET("/recordings/list", a.onRecordingsList)
group.GET("/recordings/get/*name", a.onRecordingsGet)
group.DELETE("/recordings/deletesegment", a.onRecordingDeleteSegment)
a.httpServer = &httpp.Server{
Address: a.Address,
AllowOrigins: a.AllowOrigins,
DumpPackets: a.DumpPackets,
DumpPacketsPrefix: "api_server_conn",
ReadTimeout: time.Duration(a.ReadTimeout),
WriteTimeout: time.Duration(a.WriteTimeout),
Encryption: a.Encryption,
ServerCert: a.ServerCert,
ServerKey: a.ServerKey,
Handler: router,
Parent: a,
}
err := a.httpServer.Initialize()
if err != nil {
return err
}
str := "listener opened on " + a.Address
if !a.Encryption {
str += " (TCP/HTTP)"
} else {
str += " (TCP/HTTPS)"
}
a.Log(logger.Info, str)
return nil
}
// Close closes the API.
func (a *API) Close() {
a.Log(logger.Info, "listener is closing")
a.httpServer.Close()
}
// Log implements logger.Writer.
func (a *API) Log(level logger.Level, format string, args ...any) {
a.Parent.Log(level, "[API] "+format, args...)
}
func (a *API) writeError(ctx *gin.Context, status int, err error) {
// show error in logs
a.Log(logger.Error, err.Error())
// add error to response
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func (a *API) writeErrorNoLog(ctx *gin.Context, status int, err error) {
ctx.AbortWithStatusJSON(status, &defs.APIError{
Status: defs.APIErrorStatusError,
Error: err.Error(),
})
}
func (a *API) writeOK(ctx *gin.Context) {
ctx.JSON(http.StatusOK, &defs.APIOK{Status: defs.APIOKStatusOK})
}
func (a *API) middlewarePreflightRequests(ctx *gin.Context) {
if ctx.Request.Method == http.MethodOptions &&
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE")
ctx.Header("Access-Control-Allow-Headers", "Authorization, Content-Type")
ctx.AbortWithStatus(http.StatusNoContent)
return
}
}
func (a *API) middlewareAuth(ctx *gin.Context) {
req := &auth.Request{
Action: conf.AuthActionAPI,
Query: ctx.Request.URL.RawQuery,
Credentials: httpp.Credentials(ctx.Request),
IP: net.ParseIP(ctx.ClientIP()),
}
_, err := a.AuthManager.Authenticate(req)
if err != nil {
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
a.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
a.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
a.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
}
func (a *API) onInfo(ctx *gin.Context) {
ctx.JSON(http.StatusOK, &defs.APIInfo{
Version: a.Version,
Started: a.Started,
})
}
func (a *API) onAuthJwksRefresh(ctx *gin.Context) {
a.AuthManager.RefreshJWTJWKS()
a.writeOK(ctx)
}
// ReloadConf is called by core.
func (a *API) ReloadConf(conf *conf.Conf) {
a.mutex.Lock()
defer a.mutex.Unlock()
a.Conf = conf
}