rtsp: log authentication failure reason (#4641) (#5017)

This commit is contained in:
Alessandro Ros
2025-09-23 10:18:13 +02:00
committed by GitHub
parent 5240bcb8ff
commit f987695d9d
13 changed files with 199 additions and 53 deletions

View File

@@ -260,7 +260,7 @@ func (a *API) middlewareAuth(ctx *gin.Context) {
a.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatus(http.StatusUnauthorized)

View File

@@ -164,7 +164,7 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) {
m.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatus(http.StatusUnauthorized)

View File

@@ -132,7 +132,7 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
s.Log(logger.Info, "connection %v failed to authenticate: %v",
httpp.RemoteAddr(ctx), err.Wrapped)
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.Writer.WriteHeader(http.StatusUnauthorized)

View File

@@ -110,7 +110,7 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) {
pp.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), err.Wrapped)
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.AbortWithStatus(http.StatusUnauthorized)

View File

@@ -164,7 +164,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
ctx.Writer.WriteHeader(http.StatusUnauthorized)

View File

@@ -13,9 +13,11 @@ import (
"github.com/bluenviron/gohlslib/v2/pkg/codecs"
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/bluenviron/mediamtx/internal/unit"
@@ -500,3 +502,66 @@ func TestServerDynamicAlwaysRemux(t *testing.T) {
<-done
}
func TestAuthError(t *testing.T) {
n := 0
s := &Server{
Address: "127.0.0.1:8888",
Encryption: false,
ServerKey: "",
ServerCert: "",
AlwaysRemux: true,
Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),
SegmentCount: 7,
SegmentDuration: conf.Duration(1 * time.Second),
PartDuration: conf.Duration(200 * time.Millisecond),
SegmentMaxSize: 50 * 1024 * 1024,
ReadTimeout: conf.Duration(10 * time.Second),
PathManager: &dummyPathManager{
findPathConfImpl: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
return nil, &auth.Error{AskCredentials: true}
}
return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")}
},
},
Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
if l == logger.Info {
if n == 1 {
require.Regexp(t, "failed to authenticate: auth error$", fmt.Sprintf(s, i...))
}
n++
}
}),
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close()
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8888/stream/index.m3u8", nil)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
require.Equal(t, `Basic realm="mediamtx"`, res.Header.Get("WWW-Authenticate"))
req, err = http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8888/stream/index.m3u8", nil)
require.NoError(t, err)
start := time.Now()
res, err = http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Greater(t, time.Since(start), 2*time.Second)
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
require.Equal(t, 2, n)
}

View File

@@ -168,7 +168,7 @@ func (c *conn) runRead() error {
if err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
return terr
}
@@ -256,7 +256,7 @@ func (c *conn) runPublish() error {
if err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
return terr
}

View File

@@ -11,7 +11,6 @@ import (
"github.com/bluenviron/gortsplib/v5"
rtspauth "github.com/bluenviron/gortsplib/v5/pkg/auth"
"github.com/bluenviron/gortsplib/v5/pkg/base"
"github.com/bluenviron/gortsplib/v5/pkg/headers"
"github.com/bluenviron/gortsplib/v5/pkg/liberrors"
"github.com/google/uuid"
@@ -37,12 +36,6 @@ 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 != ""
}
func tunnelLabel(t gortsplib.Tunnel) string {
switch t {
case gortsplib.TunnelHTTP:
@@ -175,7 +168,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
if res.Err != nil {
var terr *auth.Error
if errors.As(res.Err, &terr) {
res, err2 := c.handleAuthError(ctx.Request)
res, err2 := c.handleAuthError(terr)
return res, nil, err2
}
@@ -212,17 +205,19 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
}, stream, 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)
func (c *conn) handleAuthError(err *auth.Error) (*base.Response, error) {
if err.AskCredentials {
return &base.Response{
StatusCode: base.StatusUnauthorized,
}, liberrors.ErrServerAuth{}
}
// let gortsplib decide whether connection should be terminated,
// depending on whether credentials have been provided or not.
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
return &base.Response{
StatusCode: base.StatusUnauthorized,
}, liberrors.ErrServerAuth{}
}, err
}
func (c *conn) apiItem() *defs.APIRTSPConn {

View File

@@ -1,6 +1,8 @@
package rtsp
import (
"fmt"
"sync/atomic"
"testing"
"time"
@@ -13,6 +15,7 @@ import (
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/test"
"github.com/bluenviron/mediamtx/internal/unit"
@@ -393,3 +396,54 @@ func TestServerRedirect(t *testing.T) {
})
}
}
func TestAuthError(t *testing.T) {
pathManager := &test.PathManager{
DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true}}
}
return defs.PathDescribeRes{Err: &auth.Error{Wrapped: fmt.Errorf("auth error")}}
},
}
n := new(int64)
done := make(chan struct{})
s := &Server{
Address: "127.0.0.1:8557",
ReadTimeout: conf.Duration(10 * time.Second),
WriteTimeout: conf.Duration(10 * time.Second),
WriteQueueSize: 512,
PathManager: pathManager,
Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
if l == logger.Info {
if atomic.AddInt64(n, 1) == 3 {
require.Regexp(t, "authentication failed: auth error$", fmt.Sprintf(s, i...))
close(done)
}
}
}),
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close()
u, err := base.ParseURL("rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value")
require.NoError(t, err)
reader := gortsplib.Client{
Scheme: u.Scheme,
Host: u.Host,
}
err = reader.Start()
require.NoError(t, err)
defer reader.Close()
_, _, err = reader.Describe(u)
require.EqualError(t, err, "bad status code: 401 (Unauthorized)")
<-done
}

View File

@@ -179,7 +179,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
if err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
return c.handleAuthError(ctx.Request)
return c.handleAuthError(terr)
}
return &base.Response{
@@ -242,7 +242,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
if err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
res, err2 := c.handleAuthError(ctx.Request)
res, err2 := c.handleAuthError(terr)
return res, nil, err2
}

View File

@@ -148,7 +148,7 @@ func (c *conn) runPublish(streamID *streamID) error {
if err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER)
return terr
@@ -272,7 +272,7 @@ func (c *conn) runRead(streamID *streamID) error {
if err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER)
return terr

View File

@@ -141,7 +141,7 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string,
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
writeError(ctx, http.StatusUnauthorized, terr)
@@ -203,7 +203,7 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
// wait some seconds to mitigate brute force attacks
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
writeError(ctx, http.StatusUnauthorized, terr)

View File

@@ -3,6 +3,7 @@ package webrtc
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
@@ -12,9 +13,11 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/protocols/webrtc"
"github.com/bluenviron/mediamtx/internal/protocols/whip"
"github.com/bluenviron/mediamtx/internal/stream"
@@ -63,9 +66,6 @@ func initializeTestServer(t *testing.T) *Server {
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "*",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second),
@@ -78,7 +78,6 @@ func initializeTestServer(t *testing.T) *Server {
HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
@@ -148,10 +147,6 @@ func TestServerOptionsICEServer(t *testing.T) {
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
@@ -167,7 +162,6 @@ func TestServerOptionsICEServer(t *testing.T) {
HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager,
Parent: test.NilLogger,
}
@@ -234,10 +228,6 @@ func TestServerPublish(t *testing.T) {
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
@@ -249,7 +239,6 @@ func TestServerPublish(t *testing.T) {
HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager,
Parent: test.NilLogger,
}
@@ -523,11 +512,6 @@ func TestServerRead(t *testing.T) {
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
@@ -538,7 +522,6 @@ func TestServerRead(t *testing.T) {
HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager,
Parent: test.NilLogger,
}
@@ -612,10 +595,6 @@ func TestServerReadNotFound(t *testing.T) {
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
@@ -627,7 +606,6 @@ func TestServerReadNotFound(t *testing.T) {
HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
@@ -754,3 +732,57 @@ func TestICEServerClientOnly(t *testing.T) {
require.NoError(t, err)
require.Empty(t, serverICEServers)
}
func TestAuthError(t *testing.T) {
n := 0
s := &Server{
Address: "127.0.0.1:8886",
ReadTimeout: conf.Duration(10 * time.Second),
PathManager: &test.PathManager{
FindPathConfImpl: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
return nil, &auth.Error{AskCredentials: true}
}
return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")}
},
},
Parent: test.Logger(func(l logger.Level, s string, i ...interface{}) {
if l == logger.Info {
if n == 1 {
require.Regexp(t, "failed to authenticate: auth error$", fmt.Sprintf(s, i...))
}
n++
}
}),
}
err := s.Initialize()
require.NoError(t, err)
defer s.Close()
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8886/stream/publish", nil)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
require.Equal(t, `Basic realm="mediamtx"`, res.Header.Get("WWW-Authenticate"))
req, err = http.NewRequest(http.MethodGet, "http://myuser:mypass@127.0.0.1:8886/stream/publish", nil)
require.NoError(t, err)
start := time.Now()
res, err = http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Greater(t, time.Since(start), 2*time.Second)
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
require.Equal(t, 2, n)
}