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) 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) <-time.After(auth.PauseAfterError)
ctx.AbortWithStatus(http.StatusUnauthorized) 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) 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) <-time.After(auth.PauseAfterError)
ctx.AbortWithStatus(http.StatusUnauthorized) 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", s.Log(logger.Info, "connection %v failed to authenticate: %v",
httpp.RemoteAddr(ctx), err.Wrapped) 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) <-time.After(auth.PauseAfterError)
ctx.Writer.WriteHeader(http.StatusUnauthorized) 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) 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) <-time.After(auth.PauseAfterError)
ctx.AbortWithStatus(http.StatusUnauthorized) 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) 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) <-time.After(auth.PauseAfterError)
ctx.Writer.WriteHeader(http.StatusUnauthorized) ctx.Writer.WriteHeader(http.StatusUnauthorized)

View File

@@ -13,9 +13,11 @@ import (
"github.com/bluenviron/gohlslib/v2/pkg/codecs" "github.com/bluenviron/gohlslib/v2/pkg/codecs"
"github.com/bluenviron/gortsplib/v5/pkg/description" "github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" "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/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/test" "github.com/bluenviron/mediamtx/internal/test"
"github.com/bluenviron/mediamtx/internal/unit" "github.com/bluenviron/mediamtx/internal/unit"
@@ -500,3 +502,66 @@ func TestServerDynamicAlwaysRemux(t *testing.T) {
<-done <-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 { if err != nil {
var terr *auth.Error var terr *auth.Error
if errors.As(err, &terr) { 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) <-time.After(auth.PauseAfterError)
return terr return terr
} }
@@ -256,7 +256,7 @@ func (c *conn) runPublish() error {
if err != nil { if err != nil {
var terr *auth.Error var terr *auth.Error
if errors.As(err, &terr) { 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) <-time.After(auth.PauseAfterError)
return terr return terr
} }

View File

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

View File

@@ -1,6 +1,8 @@
package rtsp package rtsp
import ( import (
"fmt"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -13,6 +15,7 @@ import (
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/externalcmd"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
"github.com/bluenviron/mediamtx/internal/test" "github.com/bluenviron/mediamtx/internal/test"
"github.com/bluenviron/mediamtx/internal/unit" "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 { if err != nil {
var terr *auth.Error var terr *auth.Error
if errors.As(err, &terr) { if errors.As(err, &terr) {
return c.handleAuthError(ctx.Request) return c.handleAuthError(terr)
} }
return &base.Response{ return &base.Response{
@@ -242,7 +242,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
if err != nil { if err != nil {
var terr *auth.Error var terr *auth.Error
if errors.As(err, &terr) { if errors.As(err, &terr) {
res, err2 := c.handleAuthError(ctx.Request) res, err2 := c.handleAuthError(terr)
return res, nil, err2 return res, nil, err2
} }

View File

@@ -148,7 +148,7 @@ func (c *conn) runPublish(streamID *streamID) error {
if err != nil { if err != nil {
var terr *auth.Error var terr *auth.Error
if errors.As(err, &terr) { 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) <-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER) c.connReq.Reject(srt.REJ_PEER)
return terr return terr
@@ -272,7 +272,7 @@ func (c *conn) runRead(streamID *streamID) error {
if err != nil { if err != nil {
var terr *auth.Error var terr *auth.Error
if errors.As(err, &terr) { 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) <-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER) c.connReq.Reject(srt.REJ_PEER)
return terr 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) 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) <-time.After(auth.PauseAfterError)
writeError(ctx, http.StatusUnauthorized, terr) 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) 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) <-time.After(auth.PauseAfterError)
writeError(ctx, http.StatusUnauthorized, terr) writeError(ctx, http.StatusUnauthorized, terr)

View File

@@ -3,6 +3,7 @@ package webrtc
import ( import (
"bytes" "bytes"
"context" "context"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@@ -12,9 +13,11 @@ import (
"github.com/bluenviron/gortsplib/v5/pkg/description" "github.com/bluenviron/gortsplib/v5/pkg/description"
"github.com/bluenviron/gortsplib/v5/pkg/format" "github.com/bluenviron/gortsplib/v5/pkg/format"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/externalcmd" "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/webrtc"
"github.com/bluenviron/mediamtx/internal/protocols/whip" "github.com/bluenviron/mediamtx/internal/protocols/whip"
"github.com/bluenviron/mediamtx/internal/stream" "github.com/bluenviron/mediamtx/internal/stream"
@@ -63,9 +66,6 @@ func initializeTestServer(t *testing.T) *Server {
s := &Server{ s := &Server{
Address: "127.0.0.1:8886", Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "*", AllowOrigin: "*",
TrustedProxies: conf.IPNetworks{}, TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second), ReadTimeout: conf.Duration(10 * time.Second),
@@ -78,7 +78,6 @@ func initializeTestServer(t *testing.T) *Server {
HandshakeTimeout: conf.Duration(10 * time.Second), HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second), TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second), STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pm, PathManager: pm,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
@@ -148,10 +147,6 @@ func TestServerOptionsICEServer(t *testing.T) {
s := &Server{ s := &Server{
Address: "127.0.0.1:8886", Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{}, TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second), ReadTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887", LocalUDPAddress: "127.0.0.1:8887",
@@ -167,7 +162,6 @@ func TestServerOptionsICEServer(t *testing.T) {
HandshakeTimeout: conf.Duration(10 * time.Second), HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second), TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second), STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager, PathManager: pathManager,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
@@ -234,10 +228,6 @@ func TestServerPublish(t *testing.T) {
s := &Server{ s := &Server{
Address: "127.0.0.1:8886", Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{}, TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second), ReadTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887", LocalUDPAddress: "127.0.0.1:8887",
@@ -249,7 +239,6 @@ func TestServerPublish(t *testing.T) {
HandshakeTimeout: conf.Duration(10 * time.Second), HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second), TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second), STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager, PathManager: pathManager,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
@@ -523,11 +512,6 @@ func TestServerRead(t *testing.T) {
s := &Server{ s := &Server{
Address: "127.0.0.1:8886", Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second), ReadTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887", LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "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), HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second), TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second), STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pathManager, PathManager: pathManager,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
@@ -612,10 +595,6 @@ func TestServerReadNotFound(t *testing.T) {
s := &Server{ s := &Server{
Address: "127.0.0.1:8886", Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{}, TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.Duration(10 * time.Second), ReadTimeout: conf.Duration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887", LocalUDPAddress: "127.0.0.1:8887",
@@ -627,7 +606,6 @@ func TestServerReadNotFound(t *testing.T) {
HandshakeTimeout: conf.Duration(10 * time.Second), HandshakeTimeout: conf.Duration(10 * time.Second),
TrackGatherTimeout: conf.Duration(2 * time.Second), TrackGatherTimeout: conf.Duration(2 * time.Second),
STUNGatherTimeout: conf.Duration(5 * time.Second), STUNGatherTimeout: conf.Duration(5 * time.Second),
ExternalCmdPool: nil,
PathManager: pm, PathManager: pm,
Parent: test.NilLogger, Parent: test.NilLogger,
} }
@@ -754,3 +732,57 @@ func TestICEServerClientOnly(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Empty(t, serverICEServers) 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)
}