improve anti-brute force mechanism (#5835)

delay authentication failure responses by a random amount of time, use
the same anti-brute force mechanism with all users.
This commit is contained in:
Alessandro Ros
2026-06-05 17:01:36 +02:00
committed by GitHub
parent 5b9b6afcb8
commit f5d7ed3138
26 changed files with 100 additions and 192 deletions
+1 -1
View File
@@ -17,7 +17,7 @@
<br>
_MediaMTX_ is a ready-to-use and zero-dependency real-time media server and media proxy that allows to publish, read, proxy, record and playback video and audio streams. It has been conceived as a "media router" that routes media streams from one end to the other, with a focus on efficiency and portability.
_MediaMTX_ is a ready-to-use and zero-dependency live media server and media proxy that allows to publish, read, proxy, record and playback real-time video and audio streams. It has been conceived as a "media router" that routes media streams from one end to the other, with a focus on efficiency and portability.
<div align="center">
+1 -1
View File
@@ -2,7 +2,7 @@
Welcome to the MediaMTX documentation!
_MediaMTX_ is a ready-to-use and zero-dependency live media server and media proxy. It has been conceived as a "media router" that routes media streams from one end to the other, with a focus on efficiency and portability.
_MediaMTX_ is a ready-to-use and zero-dependency live media server and media proxy that allows to publish, read, proxy, record and playback real-time video and audio streams. It has been conceived as a "media router" that routes media streams from one end to the other, with a focus on efficiency and portability.
Main features:
+2 -2
View File
@@ -11,7 +11,7 @@ Media-over-QUIC has a wide range of features and variants, most of them in activ
- We support `draft-18` of the [main specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-transport-18).
- We only support using Media-over-QUIC through browsers and in particular through the WebTransport API. We do not support using QUIC directly.
- We support the `PUBLISH` and `SUBSCRIBE` messages only, which are the ones meant to be used by a routing solution like _MediaMTX_.
- We support the `PUBLISH` and `SUBSCRIBE` messages only, which are the ones meant to be used with a routing solution like _MediaMTX_.
- We use the MOQT Streaming Format (MSF) to advertise tracks, described in [this specification](https://datatracker.ietf.org/doc/html/draft-ietf-moq-msf-00).
- We use the Low Overhead Media Container (LOC) to ship frames, described in [this specification](https://datatracker.ietf.org/doc/draft-ietf-moq-loc/).
- We host web pages through a HTTP/2 listener and host the WebTransport endpoint through a HTTP/3 listener. This hybrid setup allows to use self-signed certificates, that are normally forbidden in pure HTTP/3.
@@ -23,7 +23,7 @@ There are some server requirements:
And there are some client (browser) requirements:
- If the server certificate is self-signed, browser must support the [serverCertificatesHashes option](https://caniuse.com/mdn-api_webtransport_webtransport_options_servercertificatehashes_parameter) (all except iOS safari do).
- If the server certificate is self-signed, browser must support the [serverCertificatesHashes option](https://caniuse.com/mdn-api_webtransport_webtransport_options_servercertificatehashes_parameter) (all except iOS Safari do).
- Browser must support [WebTransport](https://caniuse.com/webtransport) and [WebCodecs](https://caniuse.com/webcodecs) (all modern browsers do)
- When publishing tracks, the browser to support [MediaStreamTrackProcessor](https://caniuse.com/mdn-api_mediastreamtrackprocessor) (only Chrome does).
+1 -63
View File
@@ -20,68 +20,6 @@ http://localhost:8888/mystream/index.m3u8
Some clients that can read with HLS are [FFmpeg](08-ffmpeg.md), [GStreamer](09-gstreamer.md), [VLC](10-vlc.md) and [web browsers](07-web-browsers.md).
_MediaMTX_ supports generating HLS in several variants (including Low-Latency mode), and provides various parameters to tune HLS generation. These are listed in the [configuration file](../5-references/1-configuration-file.md):
```yml
# Allow clients to read streams with the HLS protocol.
hls: true
# Address of the HLS listener.
hlsAddress: :8888
# Enable HTTPS on the HLS server.
# This is required for Low-Latency HLS to function correctly on Apple devices.
hlsEncryption: false
# Path to the server key. This is needed only when encryption is yes.
# This can be generated with:
# openssl genrsa -out server.key 2048
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
hlsServerKey: server.key
# Path to the server certificate.
hlsServerCert: server.crt
# Allowed CORS origins.
# Supports wildcards: ['http://*.example.com']
hlsAllowOrigins: ["*"]
# IPs or CIDRs of proxies placed before the HLS server.
# If the server receives a request from one of these entries, IP in logs
# will be taken from the X-Forwarded-For header.
hlsTrustedProxies: []
# By default, HLS is generated only when requested by a user.
# This option allows to generate it always, avoiding the delay between request and generation.
hlsAlwaysRemux: false
# Variant of the HLS protocol to use. Available options are:
# * mpegts - uses MPEG-TS segments, for maximum compatibility.
# * fmp4 - uses fragmented MP4 segments, more efficient.
# * lowLatency - uses Low-Latency HLS.
hlsVariant: lowLatency
# Number of HLS segments to keep on the server.
# Segments allow to seek through the stream.
# Their number doesn't influence latency.
hlsSegmentCount: 7
# Minimum duration of each segment.
# A player usually puts 3 segments in a buffer before reproducing the stream.
# The final segment duration is also influenced by the interval between IDR frames,
# since the server changes the duration in order to include at least one IDR frame
# in each segment.
hlsSegmentDuration: 1s
# Minimum duration of each part.
# A player usually puts 3 parts in a buffer before reproducing the stream.
# Parts are used in Low-Latency HLS in place of segments.
# Part duration is influenced by the distance between video/audio samples
# and is adjusted in order to produce segments with a similar duration.
hlsPartDuration: 200ms
# Maximum size of each segment.
# This prevents RAM exhaustion.
hlsSegmentMaxSize: 50M
# Directory in which to save segments and non-low-latency playlists.
# This has two purposes: offloading RAM and creating a self-consistent directory
# that can be served by a CDN.
hlsDirectory: ""
# The muxer will be closed when there are no
# reader requests and this amount of time has passed.
hlsMuxerCloseAfter: 60s
# Secret to identify requests coming from a CDN.
# The CDN must insert this secret in every request in the
# 'Authorization: Bearer' header.
hlsCDNSecret: ""
```
_MediaMTX_ supports generating HLS in several variants (including Low-Latency mode), and provides various parameters to tune HLS generation. These are listed in the [configuration file](../5-references/1-configuration-file.md).
HLS can also be used to [scale the server](../2-features/20-scalability.md) through a CDN.
+2 -3
View File
@@ -257,6 +257,8 @@ func (a *API) middlewareAuth(ctx *gin.Context) {
_, err := a.AuthManager.Authenticate(req)
if err != nil {
auth.DelayBruteForce(err)
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
a.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
@@ -265,9 +267,6 @@ 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 delay brute force attacks
<-time.After(auth.PauseAfterError)
a.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
+28
View File
@@ -0,0 +1,28 @@
package auth
import (
"crypto/rand"
"errors"
"math/big"
"time"
)
const (
minPause = 0 * time.Second
maxPause = 4 * time.Second
)
// DelayBruteForce delays brute force attacks by waiting some seconds after an authentication error.
func DelayBruteForce(err error) {
if terr, ok := errors.AsType[*Error](err); ok {
if !terr.AskCredentials {
var n *big.Int
n, err = rand.Int(rand.Reader, big.NewInt(int64(maxPause-minPause)))
if err != nil {
<-time.After(maxPause)
return
}
<-time.After(minPause + time.Duration(n.Int64()))
}
}
}
-3
View File
@@ -21,9 +21,6 @@ import (
)
const (
// PauseAfterError is the pause to apply after an authentication failure.
PauseAfterError = 2 * time.Second
maxInboundBodySize = 128 * 1024
jwksRefreshPeriod = 60 * 60 * time.Second
)
+5 -3
View File
@@ -1040,12 +1040,14 @@ func (pa *path) StaticSourceHandlerSetNotReady(
}
// describe is called by a reader or publisher through pathManager.
func (pa *path) describe(req defs.PathDescribeReq) defs.PathDescribeRes {
func (pa *path) describe(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) {
select {
case pa.chDescribe <- req:
return <-req.Res
res := <-req.Res
return &res, res.Err
case <-pa.ctx.Done():
return defs.PathDescribeRes{Err: fmt.Errorf("terminated")}
return nil, fmt.Errorf("terminated")
}
}
+17 -8
View File
@@ -529,7 +529,13 @@ func (pm *pathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFin
select {
case pm.chFindPathConf <- req:
res := <-req.Res
return &res, res.Err
if res.Err != nil {
auth.DelayBruteForce(res.Err)
return nil, res.Err
}
return &res, nil
case <-pm.ctx.Done():
return nil, fmt.Errorf("terminated")
@@ -537,25 +543,26 @@ func (pm *pathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFin
}
// Describe is called by a reader or publisher.
func (pm *pathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes {
func (pm *pathManager) Describe(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) {
req.Res = make(chan defs.PathDescribeRes)
select {
case pm.chDescribe <- req:
res1 := <-req.Res
if res1.Err != nil {
return res1
auth.DelayBruteForce(res1.Err)
return nil, res1.Err
}
res2 := res1.Path.(*path).describe(req)
if res2.Err != nil {
return res2
res2, err := res1.Path.(*path).describe(req)
if err != nil {
return nil, err
}
res2.Path = res1.Path
return res2
return res2, nil
case <-pm.ctx.Done():
return defs.PathDescribeRes{Err: fmt.Errorf("terminated")}
return nil, fmt.Errorf("terminated")
}
}
@@ -566,6 +573,7 @@ func (pm *pathManager) AddPublisher(req defs.PathAddPublisherReq) (*defs.PathAdd
case pm.chAddPublisher <- req:
res1 := <-req.Res
if res1.Err != nil {
auth.DelayBruteForce(res1.Err)
return nil, res1.Err
}
@@ -591,6 +599,7 @@ func (pm *pathManager) AddReader(req defs.PathAddReaderReq) (*defs.PathAddReader
case pm.chAddReader <- req:
res1 := <-req.Res
if res1.Err != nil {
auth.DelayBruteForce(res1.Err)
return nil, res1.Err
}
+3 -3
View File
@@ -58,12 +58,12 @@ func TestPathManagerDynamicPathAutoDeletion(t *testing.T) {
func() {
if ca == "describe" {
res := pm.Describe(defs.PathDescribeReq{
_, err := pm.Describe(defs.PathDescribeReq{
AccessRequest: defs.PathAccessRequest{
Name: "mypath",
},
})
require.EqualError(t, res.Err, "no stream is available on path 'mypath'")
require.EqualError(t, err, "no stream is available on path 'mypath'")
} else {
_, err := pm.AddReader(defs.PathAddReaderReq{
Author: &dummyReader{},
@@ -104,7 +104,7 @@ func TestPathManagerDynamicPathDescribeAndPublish(t *testing.T) {
go func() {
for range 10 {
pm.Describe(defs.PathDescribeReq{
pm.Describe(defs.PathDescribeReq{ //nolint:errcheck
AccessRequest: defs.PathAccessRequest{
Name: "mypath",
},
+2 -3
View File
@@ -198,6 +198,8 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) {
_, err := m.AuthManager.Authenticate(req)
if err != nil {
auth.DelayBruteForce(err)
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
m.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
@@ -206,9 +208,6 @@ 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 delay brute force attacks
<-time.After(auth.PauseAfterError)
m.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
+2 -3
View File
@@ -142,6 +142,8 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
_, err := s.AuthManager.Authenticate(req)
if err != nil {
auth.DelayBruteForce(err)
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
@@ -151,9 +153,6 @@ 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 delay brute force attacks
<-time.After(auth.PauseAfterError)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return false
}
-4
View File
@@ -107,14 +107,10 @@ func TestAuthError(t *testing.T) {
req, err = http.NewRequest(http.MethodGet, u.String(), 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)
+2 -3
View File
@@ -118,6 +118,8 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) {
_, err := pp.AuthManager.Authenticate(req)
if err != nil {
auth.DelayBruteForce(err)
if err.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
pp.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
@@ -126,9 +128,6 @@ 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 delay brute force attacks
<-time.After(auth.PauseAfterError)
pp.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
-12
View File
@@ -224,9 +224,6 @@ 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 delay brute force attacks
<-time.After(auth.PauseAfterError)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
@@ -343,9 +340,6 @@ 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 delay brute force attacks
<-time.After(auth.PauseAfterError)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
@@ -398,9 +392,6 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
create: false,
})
if err != nil {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
@@ -412,9 +403,6 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
sx = muxer.findSession(ctx)
}
if sx == nil {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
-4
View File
@@ -645,14 +645,10 @@ func TestAuthError(t *testing.T) {
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)
-3
View File
@@ -191,9 +191,6 @@ 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 delay brute force attacks
<-time.After(auth.PauseAfterError)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return false
}
-10
View File
@@ -358,11 +358,6 @@ func (s *session) onSubscribeCatalog(wstream *webtransport.Stream, m *controlmes
},
})
if err != nil {
if _, ok := errors.AsType[*auth.Error](err); ok {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
}
var code controlmessage.RequestErrorCode
if _, ok := errors.AsType[*auth.Error](err); ok {
code = controlmessage.RequestErrorCodeUnauthorized
@@ -595,11 +590,6 @@ func (s *session) onPublishCatalog(wstream *webtransport.Stream, m *controlmessa
},
})
if err != nil {
if _, ok := errors.AsType[*auth.Error](err); ok {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
}
var code controlmessage.RequestErrorCode
if _, ok := errors.AsType[*auth.Error](err); ok {
code = controlmessage.RequestErrorCodeUnauthorized
-10
View File
@@ -171,11 +171,6 @@ func (c *conn) runRead() error {
},
})
if err != nil {
if terr, ok := errors.AsType[*auth.Error](err); ok {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
return terr
}
return err
}
@@ -265,11 +260,6 @@ func (c *conn) runPublish() error {
},
})
if err != nil {
if terr, ok := errors.AsType[*auth.Error](err); ok {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
return terr
}
return err
}
+6 -10
View File
@@ -151,7 +151,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
}
}
res := c.pathManager.Describe(defs.PathDescribeReq{
res, err := c.pathManager.Describe(defs.PathDescribeReq{
AccessRequest: defs.PathAccessRequest{
Name: ctx.Path,
Query: ctx.Query,
@@ -162,22 +162,21 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
CustomVerifyFunc: customVerifyFunc,
},
})
if res.Err != nil {
if terr, ok := errors.AsType[*auth.Error](res.Err); ok {
if err != nil {
if terr, ok := errors.AsType[*auth.Error](err); ok {
res, err2 := c.handleAuthError(terr)
return res, nil, err2
}
if _, ok := errors.AsType[*defs.PathNoStreamAvailableError](res.Err); ok {
if _, ok := errors.AsType[*defs.PathNoStreamAvailableError](err); ok {
return &base.Response{
StatusCode: base.StatusNotFound,
}, nil, res.Err
}, nil, err
}
return &base.Response{
StatusCode: base.StatusBadRequest,
}, nil, res.Err
}, nil, err
}
if res.Redirect != "" {
@@ -208,9 +207,6 @@ func (c *conn) handleAuthError(err *auth.Error) (*base.Response, error) {
}, liberrors.ErrServerAuth{}
}
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
return &base.Response{
StatusCode: base.StatusUnauthorized,
}, err
+1 -1
View File
@@ -77,7 +77,7 @@ type serverMetrics interface {
type serverPathManager interface {
FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)
Describe(req defs.PathDescribeReq) defs.PathDescribeRes
Describe(req defs.PathDescribeReq) (*defs.PathDescribeRes, error)
AddPublisher(_ defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error)
AddReader(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)
}
+16 -17
View File
@@ -376,7 +376,7 @@ func TestServerRead(t *testing.T) {
n := 0
pathManager := &test.PathManager{
DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {
DescribeImpl: func(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "param=value", req.AccessRequest.Query)
@@ -384,7 +384,7 @@ func TestServerRead(t *testing.T) {
require.Nil(t, req.AccessRequest.CustomVerifyFunc)
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}}
return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}
}
require.Equal(t, "myuser", req.AccessRequest.Credentials.User)
@@ -394,16 +394,15 @@ func TestServerRead(t *testing.T) {
if n == 0 {
require.False(t, ok)
n++
return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}}
return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}
}
require.True(t, ok)
}
return defs.PathDescribeRes{
return &defs.PathDescribeRes{
Path: &dummyPath{},
Stream: strm,
Err: nil,
}
}, nil
},
AddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {
require.Equal(t, "teststream", req.AccessRequest.Name)
@@ -552,20 +551,20 @@ func TestServerRedirect(t *testing.T) {
require.NoError(t, err)
pathManager := &test.PathManager{
DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {
DescribeImpl: func(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) {
if req.AccessRequest.Name == "path1" {
if ca == "relative" {
return defs.PathDescribeRes{
return &defs.PathDescribeRes{
Redirect: "/path2",
}
}, nil
}
return defs.PathDescribeRes{
return &defs.PathDescribeRes{
Redirect: "rtsp://localhost:8557/path2",
}
}, nil
}
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}}
return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}
}
require.Equal(t, "path2", req.AccessRequest.Name)
@@ -573,10 +572,10 @@ func TestServerRedirect(t *testing.T) {
require.Equal(t, "myuser", req.AccessRequest.Credentials.User)
require.Equal(t, "mypass", req.AccessRequest.Credentials.Pass)
return defs.PathDescribeRes{
return &defs.PathDescribeRes{
Path: &dummyPath{},
Stream: strm,
}
}, nil
},
}
@@ -616,12 +615,12 @@ func TestServerRedirect(t *testing.T) {
func TestAuthError(t *testing.T) {
pathManager := &test.PathManager{
DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {
DescribeImpl: func(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) {
if req.AccessRequest.Credentials.User == "" && req.AccessRequest.Credentials.Pass == "" {
return defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}}
return nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("auth error")}
}
return defs.PathDescribeRes{Err: &auth.Error{Wrapped: fmt.Errorf("auth error")}}
return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")}
},
}
-4
View File
@@ -149,8 +149,6 @@ func (c *conn) runPublish(streamID *streamID) error {
})
if err != nil {
if terr, ok := errors.AsType[*auth.Error](err); ok {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER)
return terr
}
@@ -275,8 +273,6 @@ func (c *conn) runRead(streamID *streamID) error {
})
if err != nil {
if terr, ok := errors.AsType[*auth.Error](err); ok {
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
c.connReq.Reject(srt.REJ_PEER)
return terr
}
-9
View File
@@ -158,9 +158,6 @@ 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 delay brute force attacks
<-time.After(auth.PauseAfterError)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return false
}
@@ -223,12 +220,6 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
// wait some seconds to delay brute force attacks
<-time.After(auth.PauseAfterError)
s.writeErrorNoLog(ctx, http.StatusUnauthorized, fmt.Errorf("authentication error"))
return
}
+9 -10
View File
@@ -10,6 +10,7 @@ import (
"net/url"
"reflect"
"regexp"
"sync/atomic"
"testing"
"time"
@@ -1160,7 +1161,7 @@ func TestAuthError(t *testing.T) {
"whip post",
} {
t.Run(ca, func(t *testing.T) {
authFailed := false
var authFailed atomic.Bool
s := &Server{
Address: "127.0.0.1:8886",
@@ -1175,11 +1176,13 @@ func TestAuthError(t *testing.T) {
return nil, &auth.Error{Wrapped: fmt.Errorf("auth error")}
},
},
Parent: test.Logger(func(l logger.Level, s string, i ...any) {
if l == logger.Info {
if regexp.MustCompile("failed to authenticate: auth error$").MatchString(fmt.Sprintf(s, i...)) {
authFailed = true
Parent: test.Logger(func(_ logger.Level, s string, i ...any) {
if ca == "whip post" {
if regexp.MustCompile("authentication failed: auth error$").MatchString(fmt.Sprintf(s, i...)) {
authFailed.Store(true)
}
} else if regexp.MustCompile("failed to authenticate: auth error$").MatchString(fmt.Sprintf(s, i...)) {
authFailed.Store(true)
}
}),
}
@@ -1250,17 +1253,13 @@ func TestAuthError(t *testing.T) {
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.True(t, authFailed)
require.True(t, authFailed.Load())
})
}
}
+2 -2
View File
@@ -7,7 +7,7 @@ import (
// PathManager is a dummy path manager.
type PathManager struct {
FindPathConfImpl func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)
DescribeImpl func(req defs.PathDescribeReq) defs.PathDescribeRes
DescribeImpl func(req defs.PathDescribeReq) (*defs.PathDescribeRes, error)
AddPublisherImpl func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error)
AddReaderImpl func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)
}
@@ -18,7 +18,7 @@ func (pm *PathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFin
}
// Describe implements PathManager.
func (pm *PathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes {
func (pm *PathManager) Describe(req defs.PathDescribeReq) (*defs.PathDescribeRes, error) {
return pm.DescribeImpl(req)
}