// 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) group.GET("/paths/forward/list", a.onForwardList) group.GET("/paths/forward/get", a.onForwardGet) 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 }