From 386be42784192f5fd55436950ca02879ac174659 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 18 Feb 2025 17:54:13 +0100 Subject: [PATCH] rtsp: rewrite authentication around ServerConn.VerifyCredentials (#4267) --- go.mod | 2 +- go.sum | 4 +- internal/api/api.go | 2 +- internal/auth/manager.go | 70 +++++++++---------------- internal/auth/manager_test.go | 41 ++++----------- internal/auth/request.go | 17 +++--- internal/conf/auth_internal_users.go | 21 ++++++++ internal/conf/conf_test.go | 18 +++++++ internal/core/core.go | 18 +++---- internal/defs/path_access_request.go | 30 +++++------ internal/metrics/metrics.go | 2 +- internal/playback/server.go | 2 +- internal/pprof/pprof.go | 2 +- internal/servers/hls/http_server.go | 2 +- internal/servers/rtmp/conn.go | 4 +- internal/servers/rtsp/conn.go | 72 ++++++++++---------------- internal/servers/rtsp/server.go | 1 + internal/servers/rtsp/server_test.go | 6 +-- internal/servers/rtsp/session.go | 61 ++++++++-------------- internal/servers/srt/conn.go | 4 +- internal/servers/webrtc/http_server.go | 4 +- 21 files changed, 165 insertions(+), 218 deletions(-) diff --git a/go.mod b/go.mod index a6704d5d..6756fbd9 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/alecthomas/kong v1.8.1 github.com/asticode/go-astits v1.13.0 github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc - github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10 + github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218163904-55556f1ecfa2 github.com/bluenviron/mediacommon/v2 v2.0.0 github.com/datarhei/gosrt v0.8.0 github.com/fsnotify/fsnotify v1.8.0 diff --git a/go.sum b/go.sum index f942213f..7f8a73e7 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,8 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc h1:t1i9foTQ+RfFT5Ke9HV845zWtz2vtWQCWV8ZXvpzM4g= github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc/go.mod h1:soTVqoidOT+L08hUSDreM7DebNyjjViUiEvpWlr7EIs= -github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10 h1:nQI+hp8j2uSSHTumlqG4JcgwdpK+jy8Hj19Z96HRmPo= -github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10/go.mod h1:87/zjOqku9cRSk7q6tZ4R8N7evB29E11GnwLVUk7sAQ= +github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218163904-55556f1ecfa2 h1:GgPHJTUYTwKBVu/bf0SVSCdXxRz9AMcLnV7tDmiVYko= +github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218163904-55556f1ecfa2/go.mod h1:87/zjOqku9cRSk7q6tZ4R8N7evB29E11GnwLVUk7sAQ= github.com/bluenviron/mediacommon/v2 v2.0.0 h1:JinZ9v2x6QeAOzA0cDA6aFe8vQuCrU8OyWEhG2iNzwY= github.com/bluenviron/mediacommon/v2 v2.0.0/go.mod h1:iHEz1SFIet6zBwAQoh1a92vTQ3dV3LpVFbom6/SLz3k= github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk= diff --git a/internal/api/api.go b/internal/api/api.go index bc842a6d..beb64b78 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -294,7 +294,7 @@ func (a *API) middlewareAuth(ctx *gin.Context) { err := a.AuthManager.Authenticate(req) if err != nil { - if err.(*auth.Error).AskCredentials { //nolint:errorlint + if err.(auth.Error).AskCredentials { //nolint:errorlint ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.AbortWithStatus(http.StatusUnauthorized) return diff --git a/internal/auth/manager.go b/internal/auth/manager.go index 88249aea..649d009b 100644 --- a/internal/auth/manager.go +++ b/internal/auth/manager.go @@ -14,8 +14,6 @@ import ( "time" "github.com/MicahParks/keyfunc/v3" - "github.com/bluenviron/gortsplib/v4/pkg/auth" - "github.com/bluenviron/gortsplib/v4/pkg/headers" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" "github.com/golang-jwt/jwt/v5" @@ -26,19 +24,19 @@ const ( // PauseAfterError is the pause to apply after an authentication failure. PauseAfterError = 2 * time.Second - rtspAuthRealm = "IPCAM" jwtRefreshPeriod = 60 * 60 * time.Second ) // Error is a authentication error. type Error struct { + Wrapped error Message string AskCredentials bool } // Error implements the error interface. -func (e *Error) Error() string { - return "authentication failed: " + e.Message +func (e Error) Error() string { + return "authentication failed: " + e.Wrapped.Error() } func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bool { @@ -102,14 +100,13 @@ func (c *customClaims) UnmarshalJSON(b []byte) error { // Manager is the authentication manager. type Manager struct { - Method conf.AuthMethod - InternalUsers []conf.AuthInternalUser - HTTPAddress string - HTTPExclude []conf.AuthInternalUserPermission - JWTJWKS string - JWTClaimKey string - ReadTimeout time.Duration - RTSPAuthMethods []auth.VerifyMethod + Method conf.AuthMethod + InternalUsers []conf.AuthInternalUser + HTTPAddress string + HTTPExclude []conf.AuthInternalUserPermission + JWTJWKS string + JWTClaimKey string + ReadTimeout time.Duration mutex sync.RWMutex jwtHTTPClient *http.Client @@ -140,8 +137,8 @@ func (m *Manager) Authenticate(req *Request) error { } if err != nil { - return &Error{ - Message: err.Error(), + return Error{ + Wrapped: err, AskCredentials: (req.User == "" && req.Pass == ""), } } @@ -150,20 +147,11 @@ func (m *Manager) Authenticate(req *Request) error { } func (m *Manager) authenticateInternal(req *Request) error { - var rtspAuthHeader *headers.Authorization - if req.RTSPRequest != nil { - var tmp headers.Authorization - err := tmp.Unmarshal(req.RTSPRequest.Header["Authorization"]) - if err == nil { - rtspAuthHeader = &tmp - } - } - m.mutex.RLock() defer m.mutex.RUnlock() for _, u := range m.InternalUsers { - if err := m.authenticateWithUser(req, rtspAuthHeader, &u); err == nil { + if ok := m.authenticateWithUser(req, &u); ok { return nil } } @@ -173,39 +161,29 @@ func (m *Manager) authenticateInternal(req *Request) error { func (m *Manager) authenticateWithUser( req *Request, - rtspAuthHeader *headers.Authorization, u *conf.AuthInternalUser, -) error { - if u.User != "any" && !u.User.Check(req.User) { - return fmt.Errorf("wrong user") - } - +) bool { if len(u.IPs) != 0 && !u.IPs.Contains(req.IP) { - return fmt.Errorf("IP not allowed") + return false } if !matchesPermission(u.Permissions, req) { - return fmt.Errorf("user doesn't have permission to perform action") + return false } if u.User != "any" { - if req.RTSPRequest != nil && rtspAuthHeader != nil && rtspAuthHeader.Method == headers.AuthMethodDigest { - err := auth.Verify( - req.RTSPRequest, - string(u.User), - string(u.Pass), - m.RTSPAuthMethods, - rtspAuthRealm, - req.RTSPNonce) - if err != nil { - return err + if req.CustomVerifyFunc != nil { + if ok := req.CustomVerifyFunc(string(u.User), string(u.Pass)); !ok { + return false + } + } else { + if !u.User.Check(req.User) || !u.Pass.Check(req.Pass) { + return false } - } else if !u.Pass.Check(req.Pass) { - return fmt.Errorf("invalid credentials") } } - return nil + return true } func (m *Manager) authenticateHTTP(req *Request) error { diff --git a/internal/auth/manager_test.go b/internal/auth/manager_test.go index a8f2f486..be95ac26 100644 --- a/internal/auth/manager_test.go +++ b/internal/auth/manager_test.go @@ -12,7 +12,6 @@ import ( "time" "github.com/MicahParks/jwkset" - "github.com/bluenviron/gortsplib/v4/pkg/auth" "github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/mediamtx/internal/conf" "github.com/golang-jwt/jwt/v5" @@ -56,8 +55,6 @@ func TestAuthInternal(t *testing.T) { }}, }, }, - HTTPAddress: "", - RTSPAuthMethods: nil, } switch encryption { @@ -142,7 +139,7 @@ func TestAuthInternal(t *testing.T) { } } -func TestAuthInternalRTSPDigest(t *testing.T) { +func TestAuthInternalCustomVerifyFunc(t *testing.T) { for _, ca := range []string{"ok", "invalid"} { t.Run(ca, func(t *testing.T) { m := Manager{ @@ -158,8 +155,6 @@ func TestAuthInternalRTSPDigest(t *testing.T) { }}, }, }, - HTTPAddress: "", - RTSPAuthMethods: []auth.VerifyMethod{auth.VerifyMethodDigestMD5}, } u, err := base.ParseURL("rtsp://127.0.0.1:8554/mypath") @@ -170,25 +165,15 @@ func TestAuthInternalRTSPDigest(t *testing.T) { URL: u, } - if ca == "ok" { - var s *auth.Sender - s, err = auth.NewSender( - auth.GenerateWWWAuthenticate([]auth.VerifyMethod{auth.VerifyMethodDigestMD5}, "IPCAM", "mynonce"), - "myuser", - "mypass", - ) - require.NoError(t, err) - s.AddAuthorization(req) - } else { - req.Header = base.Header{"Authorization": base.HeaderValue{"garbage"}} - } - req1 := &Request{ - IP: net.ParseIP("127.1.1.1"), - Action: conf.AuthActionPublish, - Path: "mypath", - RTSPRequest: req, - RTSPNonce: "mynonce", + IP: net.ParseIP("127.1.1.1"), + Action: conf.AuthActionPublish, + Path: "mypath", + CustomVerifyFunc: func(expectedUser, expectedPass string) bool { + require.Equal(t, "myuser", expectedUser) + require.Equal(t, "mypass", expectedPass) + return (ca == "ok") + }, } req1.FillFromRTSPRequest(req) err = m.Authenticate(req1) @@ -216,8 +201,6 @@ func TestAuthInternalCredentialsInBearer(t *testing.T) { }}, }, }, - HTTPAddress: "", - RTSPAuthMethods: []auth.VerifyMethod{auth.VerifyMethodDigestMD5}, } req := &Request{ @@ -282,9 +265,8 @@ func TestAuthHTTP(t *testing.T) { defer httpServ.Shutdown(context.Background()) m := Manager{ - Method: conf.AuthMethodHTTP, - HTTPAddress: "http://127.0.0.1:9120/auth", - RTSPAuthMethods: nil, + Method: conf.AuthMethodHTTP, + HTTPAddress: "http://127.0.0.1:9120/auth", } if outcome == "ok" { @@ -321,7 +303,6 @@ func TestAuthHTTPExclude(t *testing.T) { HTTPExclude: []conf.AuthInternalUserPermission{{ Action: conf.AuthActionPublish, }}, - RTSPAuthMethods: nil, } err := m.Authenticate(&Request{ diff --git a/internal/auth/request.go b/internal/auth/request.go index fd59e3ff..b469e79c 100644 --- a/internal/auth/request.go +++ b/internal/auth/request.go @@ -37,20 +37,17 @@ const ( // Request is an authentication request. type Request struct { - User string - Pass string - IP net.IP - Action conf.AuthAction + User string + Pass string + IP net.IP + Action conf.AuthAction + CustomVerifyFunc func(expectedUser string, expectedPass string) bool // only for ActionPublish, ActionRead, ActionPlayback Path string Protocol Protocol ID *uuid.UUID Query string - - // RTSP only - RTSPRequest *base.Request - RTSPNonce string } // FillFromRTSPRequest fills User and Pass from a RTSP request. @@ -58,11 +55,9 @@ func (r *Request) FillFromRTSPRequest(rt *base.Request) { var rtspAuthHeader headers.Authorization err := rtspAuthHeader.Unmarshal(rt.Header["Authorization"]) if err == nil { + r.User = rtspAuthHeader.Username if rtspAuthHeader.Method == headers.AuthMethodBasic { - r.User = rtspAuthHeader.BasicUser r.Pass = rtspAuthHeader.BasicPass - } else { - r.User = rtspAuthHeader.Username } } } diff --git a/internal/conf/auth_internal_users.go b/internal/conf/auth_internal_users.go index d8a9ba56..720a132c 100644 --- a/internal/conf/auth_internal_users.go +++ b/internal/conf/auth_internal_users.go @@ -1,6 +1,8 @@ package conf import ( + "fmt" + "github.com/bluenviron/mediamtx/internal/conf/jsonwrapper" ) @@ -18,6 +20,25 @@ type AuthInternalUser struct { Permissions []AuthInternalUserPermission `json:"permissions"` } +// UnmarshalJSON implements json.Unmarshaler. +func (d *AuthInternalUser) UnmarshalJSON(b []byte) error { + type alias AuthInternalUser + if err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil { + return err + } + + // https://github.com/bluenviron/gortsplib/blob/55556f1ecfa2bd51b29fe14eddd70512a0361cbd/server_conn.go#L155-L156 + if d.User == "" { + return fmt.Errorf("empty usernames are not supported") + } + + if d.User == "any" && d.Pass != "" { + return fmt.Errorf("using a password with 'any' user is not supported") + } + + return nil +} + // AuthInternalUsers is a list of AuthInternalUser type AuthInternalUsers []AuthInternalUser diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 884e2760..41bc6cf4 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -407,6 +407,24 @@ func TestConfErrors(t *testing.T) { " sourceRedirect: invalid://invalid", `'sourceRedirect' is useless when source is not 'redirect'`, }, + { + "invalid user", + "authInternalUsers:\n" + + "- user:\n" + + " pass: test\n" + + " permissions:\n" + + " - action: publish\n", + "empty usernames are not supported", + }, + { + "invalid pass", + "authInternalUsers:\n" + + "- user: any\n" + + " pass: test\n" + + " permissions:\n" + + " - action: publish\n", + `using a password with 'any' user is not supported`, + }, } { t.Run(ca.name, func(t *testing.T) { tmpf, err := createTempFile([]byte(ca.conf)) diff --git a/internal/core/core.go b/internal/core/core.go index 8c98bd4e..357601bd 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -264,14 +264,13 @@ func (p *Core) createResources(initial bool) error { if p.authManager == nil { p.authManager = &auth.Manager{ - Method: p.conf.AuthMethod, - InternalUsers: p.conf.AuthInternalUsers, - HTTPAddress: p.conf.AuthHTTPAddress, - HTTPExclude: p.conf.AuthHTTPExclude, - JWTJWKS: p.conf.AuthJWTJWKS, - JWTClaimKey: p.conf.AuthJWTClaimKey, - ReadTimeout: time.Duration(p.conf.ReadTimeout), - RTSPAuthMethods: p.conf.RTSPAuthMethods, + Method: p.conf.AuthMethod, + InternalUsers: p.conf.AuthInternalUsers, + HTTPAddress: p.conf.AuthHTTPAddress, + HTTPExclude: p.conf.AuthHTTPExclude, + JWTJWKS: p.conf.AuthJWTJWKS, + JWTClaimKey: p.conf.AuthJWTClaimKey, + ReadTimeout: time.Duration(p.conf.ReadTimeout), } } @@ -653,8 +652,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { !reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) || newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS || newConf.AuthJWTClaimKey != p.conf.AuthJWTClaimKey || - newConf.ReadTimeout != p.conf.ReadTimeout || - !reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) + newConf.ReadTimeout != p.conf.ReadTimeout if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) { p.authManager.ReloadInternalUsers(newConf.AuthInternalUsers) } diff --git a/internal/defs/path_access_request.go b/internal/defs/path_access_request.go index daf17f2e..7d647532 100644 --- a/internal/defs/path_access_request.go +++ b/internal/defs/path_access_request.go @@ -32,15 +32,12 @@ type PathAccessRequest struct { SkipAuth bool // only if skipAuth = false - User string - Pass string - IP net.IP - Proto auth.Protocol - ID *uuid.UUID - - // RTSP only - RTSPRequest *base.Request - RTSPNonce string + User string + Pass string + IP net.IP + CustomVerifyFunc func(expectedUser string, expectedPass string) bool + Proto auth.Protocol + ID *uuid.UUID } // ToAuthRequest converts a path access request into an authentication request. @@ -55,12 +52,11 @@ func (r *PathAccessRequest) ToAuthRequest() *auth.Request { } return conf.AuthActionRead }(), - Path: r.Name, - Protocol: r.Proto, - ID: r.ID, - Query: r.Query, - RTSPRequest: r.RTSPRequest, - RTSPNonce: r.RTSPNonce, + CustomVerifyFunc: r.CustomVerifyFunc, + Path: r.Name, + Protocol: r.Proto, + ID: r.ID, + Query: r.Query, } } @@ -69,11 +65,9 @@ func (r *PathAccessRequest) FillFromRTSPRequest(rt *base.Request) { var rtspAuthHeader headers.Authorization err := rtspAuthHeader.Unmarshal(rt.Header["Authorization"]) if err == nil { + r.User = rtspAuthHeader.Username if rtspAuthHeader.Method == headers.AuthMethodBasic { - r.User = rtspAuthHeader.BasicUser r.Pass = rtspAuthHeader.BasicPass - } else { - r.User = rtspAuthHeader.Username } } } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index cadabc1a..3b710f2a 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -130,7 +130,7 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) { err := m.AuthManager.Authenticate(req) if err != nil { - if err.(*auth.Error).AskCredentials { //nolint:errorlint + if err.(auth.Error).AskCredentials { //nolint:errorlint ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.AbortWithStatus(http.StatusUnauthorized) return diff --git a/internal/playback/server.go b/internal/playback/server.go index 76fdf6fa..c32a0b9e 100644 --- a/internal/playback/server.go +++ b/internal/playback/server.go @@ -126,7 +126,7 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool { err := s.AuthManager.Authenticate(req) if err != nil { - if err.(*auth.Error).AskCredentials { //nolint:errorlint + if err.(auth.Error).AskCredentials { //nolint:errorlint ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Writer.WriteHeader(http.StatusUnauthorized) return false diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go index acf47274..88c88966 100644 --- a/internal/pprof/pprof.go +++ b/internal/pprof/pprof.go @@ -105,7 +105,7 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) { err := pp.AuthManager.Authenticate(req) if err != nil { - if err.(*auth.Error).AskCredentials { //nolint:errorlint + if err.(auth.Error).AskCredentials { //nolint:errorlint ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.AbortWithStatus(http.StatusUnauthorized) return diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index 3e8b1057..c2b7c04b 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -159,7 +159,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) { AccessRequest: req, }) if err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { if terr.AskCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) diff --git a/internal/servers/rtmp/conn.go b/internal/servers/rtmp/conn.go index 4295f8ac..4fff5b0d 100644 --- a/internal/servers/rtmp/conn.go +++ b/internal/servers/rtmp/conn.go @@ -168,7 +168,7 @@ func (c *conn) runRead(conn *rtmp.Conn, u *url.URL) error { }, }) if err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) @@ -235,7 +235,7 @@ func (c *conn) runPublish(conn *rtmp.Conn, u *url.URL) error { }, }) if err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) diff --git a/internal/servers/rtsp/conn.go b/internal/servers/rtsp/conn.go index 85748c36..9fff786b 100644 --- a/internal/servers/rtsp/conn.go +++ b/internal/servers/rtsp/conn.go @@ -10,6 +10,8 @@ import ( "github.com/bluenviron/gortsplib/v4" rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth" "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" "github.com/google/uuid" "github.com/bluenviron/mediamtx/internal/auth" @@ -20,10 +22,6 @@ import ( "github.com/bluenviron/mediamtx/internal/logger" ) -const ( - rtspAuthRealm = "IPCAM" -) - func absoluteURL(req *base.Request, v string) string { if strings.HasPrefix(v, "/") { ur := base.URL{ @@ -37,6 +35,12 @@ func absoluteURL(req *base.Request, v string) string { return v } +func credentialsProvided(req *base.Request) bool { + var auth headers.Authorization + err := auth.Unmarshal(req.Header["Authorization"]) + return err == nil && auth.Username != "" +} + type connParent interface { logger.Writer findSessionByRSessionUnsafe(rsession *gortsplib.ServerSession) *session @@ -59,8 +63,6 @@ type conn struct { uuid uuid.UUID created time.Time onDisconnectHook func() - authNonce string - authFailures int } func (c *conn) initialize() { @@ -135,24 +137,15 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, } ctx.Path = ctx.Path[1:] - if c.authNonce == "" { - var err error - c.authNonce, err = rtspauth.GenerateNonce() - if err != nil { - return &base.Response{ - StatusCode: base.StatusInternalServerError, - }, nil, err - } - } - req := defs.PathAccessRequest{ - Name: ctx.Path, - Query: ctx.Query, - IP: c.ip(), - Proto: auth.ProtocolRTSP, - ID: &c.uuid, - RTSPRequest: ctx.Request, - RTSPNonce: c.authNonce, + Name: ctx.Path, + Query: ctx.Query, + IP: c.ip(), + Proto: auth.ProtocolRTSP, + ID: &c.uuid, + CustomVerifyFunc: func(expectedUser, expectedPass string) bool { + return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass) + }, } req.FillFromRTSPRequest(ctx.Request) @@ -161,10 +154,10 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, }) if res.Err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(res.Err, &terr) { - res, err := c.handleAuthError(terr) - return res, nil, err + res, err2 := c.handleAuthError(ctx.Request) + return res, nil, err2 } var terr2 defs.PathNoOnePublishingError @@ -200,30 +193,17 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, }, stream, nil } -func (c *conn) 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": rtspauth.GenerateWWWAuthenticate(c.authMethods, rtspAuthRealm, c.authNonce), - }, - }, nil +func (c *conn) handleAuthError(req *base.Request) (*base.Response, error) { + if credentialsProvided(req) { + // wait some seconds to mitigate brute force attacks + <-time.After(auth.PauseAfterError) } - // wait some seconds to mitigate brute force attacks - <-time.After(auth.PauseAfterError) - + // let gortsplib decide whether connection should be terminated, + // depending on whether credentials have been provided or not. return &base.Response{ StatusCode: base.StatusUnauthorized, - }, authErr + }, liberrors.ErrServerAuth{} } func (c *conn) apiItem() *defs.APIRTSPConn { diff --git a/internal/servers/rtsp/server.go b/internal/servers/rtsp/server.go index 6229b279..6d1ed7de 100644 --- a/internal/servers/rtsp/server.go +++ b/internal/servers/rtsp/server.go @@ -106,6 +106,7 @@ func (s *Server) Initialize() error { WriteTimeout: time.Duration(s.WriteTimeout), WriteQueueSize: s.WriteQueueSize, RTSPAddress: s.Address, + AuthMethods: s.AuthMethods, } if s.UseUDP { diff --git a/internal/servers/rtsp/server_test.go b/internal/servers/rtsp/server_test.go index c0af9906..b456e074 100644 --- a/internal/servers/rtsp/server_test.go +++ b/internal/servers/rtsp/server_test.go @@ -72,7 +72,7 @@ func TestServerPublish(t *testing.T) { pathManager := &test.PathManager{ AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, error) { if req.AccessRequest.User == "" && req.AccessRequest.Pass == "" { - return nil, &auth.Error{Message: "", AskCredentials: true} + return nil, auth.Error{Message: "", AskCredentials: true} } require.Equal(t, "teststream", req.AccessRequest.Name) require.Equal(t, "param=value", req.AccessRequest.Query) @@ -162,7 +162,7 @@ func TestServerRead(t *testing.T) { pathManager := &test.PathManager{ DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes { if req.AccessRequest.User == "" && req.AccessRequest.Pass == "" { - return defs.PathDescribeRes{Err: &auth.Error{Message: "", AskCredentials: true}} + return defs.PathDescribeRes{Err: auth.Error{Message: "", AskCredentials: true}} } require.Equal(t, "teststream", req.AccessRequest.Name) require.Equal(t, "param=value", req.AccessRequest.Query) @@ -282,7 +282,7 @@ func TestServerRedirect(t *testing.T) { } if req.AccessRequest.User == "" && req.AccessRequest.Pass == "" { - return defs.PathDescribeRes{Err: &auth.Error{Message: "", AskCredentials: true}} + return defs.PathDescribeRes{Err: auth.Error{Message: "", AskCredentials: true}} } require.Equal(t, "path2", req.AccessRequest.Name) diff --git a/internal/servers/rtsp/session.go b/internal/servers/rtsp/session.go index d1d13de3..8ed97c69 100644 --- a/internal/servers/rtsp/session.go +++ b/internal/servers/rtsp/session.go @@ -9,7 +9,6 @@ import ( "time" "github.com/bluenviron/gortsplib/v4" - rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth" "github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/google/uuid" "github.com/pion/rtp" @@ -101,25 +100,16 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) } ctx.Path = ctx.Path[1:] - if c.authNonce == "" { - var err error - c.authNonce, err = rtspauth.GenerateNonce() - if err != nil { - return &base.Response{ - StatusCode: base.StatusInternalServerError, - }, err - } - } - req := defs.PathAccessRequest{ - Name: ctx.Path, - Query: ctx.Query, - Publish: true, - IP: c.ip(), - Proto: auth.ProtocolRTSP, - ID: &c.uuid, - RTSPRequest: ctx.Request, - RTSPNonce: c.authNonce, + Name: ctx.Path, + Query: ctx.Query, + Publish: true, + IP: c.ip(), + Proto: auth.ProtocolRTSP, + ID: &c.uuid, + CustomVerifyFunc: func(expectedUser, expectedPass string) bool { + return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass) + }, } req.FillFromRTSPRequest(ctx.Request) @@ -128,9 +118,9 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) AccessRequest: req, }) if err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { - return c.handleAuthError(terr) + return c.handleAuthError(ctx.Request) } return &base.Response{ @@ -175,24 +165,15 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx, switch s.rsession.State() { case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play - if c.authNonce == "" { - var err error - c.authNonce, err = rtspauth.GenerateNonce() - if err != nil { - return &base.Response{ - StatusCode: base.StatusInternalServerError, - }, nil, err - } - } - req := defs.PathAccessRequest{ - Name: ctx.Path, - Query: ctx.Query, - IP: c.ip(), - Proto: auth.ProtocolRTSP, - ID: &c.uuid, - RTSPRequest: ctx.Request, - RTSPNonce: c.authNonce, + Name: ctx.Path, + Query: ctx.Query, + IP: c.ip(), + Proto: auth.ProtocolRTSP, + ID: &c.uuid, + CustomVerifyFunc: func(expectedUser, expectedPass string) bool { + return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass) + }, } req.FillFromRTSPRequest(ctx.Request) @@ -201,9 +182,9 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx, AccessRequest: req, }) if err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { - res, err2 := c.handleAuthError(terr) + res, err2 := c.handleAuthError(ctx.Request) return res, nil, err2 } diff --git a/internal/servers/srt/conn.go b/internal/servers/srt/conn.go index 80de7068..dc2ec350 100644 --- a/internal/servers/srt/conn.go +++ b/internal/servers/srt/conn.go @@ -151,7 +151,7 @@ func (c *conn) runPublish(streamID *streamID) error { }, }) if err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) @@ -250,7 +250,7 @@ func (c *conn) runRead(streamID *streamID) error { }, }) if err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index 5d96848d..6df4a23f 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -136,7 +136,7 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, AccessRequest: req, }) if err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { if terr.AskCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) @@ -198,7 +198,7 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) httpRequest: ctx.Request, }) if res.err != nil { - var terr *auth.Error + var terr auth.Error if errors.As(err, &terr) { if terr.AskCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)