diff --git a/internal/api/api.go b/internal/api/api.go index 1c9a546b..181a10be 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -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) diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 2b785488..c1735792 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -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) diff --git a/internal/playback/server.go b/internal/playback/server.go index d67330d7..a3f11fc6 100644 --- a/internal/playback/server.go +++ b/internal/playback/server.go @@ -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) diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go index 12d65173..90dac014 100644 --- a/internal/pprof/pprof.go +++ b/internal/pprof/pprof.go @@ -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) diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index fb053a16..2dd5ccba 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -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) diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go index 1244616d..75bd0e72 100644 --- a/internal/servers/hls/server_test.go +++ b/internal/servers/hls/server_test.go @@ -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) +} diff --git a/internal/servers/rtmp/conn.go b/internal/servers/rtmp/conn.go index f710d92b..9d511d76 100644 --- a/internal/servers/rtmp/conn.go +++ b/internal/servers/rtmp/conn.go @@ -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 } diff --git a/internal/servers/rtsp/conn.go b/internal/servers/rtsp/conn.go index 550762a2..9d3e51e4 100644 --- a/internal/servers/rtsp/conn.go +++ b/internal/servers/rtsp/conn.go @@ -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 { diff --git a/internal/servers/rtsp/server_test.go b/internal/servers/rtsp/server_test.go index cb7fac3c..fded66bc 100644 --- a/internal/servers/rtsp/server_test.go +++ b/internal/servers/rtsp/server_test.go @@ -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 +} diff --git a/internal/servers/rtsp/session.go b/internal/servers/rtsp/session.go index 2ccda113..03f2376a 100644 --- a/internal/servers/rtsp/session.go +++ b/internal/servers/rtsp/session.go @@ -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 } diff --git a/internal/servers/srt/conn.go b/internal/servers/srt/conn.go index f9130ef1..a5498eba 100644 --- a/internal/servers/srt/conn.go +++ b/internal/servers/srt/conn.go @@ -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 diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index ee22f2f5..ca96253b 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -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) diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 3e3b5f8a..ed59bdd1 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -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) +}