mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 10:10:03 -08:00
Reply with "status": "ok" in case of success, and with "status": "error" in case of error. This makes the API more accessible and user friendly.
291 lines
7.6 KiB
Go
291 lines
7.6 KiB
Go
// Package api contains the API server.
|
|
package api //nolint:revive
|
|
|
|
import (
|
|
"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"
|
|
"github.com/bluenviron/mediamtx/internal/recordstore"
|
|
)
|
|
|
|
func interfaceIsEmpty(i any) bool {
|
|
return reflect.ValueOf(i).Kind() != reflect.Ptr || 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
|
|
}
|
|
|
|
func recordingsOfPath(
|
|
pathConf *conf.Path,
|
|
pathName string,
|
|
) *defs.APIRecording {
|
|
ret := &defs.APIRecording{
|
|
Name: pathName,
|
|
}
|
|
|
|
segments, _ := recordstore.FindSegments(pathConf, pathName, nil, nil)
|
|
|
|
ret.Segments = make([]*defs.APIRecordingSegment, len(segments))
|
|
|
|
for i, seg := range segments {
|
|
ret.Segments[i] = &defs.APIRecordingSegment{
|
|
Start: seg.Start,
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
type apiAuthManager interface {
|
|
Authenticate(req *auth.Request) *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
|
|
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)
|
|
}
|
|
|
|
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,
|
|
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
|
|
}
|
|
|
|
a.Log(logger.Info, "listener opened on "+a.Address)
|
|
|
|
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.JSON(status, &defs.APIError{
|
|
Status: "error",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
func (a *API) writeOK(ctx *gin.Context) {
|
|
ctx.JSON(http.StatusOK, &defs.APIOK{Status: "ok"})
|
|
}
|
|
|
|
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"`)
|
|
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
|
|
Status: "error",
|
|
Error: "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)
|
|
|
|
ctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{
|
|
Status: "error",
|
|
Error: "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
|
|
}
|