mirror of
https://github.com/bluenviron/mediamtx.git
synced 2026-07-04 15:07:51 +00:00
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:
@@ -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">
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user