diff --git a/go.mod b/go.mod index 97a4e300..74960b41 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/alecthomas/kong v0.7.1 github.com/asticode/go-astits v1.11.0 github.com/bluenviron/gohlslib v0.2.3 - github.com/bluenviron/gortsplib/v3 v3.5.0 + github.com/bluenviron/gortsplib/v3 v3.6.1 github.com/bluenviron/mediacommon v0.5.0 github.com/fsnotify/fsnotify v1.6.0 github.com/gin-gonic/gin v1.9.0 diff --git a/go.sum b/go.sum index 7656cbce..9704e106 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/asticode/go-astits v1.11.0 h1:GTHUXht0ZXAJXsVbsLIcyfHr1Bchi4QQwMARw2Z github.com/asticode/go-astits v1.11.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/bluenviron/gohlslib v0.2.3 h1:vZmpjh2qWHaCvwwha04tgu8Kz9p4CuSBRLayD2yf89A= github.com/bluenviron/gohlslib v0.2.3/go.mod h1:loD97sTtBh/nBcw8yZJgXc71A6XQb0FsDWXFRkl7Yj4= -github.com/bluenviron/gortsplib/v3 v3.5.0 h1:8d6DYcwVhghObgBFOnoJwK6xf1ZiAQ8Vi7DRv6DGLdw= -github.com/bluenviron/gortsplib/v3 v3.5.0/go.mod h1:gc6Z8pBUMC9QBqYxcOY9eVxjDPOrmFcwVH61Xs3Gu2A= +github.com/bluenviron/gortsplib/v3 v3.6.1 h1:+/kPiwmdRwUasU5thOBATJQ4/yD+vrIEutJyRTB/f+0= +github.com/bluenviron/gortsplib/v3 v3.6.1/go.mod h1:gc6Z8pBUMC9QBqYxcOY9eVxjDPOrmFcwVH61Xs3Gu2A= github.com/bluenviron/mediacommon v0.5.0 h1:YsVFlEknaXWhZGfz+Y1QbuzXLMVSmHODc7OnRqZoITY= github.com/bluenviron/mediacommon v0.5.0/go.mod h1:t0dqPsWUTchyvib0MhixIwXEgvDX4V9G+I0GzWLQRb8= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= diff --git a/internal/conf/conf.go b/internal/conf/conf.go index abf0974a..c6ac6792 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -195,6 +195,15 @@ func (conf Conf) Clone() *Conf { return &dest } +func contains(list []headers.AuthMethod, item headers.AuthMethod) bool { + for _, i := range list { + if i == item { + return true + } + } + return false +} + // Check checks the configuration for errors. func (conf *Conf) Check() error { // general @@ -209,6 +218,10 @@ func (conf *Conf) Check() error { !strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") { return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL") } + + if contains(conf.AuthMethods, headers.AuthDigest) { + return fmt.Errorf("'externalAuthenticationURL' can't be used when 'digest' is in authMethods") + } } // RTSP @@ -280,7 +293,7 @@ func (conf *Conf) UnmarshalJSON(b []byte) error { conf.MulticastRTCPPort = 8003 conf.ServerKey = "server.key" conf.ServerCert = "server.crt" - conf.AuthMethods = AuthMethods{headers.AuthBasic, headers.AuthDigest} + conf.AuthMethods = AuthMethods{headers.AuthBasic} // RTMP conf.RTMPAddress = ":1935" diff --git a/internal/conf/path.go b/internal/conf/path.go index 3523ea15..fbf33557 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/bluenviron/gortsplib/v3/pkg/headers" "github.com/bluenviron/gortsplib/v3/pkg/url" ) @@ -252,30 +253,32 @@ func (pconf *PathConf) check(conf *Conf, name string) error { "the stream is not provided by a publisher, but by a fixed source") } - if pconf.PublishUser != "" && conf.ExternalAuthenticationURL != "" { - return fmt.Errorf("'publishUser' can't be used with 'externalAuthenticationURL'") - } - if len(pconf.PublishIPs) > 0 && pconf.Source != "publisher" { return fmt.Errorf("'publishIPs' is useless when source is not 'publisher', since " + "the stream is not provided by a publisher, but by a fixed source") } - if len(pconf.PublishIPs) > 0 && conf.ExternalAuthenticationURL != "" { - return fmt.Errorf("'publishIPs' can't be used with 'externalAuthenticationURL'") - } - if (pconf.ReadUser != "" && pconf.ReadPass == "") || (pconf.ReadUser == "" && pconf.ReadPass != "") { return fmt.Errorf("read username and password must be both filled") } - if pconf.ReadUser != "" && conf.ExternalAuthenticationURL != "" { - return fmt.Errorf("'readUser' can't be used with 'externalAuthenticationURL'") + if contains(conf.AuthMethods, headers.AuthDigest) { + if strings.HasPrefix(string(pconf.PublishUser), "sha256:") || + strings.HasPrefix(string(pconf.PublishPass), "sha256:") || + strings.HasPrefix(string(pconf.ReadUser), "sha256:") || + strings.HasPrefix(string(pconf.ReadPass), "sha256:") { + return fmt.Errorf("hashed credentials can't be used when the digest auth method is available") + } } - if len(pconf.ReadIPs) > 0 && conf.ExternalAuthenticationURL != "" { - return fmt.Errorf("'readIPs' can't be used with 'externalAuthenticationURL'") + if conf.ExternalAuthenticationURL != "" { + if pconf.PublishUser != "" || + len(pconf.PublishIPs) > 0 || + pconf.ReadUser != "" || + len(pconf.ReadIPs) > 0 { + return fmt.Errorf("credentials or IPs can't be used together with 'externalAuthenticationURL'") + } } if pconf.RunOnInit != "" && pconf.Regexp != nil { diff --git a/internal/conf/yaml/load.go b/internal/conf/yaml/load.go index 412df611..33abe950 100644 --- a/internal/conf/yaml/load.go +++ b/internal/conf/yaml/load.go @@ -2,7 +2,6 @@ package yaml import ( - "bytes" "encoding/json" "fmt" @@ -64,7 +63,5 @@ func Load(buf []byte, dest interface{}) error { } // load JSON into destination - d := json.NewDecoder(bytes.NewReader(buf)) - d.DisallowUnknownFields() - return d.Decode(dest) + return json.Unmarshal(buf, dest) } diff --git a/internal/core/authentication.go b/internal/core/authentication.go new file mode 100644 index 00000000..6c847835 --- /dev/null +++ b/internal/core/authentication.go @@ -0,0 +1,183 @@ +package core + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + + "github.com/bluenviron/gortsplib/v3/pkg/auth" + "github.com/bluenviron/gortsplib/v3/pkg/base" + "github.com/bluenviron/gortsplib/v3/pkg/headers" + "github.com/bluenviron/gortsplib/v3/pkg/url" + "github.com/google/uuid" + + "github.com/aler9/mediamtx/internal/conf" +) + +func sha256Base64(in string) string { + h := sha256.New() + h.Write([]byte(in)) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} + +func checkCredential(right string, guess string) bool { + if strings.HasPrefix(right, "sha256:") { + return right[len("sha256:"):] == sha256Base64(guess) + } + + return right == guess +} + +type authProtocol string + +const ( + authProtocolRTSP authProtocol = "rtsp" + authProtocolRTMP authProtocol = "rtmp" + authProtocolHLS authProtocol = "hls" + authProtocolWebRTC authProtocol = "webrtc" +) + +func externalAuth( + ur string, + ip string, + user string, + password string, + path string, + protocol authProtocol, + id *uuid.UUID, + publish bool, + query string, +) error { + enc, _ := json.Marshal(struct { + IP string `json:"ip"` + User string `json:"user"` + Password string `json:"password"` + Path string `json:"path"` + Protocol string `json:"protocol"` + ID *uuid.UUID `json:"id"` + Action string `json:"action"` + Query string `json:"query"` + }{ + IP: ip, + User: user, + Password: password, + Path: path, + Protocol: string(protocol), + Action: func() string { + if publish { + return "publish" + } + return "read" + }(), + Query: query, + }) + res, err := http.Post(ur, "application/json", bytes.NewReader(enc)) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode > 299 { + if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 { + return fmt.Errorf("external authentication replied with code %d: %s", res.StatusCode, string(resBody)) + } + + return fmt.Errorf("external authentication replied with code %d", res.StatusCode) + } + + return nil +} + +type authCredentials struct { + query string + ip net.IP + user string + pass string + proto authProtocol + id *uuid.UUID + rtspRequest *base.Request + rtspBaseURL *url.URL + rtspNonce string +} + +func authenticate( + externalAuthenticationURL string, + rtspAuthMethods conf.AuthMethods, + pathName string, + pathConf *conf.PathConf, + publish bool, + credentials authCredentials, +) error { + var rtspAuth headers.Authorization + if credentials.rtspRequest != nil { + err := rtspAuth.Unmarshal(credentials.rtspRequest.Header["Authorization"]) + if err == nil && rtspAuth.Method == headers.AuthBasic { + credentials.user = rtspAuth.BasicUser + credentials.pass = rtspAuth.BasicPass + } + } + + if externalAuthenticationURL != "" { + err := externalAuth( + externalAuthenticationURL, + credentials.ip.String(), + credentials.user, + credentials.pass, + pathName, + credentials.proto, + credentials.id, + publish, + credentials.query, + ) + if err != nil { + return fmt.Errorf("external authentication failed: %s", err) + } + } + + var pathIPs conf.IPsOrCIDRs + var pathUser string + var pathPass string + + if publish { + pathIPs = pathConf.PublishIPs + pathUser = string(pathConf.PublishUser) + pathPass = string(pathConf.PublishPass) + } else { + pathIPs = pathConf.ReadIPs + pathUser = string(pathConf.ReadUser) + pathPass = string(pathConf.ReadPass) + } + + if pathIPs != nil { + if !ipEqualOrInRange(credentials.ip, pathIPs) { + return fmt.Errorf("IP '%s' not allowed", credentials.ip) + } + } + + if pathUser != "" { + if credentials.rtspRequest != nil && rtspAuth.Method == headers.AuthDigest { + err := auth.Validate( + credentials.rtspRequest, + pathUser, + pathPass, + credentials.rtspBaseURL, + rtspAuthMethods, + "IPCAM", + credentials.rtspNonce) + if err != nil { + return err + } + } else if !checkCredential(pathUser, credentials.user) || + !checkCredential(pathPass, credentials.pass) { + return fmt.Errorf("invalid credentials") + } + } + + return nil +} diff --git a/internal/core/core.go b/internal/core/core.go index 806b49c7..1a7a4472 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -242,7 +242,9 @@ func (p *Core) createResources(initial bool) error { if p.pathManager == nil { p.pathManager = newPathManager( p.ctx, + p.conf.ExternalAuthenticationURL, p.conf.RTSPAddress, + p.conf.AuthMethods, p.conf.ReadTimeout, p.conf.WriteTimeout, p.conf.ReadBufferCount, @@ -262,7 +264,6 @@ func (p *Core) createResources(initial bool) error { _, useMulticast := p.conf.Protocols[conf.Protocol(gortsplib.TransportUDPMulticast)] p.rtspServer, err = newRTSPServer( p.ctx, - p.conf.ExternalAuthenticationURL, p.conf.RTSPAddress, p.conf.AuthMethods, p.conf.ReadTimeout, @@ -299,7 +300,6 @@ func (p *Core) createResources(initial bool) error { if p.rtspsServer == nil { p.rtspsServer, err = newRTSPServer( p.ctx, - p.conf.ExternalAuthenticationURL, p.conf.RTSPSAddress, p.conf.AuthMethods, p.conf.ReadTimeout, @@ -336,7 +336,6 @@ func (p *Core) createResources(initial bool) error { if p.rtmpServer == nil { p.rtmpServer, err = newRTMPServer( p.ctx, - p.conf.ExternalAuthenticationURL, p.conf.RTMPAddress, p.conf.ReadTimeout, p.conf.WriteTimeout, @@ -364,7 +363,6 @@ func (p *Core) createResources(initial bool) error { if p.rtmpsServer == nil { p.rtmpsServer, err = newRTMPServer( p.ctx, - p.conf.ExternalAuthenticationURL, p.conf.RTMPSAddress, p.conf.ReadTimeout, p.conf.WriteTimeout, @@ -420,7 +418,6 @@ func (p *Core) createResources(initial bool) error { if p.webRTCServer == nil { p.webRTCServer, err = newWebRTCServer( p.ctx, - p.conf.ExternalAuthenticationURL, p.conf.WebRTCAddress, p.conf.WebRTCEncryption, p.conf.WebRTCServerKey, @@ -490,7 +487,9 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { newConf.ReadTimeout != p.conf.ReadTimeout closePathManager := newConf == nil || + newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.RTSPAddress != p.conf.RTSPAddress || + !reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) || newConf.ReadTimeout != p.conf.ReadTimeout || newConf.WriteTimeout != p.conf.WriteTimeout || newConf.ReadBufferCount != p.conf.ReadBufferCount || @@ -503,7 +502,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closeRTSPServer := newConf == nil || newConf.RTSPDisable != p.conf.RTSPDisable || newConf.Encryption != p.conf.Encryption || - newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.RTSPAddress != p.conf.RTSPAddress || !reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) || newConf.ReadTimeout != p.conf.ReadTimeout || @@ -525,7 +523,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closeRTSPSServer := newConf == nil || newConf.RTSPDisable != p.conf.RTSPDisable || newConf.Encryption != p.conf.Encryption || - newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.RTSPSAddress != p.conf.RTSPSAddress || !reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) || newConf.ReadTimeout != p.conf.ReadTimeout || @@ -544,7 +541,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { newConf.RTMPDisable != p.conf.RTMPDisable || newConf.RTMPEncryption != p.conf.RTMPEncryption || newConf.RTMPAddress != p.conf.RTMPAddress || - newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.ReadTimeout != p.conf.ReadTimeout || newConf.WriteTimeout != p.conf.WriteTimeout || newConf.ReadBufferCount != p.conf.ReadBufferCount || @@ -558,7 +554,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { newConf.RTMPDisable != p.conf.RTMPDisable || newConf.RTMPEncryption != p.conf.RTMPEncryption || newConf.RTMPSAddress != p.conf.RTMPSAddress || - newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.ReadTimeout != p.conf.ReadTimeout || newConf.WriteTimeout != p.conf.WriteTimeout || newConf.ReadBufferCount != p.conf.ReadBufferCount || @@ -593,7 +588,6 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closeWebRTCServer := newConf == nil || newConf.WebRTCDisable != p.conf.WebRTCDisable || - newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.WebRTCAddress != p.conf.WebRTCAddress || newConf.WebRTCEncryption != p.conf.WebRTCEncryption || newConf.WebRTCServerKey != p.conf.WebRTCServerKey || diff --git a/internal/core/external_auth.go b/internal/core/external_auth.go deleted file mode 100644 index bd14e628..00000000 --- a/internal/core/external_auth.go +++ /dev/null @@ -1,71 +0,0 @@ -package core - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/google/uuid" -) - -type externalAuthProto string - -const ( - externalAuthProtoRTSP externalAuthProto = "rtsp" - externalAuthProtoRTMP externalAuthProto = "rtmp" - externalAuthProtoHLS externalAuthProto = "hls" - externalAuthProtoWebRTC externalAuthProto = "webrtc" -) - -func externalAuth( - ur string, - ip string, - user string, - password string, - path string, - protocol externalAuthProto, - id *uuid.UUID, - publish bool, - query string, -) error { - enc, _ := json.Marshal(struct { - IP string `json:"ip"` - User string `json:"user"` - Password string `json:"password"` - Path string `json:"path"` - Protocol string `json:"protocol"` - ID *uuid.UUID `json:"id"` - Action string `json:"action"` - Query string `json:"query"` - }{ - IP: ip, - User: user, - Password: password, - Path: path, - Protocol: string(protocol), - Action: func() string { - if publish { - return "publish" - } - return "read" - }(), - Query: query, - }) - res, err := http.Post(ur, "application/json", bytes.NewReader(enc)) - if err != nil { - return err - } - defer res.Body.Close() - - if res.StatusCode < 200 || res.StatusCode > 299 { - if resBody, err := io.ReadAll(res.Body); err == nil { - return fmt.Errorf("external authentication replied with code %d: %s", res.StatusCode, resBody) - } - - return fmt.Errorf("external authentication replied with code %d", res.StatusCode) - } - - return nil -} diff --git a/internal/core/hls_muxer.go b/internal/core/hls_muxer.go index 3c06cd02..f089d04d 100644 --- a/internal/core/hls_muxer.go +++ b/internal/core/hls_muxer.go @@ -267,6 +267,7 @@ func (m *hlsMuxer) runInner(innerCtx context.Context, innerReady chan struct{}) res := m.pathManager.readerAdd(pathReaderAddReq{ author: m, pathName: m.pathName, + skipAuth: true, }) if res.err != nil { return res.err @@ -554,13 +555,30 @@ func (m *hlsMuxer) handleRequest(ctx *gin.Context) { bytesSent: m.bytesSent, } - err := m.authenticate(ctx) + user, pass, hasCredentials := ctx.Request.BasicAuth() + + err := authenticate( + m.externalAuthenticationURL, + nil, + m.pathName, + m.path.safeConf(), + false, + authCredentials{ + query: ctx.Request.URL.RawQuery, + ip: net.ParseIP(ctx.ClientIP()), + user: user, + pass: pass, + proto: authProtocolHLS, + }, + ) if err != nil { - if terr, ok := err.(pathErrAuthCritical); ok { - m.Log(logger.Info, "authentication error: %s", terr.message) + if !hasCredentials { + ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) + w.WriteHeader(http.StatusUnauthorized) + return } - ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) + m.Log(logger.Info, "authentication error: %s", err) w.WriteHeader(http.StatusUnauthorized) return } @@ -575,63 +593,6 @@ func (m *hlsMuxer) handleRequest(ctx *gin.Context) { m.muxer.Handle(w, ctx.Request) } -func (m *hlsMuxer) authenticate(ctx *gin.Context) error { - pathConf := m.path.safeConf() - pathIPs := pathConf.ReadIPs - pathUser := pathConf.ReadUser - pathPass := pathConf.ReadPass - - if m.externalAuthenticationURL != "" { - ip := net.ParseIP(ctx.ClientIP()) - user, pass, ok := ctx.Request.BasicAuth() - - err := externalAuth( - m.externalAuthenticationURL, - ip.String(), - user, - pass, - m.pathName, - externalAuthProtoHLS, - nil, - false, - ctx.Request.URL.RawQuery) - if err != nil { - if !ok { - return pathErrAuthNotCritical{} - } - - return pathErrAuthCritical{ - message: fmt.Sprintf("external authentication failed: %s", err), - } - } - } - - if pathIPs != nil { - ip := net.ParseIP(ctx.ClientIP()) - - if !ipEqualOrInRange(ip, pathIPs) { - return pathErrAuthCritical{ - message: fmt.Sprintf("IP '%s' not allowed", ip), - } - } - } - - if pathUser != "" { - user, pass, ok := ctx.Request.BasicAuth() - if !ok { - return pathErrAuthNotCritical{} - } - - if user != string(pathUser) || pass != string(pathPass) { - return pathErrAuthCritical{ - message: "invalid credentials", - } - } - } - - return nil -} - // processRequest is called by hlsserver.Server (forwarded from ServeHTTP). func (m *hlsMuxer) processRequest(req *hlsMuxerRequest) { select { diff --git a/internal/core/path.go b/internal/core/path.go index ef5f63ce..a3927703 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -10,7 +10,6 @@ import ( "sync/atomic" "time" - "github.com/bluenviron/gortsplib/v3/pkg/base" "github.com/bluenviron/gortsplib/v3/pkg/media" "github.com/bluenviron/gortsplib/v3/pkg/url" @@ -25,11 +24,14 @@ func newEmptyTimer() *time.Timer { return t } -type authenticateFunc func( - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, -) error +type pathErrAuth struct { + wrapped error +} + +// Error implements the error interface. +func (e pathErrAuth) Error() string { + return "authentication error" +} type pathErrNoOnePublishing struct { pathName string @@ -40,26 +42,6 @@ func (e pathErrNoOnePublishing) Error() string { return fmt.Sprintf("no one is publishing to path '%s'", e.pathName) } -type pathErrAuthNotCritical struct { - message string - response *base.Response -} - -// Error implements the error interface. -func (pathErrAuthNotCritical) Error() string { - return "non-critical authentication error" -} - -type pathErrAuthCritical struct { - message string - response *base.Response -} - -// Error implements the error interface. -func (pathErrAuthCritical) Error() string { - return "critical authentication error" -} - type pathParent interface { logger.Writer pathSourceReady(*path) @@ -101,6 +83,17 @@ type pathPublisherRemoveReq struct { res chan struct{} } +type pathGetPathConfRes struct { + conf *conf.PathConf + err error +} + +type pathGetPathConfReq struct { + name string + credentials authCredentials + res chan pathGetPathConfRes +} + type pathDescribeRes struct { path *path stream *stream @@ -109,10 +102,10 @@ type pathDescribeRes struct { } type pathDescribeReq struct { - pathName string - url *url.URL - authenticate authenticateFunc - res chan pathDescribeRes + pathName string + url *url.URL + credentials authCredentials + res chan pathDescribeRes } type pathReaderSetupPlayRes struct { @@ -122,10 +115,11 @@ type pathReaderSetupPlayRes struct { } type pathReaderAddReq struct { - author reader - pathName string - authenticate authenticateFunc - res chan pathReaderSetupPlayRes + author reader + pathName string + skipAuth bool + credentials authCredentials + res chan pathReaderSetupPlayRes } type pathPublisherAnnounceRes struct { @@ -134,10 +128,10 @@ type pathPublisherAnnounceRes struct { } type pathPublisherAddReq struct { - author publisher - pathName string - authenticate authenticateFunc - res chan pathPublisherAnnounceRes + author publisher + pathName string + credentials authCredentials + res chan pathPublisherAnnounceRes } type pathPublisherRecordRes struct { diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index 6816a189..31972bd6 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -39,15 +39,17 @@ type pathManagerParent interface { } type pathManager struct { - rtspAddress string - readTimeout conf.StringDuration - writeTimeout conf.StringDuration - readBufferCount int - udpMaxPayloadSize int - pathConfs map[string]*conf.PathConf - externalCmdPool *externalcmd.Pool - metrics *metrics - parent pathManagerParent + externalAuthenticationURL string + rtspAddress string + authMethods conf.AuthMethods + readTimeout conf.StringDuration + writeTimeout conf.StringDuration + readBufferCount int + udpMaxPayloadSize int + pathConfs map[string]*conf.PathConf + externalCmdPool *externalcmd.Pool + metrics *metrics + parent pathManagerParent ctx context.Context ctxCancel func() @@ -61,6 +63,7 @@ type pathManager struct { chPathClose chan *path chPathSourceReady chan *path chPathSourceNotReady chan *path + chPathGetPathConf chan pathGetPathConfReq chDescribe chan pathDescribeReq chReaderAdd chan pathReaderAddReq chPublisherAdd chan pathPublisherAddReq @@ -70,7 +73,9 @@ type pathManager struct { func newPathManager( parentCtx context.Context, + externalAuthenticationURL string, rtspAddress string, + authMethods conf.AuthMethods, readTimeout conf.StringDuration, writeTimeout conf.StringDuration, readBufferCount int, @@ -83,28 +88,31 @@ func newPathManager( ctx, ctxCancel := context.WithCancel(parentCtx) pm := &pathManager{ - rtspAddress: rtspAddress, - readTimeout: readTimeout, - writeTimeout: writeTimeout, - readBufferCount: readBufferCount, - udpMaxPayloadSize: udpMaxPayloadSize, - pathConfs: pathConfs, - externalCmdPool: externalCmdPool, - metrics: metrics, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - paths: make(map[string]*path), - pathsByConf: make(map[string]map[*path]struct{}), - chConfReload: make(chan map[string]*conf.PathConf), - chPathClose: make(chan *path), - chPathSourceReady: make(chan *path), - chPathSourceNotReady: make(chan *path), - chDescribe: make(chan pathDescribeReq), - chReaderAdd: make(chan pathReaderAddReq), - chPublisherAdd: make(chan pathPublisherAddReq), - chHLSServerSet: make(chan pathManagerHLSServer), - chAPIPathsList: make(chan pathAPIPathsListReq), + externalAuthenticationURL: externalAuthenticationURL, + rtspAddress: rtspAddress, + authMethods: authMethods, + readTimeout: readTimeout, + writeTimeout: writeTimeout, + readBufferCount: readBufferCount, + udpMaxPayloadSize: udpMaxPayloadSize, + pathConfs: pathConfs, + externalCmdPool: externalCmdPool, + metrics: metrics, + parent: parent, + ctx: ctx, + ctxCancel: ctxCancel, + paths: make(map[string]*path), + pathsByConf: make(map[string]map[*path]struct{}), + chConfReload: make(chan map[string]*conf.PathConf), + chPathClose: make(chan *path), + chPathSourceReady: make(chan *path), + chPathSourceNotReady: make(chan *path), + chPathGetPathConf: make(chan pathGetPathConfReq), + chDescribe: make(chan pathDescribeReq), + chReaderAdd: make(chan pathReaderAddReq), + chPublisherAdd: make(chan pathPublisherAddReq), + chHLSServerSet: make(chan pathManagerHLSServer), + chAPIPathsList: make(chan pathAPIPathsListReq), } for pathConfName, pathConf := range pm.pathConfs { @@ -194,22 +202,32 @@ outer: pm.hlsServer.pathSourceNotReady(pa) } + case req := <-pm.chPathGetPathConf: + _, pathConf, _, err := pm.getPathConfInternal(req.name) + if err != nil { + req.res <- pathGetPathConfRes{err: err} + continue + } + + err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.name, pathConf, false, req.credentials) + if err != nil { + req.res <- pathGetPathConfRes{err: pathErrAuth{wrapped: err}} + continue + } + + req.res <- pathGetPathConfRes{conf: pathConf} + case req := <-pm.chDescribe: - pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.pathName) + pathConfName, pathConf, pathMatches, err := pm.getPathConfInternal(req.pathName) if err != nil { req.res <- pathDescribeRes{err: err} continue } - if req.authenticate != nil { - err = req.authenticate( - pathConf.ReadIPs, - pathConf.ReadUser, - pathConf.ReadPass) - if err != nil { - req.res <- pathDescribeRes{err: err} - continue - } + err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials) + if err != nil { + req.res <- pathDescribeRes{err: pathErrAuth{wrapped: err}} + continue } // create path if it doesn't exist @@ -220,19 +238,16 @@ outer: req.res <- pathDescribeRes{path: pm.paths[req.pathName]} case req := <-pm.chReaderAdd: - pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.pathName) + pathConfName, pathConf, pathMatches, err := pm.getPathConfInternal(req.pathName) if err != nil { req.res <- pathReaderSetupPlayRes{err: err} continue } - if req.authenticate != nil { - err = req.authenticate( - pathConf.ReadIPs, - pathConf.ReadUser, - pathConf.ReadPass) + if !req.skipAuth { + err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, false, req.credentials) if err != nil { - req.res <- pathReaderSetupPlayRes{err: err} + req.res <- pathReaderSetupPlayRes{err: pathErrAuth{wrapped: err}} continue } } @@ -245,18 +260,15 @@ outer: req.res <- pathReaderSetupPlayRes{path: pm.paths[req.pathName]} case req := <-pm.chPublisherAdd: - pathConfName, pathConf, pathMatches, err := pm.findPathConf(req.pathName) + pathConfName, pathConf, pathMatches, err := pm.getPathConfInternal(req.pathName) if err != nil { req.res <- pathPublisherAnnounceRes{err: err} continue } - err = req.authenticate( - pathConf.PublishIPs, - pathConf.PublishUser, - pathConf.PublishPass) + err = authenticate(pm.externalAuthenticationURL, pm.authMethods, req.pathName, pathConf, true, req.credentials) if err != nil { - req.res <- pathPublisherAnnounceRes{err: err} + req.res <- pathPublisherAnnounceRes{err: pathErrAuth{wrapped: err}} continue } @@ -330,7 +342,7 @@ func (pm *pathManager) removePath(pa *path) { delete(pm.paths, pa.name) } -func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []string, error) { +func (pm *pathManager) getPathConfInternal(name string) (string, *conf.PathConf, []string, error) { err := conf.IsValidPathName(name) if err != nil { return "", nil, nil, fmt.Errorf("invalid path name: %s (%s)", err, name) @@ -389,6 +401,18 @@ func (pm *pathManager) onPathClose(pa *path) { } } +// getPathConf is called by a reader or publisher. +func (pm *pathManager) getPathConf(req pathGetPathConfReq) pathGetPathConfRes { + req.res = make(chan pathGetPathConfRes) + select { + case pm.chPathGetPathConf <- req: + return <-req.res + + case <-pm.ctx.Done(): + return pathGetPathConfRes{err: fmt.Errorf("terminated")} + } +} + // describe is called by a reader or publisher. func (pm *pathManager) describe(req pathDescribeReq) pathDescribeRes { req.res = make(chan pathDescribeRes) diff --git a/internal/core/rtmp_conn.go b/internal/core/rtmp_conn.go index 5d069cb6..85c1eb71 100644 --- a/internal/core/rtmp_conn.go +++ b/internal/core/rtmp_conn.go @@ -196,20 +196,19 @@ type rtmpConnParent interface { } type rtmpConn struct { - isTLS bool - externalAuthenticationURL string - rtspAddress string - readTimeout conf.StringDuration - writeTimeout conf.StringDuration - readBufferCount int - runOnConnect string - runOnConnectRestart bool - wg *sync.WaitGroup - conn *rtmp.Conn - nconn net.Conn - externalCmdPool *externalcmd.Pool - pathManager rtmpConnPathManager - parent rtmpConnParent + isTLS bool + rtspAddress string + readTimeout conf.StringDuration + writeTimeout conf.StringDuration + readBufferCount int + runOnConnect string + runOnConnectRestart bool + wg *sync.WaitGroup + conn *rtmp.Conn + nconn net.Conn + externalCmdPool *externalcmd.Pool + pathManager rtmpConnPathManager + parent rtmpConnParent ctx context.Context ctxCancel func() @@ -222,7 +221,6 @@ type rtmpConn struct { func newRTMPConn( parentCtx context.Context, isTLS bool, - externalAuthenticationURL string, rtspAddress string, readTimeout conf.StringDuration, writeTimeout conf.StringDuration, @@ -238,24 +236,23 @@ func newRTMPConn( ctx, ctxCancel := context.WithCancel(parentCtx) c := &rtmpConn{ - isTLS: isTLS, - externalAuthenticationURL: externalAuthenticationURL, - rtspAddress: rtspAddress, - readTimeout: readTimeout, - writeTimeout: writeTimeout, - readBufferCount: readBufferCount, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - wg: wg, - conn: rtmp.NewConn(nconn), - nconn: nconn, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - uuid: uuid.New(), - created: time.Now(), + isTLS: isTLS, + rtspAddress: rtspAddress, + readTimeout: readTimeout, + writeTimeout: writeTimeout, + readBufferCount: readBufferCount, + runOnConnect: runOnConnect, + runOnConnectRestart: runOnConnectRestart, + wg: wg, + conn: rtmp.NewConn(nconn), + nconn: nconn, + externalCmdPool: externalCmdPool, + pathManager: pathManager, + parent: parent, + ctx: ctx, + ctxCancel: ctxCancel, + uuid: uuid.New(), + created: time.Now(), } c.Log(logger.Info, "opened") @@ -344,12 +341,12 @@ func (c *rtmpConn) runInner(ctx context.Context) error { c.nconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout))) c.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout))) - u, isPublishing, err := c.conn.InitializeServer() + u, publish, err := c.conn.InitializeServer() if err != nil { return err } - if !isPublishing { + if !publish { return c.runRead(ctx, u) } return c.runPublish(ctx, u) @@ -361,20 +358,21 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error { res := c.pathManager.readerAdd(pathReaderAddReq{ author: c, pathName: pathName, - authenticate: func( - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, - ) error { - return c.authenticate(pathName, pathIPs, pathUser, pathPass, false, query, rawQuery) + credentials: authCredentials{ + query: rawQuery, + ip: c.ip(), + user: query.Get("user"), + pass: query.Get("pass"), + proto: authProtocolRTMP, + id: &c.uuid, }, }) if res.err != nil { - if terr, ok := res.err.(pathErrAuthCritical); ok { + if terr, ok := res.err.(pathErrAuth); ok { // wait some seconds to stop brute force attacks <-time.After(rtmpConnPauseAfterAuthError) - return errors.New(terr.message) + return terr.wrapped } return res.err } @@ -716,20 +714,21 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error { res := c.pathManager.publisherAdd(pathPublisherAddReq{ author: c, pathName: pathName, - authenticate: func( - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, - ) error { - return c.authenticate(pathName, pathIPs, pathUser, pathPass, true, query, rawQuery) + credentials: authCredentials{ + query: rawQuery, + ip: c.ip(), + user: query.Get("user"), + pass: query.Get("pass"), + proto: authProtocolRTMP, + id: &c.uuid, }, }) if res.err != nil { - if terr, ok := res.err.(pathErrAuthCritical); ok { + if terr, ok := res.err.(pathErrAuth); ok { // wait some seconds to stop brute force attacks <-time.After(rtmpConnPauseAfterAuthError) - return errors.New(terr.message) + return terr.wrapped } return res.err } @@ -819,54 +818,6 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error { } } -func (c *rtmpConn) authenticate( - pathName string, - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, - isPublishing bool, - query url.Values, - rawQuery string, -) error { - if c.externalAuthenticationURL != "" { - err := externalAuth( - c.externalAuthenticationURL, - c.ip().String(), - query.Get("user"), - query.Get("pass"), - pathName, - externalAuthProtoRTMP, - &c.uuid, - isPublishing, - rawQuery) - if err != nil { - return pathErrAuthCritical{ - message: fmt.Sprintf("external authentication failed: %s", err), - } - } - } - - if pathIPs != nil { - ip := c.ip() - if !ipEqualOrInRange(ip, pathIPs) { - return pathErrAuthCritical{ - message: fmt.Sprintf("IP '%s' not allowed", ip), - } - } - } - - if pathUser != "" { - if query.Get("user") != string(pathUser) || - query.Get("pass") != string(pathPass) { - return pathErrAuthCritical{ - message: "invalid credentials", - } - } - } - - return nil -} - // apiReaderDescribe implements reader. func (c *rtmpConn) apiReaderDescribe() interface{} { return struct { diff --git a/internal/core/rtmp_server.go b/internal/core/rtmp_server.go index 4762a70f..5ed95e15 100644 --- a/internal/core/rtmp_server.go +++ b/internal/core/rtmp_server.go @@ -48,18 +48,17 @@ type rtmpServerParent interface { } type rtmpServer struct { - externalAuthenticationURL string - readTimeout conf.StringDuration - writeTimeout conf.StringDuration - readBufferCount int - isTLS bool - rtspAddress string - runOnConnect string - runOnConnectRestart bool - externalCmdPool *externalcmd.Pool - metrics *metrics - pathManager *pathManager - parent rtmpServerParent + readTimeout conf.StringDuration + writeTimeout conf.StringDuration + readBufferCount int + isTLS bool + rtspAddress string + runOnConnect string + runOnConnectRestart bool + externalCmdPool *externalcmd.Pool + metrics *metrics + pathManager *pathManager + parent rtmpServerParent ctx context.Context ctxCancel func() @@ -75,7 +74,6 @@ type rtmpServer struct { func newRTMPServer( parentCtx context.Context, - externalAuthenticationURL string, address string, readTimeout conf.StringDuration, writeTimeout conf.StringDuration, @@ -111,25 +109,24 @@ func newRTMPServer( ctx, ctxCancel := context.WithCancel(parentCtx) s := &rtmpServer{ - externalAuthenticationURL: externalAuthenticationURL, - readTimeout: readTimeout, - writeTimeout: writeTimeout, - readBufferCount: readBufferCount, - rtspAddress: rtspAddress, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - isTLS: isTLS, - externalCmdPool: externalCmdPool, - metrics: metrics, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - ln: ln, - conns: make(map[*rtmpConn]struct{}), - chConnClose: make(chan *rtmpConn), - chAPIConnsList: make(chan rtmpServerAPIConnsListReq), - chAPIConnsKick: make(chan rtmpServerAPIConnsKickReq), + readTimeout: readTimeout, + writeTimeout: writeTimeout, + readBufferCount: readBufferCount, + rtspAddress: rtspAddress, + runOnConnect: runOnConnect, + runOnConnectRestart: runOnConnectRestart, + isTLS: isTLS, + externalCmdPool: externalCmdPool, + metrics: metrics, + pathManager: pathManager, + parent: parent, + ctx: ctx, + ctxCancel: ctxCancel, + ln: ln, + conns: make(map[*rtmpConn]struct{}), + chConnClose: make(chan *rtmpConn), + chAPIConnsList: make(chan rtmpServerAPIConnsListReq), + chAPIConnsKick: make(chan rtmpServerAPIConnsKickReq), } s.Log(logger.Info, "listener opened on %s", address) @@ -200,7 +197,6 @@ outer: c := newRTMPConn( s.ctx, s.isTLS, - s.externalAuthenticationURL, s.rtspAddress, s.readTimeout, s.writeTimeout, diff --git a/internal/core/rtsp_conn.go b/internal/core/rtsp_conn.go index f5b7e3a1..cdc09964 100644 --- a/internal/core/rtsp_conn.go +++ b/internal/core/rtsp_conn.go @@ -1,7 +1,6 @@ package core import ( - "errors" "fmt" "net" "time" @@ -10,7 +9,6 @@ import ( "github.com/bluenviron/gortsplib/v3/pkg/auth" "github.com/bluenviron/gortsplib/v3/pkg/base" "github.com/bluenviron/gortsplib/v3/pkg/headers" - "github.com/bluenviron/gortsplib/v3/pkg/url" "github.com/google/uuid" "github.com/aler9/mediamtx/internal/conf" @@ -27,28 +25,24 @@ type rtspConnParent interface { } type rtspConn struct { - externalAuthenticationURL string - rtspAddress string - authMethods []headers.AuthMethod - readTimeout conf.StringDuration - runOnConnect string - runOnConnectRestart bool - externalCmdPool *externalcmd.Pool - pathManager *pathManager - conn *gortsplib.ServerConn - parent rtspConnParent + rtspAddress string + authMethods []headers.AuthMethod + readTimeout conf.StringDuration + runOnConnect string + runOnConnectRestart bool + externalCmdPool *externalcmd.Pool + pathManager *pathManager + conn *gortsplib.ServerConn + parent rtspConnParent - uuid uuid.UUID - created time.Time - onConnectCmd *externalcmd.Cmd - authUser string - authPass string - authValidator *auth.Validator - authFailures int + uuid uuid.UUID + created time.Time + onConnectCmd *externalcmd.Cmd + authNonce string + authFailures int } func newRTSPConn( - externalAuthenticationURL string, rtspAddress string, authMethods []headers.AuthMethod, readTimeout conf.StringDuration, @@ -60,18 +54,17 @@ func newRTSPConn( parent rtspConnParent, ) *rtspConn { c := &rtspConn{ - externalAuthenticationURL: externalAuthenticationURL, - rtspAddress: rtspAddress, - authMethods: authMethods, - readTimeout: readTimeout, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - externalCmdPool: externalCmdPool, - pathManager: pathManager, - conn: conn, - parent: parent, - uuid: uuid.New(), - created: time.Now(), + rtspAddress: rtspAddress, + authMethods: authMethods, + readTimeout: readTimeout, + runOnConnect: runOnConnect, + runOnConnectRestart: runOnConnectRestart, + externalCmdPool: externalCmdPool, + pathManager: pathManager, + conn: conn, + parent: parent, + uuid: uuid.New(), + created: time.Now(), } c.Log(logger.Info, "opened") @@ -112,127 +105,6 @@ func (c *rtspConn) ip() net.IP { return c.conn.NetConn().RemoteAddr().(*net.TCPAddr).IP } -func (c *rtspConn) authenticate( - path string, - query string, - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, - isPublishing bool, - req *base.Request, - baseURL *url.URL, -) error { - if c.externalAuthenticationURL != "" { - username := "" - password := "" - - var auth headers.Authorization - err := auth.Unmarshal(req.Header["Authorization"]) - if err == nil && auth.Method == headers.AuthBasic { - username = auth.BasicUser - password = auth.BasicPass - } - - err = externalAuth( - c.externalAuthenticationURL, - c.ip().String(), - username, - password, - path, - externalAuthProtoRTSP, - &c.uuid, - isPublishing, - query) - if err != nil { - c.authFailures++ - - // VLC with login prompt sends 4 requests: - // 1) without credentials - // 2) with password but without username - // 3) without credentials - // 4) with password and username - // therefore we must allow up to 3 failures - if c.authFailures > 3 { - return pathErrAuthCritical{ - message: "unauthorized: " + err.Error(), - response: &base.Response{ - StatusCode: base.StatusUnauthorized, - }, - } - } - - v := "IPCAM" - return pathErrAuthNotCritical{ - message: "unauthorized: " + err.Error(), - response: &base.Response{ - StatusCode: base.StatusUnauthorized, - Header: base.Header{ - "WWW-Authenticate": headers.Authenticate{ - Method: headers.AuthBasic, - Realm: &v, - }.Marshal(), - }, - }, - } - } - } - - if pathIPs != nil { - ip := c.ip() - if !ipEqualOrInRange(ip, pathIPs) { - return pathErrAuthCritical{ - message: fmt.Sprintf("IP '%s' not allowed", ip), - response: &base.Response{ - StatusCode: base.StatusUnauthorized, - }, - } - } - } - - if pathUser != "" { - // reset authValidator every time the credentials change - if c.authValidator == nil || c.authUser != string(pathUser) || c.authPass != string(pathPass) { - c.authUser = string(pathUser) - c.authPass = string(pathPass) - c.authValidator = auth.NewValidator(string(pathUser), string(pathPass), c.authMethods) - } - - err := c.authValidator.ValidateRequest(req, baseURL) - if err != nil { - c.authFailures++ - - // VLC with login prompt sends 4 requests: - // 1) without credentials - // 2) with password but without username - // 3) without credentials - // 4) with password and username - // therefore we must allow up to 3 failures - if c.authFailures > 3 { - return pathErrAuthCritical{ - message: "unauthorized: " + err.Error(), - response: &base.Response{ - StatusCode: base.StatusUnauthorized, - }, - } - } - - return pathErrAuthNotCritical{ - response: &base.Response{ - StatusCode: base.StatusUnauthorized, - Header: base.Header{ - "WWW-Authenticate": c.authValidator.Header(), - }, - }, - } - } - - // login successful, reset authFailures - c.authFailures = 0 - } - - return nil -} - // onClose is called by rtspServer. func (c *rtspConn) onClose(err error) { c.Log(logger.Info, "closed (%v)", err) @@ -263,29 +135,28 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, } ctx.Path = ctx.Path[1:] + if c.authNonce == "" { + c.authNonce = auth.GenerateNonce() + } + res := c.pathManager.describe(pathDescribeReq{ pathName: ctx.Path, url: ctx.Request.URL, - authenticate: func( - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, - ) error { - return c.authenticate(ctx.Path, ctx.Query, pathIPs, pathUser, pathPass, false, ctx.Request, nil) + credentials: authCredentials{ + query: ctx.Query, + ip: c.ip(), + proto: authProtocolRTSP, + id: &c.uuid, + rtspRequest: ctx.Request, + rtspNonce: c.authNonce, }, }) if res.err != nil { switch terr := res.err.(type) { - case pathErrAuthNotCritical: - c.Log(logger.Debug, "non-critical authentication error: %s", terr.message) - return terr.response, nil, nil - - case pathErrAuthCritical: - // wait some seconds to stop brute force attacks - <-time.After(rtspConnPauseAfterAuthError) - - return terr.response, nil, errors.New(terr.message) + case pathErrAuth: + res, err := c.handleAuthError(terr.wrapped) + return res, nil, err case pathErrNoOnePublishing: return &base.Response{ @@ -312,3 +183,29 @@ func (c *rtspConn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, StatusCode: base.StatusOK, }, res.stream.rtspStream, nil } + +func (c *rtspConn) handleAuthError(authErr error) (*base.Response, error) { + c.authFailures++ + + // VLC with login prompt sends 4 requests: + // 1) without credentials + // 2) with password but without username + // 3) without credentials + // 4) with password and username + // therefore we must allow up to 3 failures + if c.authFailures <= 3 { + return &base.Response{ + StatusCode: base.StatusUnauthorized, + Header: base.Header{ + "WWW-Authenticate": auth.GenerateWWWAuthenticate(c.authMethods, "IPCAM", c.authNonce), + }, + }, nil + } + + // wait some seconds to stop brute force attacks + <-time.After(rtspConnPauseAfterAuthError) + + return &base.Response{ + StatusCode: base.StatusUnauthorized, + }, authErr +} diff --git a/internal/core/rtsp_server.go b/internal/core/rtsp_server.go index 635c75aa..2b4b3fa5 100644 --- a/internal/core/rtsp_server.go +++ b/internal/core/rtsp_server.go @@ -76,18 +76,17 @@ func printAddresses(srv *gortsplib.Server) string { } type rtspServer struct { - externalAuthenticationURL string - authMethods []headers.AuthMethod - readTimeout conf.StringDuration - isTLS bool - rtspAddress string - protocols map[conf.Protocol]struct{} - runOnConnect string - runOnConnectRestart bool - externalCmdPool *externalcmd.Pool - metrics *metrics - pathManager *pathManager - parent rtspServerParent + authMethods []headers.AuthMethod + readTimeout conf.StringDuration + isTLS bool + rtspAddress string + protocols map[conf.Protocol]struct{} + runOnConnect string + runOnConnectRestart bool + externalCmdPool *externalcmd.Pool + metrics *metrics + pathManager *pathManager + parent rtspServerParent ctx context.Context ctxCancel func() @@ -100,7 +99,6 @@ type rtspServer struct { func newRTSPServer( parentCtx context.Context, - externalAuthenticationURL string, address string, authMethods []headers.AuthMethod, readTimeout conf.StringDuration, @@ -128,22 +126,21 @@ func newRTSPServer( ctx, ctxCancel := context.WithCancel(parentCtx) s := &rtspServer{ - externalAuthenticationURL: externalAuthenticationURL, - authMethods: authMethods, - readTimeout: readTimeout, - isTLS: isTLS, - rtspAddress: rtspAddress, - protocols: protocols, - runOnConnect: runOnConnect, - runOnConnectRestart: runOnConnectRestart, - externalCmdPool: externalCmdPool, - metrics: metrics, - pathManager: pathManager, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - conns: make(map[*gortsplib.ServerConn]*rtspConn), - sessions: make(map[*gortsplib.ServerSession]*rtspSession), + authMethods: authMethods, + readTimeout: readTimeout, + isTLS: isTLS, + rtspAddress: rtspAddress, + protocols: protocols, + runOnConnect: runOnConnect, + runOnConnectRestart: runOnConnectRestart, + externalCmdPool: externalCmdPool, + metrics: metrics, + pathManager: pathManager, + parent: parent, + ctx: ctx, + ctxCancel: ctxCancel, + conns: make(map[*gortsplib.ServerConn]*rtspConn), + sessions: make(map[*gortsplib.ServerSession]*rtspSession), } s.srv = &gortsplib.Server{ @@ -246,7 +243,6 @@ outer: // OnConnOpen implements gortsplib.ServerHandlerOnConnOpen. func (s *rtspServer) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) { c := newRTSPConn( - s.externalAuthenticationURL, s.rtspAddress, s.authMethods, s.readTimeout, diff --git a/internal/core/rtsp_server_test.go b/internal/core/rtsp_server_test.go index b2999450..2a276101 100644 --- a/internal/core/rtsp_server_test.go +++ b/internal/core/rtsp_server_test.go @@ -115,13 +115,14 @@ func TestRTSPServerAuth(t *testing.T) { } t.Run("hashed", func(t *testing.T) { - p, ok := newInstance("rtmpDisable: yes\n" + - "hlsDisable: yes\n" + - "webrtcDisable: yes\n" + - "paths:\n" + - " all:\n" + - " publishUser: sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=\n" + - " publishPass: sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=\n") + p, ok := newInstance( + "rtmpDisable: yes\n" + + "hlsDisable: yes\n" + + "webrtcDisable: yes\n" + + "paths:\n" + + " all:\n" + + " publishUser: sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=\n" + + " publishPass: sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=\n") require.Equal(t, true, ok) defer p.Close() diff --git a/internal/core/rtsp_session.go b/internal/core/rtsp_session.go index 8c19610c..7a89a85e 100644 --- a/internal/core/rtsp_session.go +++ b/internal/core/rtsp_session.go @@ -2,13 +2,13 @@ package core import ( "encoding/hex" - "errors" "fmt" "net" "sync" "time" "github.com/bluenviron/gortsplib/v3" + "github.com/bluenviron/gortsplib/v3/pkg/auth" "github.com/bluenviron/gortsplib/v3/pkg/base" "github.com/bluenviron/gortsplib/v3/pkg/formats" "github.com/bluenviron/gortsplib/v3/pkg/media" @@ -22,10 +22,6 @@ import ( "github.com/aler9/mediamtx/internal/logger" ) -const ( - pauseAfterAuthError = 2 * time.Second -) - type rtspWriteFunc func(*rtp.Packet) func getRTSPWriteFunc(medi *media.Media, forma formats.Format, stream *stream) rtspWriteFunc { @@ -202,29 +198,28 @@ func (s *rtspSession) onAnnounce(c *rtspConn, ctx *gortsplib.ServerHandlerOnAnno } ctx.Path = ctx.Path[1:] + if c.authNonce == "" { + c.authNonce = auth.GenerateNonce() + } + res := s.pathManager.publisherAdd(pathPublisherAddReq{ author: s, pathName: ctx.Path, - authenticate: func( - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, - ) error { - return c.authenticate(ctx.Path, ctx.Query, pathIPs, pathUser, pathPass, true, ctx.Request, nil) + credentials: authCredentials{ + query: ctx.Query, + ip: c.ip(), + proto: authProtocolRTSP, + id: &c.uuid, + rtspRequest: ctx.Request, + rtspBaseURL: nil, + rtspNonce: c.authNonce, }, }) if res.err != nil { switch terr := res.err.(type) { - case pathErrAuthNotCritical: - s.Log(logger.Debug, "non-critical authentication error: %s", terr.message) - return terr.response, nil - - case pathErrAuthCritical: - // wait some seconds to stop brute force attacks - <-time.After(pauseAfterAuthError) - - return terr.response, errors.New(terr.message) + case pathErrAuth: + return c.handleAuthError(terr.wrapped) default: return &base.Response{ @@ -268,42 +263,42 @@ func (s *rtspSession) onSetup(c *rtspConn, ctx *gortsplib.ServerHandlerOnSetupCt switch s.session.State() { case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play + baseURL := &url.URL{ + Scheme: ctx.Request.URL.Scheme, + Host: ctx.Request.URL.Host, + Path: ctx.Path, + RawQuery: ctx.Query, + } + + if ctx.Query != "" { + baseURL.RawQuery += "/" + } else { + baseURL.Path += "/" + } + + if c.authNonce == "" { + c.authNonce = auth.GenerateNonce() + } + res := s.pathManager.readerAdd(pathReaderAddReq{ author: s, pathName: ctx.Path, - authenticate: func( - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, - ) error { - baseURL := &url.URL{ - Scheme: ctx.Request.URL.Scheme, - Host: ctx.Request.URL.Host, - Path: ctx.Path, - RawQuery: ctx.Query, - } - - if ctx.Query != "" { - baseURL.RawQuery += "/" - } else { - baseURL.Path += "/" - } - - return c.authenticate(ctx.Path, ctx.Query, pathIPs, pathUser, pathPass, false, ctx.Request, baseURL) + credentials: authCredentials{ + query: ctx.Query, + ip: c.ip(), + proto: authProtocolRTSP, + id: &c.uuid, + rtspRequest: ctx.Request, + rtspBaseURL: baseURL, + rtspNonce: c.authNonce, }, }) if res.err != nil { switch terr := res.err.(type) { - case pathErrAuthNotCritical: - s.Log(logger.Debug, "non-critical authentication error: %s", terr.message) - return terr.response, nil, nil - - case pathErrAuthCritical: - // wait some seconds to stop brute force attacks - <-time.After(pauseAfterAuthError) - - return terr.response, nil, errors.New(terr.message) + case pathErrAuth: + res, err := c.handleAuthError(terr.wrapped) + return res, nil, err case pathErrNoOnePublishing: return &base.Response{ diff --git a/internal/core/webrtc_conn.go b/internal/core/webrtc_conn.go index 25af5240..953dbc7b 100644 --- a/internal/core/webrtc_conn.go +++ b/internal/core/webrtc_conn.go @@ -26,7 +26,6 @@ import ( "github.com/pion/interceptor" "github.com/pion/webrtc/v3" - "github.com/aler9/mediamtx/internal/conf" "github.com/aler9/mediamtx/internal/formatprocessor" "github.com/aler9/mediamtx/internal/logger" "github.com/aler9/mediamtx/internal/websocket" @@ -302,13 +301,7 @@ func (c *webRTCConn) runInner(ctx context.Context) error { res := c.pathManager.readerAdd(pathReaderAddReq{ author: c, pathName: c.pathName, - authenticate: func( - pathIPs []fmt.Stringer, - pathUser conf.Credential, - pathPass conf.Credential, - ) error { - return nil - }, + skipAuth: true, }) if res.err != nil { return res.err diff --git a/internal/core/webrtc_server.go b/internal/core/webrtc_server.go index c899f8f6..6f65db30 100644 --- a/internal/core/webrtc_server.go +++ b/internal/core/webrtc_server.go @@ -68,14 +68,13 @@ type webRTCServerParent interface { } type webRTCServer struct { - externalAuthenticationURL string - allowOrigin string - trustedProxies conf.IPsOrCIDRs - iceServers []string - readBufferCount int - pathManager *pathManager - metrics *metrics - parent webRTCServerParent + allowOrigin string + trustedProxies conf.IPsOrCIDRs + iceServers []string + readBufferCount int + pathManager *pathManager + metrics *metrics + parent webRTCServerParent ctx context.Context ctxCancel func() @@ -101,7 +100,6 @@ type webRTCServer struct { func newWebRTCServer( parentCtx context.Context, - externalAuthenticationURL string, address string, encryption bool, serverKey string, @@ -159,28 +157,27 @@ func newWebRTCServer( ctx, ctxCancel := context.WithCancel(parentCtx) s := &webRTCServer{ - externalAuthenticationURL: externalAuthenticationURL, - allowOrigin: allowOrigin, - trustedProxies: trustedProxies, - iceServers: iceServers, - readBufferCount: readBufferCount, - pathManager: pathManager, - metrics: metrics, - parent: parent, - ctx: ctx, - ctxCancel: ctxCancel, - ln: ln, - udpMuxLn: udpMuxLn, - tcpMuxLn: tcpMuxLn, - iceUDPMux: iceUDPMux, - iceTCPMux: iceTCPMux, - iceHostNAT1To1IPs: iceHostNAT1To1IPs, - conns: make(map[*webRTCConn]struct{}), - connNew: make(chan webRTCConnNewReq), - chConnClose: make(chan *webRTCConn), - chAPIConnsList: make(chan webRTCServerAPIConnsListReq), - chAPIConnsKick: make(chan webRTCServerAPIConnsKickReq), - done: make(chan struct{}), + allowOrigin: allowOrigin, + trustedProxies: trustedProxies, + iceServers: iceServers, + readBufferCount: readBufferCount, + pathManager: pathManager, + metrics: metrics, + parent: parent, + ctx: ctx, + ctxCancel: ctxCancel, + ln: ln, + udpMuxLn: udpMuxLn, + tcpMuxLn: tcpMuxLn, + iceUDPMux: iceUDPMux, + iceTCPMux: iceTCPMux, + iceHostNAT1To1IPs: iceHostNAT1To1IPs, + conns: make(map[*webRTCConn]struct{}), + connNew: make(chan webRTCConnNewReq), + chConnClose: make(chan *webRTCConn), + chAPIConnsList: make(chan webRTCServerAPIConnsListReq), + chAPIConnsKick: make(chan webRTCServerAPIConnsKickReq), + done: make(chan struct{}), } s.requestPool = newHTTPRequestPool() @@ -357,26 +354,32 @@ func (s *webRTCServer) onRequest(ctx *gin.Context) { } dir = strings.TrimSuffix(dir, "/") + user, pass, hasCredentials := ctx.Request.BasicAuth() - res := s.pathManager.describe(pathDescribeReq{ - pathName: dir, + res := s.pathManager.getPathConf(pathGetPathConfReq{ + name: dir, + credentials: authCredentials{ + query: ctx.Request.URL.RawQuery, + ip: net.ParseIP(ctx.ClientIP()), + user: user, + pass: pass, + proto: authProtocolWebRTC, + }, }) if res.err != nil { - ctx.Writer.WriteHeader(http.StatusNotFound) - return - } + if terr, ok := res.err.(pathErrAuth); ok { + if !hasCredentials { + ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) + ctx.Writer.WriteHeader(http.StatusUnauthorized) + return + } - err := s.authenticate(res.path, ctx) - if err != nil { - if terr, ok := err.(pathErrAuthCritical); ok { - s.Log(logger.Info, "authentication error: %s", terr.message) - ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`) + s.Log(logger.Info, "authentication error: %v", terr.wrapped) ctx.Writer.WriteHeader(http.StatusUnauthorized) return } - ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`) - ctx.Writer.WriteHeader(http.StatusUnauthorized) + ctx.Writer.WriteHeader(http.StatusNotFound) return } @@ -394,7 +397,10 @@ func (s *webRTCServer) onRequest(ctx *gin.Context) { } defer wsconn.Close() - c := s.newConn(dir, wsconn) + c := s.newConn(webRTCConnNewReq{ + pathName: dir, + wsconn: wsconn, + }) if c == nil { return } @@ -403,12 +409,8 @@ func (s *webRTCServer) onRequest(ctx *gin.Context) { } } -func (s *webRTCServer) newConn(dir string, wsconn *websocket.ServerConn) *webRTCConn { - req := webRTCConnNewReq{ - pathName: dir, - wsconn: wsconn, - res: make(chan *webRTCConn), - } +func (s *webRTCServer) newConn(req webRTCConnNewReq) *webRTCConn { + req.res = make(chan *webRTCConn) select { case s.connNew <- req: @@ -418,63 +420,6 @@ func (s *webRTCServer) newConn(dir string, wsconn *websocket.ServerConn) *webRTC } } -func (s *webRTCServer) authenticate(pa *path, ctx *gin.Context) error { - pathConf := pa.safeConf() - pathIPs := pathConf.ReadIPs - pathUser := pathConf.ReadUser - pathPass := pathConf.ReadPass - - if s.externalAuthenticationURL != "" { - ip := net.ParseIP(ctx.ClientIP()) - user, pass, ok := ctx.Request.BasicAuth() - - err := externalAuth( - s.externalAuthenticationURL, - ip.String(), - user, - pass, - pa.name, - externalAuthProtoWebRTC, - nil, - false, - ctx.Request.URL.RawQuery) - if err != nil { - if !ok { - return pathErrAuthNotCritical{} - } - - return pathErrAuthCritical{ - message: fmt.Sprintf("external authentication failed: %s", err), - } - } - } - - if pathIPs != nil { - ip := net.ParseIP(ctx.ClientIP()) - - if !ipEqualOrInRange(ip, pathIPs) { - return pathErrAuthCritical{ - message: fmt.Sprintf("IP '%s' not allowed", ip), - } - } - } - - if pathUser != "" { - user, pass, ok := ctx.Request.BasicAuth() - if !ok { - return pathErrAuthNotCritical{} - } - - if user != string(pathUser) || pass != string(pathPass) { - return pathErrAuthCritical{ - message: "invalid credentials", - } - } - } - - return nil -} - // connClose is called by webRTCConn. func (s *webRTCServer) connClose(c *webRTCConn) { select { diff --git a/internal/websocket/serverconn.go b/internal/websocket/serverconn.go index a646ddc0..1edc50fe 100644 --- a/internal/websocket/serverconn.go +++ b/internal/websocket/serverconn.go @@ -23,7 +23,8 @@ var upgrader = websocket.Upgrader{ }, } -// ServerConn is a server-side WebSocket connection with automatic, periodic ping / pong. +// ServerConn is a server-side WebSocket connection with +// automatic, periodic ping-pong type ServerConn struct { wc *websocket.Conn diff --git a/mediamtx.yml b/mediamtx.yml index ce30033c..22628c88 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -96,8 +96,9 @@ multicastRTCPPort: 8003 serverKey: server.key # Path to the server certificate. This is needed only when encryption is "strict" or "optional". serverCert: server.crt -# Authentication methods. -authMethods: [basic, digest] +# Authentication methods. Available are "basic" and "digest". +# "digest" doesn't provide any additional security and is available for compatibility reasons only. +authMethods: [basic] ############################################### # RTMP parameters