From 87b7956879e24842d74580baa6ad7c9e594ef82c Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Sat, 20 Apr 2024 23:45:49 +0200 Subject: [PATCH] support HTTPS, Allow-Origin and trusted proxies in API, playback server, metrics server and pprof server (#2658) (#2491) (#3235) --- apidocs/openapi.yaml | 82 +++++++++++--- internal/api/api.go | 63 ++++++----- internal/conf/conf.go | 70 +++++++++--- internal/core/core.go | 100 ++++++++++++----- internal/metrics/metrics.go | 50 +++++---- internal/playback/server.go | 97 ++++++++-------- internal/pprof/pprof.go | 59 ++++++---- internal/protocols/httpp/wrapped_server.go | 62 +++++----- .../protocols/httpp/wrapped_server_test.go | 22 ++-- internal/servers/hls/http_server.go | 32 ++---- internal/servers/hls/server.go | 4 +- internal/servers/webrtc/http_server.go | 30 ++--- mediamtx.yml | 106 ++++++++++++++---- 13 files changed, 497 insertions(+), 280 deletions(-) diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index d7c2664ab9e..05bab0e95e2 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -43,14 +43,6 @@ components: type: integer externalAuthenticationURL: type: string - metrics: - type: boolean - metricsAddress: - type: string - pprof: - type: boolean - pprofAddress: - type: string runOnConnect: type: string runOnConnectRestart: @@ -58,17 +50,77 @@ components: runOnDisconnect: type: string - # API + # Control API api: type: boolean apiAddress: type: string + apiEncryption: + type: boolean + apiServerKey: + type: string + apiServerCert: + type: string + apiAllowOrigin: + type: string + apiTrustedProxies: + type: array + items: + type: string + + # Metrics + metrics: + type: boolean + metricsAddress: + type: string + metricsEncryption: + type: boolean + metricsServerKey: + type: string + metricsServerCert: + type: string + metricsAllowOrigin: + type: string + metricsTrustedProxies: + type: array + items: + type: string + + # PPROF + pprof: + type: boolean + pprofAddress: + type: string + pprofEncryption: + type: boolean + pprofServerKey: + type: string + pprofServerCert: + type: string + pprofAllowOrigin: + type: string + pprofTrustedProxies: + type: array + items: + type: string # Playback server playback: type: boolean playbackAddress: type: string + playbackEncryption: + type: boolean + playbackServerKey: + type: string + playbackServerCert: + type: string + playbackAllowOrigin: + type: string + playbackTrustedProxies: + type: array + items: + type: string # RTSP server rtsp: @@ -127,6 +179,12 @@ components: type: string hlsServerCert: type: string + hlsAllowOrigin: + type: string + hlsTrustedProxies: + type: array + items: + type: string hlsAlwaysRemux: type: boolean hlsVariant: @@ -139,12 +197,6 @@ components: type: string hlsSegmentMaxSize: type: string - hlsAllowOrigin: - type: string - hlsTrustedProxies: - type: array - items: - type: string hlsDirectory: type: string diff --git a/internal/api/api.go b/internal/api/api.go index 607a69cee3d..642b99cb2fb 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -162,19 +162,24 @@ type apiParent interface { // API is an API server. type API struct { - Address string - ReadTimeout conf.StringDuration - Conf *conf.Conf - AuthManager apiAuthManager - PathManager PathManager - RTSPServer RTSPServer - RTSPSServer RTSPServer - RTMPServer RTMPServer - RTMPSServer RTMPServer - HLSServer HLSServer - WebRTCServer WebRTCServer - SRTServer SRTServer - Parent apiParent + Address string + Encryption bool + ServerKey string + ServerCert string + AllowOrigin string + TrustedProxies conf.IPNetworks + ReadTimeout conf.StringDuration + Conf *conf.Conf + AuthManager apiAuthManager + PathManager PathManager + RTSPServer RTSPServer + RTSPSServer RTSPServer + RTMPServer RTMPServer + RTMPSServer RTMPServer + HLSServer HLSServer + WebRTCServer WebRTCServer + SRTServer SRTServer + Parent apiParent httpServer *httpp.WrappedServer mutex sync.RWMutex @@ -183,9 +188,9 @@ type API struct { // Initialize initializes API. func (a *API) Initialize() error { router := gin.New() - router.SetTrustedProxies(nil) //nolint:errcheck + router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck - group := router.Group("/", a.mwAuth) + group := router.Group("/", a.middlewareOrigin, a.middlewareAuth) group.GET("/v3/config/global/get", a.onConfigGlobalGet) group.PATCH("/v3/config/global/patch", a.onConfigGlobalPatch) @@ -254,16 +259,17 @@ func (a *API) Initialize() error { network, address := restrictnetwork.Restrict("tcp", a.Address) - var err error - a.httpServer, err = httpp.NewWrappedServer( - network, - address, - time.Duration(a.ReadTimeout), - "", - "", - router, - a, - ) + a.httpServer = &httpp.WrappedServer{ + Network: network, + Address: address, + ReadTimeout: time.Duration(a.ReadTimeout), + Encryption: a.Encryption, + ServerCert: a.ServerCert, + ServerKey: a.ServerKey, + Handler: router, + Parent: a, + } + err := a.httpServer.Initialize() if err != nil { return err } @@ -294,7 +300,12 @@ func (a *API) writeError(ctx *gin.Context, status int, err error) { }) } -func (a *API) mwAuth(ctx *gin.Context) { +func (a *API) middlewareOrigin(ctx *gin.Context) { + ctx.Writer.Header().Set("Access-Control-Allow-Origin", a.AllowOrigin) + ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") +} + +func (a *API) middlewareAuth(ctx *gin.Context) { user, pass, hasCredentials := ctx.Request.BasicAuth() err := a.AuthManager.Authenticate(&auth.Request{ diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 1759a6216cb..36a709e6ab4 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -130,10 +130,6 @@ type Conf struct { ReadBufferCount *int `json:"readBufferCount,omitempty"` // deprecated WriteQueueSize int `json:"writeQueueSize"` UDPMaxPayloadSize int `json:"udpMaxPayloadSize"` - Metrics bool `json:"metrics"` - MetricsAddress string `json:"metricsAddress"` - PPROF bool `json:"pprof"` - PPROFAddress string `json:"pprofAddress"` RunOnConnect string `json:"runOnConnect"` RunOnConnectRestart bool `json:"runOnConnectRestart"` RunOnDisconnect string `json:"runOnDisconnect"` @@ -146,13 +142,41 @@ type Conf struct { AuthHTTPExclude []AuthInternalUserPermission `json:"authHTTPExclude"` AuthJWTJWKS string `json:"authJWTJWKS"` - // API - API bool `json:"api"` - APIAddress string `json:"apiAddress"` + // Control API + API bool `json:"api"` + APIAddress string `json:"apiAddress"` + APIEncryption bool `json:"apiEncryption"` + APIServerKey string `json:"apiServerKey"` + APIServerCert string `json:"apiServerCert"` + APIAllowOrigin string `json:"apiAllowOrigin"` + APITrustedProxies IPNetworks `json:"apiTrustedProxies"` + + // Metrics + Metrics bool `json:"metrics"` + MetricsAddress string `json:"metricsAddress"` + MetricsEncryption bool `json:"metricsEncryption"` + MetricsServerKey string `json:"metricsServerKey"` + MetricsServerCert string `json:"metricsServerCert"` + MetricsAllowOrigin string `json:"metricsAllowOrigin"` + MetricsTrustedProxies IPNetworks `json:"metricsTrustedProxies"` + + // PPROF + PPROF bool `json:"pprof"` + PPROFAddress string `json:"pprofAddress"` + PPROFEncryption bool `json:"pprofEncryption"` + PPROFServerKey string `json:"pprofServerKey"` + PPROFServerCert string `json:"pprofServerCert"` + PPROFAllowOrigin string `json:"pprofAllowOrigin"` + PPROFTrustedProxies IPNetworks `json:"pprofTrustedProxies"` // Playback - Playback bool `json:"playback"` - PlaybackAddress string `json:"playbackAddress"` + Playback bool `json:"playback"` + PlaybackAddress string `json:"playbackAddress"` + PlaybackEncryption bool `json:"playbackEncryption"` + PlaybackServerKey string `json:"playbackServerKey"` + PlaybackServerCert string `json:"playbackServerCert"` + PlaybackAllowOrigin string `json:"playbackAllowOrigin"` + PlaybackTrustedProxies IPNetworks `json:"playbackTrustedProxies"` // RTSP server RTSP bool `json:"rtsp"` @@ -187,14 +211,14 @@ type Conf struct { HLSEncryption bool `json:"hlsEncryption"` HLSServerKey string `json:"hlsServerKey"` HLSServerCert string `json:"hlsServerCert"` + HLSAllowOrigin string `json:"hlsAllowOrigin"` + HLSTrustedProxies IPNetworks `json:"hlsTrustedProxies"` HLSAlwaysRemux bool `json:"hlsAlwaysRemux"` HLSVariant HLSVariant `json:"hlsVariant"` HLSSegmentCount int `json:"hlsSegmentCount"` HLSSegmentDuration StringDuration `json:"hlsSegmentDuration"` HLSPartDuration StringDuration `json:"hlsPartDuration"` HLSSegmentMaxSize StringSize `json:"hlsSegmentMaxSize"` - HLSAllowOrigin string `json:"hlsAllowOrigin"` - HLSTrustedProxies IPNetworks `json:"hlsTrustedProxies"` HLSDirectory string `json:"hlsDirectory"` // WebRTC server @@ -246,8 +270,6 @@ func (conf *Conf) setDefaults() { conf.WriteTimeout = 10 * StringDuration(time.Second) conf.WriteQueueSize = 512 conf.UDPMaxPayloadSize = 1472 - conf.MetricsAddress = ":9998" - conf.PPROFAddress = ":9999" // Authentication conf.AuthInternalUsers = []AuthInternalUser{ @@ -295,11 +317,29 @@ func (conf *Conf) setDefaults() { }, } - // API + // Control API conf.APIAddress = ":9997" + conf.APIServerKey = "server.key" + conf.APIServerCert = "server.crt" + conf.APIAllowOrigin = "*" + + // Metrics + conf.MetricsAddress = ":9998" + conf.MetricsServerKey = "server.key" + conf.MetricsServerCert = "server.crt" + conf.MetricsAllowOrigin = "*" + + // PPROF + conf.PPROFAddress = ":9999" + conf.PPROFServerKey = "server.key" + conf.PPROFServerCert = "server.crt" + conf.PPROFAllowOrigin = "*" // Playback server conf.PlaybackAddress = ":9996" + conf.PlaybackServerKey = "server.key" + conf.PlaybackServerCert = "server.crt" + conf.PlaybackAllowOrigin = "*" // RTSP server conf.RTSP = true @@ -331,12 +371,12 @@ func (conf *Conf) setDefaults() { conf.HLSAddress = ":8888" conf.HLSServerKey = "server.key" conf.HLSServerCert = "server.crt" + conf.HLSAllowOrigin = "*" conf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency) conf.HLSSegmentCount = 7 conf.HLSSegmentDuration = 1 * StringDuration(time.Second) conf.HLSPartDuration = 200 * StringDuration(time.Millisecond) conf.HLSSegmentMaxSize = 50 * 1024 * 1024 - conf.HLSAllowOrigin = "*" // WebRTC server conf.WebRTC = true diff --git a/internal/core/core.go b/internal/core/core.go index 06c14bc53bb..a749cf19055 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -295,10 +295,15 @@ func (p *Core) createResources(initial bool) error { if p.conf.Metrics && p.metrics == nil { i := &metrics.Metrics{ - Address: p.conf.MetricsAddress, - ReadTimeout: p.conf.ReadTimeout, - AuthManager: p.authManager, - Parent: p, + Address: p.conf.MetricsAddress, + Encryption: p.conf.MetricsEncryption, + ServerKey: p.conf.MetricsServerKey, + ServerCert: p.conf.MetricsServerCert, + AllowOrigin: p.conf.MetricsAllowOrigin, + TrustedProxies: p.conf.MetricsTrustedProxies, + ReadTimeout: p.conf.ReadTimeout, + AuthManager: p.authManager, + Parent: p, } err := i.Initialize() if err != nil { @@ -310,10 +315,15 @@ func (p *Core) createResources(initial bool) error { if p.conf.PPROF && p.pprof == nil { i := &pprof.PPROF{ - Address: p.conf.PPROFAddress, - ReadTimeout: p.conf.ReadTimeout, - AuthManager: p.authManager, - Parent: p, + Address: p.conf.PPROFAddress, + Encryption: p.conf.PPROFEncryption, + ServerKey: p.conf.PPROFServerKey, + ServerCert: p.conf.PPROFServerCert, + AllowOrigin: p.conf.PPROFAllowOrigin, + TrustedProxies: p.conf.PPROFTrustedProxies, + ReadTimeout: p.conf.ReadTimeout, + AuthManager: p.authManager, + Parent: p, } err := i.Initialize() if err != nil { @@ -335,11 +345,16 @@ func (p *Core) createResources(initial bool) error { if p.conf.Playback && p.playbackServer == nil { i := &playback.Server{ - Address: p.conf.PlaybackAddress, - ReadTimeout: p.conf.ReadTimeout, - PathConfs: p.conf.Paths, - AuthManager: p.authManager, - Parent: p, + Address: p.conf.PlaybackAddress, + Encryption: p.conf.PlaybackEncryption, + ServerKey: p.conf.PlaybackServerKey, + ServerCert: p.conf.PlaybackServerCert, + AllowOrigin: p.conf.PlaybackAllowOrigin, + TrustedProxies: p.conf.PlaybackTrustedProxies, + ReadTimeout: p.conf.ReadTimeout, + PathConfs: p.conf.Paths, + AuthManager: p.authManager, + Parent: p, } err := i.Initialize() if err != nil { @@ -520,14 +535,14 @@ func (p *Core) createResources(initial bool) error { Encryption: p.conf.HLSEncryption, ServerKey: p.conf.HLSServerKey, ServerCert: p.conf.HLSServerCert, + AllowOrigin: p.conf.HLSAllowOrigin, + TrustedProxies: p.conf.HLSTrustedProxies, AlwaysRemux: p.conf.HLSAlwaysRemux, Variant: p.conf.HLSVariant, SegmentCount: p.conf.HLSSegmentCount, SegmentDuration: p.conf.HLSSegmentDuration, PartDuration: p.conf.HLSPartDuration, SegmentMaxSize: p.conf.HLSSegmentMaxSize, - AllowOrigin: p.conf.HLSAllowOrigin, - TrustedProxies: p.conf.HLSTrustedProxies, Directory: p.conf.HLSDirectory, ReadTimeout: p.conf.ReadTimeout, WriteQueueSize: p.conf.WriteQueueSize, @@ -609,19 +624,24 @@ func (p *Core) createResources(initial bool) error { if p.conf.API && p.api == nil { i := &api.API{ - Address: p.conf.APIAddress, - ReadTimeout: p.conf.ReadTimeout, - Conf: p.conf, - AuthManager: p.authManager, - PathManager: p.pathManager, - RTSPServer: p.rtspServer, - RTSPSServer: p.rtspsServer, - RTMPServer: p.rtmpServer, - RTMPSServer: p.rtmpsServer, - HLSServer: p.hlsServer, - WebRTCServer: p.webRTCServer, - SRTServer: p.srtServer, - Parent: p, + Address: p.conf.APIAddress, + Encryption: p.conf.APIEncryption, + ServerKey: p.conf.APIServerKey, + ServerCert: p.conf.APIServerCert, + AllowOrigin: p.conf.APIAllowOrigin, + TrustedProxies: p.conf.APITrustedProxies, + ReadTimeout: p.conf.ReadTimeout, + Conf: p.conf, + AuthManager: p.authManager, + PathManager: p.pathManager, + RTSPServer: p.rtspServer, + RTSPSServer: p.rtspsServer, + RTMPServer: p.rtmpServer, + RTMPSServer: p.rtmpsServer, + HLSServer: p.hlsServer, + WebRTCServer: p.webRTCServer, + SRTServer: p.srtServer, + Parent: p, } err := i.Initialize() if err != nil { @@ -660,6 +680,11 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closeMetrics := newConf == nil || newConf.Metrics != p.conf.Metrics || newConf.MetricsAddress != p.conf.MetricsAddress || + newConf.MetricsEncryption != p.conf.MetricsEncryption || + newConf.MetricsServerKey != p.conf.MetricsServerKey || + newConf.MetricsServerCert != p.conf.MetricsServerCert || + newConf.MetricsAllowOrigin != p.conf.MetricsAllowOrigin || + !reflect.DeepEqual(newConf.MetricsTrustedProxies, p.conf.MetricsTrustedProxies) || newConf.ReadTimeout != p.conf.ReadTimeout || closeAuthManager || closeLogger @@ -667,6 +692,11 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closePPROF := newConf == nil || newConf.PPROF != p.conf.PPROF || newConf.PPROFAddress != p.conf.PPROFAddress || + newConf.PPROFEncryption != p.conf.PPROFEncryption || + newConf.PPROFServerKey != p.conf.PPROFServerKey || + newConf.PPROFServerCert != p.conf.PPROFServerCert || + newConf.PPROFAllowOrigin != p.conf.PPROFAllowOrigin || + !reflect.DeepEqual(newConf.PPROFTrustedProxies, p.conf.PPROFTrustedProxies) || newConf.ReadTimeout != p.conf.ReadTimeout || closeAuthManager || closeLogger @@ -678,6 +708,11 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closePlaybackServer := newConf == nil || newConf.Playback != p.conf.Playback || newConf.PlaybackAddress != p.conf.PlaybackAddress || + newConf.PlaybackEncryption != p.conf.PlaybackEncryption || + newConf.PlaybackServerKey != p.conf.PlaybackServerKey || + newConf.PlaybackServerCert != p.conf.PlaybackServerCert || + newConf.PlaybackAllowOrigin != p.conf.PlaybackAllowOrigin || + !reflect.DeepEqual(newConf.PlaybackTrustedProxies, p.conf.PlaybackTrustedProxies) || newConf.ReadTimeout != p.conf.ReadTimeout || closeAuthManager || closeLogger @@ -780,14 +815,14 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { newConf.HLSEncryption != p.conf.HLSEncryption || newConf.HLSServerKey != p.conf.HLSServerKey || newConf.HLSServerCert != p.conf.HLSServerCert || + newConf.HLSAllowOrigin != p.conf.HLSAllowOrigin || + !reflect.DeepEqual(newConf.HLSTrustedProxies, p.conf.HLSTrustedProxies) || newConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux || newConf.HLSVariant != p.conf.HLSVariant || newConf.HLSSegmentCount != p.conf.HLSSegmentCount || newConf.HLSSegmentDuration != p.conf.HLSSegmentDuration || newConf.HLSPartDuration != p.conf.HLSPartDuration || newConf.HLSSegmentMaxSize != p.conf.HLSSegmentMaxSize || - newConf.HLSAllowOrigin != p.conf.HLSAllowOrigin || - !reflect.DeepEqual(newConf.HLSTrustedProxies, p.conf.HLSTrustedProxies) || newConf.HLSDirectory != p.conf.HLSDirectory || newConf.ReadTimeout != p.conf.ReadTimeout || newConf.WriteQueueSize != p.conf.WriteQueueSize || @@ -832,6 +867,11 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closeAPI := newConf == nil || newConf.API != p.conf.API || newConf.APIAddress != p.conf.APIAddress || + newConf.APIEncryption != p.conf.APIEncryption || + newConf.APIServerKey != p.conf.APIServerKey || + newConf.APIServerCert != p.conf.APIServerCert || + newConf.APIAllowOrigin != p.conf.APIAllowOrigin || + !reflect.DeepEqual(newConf.APITrustedProxies, p.conf.APITrustedProxies) || newConf.ReadTimeout != p.conf.ReadTimeout || closeAuthManager || closePathManager || diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 507d5a303d7..96c1a64523f 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -42,10 +42,15 @@ type metricsParent interface { // Metrics is a metrics provider. type Metrics struct { - Address string - ReadTimeout conf.StringDuration - AuthManager metricsAuthManager - Parent metricsParent + Address string + Encryption bool + ServerKey string + ServerCert string + AllowOrigin string + TrustedProxies conf.IPNetworks + ReadTimeout conf.StringDuration + AuthManager metricsAuthManager + Parent metricsParent httpServer *httpp.WrappedServer mutex sync.Mutex @@ -62,22 +67,22 @@ type Metrics struct { // Initialize initializes metrics. func (m *Metrics) Initialize() error { router := gin.New() - router.SetTrustedProxies(nil) //nolint:errcheck - - router.GET("/metrics", m.mwAuth, m.onMetrics) + router.SetTrustedProxies(m.TrustedProxies.ToTrustedProxies()) //nolint:errcheck + router.NoRoute(m.onRequest) network, address := restrictnetwork.Restrict("tcp", m.Address) - var err error - m.httpServer, err = httpp.NewWrappedServer( - network, - address, - time.Duration(m.ReadTimeout), - "", - "", - router, - m, - ) + m.httpServer = &httpp.WrappedServer{ + Network: network, + Address: address, + ReadTimeout: time.Duration(m.ReadTimeout), + Encryption: m.Encryption, + ServerCert: m.ServerCert, + ServerKey: m.ServerKey, + Handler: router, + Parent: m, + } + err := m.httpServer.Initialize() if err != nil { return err } @@ -98,7 +103,14 @@ func (m *Metrics) Log(level logger.Level, format string, args ...interface{}) { m.Parent.Log(level, "[metrics] "+format, args...) } -func (m *Metrics) mwAuth(ctx *gin.Context) { +func (m *Metrics) onRequest(ctx *gin.Context) { + ctx.Writer.Header().Set("Access-Control-Allow-Origin", m.AllowOrigin) + ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + + if ctx.Request.URL.Path != "/metrics" || ctx.Request.Method != http.MethodGet { + return + } + user, pass, hasCredentials := ctx.Request.BasicAuth() err := m.AuthManager.Authenticate(&auth.Request{ @@ -121,9 +133,7 @@ func (m *Metrics) mwAuth(ctx *gin.Context) { ctx.Writer.WriteHeader(http.StatusUnauthorized) return } -} -func (m *Metrics) onMetrics(ctx *gin.Context) { out := "" data, err := m.pathManager.APIPathsList() diff --git a/internal/playback/server.go b/internal/playback/server.go index 8f18e7260a7..7d4dfea94a2 100644 --- a/internal/playback/server.go +++ b/internal/playback/server.go @@ -24,85 +24,94 @@ type serverAuthManager interface { // Server is the playback server. type Server struct { - Address string - ReadTimeout conf.StringDuration - PathConfs map[string]*conf.Path - AuthManager serverAuthManager - Parent logger.Writer + Address string + Encryption bool + ServerKey string + ServerCert string + AllowOrigin string + TrustedProxies conf.IPNetworks + ReadTimeout conf.StringDuration + PathConfs map[string]*conf.Path + AuthManager serverAuthManager + Parent logger.Writer httpServer *httpp.WrappedServer mutex sync.RWMutex } // Initialize initializes Server. -func (p *Server) Initialize() error { +func (s *Server) Initialize() error { router := gin.New() - router.SetTrustedProxies(nil) //nolint:errcheck - - group := router.Group("/") - - group.GET("/list", p.onList) - group.GET("/get", p.onGet) - - network, address := restrictnetwork.Restrict("tcp", p.Address) - - var err error - p.httpServer, err = httpp.NewWrappedServer( - network, - address, - time.Duration(p.ReadTimeout), - "", - "", - router, - p, - ) + router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck + group := router.Group("/", s.middlewareOrigin) + group.GET("/list", s.onList) + group.GET("/get", s.onGet) + + network, address := restrictnetwork.Restrict("tcp", s.Address) + + s.httpServer = &httpp.WrappedServer{ + Network: network, + Address: address, + ReadTimeout: time.Duration(s.ReadTimeout), + Encryption: s.Encryption, + ServerCert: s.ServerCert, + ServerKey: s.ServerKey, + Handler: router, + Parent: s, + } + err := s.httpServer.Initialize() if err != nil { return err } - p.Log(logger.Info, "listener opened on "+address) + s.Log(logger.Info, "listener opened on "+address) return nil } // Close closes Server. -func (p *Server) Close() { - p.Log(logger.Info, "listener is closing") - p.httpServer.Close() +func (s *Server) Close() { + s.Log(logger.Info, "listener is closing") + s.httpServer.Close() } // Log implements logger.Writer. -func (p *Server) Log(level logger.Level, format string, args ...interface{}) { - p.Parent.Log(level, "[playback] "+format, args...) +func (s *Server) Log(level logger.Level, format string, args ...interface{}) { + s.Parent.Log(level, "[playback] "+format, args...) } // ReloadPathConfs is called by core.Core. -func (p *Server) ReloadPathConfs(pathConfs map[string]*conf.Path) { - p.mutex.Lock() - defer p.mutex.Unlock() - p.PathConfs = pathConfs +func (s *Server) ReloadPathConfs(pathConfs map[string]*conf.Path) { + s.mutex.Lock() + defer s.mutex.Unlock() + s.PathConfs = pathConfs } -func (p *Server) writeError(ctx *gin.Context, status int, err error) { +func (s *Server) writeError(ctx *gin.Context, status int, err error) { // show error in logs - p.Log(logger.Error, err.Error()) + s.Log(logger.Error, err.Error()) // add error to response ctx.String(status, err.Error()) } -func (p *Server) safeFindPathConf(name string) (*conf.Path, error) { - p.mutex.RLock() - defer p.mutex.RUnlock() +func (s *Server) safeFindPathConf(name string) (*conf.Path, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() - _, pathConf, _, err := conf.FindPathConf(p.PathConfs, name) + _, pathConf, _, err := conf.FindPathConf(s.PathConfs, name) return pathConf, err } -func (p *Server) doAuth(ctx *gin.Context, pathName string) bool { +func (s *Server) middlewareOrigin(ctx *gin.Context) { + ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin) + ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") +} + +func (s *Server) doAuth(ctx *gin.Context, pathName string) bool { user, pass, hasCredentials := ctx.Request.BasicAuth() - err := p.AuthManager.Authenticate(&auth.Request{ + err := s.AuthManager.Authenticate(&auth.Request{ User: user, Pass: pass, Query: ctx.Request.URL.RawQuery, @@ -120,7 +129,7 @@ func (p *Server) doAuth(ctx *gin.Context, pathName string) bool { var terr auth.Error errors.As(err, &terr) - p.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message) + s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message) // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go index bd3b1e1b3cb..ff6f2dc2499 100644 --- a/internal/pprof/pprof.go +++ b/internal/pprof/pprof.go @@ -4,7 +4,6 @@ package pprof import ( "net" "net/http" - "strings" "time" // start pprof @@ -15,6 +14,7 @@ import ( "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpp" "github.com/bluenviron/mediamtx/internal/restrictnetwork" + "github.com/gin-gonic/gin" ) type pprofAuthManager interface { @@ -27,28 +27,38 @@ type pprofParent interface { // PPROF is a pprof exporter. type PPROF struct { - Address string - ReadTimeout conf.StringDuration - AuthManager pprofAuthManager - Parent pprofParent + Address string + Encryption bool + ServerKey string + ServerCert string + AllowOrigin string + TrustedProxies conf.IPNetworks + ReadTimeout conf.StringDuration + AuthManager pprofAuthManager + Parent pprofParent httpServer *httpp.WrappedServer } // Initialize initializes PPROF. func (pp *PPROF) Initialize() error { + router := gin.New() + router.SetTrustedProxies(pp.TrustedProxies.ToTrustedProxies()) //nolint:errcheck + router.NoRoute(pp.onRequest) + network, address := restrictnetwork.Restrict("tcp", pp.Address) - var err error - pp.httpServer, err = httpp.NewWrappedServer( - network, - address, - time.Duration(pp.ReadTimeout), - "", - "", - pp, - pp, - ) + pp.httpServer = &httpp.WrappedServer{ + Network: network, + Address: address, + ReadTimeout: time.Duration(pp.ReadTimeout), + Encryption: pp.Encryption, + ServerCert: pp.ServerCert, + ServerKey: pp.ServerKey, + Handler: router, + Parent: pp, + } + err := pp.httpServer.Initialize() if err != nil { return err } @@ -69,31 +79,32 @@ func (pp *PPROF) Log(level logger.Level, format string, args ...interface{}) { pp.Parent.Log(level, "[pprof] "+format, args...) } -func (pp *PPROF) ServeHTTP(w http.ResponseWriter, r *http.Request) { - user, pass, hasCredentials := r.BasicAuth() +func (pp *PPROF) onRequest(ctx *gin.Context) { + ctx.Writer.Header().Set("Access-Control-Allow-Origin", pp.AllowOrigin) + ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - ip, _, _ := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr)) + user, pass, hasCredentials := ctx.Request.BasicAuth() err := pp.AuthManager.Authenticate(&auth.Request{ User: user, Pass: pass, - Query: r.URL.RawQuery, - IP: net.ParseIP(ip), + Query: ctx.Request.URL.RawQuery, + IP: net.ParseIP(ctx.ClientIP()), Action: conf.AuthActionMetrics, }) if err != nil { if !hasCredentials { - w.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`) - w.WriteHeader(http.StatusUnauthorized) + ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`) + ctx.Writer.WriteHeader(http.StatusUnauthorized) return } // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) - w.WriteHeader(http.StatusUnauthorized) + ctx.Writer.WriteHeader(http.StatusUnauthorized) return } - http.DefaultServeMux.ServeHTTP(w, r) + http.DefaultServeMux.ServeHTTP(ctx.Writer, ctx.Request) } diff --git a/internal/protocols/httpp/wrapped_server.go b/internal/protocols/httpp/wrapped_server.go index 4e23f35a07d..86579ee5abb 100644 --- a/internal/protocols/httpp/wrapped_server.go +++ b/internal/protocols/httpp/wrapped_server.go @@ -4,6 +4,7 @@ package httpp import ( "context" "crypto/tls" + "fmt" "log" "net" "net/http" @@ -26,31 +27,29 @@ func (nilWriter) Write(p []byte) (int, error) { // - server header // - filtering of invalid requests type WrappedServer struct { + Network string + Address string + ReadTimeout time.Duration + Encryption bool + ServerCert string + ServerKey string + Handler http.Handler + Parent logger.Writer + ln net.Listener inner *http.Server } -// NewWrappedServer allocates a WrappedServer. -func NewWrappedServer( - network string, - address string, - readTimeout time.Duration, - serverCert string, - serverKey string, - handler http.Handler, - parent logger.Writer, -) (*WrappedServer, error) { - ln, err := net.Listen(network, address) - if err != nil { - return nil, err - } - +// Initialize initializes a WrappedServer. +func (s *WrappedServer) Initialize() error { var tlsConfig *tls.Config - if serverCert != "" { - crt, err := tls.LoadX509KeyPair(serverCert, serverKey) + if s.Encryption { + if s.ServerCert == "" { + return fmt.Errorf("server cert is missing") + } + crt, err := tls.LoadX509KeyPair(s.ServerCert, s.ServerKey) if err != nil { - ln.Close() - return nil, err + return err } tlsConfig = &tls.Config{ @@ -58,21 +57,24 @@ func NewWrappedServer( } } - h := handler + var err error + s.ln, err = net.Listen(s.Network, s.Address) + if err != nil { + return err + } + + h := s.Handler h = &handlerFilterRequests{h} h = &handlerFilterRequests{h} h = &handlerServerHeader{h} - h = &handlerLogger{h, parent} + h = &handlerLogger{h, s.Parent} h = &handlerExitOnPanic{h} - s := &WrappedServer{ - ln: ln, - inner: &http.Server{ - Handler: h, - TLSConfig: tlsConfig, - ReadHeaderTimeout: readTimeout, - ErrorLog: log.New(&nilWriter{}, "", 0), - }, + s.inner = &http.Server{ + Handler: h, + TLSConfig: tlsConfig, + ReadHeaderTimeout: s.ReadTimeout, + ErrorLog: log.New(&nilWriter{}, "", 0), } if tlsConfig != nil { @@ -81,7 +83,7 @@ func NewWrappedServer( go s.inner.Serve(s.ln) } - return s, nil + return nil } // Close closes all resources and waits for all routines to return. diff --git a/internal/protocols/httpp/wrapped_server_test.go b/internal/protocols/httpp/wrapped_server_test.go index e30e2b854b9..e5deda644ee 100644 --- a/internal/protocols/httpp/wrapped_server_test.go +++ b/internal/protocols/httpp/wrapped_server_test.go @@ -8,23 +8,17 @@ import ( "github.com/stretchr/testify/require" - "github.com/bluenviron/mediamtx/internal/logger" + "github.com/bluenviron/mediamtx/internal/test" ) -type testLogger struct{} - -func (testLogger) Log(_ logger.Level, _ string, _ ...interface{}) { -} - func TestFilterEmptyPath(t *testing.T) { - s, err := NewWrappedServer( - "tcp", - "localhost:4555", - 10*time.Second, - "", - "", - nil, - &testLogger{}) + s := &WrappedServer{ + Network: "tcp", + Address: "localhost:4555", + ReadTimeout: 10 * time.Second, + Parent: test.NilLogger, + } + err := s.Initialize() require.NoError(t, err) defer s.Close() diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index ebd451d3d34..50d019c0cec 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -3,7 +3,6 @@ package hls import ( _ "embed" "errors" - "fmt" "net" "net/http" gopath "path" @@ -52,32 +51,23 @@ type httpServer struct { } func (s *httpServer) initialize() error { - if s.encryption { - if s.serverCert == "" { - return fmt.Errorf("server cert is missing") - } - } else { - s.serverKey = "" - s.serverCert = "" - } - router := gin.New() router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck - router.NoRoute(s.onRequest) network, address := restrictnetwork.Restrict("tcp", s.address) - var err error - s.inner, err = httpp.NewWrappedServer( - network, - address, - time.Duration(s.readTimeout), - s.serverCert, - s.serverKey, - router, - s, - ) + s.inner = &httpp.WrappedServer{ + Network: network, + Address: address, + ReadTimeout: time.Duration(s.readTimeout), + Encryption: s.encryption, + ServerCert: s.serverCert, + ServerKey: s.serverKey, + Handler: router, + Parent: s, + } + err := s.inner.Initialize() if err != nil { return err } diff --git a/internal/servers/hls/server.go b/internal/servers/hls/server.go index 8431ca0cf1e..689d22b0022 100644 --- a/internal/servers/hls/server.go +++ b/internal/servers/hls/server.go @@ -64,14 +64,14 @@ type Server struct { Encryption bool ServerKey string ServerCert string + AllowOrigin string + TrustedProxies conf.IPNetworks AlwaysRemux bool Variant conf.HLSVariant SegmentCount int SegmentDuration conf.StringDuration PartDuration conf.StringDuration SegmentMaxSize conf.StringSize - AllowOrigin string - TrustedProxies conf.IPNetworks Directory string ReadTimeout conf.StringDuration WriteQueueSize int diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index 7ed96f1ba91..1824801a9b3 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -74,31 +74,23 @@ type httpServer struct { } func (s *httpServer) initialize() error { - if s.encryption { - if s.serverCert == "" { - return fmt.Errorf("server cert is missing") - } - } else { - s.serverKey = "" - s.serverCert = "" - } - router := gin.New() router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck router.NoRoute(s.onRequest) network, address := restrictnetwork.Restrict("tcp", s.address) - var err error - s.inner, err = httpp.NewWrappedServer( - network, - address, - time.Duration(s.readTimeout), - s.serverCert, - s.serverKey, - router, - s, - ) + s.inner = &httpp.WrappedServer{ + Network: network, + Address: address, + ReadTimeout: time.Duration(s.readTimeout), + Encryption: s.encryption, + ServerCert: s.serverCert, + ServerKey: s.serverKey, + Handler: router, + Parent: s, + } + err := s.inner.Initialize() if err != nil { return err } diff --git a/mediamtx.yml b/mediamtx.yml index 94d63d402a6..822ab449d3a 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -24,16 +24,6 @@ writeQueueSize: 512 # This can be decreased to avoid fragmentation on networks with a low UDP MTU. udpMaxPayloadSize: 1472 -# Enable Prometheus-compatible metrics. -metrics: no -# Address of the metrics listener. -metricsAddress: :9998 - -# Enable pprof-compatible endpoint to monitor performances. -pprof: no -# Address of the pprof listener. -pprofAddress: :9999 - # Command to run when a client connects to the server. # This is terminated with SIGINT when a client disconnects from the server. # The following environment variables are available: @@ -133,12 +123,73 @@ authHTTPExclude: authJWTJWKS: ############################################### -# Global settings -> API +# Global settings -> Control API -# Enable controlling the server through the API. +# Enable controlling the server through the Control API. api: no -# Address of the API listener. +# Address of the Control API listener. apiAddress: :9997 +# Enable TLS/HTTPS on the Control API server. +apiEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +apiServerKey: server.key +# Path to the server certificate. +apiServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +apiAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HTTP server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +apiTrustedProxies: [] + +############################################### +# Global settings -> Metrics + +# Enable Prometheus-compatible metrics. +metrics: no +# Address of the metrics HTTP listener. +metricsAddress: :9998 +# Enable TLS/HTTPS on the Metrics server. +metricsEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +metricsServerKey: server.key +# Path to the server certificate. +metricsServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +metricsAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HTTP server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +metricsTrustedProxies: [] + +############################################### +# Global settings -> PPROF + +# Enable pprof-compatible endpoint to monitor performances. +pprof: no +# Address of the pprof listener. +pprofAddress: :9999 +# Enable TLS/HTTPS on the pprof server. +pprofEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +pprofServerKey: server.key +# Path to the server certificate. +pprofServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +pprofAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HTTP server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +pprofTrustedProxies: [] ############################################### # Global settings -> Playback server @@ -147,6 +198,21 @@ apiAddress: :9997 playback: no # Address of the playback server listener. playbackAddress: :9996 +# Enable TLS/HTTPS on the playback server. +playbackEncryption: no +# Path to the server key. This is needed only when encryption is yes. +# This can be generated with: +# openssl genrsa -out server.key 2048 +# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 +playbackServerKey: server.key +# Path to the server certificate. +playbackServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +playbackAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HTTP server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +playbackTrustedProxies: [] ############################################### # Global settings -> RTSP server @@ -225,6 +291,13 @@ hlsEncryption: no hlsServerKey: server.key # Path to the server certificate. hlsServerCert: server.crt +# Value of the Access-Control-Allow-Origin header provided in every HTTP response. +# This allows to play the HLS stream from an external website. +hlsAllowOrigin: '*' +# List of IPs or CIDRs of proxies placed before the HLS server. +# If the server receives a request from one of these entries, IP in logs +# will be taken from the X-Forwarded-For header. +hlsTrustedProxies: [] # By default, HLS is generated only when requested by a user. # This option allows to generate it always, avoiding the delay between request and generation. hlsAlwaysRemux: no @@ -252,13 +325,6 @@ hlsPartDuration: 200ms # Maximum size of each segment. # This prevents RAM exhaustion. hlsSegmentMaxSize: 50M -# Value of the Access-Control-Allow-Origin header provided in every HTTP response. -# This allows to play the HLS stream from an external website. -hlsAllowOrigin: '*' -# List of IPs or CIDRs of proxies placed before the HLS server. -# If the server receives a request from one of these entries, IP in logs -# will be taken from the X-Forwarded-For header. -hlsTrustedProxies: [] # Directory in which to save segments, instead of keeping them in the RAM. # This decreases performance, since reading from disk is less performant than # reading from RAM, but allows to save RAM.