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.
295 lines
8.0 KiB
Go
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
|
|
}
|