Files
mediamtx/internal/api/api.go
T
Alessandro RosandGitHub 58c6099e50 ask for credentials only in case of protocols that support it (#5966)
When clients connect with some protocols (SRT, RTMP), they are unable
to provide credentials even if they are asked to. In this case, it's
useless to wait for credentials, and it's better to immediately log
authentication errors and apply the anti-brute force algorithm.
2026-07-19 16:52:14 +02:00

295 lines
8.0 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
MoQServer defs.APIMoQServer
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)
}
if !interfaceIsEmpty(a.MoQServer) {
group.GET("/moqsessions/list", a.onMoQSessionsList)
group.GET("/moqsessions/get/:id", a.onMoQSessionsGet)
group.POST("/moqsessions/kick/:id", a.onMoQSessionsKick)
}
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 := "started with listener 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, "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()),
EnableAskCredentials: true,
}
_, 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
}
auth.LogAndDelayError(&logger.InlineWriter{
Parent: a,
Prefix: fmt.Sprintf("[conn %v]", httpp.RemoteAddr(ctx)),
}, err)
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
}