Files
mediamtx/internal/api/api.go
T
Alessandro RosandGitHub b5b63d02fc support reading and publishing with Media-over-QUIC (#5815)
Media-over-QUIC is a streaming protocol built upon cutting edge
protocols (QUIC, HTTP3) and browser APIs (WebTransport, WebCodecs).
It's slightly faster than WebRTC, has an advanced data recovery
mechanism (placed at the frame level and not at the packet level), it
supports additional codecs (FLAC) and is less complicated to route.
2026-06-02 23:04:24 +02:00

294 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()),
}
_, 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
}