support using JWT in Authorization header with API, Metrics, PProf (#3630) (#3795)

This commit is contained in:
Alessandro Ros
2024-10-05 21:15:21 +02:00
committed by GitHub
parent 4b9d3ceb89
commit 534b637bc7
24 changed files with 275 additions and 515 deletions

View File

@@ -1188,7 +1188,7 @@ The JWT is expected to contain a claim, with a list of permissions in the same f
} }
``` ```
Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of all other protocols), for instance: Clients are expected to pass the JWT in the Authorization header (in case of HLS, WebRTC and all web-based features) or in query parameters (in case of all other protocols), for instance:
``` ```
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT

View File

@@ -284,17 +284,13 @@ func (a *API) middlewareOrigin(ctx *gin.Context) {
} }
func (a *API) middlewareAuth(ctx *gin.Context) { func (a *API) middlewareAuth(ctx *gin.Context) {
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := a.AuthManager.Authenticate(&auth.Request{ err := a.AuthManager.Authenticate(&auth.Request{
User: user, IP: net.ParseIP(ctx.ClientIP()),
Pass: pass, Action: conf.AuthActionAPI,
Query: ctx.Request.URL.RawQuery, HTTPRequest: ctx.Request,
IP: net.ParseIP(ctx.ClientIP()),
Action: conf.AuthActionAPI,
}) })
if err != nil { if err != nil {
if !hasCredentials { if err.(*auth.Error).AskCredentials { //nolint:errorlint
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatus(http.StatusUnauthorized) ctx.AbortWithStatus(http.StatusUnauthorized)
return return

View File

@@ -11,7 +11,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/test" "github.com/bluenviron/mediamtx/internal/test"
@@ -111,40 +110,6 @@ func TestPreflightRequest(t *testing.T) {
require.Equal(t, byts, []byte{}) require.Equal(t, byts, []byte{})
} }
func TestConfigAuth(t *testing.T) {
cnf := tempConf(t, "api: yes\n")
api := API{
Address: "localhost:9997",
ReadTimeout: conf.StringDuration(10 * time.Second),
Conf: cnf,
AuthManager: &test.AuthManager{
Func: func(req *auth.Request) error {
require.Equal(t, &auth.Request{
User: "myuser",
Pass: "mypass",
IP: req.IP,
Action: "api",
Query: "key=val",
}, req)
return nil
},
},
Parent: &testParent{},
}
err := api.Initialize()
require.NoError(t, err)
defer api.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
var out map[string]interface{}
httpRequest(t, hc, http.MethodGet, "http://myuser:mypass@localhost:9997/v3/config/global/get?key=val", nil, &out)
require.Equal(t, true, out["api"])
}
func TestConfigGlobalGet(t *testing.T) { func TestConfigGlobalGet(t *testing.T) {
cnf := tempConf(t, "api: yes\n") cnf := tempConf(t, "api: yes\n")

View File

@@ -31,6 +31,17 @@ const (
jwtRefreshPeriod = 60 * 60 * time.Second jwtRefreshPeriod = 60 * 60 * time.Second
) )
func addJWTFromAuthorization(rawQuery string, auth string) string {
jwt := strings.TrimPrefix(auth, "Bearer ")
if rawQuery != "" {
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
v.Set("jwt", jwt)
return v.Encode()
}
}
return url.Values{"jwt": []string{jwt}}.Encode()
}
// Protocol is a protocol. // Protocol is a protocol.
type Protocol string type Protocol string
@@ -51,21 +62,27 @@ type Request struct {
Action conf.AuthAction Action conf.AuthAction
// only for ActionPublish, ActionRead, ActionPlayback // only for ActionPublish, ActionRead, ActionPlayback
Path string Path string
Protocol Protocol Protocol Protocol
ID *uuid.UUID ID *uuid.UUID
Query string Query string
// RTSP only
RTSPRequest *base.Request RTSPRequest *base.Request
RTSPNonce string RTSPNonce string
// HTTP only
HTTPRequest *http.Request
} }
// Error is a authentication error. // Error is a authentication error.
type Error struct { type Error struct {
Message string Message string
AskCredentials bool
} }
// Error implements the error interface. // Error implements the error interface.
func (e Error) Error() string { func (e *Error) Error() string {
return "authentication failed: " + e.Message return "authentication failed: " + e.Message
} }
@@ -154,15 +171,6 @@ func (m *Manager) ReloadInternalUsers(u []conf.AuthInternalUser) {
// Authenticate authenticates a request. // Authenticate authenticates a request.
func (m *Manager) Authenticate(req *Request) error { func (m *Manager) Authenticate(req *Request) error {
err := m.authenticateInner(req)
if err != nil {
return Error{Message: err.Error()}
}
return nil
}
func (m *Manager) authenticateInner(req *Request) error {
// if this is a RTSP request, fill username and password
var rtspAuthHeader headers.Authorization var rtspAuthHeader headers.Authorization
if req.RTSPRequest != nil { if req.RTSPRequest != nil {
@@ -175,18 +183,42 @@ func (m *Manager) authenticateInner(req *Request) error {
req.User = rtspAuthHeader.Username req.User = rtspAuthHeader.Username
} }
} }
} else if req.HTTPRequest != nil {
req.User, req.Pass, _ = req.HTTPRequest.BasicAuth()
req.Query = req.HTTPRequest.URL.RawQuery
if h := req.HTTPRequest.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// support passing username and password through Authorization header
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
req.User = parts[0]
req.Pass = parts[1]
} else {
req.Query = addJWTFromAuthorization(req.Query, h)
}
}
} }
var err error
switch m.Method { switch m.Method {
case conf.AuthMethodInternal: case conf.AuthMethodInternal:
return m.authenticateInternal(req, &rtspAuthHeader) err = m.authenticateInternal(req, &rtspAuthHeader)
case conf.AuthMethodHTTP: case conf.AuthMethodHTTP:
return m.authenticateHTTP(req) err = m.authenticateHTTP(req)
default: default:
return m.authenticateJWT(req) err = m.authenticateJWT(req)
} }
if err != nil {
return &Error{
Message: err.Error(),
AskCredentials: (req.User == "" && req.Pass == ""),
}
}
return nil
} }
func (m *Manager) authenticateInternal(req *Request, rtspAuthHeader *headers.Authorization) error { func (m *Manager) authenticateInternal(req *Request, rtspAuthHeader *headers.Authorization) error {

View File

@@ -7,6 +7,7 @@ import (
"encoding/json" "encoding/json"
"net" "net"
"net/http" "net/http"
"net/url"
"testing" "testing"
"time" "time"
@@ -186,6 +187,37 @@ func TestAuthInternalRTSPDigest(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
func TestAuthInternalCredentialsInBearer(t *testing.T) {
m := Manager{
Method: conf.AuthMethodInternal,
InternalUsers: []conf.AuthInternalUser{
{
User: "myuser",
Pass: "mypass",
IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")},
Permissions: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}},
},
},
HTTPAddress: "",
RTSPAuthMethods: []auth.ValidateMethod{auth.ValidateMethodDigestMD5},
}
err := m.Authenticate(&Request{
IP: net.ParseIP("127.1.1.1"),
Action: conf.AuthActionPublish,
Path: "mypath",
Protocol: ProtocolRTSP,
HTTPRequest: &http.Request{
Header: http.Header{"Authorization": []string{"Bearer myuser:mypass"}},
URL: &url.URL{},
},
})
require.NoError(t, err)
}
func TestAuthHTTP(t *testing.T) { func TestAuthHTTP(t *testing.T) {
for _, outcome := range []string{"ok", "fail"} { for _, outcome := range []string{"ok", "fail"} {
t.Run(outcome, func(t *testing.T) { t.Run(outcome, func(t *testing.T) {
@@ -292,78 +324,93 @@ func TestAuthJWT(t *testing.T) {
// taken from // taken from
// https://github.com/MicahParks/jwkset/blob/master/examples/http_server/main.go // https://github.com/MicahParks/jwkset/blob/master/examples/http_server/main.go
key, err := rsa.GenerateKey(rand.Reader, 1024) for _, ca := range []string{"query", "auth header"} {
require.NoError(t, err) t.Run(ca, func(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 1024)
require.NoError(t, err)
jwk, err := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{ jwk, err := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{
Metadata: jwkset.JWKMetadataOptions{ Metadata: jwkset.JWKMetadataOptions{
KID: "test-key-id", KID: "test-key-id",
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
jwkSet := jwkset.NewMemoryStorage() jwkSet := jwkset.NewMemoryStorage()
err = jwkSet.KeyWrite(context.Background(), jwk) err = jwkSet.KeyWrite(context.Background(), jwk)
require.NoError(t, err) require.NoError(t, err)
httpServ := &http.Server{ httpServ := &http.Server{
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response, err2 := jwkSet.JSONPublic(r.Context()) response, err2 := jwkSet.JSONPublic(r.Context())
if err2 != nil { if err2 != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
return return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(response)
}),
} }
w.Header().Set("Content-Type", "application/json") ln, err := net.Listen("tcp", "localhost:4567")
_, _ = w.Write(response) require.NoError(t, err)
}),
go httpServ.Serve(ln)
defer httpServ.Shutdown(context.Background())
type customClaims struct {
jwt.RegisteredClaims
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"my_permission_key"`
}
claims := customClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "test",
Subject: "somebody",
ID: "1",
},
MediaMTXPermissions: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header[jwkset.HeaderKID] = "test-key-id"
ss, err := token.SignedString(key)
require.NoError(t, err)
m := Manager{
Method: conf.AuthMethodJWT,
JWTJWKS: "http://localhost:4567/jwks",
JWTClaimKey: "my_permission_key",
}
if ca == "query" {
err = m.Authenticate(&Request{
IP: net.ParseIP("127.0.0.1"),
Action: conf.AuthActionPublish,
Path: "mypath",
Protocol: ProtocolRTSP,
Query: "param=value&jwt=" + ss,
})
} else {
err = m.Authenticate(&Request{
IP: net.ParseIP("127.0.0.1"),
Action: conf.AuthActionPublish,
Path: "mypath",
Protocol: ProtocolWebRTC,
HTTPRequest: &http.Request{
Header: http.Header{"Authorization": []string{"Bearer " + ss}},
URL: &url.URL{},
},
})
}
require.NoError(t, err)
})
} }
ln, err := net.Listen("tcp", "localhost:4567")
require.NoError(t, err)
go httpServ.Serve(ln)
defer httpServ.Shutdown(context.Background())
type customClaims struct {
jwt.RegisteredClaims
MediaMTXPermissions []conf.AuthInternalUserPermission `json:"my_permission_key"`
}
claims := customClaims{
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "test",
Subject: "somebody",
ID: "1",
},
MediaMTXPermissions: []conf.AuthInternalUserPermission{{
Action: conf.AuthActionPublish,
Path: "mypath",
}},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header[jwkset.HeaderKID] = "test-key-id"
ss, err := token.SignedString(key)
require.NoError(t, err)
m := Manager{
Method: conf.AuthMethodJWT,
JWTJWKS: "http://localhost:4567/jwks",
JWTClaimKey: "my_permission_key",
}
err = m.Authenticate(&Request{
User: "",
Pass: "",
IP: net.ParseIP("127.0.0.1"),
Action: conf.AuthActionPublish,
Path: "mypath",
Protocol: ProtocolRTSP,
Query: "param=value&jwt=" + ss,
})
require.NoError(t, err)
} }

View File

@@ -3,6 +3,7 @@ package defs
import ( import (
"fmt" "fmt"
"net" "net"
"net/http"
"github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/description"
@@ -35,7 +36,7 @@ type Path interface {
RemoveReader(req PathRemoveReaderReq) RemoveReader(req PathRemoveReaderReq)
} }
// PathAccessRequest is an access request. // PathAccessRequest is a path access request.
type PathAccessRequest struct { type PathAccessRequest struct {
Name string Name string
Query string Query string
@@ -43,13 +44,18 @@ type PathAccessRequest struct {
SkipAuth bool SkipAuth bool
// only if skipAuth = false // only if skipAuth = false
IP net.IP User string
User string Pass string
Pass string IP net.IP
Proto auth.Protocol Proto auth.Protocol
ID *uuid.UUID ID *uuid.UUID
// RTSP only
RTSPRequest *base.Request RTSPRequest *base.Request
RTSPNonce string RTSPNonce string
// HTTP only
HTTPRequest *http.Request
} }
// ToAuthRequest converts a path access request into an authentication request. // ToAuthRequest converts a path access request into an authentication request.
@@ -70,6 +76,7 @@ func (r *PathAccessRequest) ToAuthRequest() *auth.Request {
Query: r.Query, Query: r.Query,
RTSPRequest: r.RTSPRequest, RTSPRequest: r.RTSPRequest,
RTSPNonce: r.RTSPNonce, RTSPNonce: r.RTSPNonce,
HTTPRequest: r.HTTPRequest,
} }
} }

View File

@@ -120,17 +120,13 @@ func (m *Metrics) onRequest(ctx *gin.Context) {
return return
} }
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := m.AuthManager.Authenticate(&auth.Request{ err := m.AuthManager.Authenticate(&auth.Request{
User: user, IP: net.ParseIP(ctx.ClientIP()),
Pass: pass, Action: conf.AuthActionMetrics,
Query: ctx.Request.URL.RawQuery, HTTPRequest: ctx.Request,
IP: net.ParseIP(ctx.ClientIP()),
Action: conf.AuthActionMetrics,
}) })
if err != nil { if err != nil {
if !hasCredentials { if err.(*auth.Error).AskCredentials { //nolint:errorlint
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatus(http.StatusUnauthorized) ctx.AbortWithStatus(http.StatusUnauthorized)
return return

View File

@@ -12,7 +12,6 @@ import (
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio" "github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4" "github.com/bluenviron/mediacommon/pkg/formats/fmp4"
"github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer" "github.com/bluenviron/mediacommon/pkg/formats/fmp4/seekablebuffer"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test" "github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -239,20 +238,8 @@ func TestOnGet(t *testing.T) {
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"), RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
}, },
}, },
AuthManager: &test.AuthManager{ AuthManager: test.NilAuthManager,
Func: func(req *auth.Request) error { Parent: test.NilLogger,
require.Equal(t, &auth.Request{
User: "myuser",
Pass: "mypass",
IP: req.IP,
Action: "playback",
Path: "mypath",
Query: req.Query,
}, req)
return nil
},
},
Parent: test.NilLogger,
} }
err = s.Initialize() err = s.Initialize()
require.NoError(t, err) require.NoError(t, err)

View File

@@ -9,7 +9,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/bluenviron/mediamtx/internal/auth"
"github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/test" "github.com/bluenviron/mediamtx/internal/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -36,20 +35,8 @@ func TestOnList(t *testing.T) {
RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"), RecordPath: filepath.Join(dir, "%path/%Y-%m-%d_%H-%M-%S-%f"),
}, },
}, },
AuthManager: &test.AuthManager{ AuthManager: test.NilAuthManager,
Func: func(req *auth.Request) error { Parent: test.NilLogger,
require.Equal(t, &auth.Request{
User: "myuser",
Pass: "mypass",
IP: req.IP,
Action: "playback",
Query: "path=mypath",
Path: "mypath",
}, req)
return nil
},
},
Parent: test.NilLogger,
} }
err = s.Initialize() err = s.Initialize()
require.NoError(t, err) require.NoError(t, err)

View File

@@ -2,7 +2,6 @@
package playback package playback
import ( import (
"errors"
"net" "net"
"net/http" "net/http"
"sync" "sync"
@@ -119,27 +118,21 @@ func (s *Server) middlewareOrigin(ctx *gin.Context) {
} }
func (s *Server) doAuth(ctx *gin.Context, pathName string) bool { func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := s.AuthManager.Authenticate(&auth.Request{ err := s.AuthManager.Authenticate(&auth.Request{
User: user, IP: net.ParseIP(ctx.ClientIP()),
Pass: pass, Action: conf.AuthActionPlayback,
Query: ctx.Request.URL.RawQuery, Path: pathName,
IP: net.ParseIP(ctx.ClientIP()), HTTPRequest: ctx.Request,
Action: conf.AuthActionPlayback,
Path: pathName,
}) })
if err != nil { if err != nil {
if !hasCredentials { if err.(*auth.Error).AskCredentials { //nolint:errorlint
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized) ctx.Writer.WriteHeader(http.StatusUnauthorized)
return false return false
} }
var terr auth.Error s.Log(logger.Info, "connection %v failed to authenticate: %v",
errors.As(err, &terr) httpp.RemoteAddr(ctx), err.(*auth.Error).Message) //nolint:errorlint
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
// wait some seconds to mitigate brute force attacks // wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError) <-time.After(auth.PauseAfterError)

View File

@@ -92,17 +92,13 @@ func (pp *PPROF) onRequest(ctx *gin.Context) {
return return
} }
user, pass, hasCredentials := ctx.Request.BasicAuth()
err := pp.AuthManager.Authenticate(&auth.Request{ err := pp.AuthManager.Authenticate(&auth.Request{
User: user, IP: net.ParseIP(ctx.ClientIP()),
Pass: pass, Action: conf.AuthActionMetrics,
Query: ctx.Request.URL.RawQuery, HTTPRequest: ctx.Request,
IP: net.ParseIP(ctx.ClientIP()),
Action: conf.AuthActionMetrics,
}) })
if err != nil { if err != nil {
if !hasCredentials { if err.(*auth.Error).AskCredentials { //nolint:errorlint
ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized) ctx.Writer.WriteHeader(http.StatusUnauthorized)
return return

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"net" "net"
"net/http" "net/http"
"net/url"
gopath "path" gopath "path"
"strings" "strings"
"time" "time"
@@ -37,17 +36,6 @@ func mergePathAndQuery(path string, rawQuery string) string {
return res return res
} }
func addJWTFromAuthorization(rawQuery string, auth string) string {
jwt := strings.TrimPrefix(auth, "Bearer ")
if rawQuery != "" {
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
v.Set("jwt", jwt)
return v.Encode()
}
}
return url.Values{"jwt": []string{jwt}}.Encode()
}
type httpServer struct { type httpServer struct {
address string address string
encryption bool encryption bool
@@ -157,28 +145,19 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
return return
} }
user, pass, hasCredentials := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
q = addJWTFromAuthorization(q, h)
}
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{ pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
AccessRequest: defs.PathAccessRequest{ AccessRequest: defs.PathAccessRequest{
Name: dir, Name: dir,
Query: q, Publish: false,
Publish: false, IP: net.ParseIP(ctx.ClientIP()),
IP: net.ParseIP(ctx.ClientIP()), Proto: auth.ProtocolHLS,
User: user, HTTPRequest: ctx.Request,
Pass: pass,
Proto: auth.ProtocolHLS,
}, },
}) })
if err != nil { if err != nil {
var terr auth.Error var terr *auth.Error
if errors.As(err, &terr) { if errors.As(err, &terr) {
if !hasCredentials { if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized) ctx.Writer.WriteHeader(http.StatusUnauthorized)
return return

View File

@@ -106,8 +106,6 @@ func TestServerNotFound(t *testing.T) {
pm := &dummyPathManager{ pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "nonexisting", req.AccessRequest.Name) require.Equal(t, "nonexisting", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
@@ -181,8 +179,6 @@ func TestServerRead(t *testing.T) {
pm := &dummyPathManager{ pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "mystream", req.AccessRequest.Name) require.Equal(t, "mystream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
@@ -277,8 +273,6 @@ func TestServerRead(t *testing.T) {
pm := &dummyPathManager{ pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "mystream", req.AccessRequest.Name) require.Equal(t, "mystream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
@@ -372,8 +366,7 @@ func TestServerReadAuthorizationHeader(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
pm := &dummyPathManager{ pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {

View File

@@ -168,7 +168,7 @@ func (c *conn) runRead(conn *rtmp.Conn, u *url.URL) 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 mitigate brute force attacks
<-time.After(auth.PauseAfterError) <-time.After(auth.PauseAfterError)
@@ -235,7 +235,7 @@ func (c *conn) runPublish(conn *rtmp.Conn, u *url.URL) 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 mitigate brute force attacks
<-time.After(auth.PauseAfterError) <-time.After(auth.PauseAfterError)

View File

@@ -68,14 +68,14 @@ type dummyPathManager struct {
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) { func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" { if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, auth.Error{} return nil, &auth.Error{}
} }
return pm.path, nil return pm.path, nil
} }
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" { if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, nil, auth.Error{} return nil, nil, &auth.Error{}
} }
return pm.path, pm.path.stream, nil return pm.path, pm.path.stream, nil
} }

View File

@@ -139,7 +139,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, err := c.handleAuthError(terr) res, err := c.handleAuthError(terr)
return res, nil, err return res, nil, err

View File

@@ -125,7 +125,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(terr) return c.handleAuthError(terr)
} }
@@ -195,7 +195,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(terr) res, err2 := c.handleAuthError(terr)
return res, nil, err2 return res, nil, err2

View File

@@ -151,7 +151,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 mitigate brute force attacks
<-time.After(auth.PauseAfterError) <-time.After(auth.PauseAfterError)
@@ -250,7 +250,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 mitigate brute force attacks
<-time.After(auth.PauseAfterError) <-time.After(auth.PauseAfterError)

View File

@@ -66,14 +66,14 @@ type dummyPathManager struct {
func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) { func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" { if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, auth.Error{} return nil, &auth.Error{}
} }
return pm.path, nil return pm.path, nil
} }
func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" { if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" {
return nil, nil, auth.Error{} return nil, nil, &auth.Error{}
} }
return pm.path, pm.path.stream, nil return pm.path, pm.path.stream, nil
} }

View File

@@ -7,7 +7,6 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@@ -60,17 +59,6 @@ func sessionLocation(publish bool, path string, secret uuid.UUID) string {
return ret return ret
} }
func addJWTFromAuthorization(rawQuery string, auth string) string {
jwt := strings.TrimPrefix(auth, "Bearer ")
if rawQuery != "" {
if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" {
v.Set("jwt", jwt)
return v.Encode()
}
}
return url.Values{"jwt": []string{jwt}}.Encode()
}
type httpServer struct { type httpServer struct {
address string address string
encryption bool encryption bool
@@ -120,35 +108,19 @@ func (s *httpServer) close() {
} }
func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool { func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {
user, pass, hasCredentials := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// JWT in authorization bearer -> JWT in query parameters
q = addJWTFromAuthorization(q, h)
// credentials in authorization bearer -> credentials in authorization basic
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
user = parts[0]
pass = parts[1]
}
}
_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{ _, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
AccessRequest: defs.PathAccessRequest{ AccessRequest: defs.PathAccessRequest{
Name: pathName, Name: pathName,
Query: q, Publish: publish,
Publish: publish, IP: net.ParseIP(ctx.ClientIP()),
IP: net.ParseIP(ctx.ClientIP()), Proto: auth.ProtocolWebRTC,
User: user, HTTPRequest: ctx.Request,
Pass: pass,
Proto: auth.ProtocolWebRTC,
}, },
}) })
if err != nil { if err != nil {
var terr auth.Error var terr *auth.Error
if errors.As(err, &terr) { if errors.As(err, &terr) {
if !hasCredentials { if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized) ctx.Writer.WriteHeader(http.StatusUnauthorized)
return false return false
@@ -200,30 +172,31 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
return return
} }
user, pass, _ := ctx.Request.BasicAuth()
q := ctx.Request.URL.RawQuery
if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") {
// JWT in authorization bearer -> JWT in query parameters
q = addJWTFromAuthorization(q, h)
// credentials in authorization bearer -> credentials in authorization basic
if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 {
user = parts[0]
pass = parts[1]
}
}
res := s.parent.newSession(webRTCNewSessionReq{ res := s.parent.newSession(webRTCNewSessionReq{
pathName: pathName, pathName: pathName,
remoteAddr: httpp.RemoteAddr(ctx), remoteAddr: httpp.RemoteAddr(ctx),
query: q, offer: offer,
user: user, publish: publish,
pass: pass, httpRequest: ctx.Request,
offer: offer,
publish: publish,
}) })
if res.err != nil { if res.err != nil {
var terr *auth.Error
if errors.As(err, &terr) {
if terr.AskCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.AbortWithStatus(http.StatusUnauthorized)
return
}
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Message)
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
writeError(ctx, http.StatusUnauthorized, terr)
return
}
writeError(ctx, res.errStatusCode, res.err) writeError(ctx, res.errStatusCode, res.err)
return return
} }

View File

@@ -133,14 +133,12 @@ type webRTCNewSessionRes struct {
} }
type webRTCNewSessionReq struct { type webRTCNewSessionReq struct {
pathName string pathName string
remoteAddr string remoteAddr string
query string offer []byte
user string publish bool
pass string httpRequest *http.Request
offer []byte res chan webRTCNewSessionRes
publish bool
res chan webRTCNewSessionRes
} }
type webRTCAddSessionCandidatesRes struct { type webRTCAddSessionCandidatesRes struct {

View File

@@ -96,9 +96,7 @@ func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *st
func initializeTestServer(t *testing.T) *Server { func initializeTestServer(t *testing.T) *Server {
pm := &dummyPathManager{ pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
} }
@@ -182,9 +180,7 @@ func TestPreflightRequest(t *testing.T) {
func TestServerOptionsICEServer(t *testing.T) { func TestServerOptionsICEServer(t *testing.T) {
pathManager := &dummyPathManager{ pathManager := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
} }
@@ -249,14 +245,10 @@ func TestServerPublish(t *testing.T) {
pathManager := &dummyPathManager{ pathManager := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name) require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) { addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name) require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, nil return path, nil
}, },
} }
@@ -534,14 +526,10 @@ func TestServerRead(t *testing.T) {
pathManager := &dummyPathManager{ pathManager := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "teststream", req.AccessRequest.Name) require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "teststream", req.AccessRequest.Name) require.Equal(t, "teststream", req.AccessRequest.Name)
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, str, nil return path, str, nil
}, },
} }
@@ -632,167 +620,9 @@ func TestServerRead(t *testing.T) {
} }
} }
func TestServerReadAuthorizationBearerJWT(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
512,
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)
path := &dummyPath{stream: str}
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "jwt=testing", req.AccessRequest.Query)
return path, str, nil
},
}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
require.NoError(t, err)
defer pc.Close() //nolint:errcheck
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodPost,
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/sdp")
req.Header.Set("Authorization", "Bearer testing")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusCreated, res.StatusCode)
}
func TestServerReadAuthorizationUserPass(t *testing.T) {
desc := &description.Session{Medias: []*description.Media{test.MediaH264}}
str, err := stream.New(
512,
1460,
desc,
true,
test.NilLogger,
)
require.NoError(t, err)
path := &dummyPath{stream: str}
pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil
},
addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return path, str, nil
},
}
s := &Server{
Address: "127.0.0.1:8886",
Encryption: false,
ServerKey: "",
ServerCert: "",
AllowOrigin: "",
TrustedProxies: conf.IPNetworks{},
ReadTimeout: conf.StringDuration(10 * time.Second),
LocalUDPAddress: "127.0.0.1:8887",
LocalTCPAddress: "127.0.0.1:8887",
IPsFromInterfaces: true,
IPsFromInterfacesList: []string{},
AdditionalHosts: []string{},
ICEServers: []conf.WebRTCICEServer{},
HandshakeTimeout: conf.StringDuration(10 * time.Second),
TrackGatherTimeout: conf.StringDuration(2 * time.Second),
ExternalCmdPool: nil,
PathManager: pm,
Parent: test.NilLogger,
}
err = s.Initialize()
require.NoError(t, err)
defer s.Close()
tr := &http.Transport{}
defer tr.CloseIdleConnections()
hc := &http.Client{Transport: tr}
pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})
require.NoError(t, err)
defer pc.Close() //nolint:errcheck
_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)
require.NoError(t, err)
offer, err := pc.CreateOffer(nil)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodPost,
"http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP)))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/sdp")
req.Header.Set("Authorization", "Bearer myuser:mypass")
res, err := hc.Do(req)
require.NoError(t, err)
defer res.Body.Close()
require.Equal(t, http.StatusCreated, res.StatusCode)
}
func TestServerReadNotFound(t *testing.T) { func TestServerReadNotFound(t *testing.T) {
pm := &dummyPathManager{ pm := &dummyPathManager{
findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { findPathConf: func(_ defs.PathFindPathConfReq) (*conf.Path, error) {
require.Equal(t, "myuser", req.AccessRequest.User)
require.Equal(t, "mypass", req.AccessRequest.Pass)
return &conf.Path{}, nil return &conf.Path{}, nil
}, },
addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {

View File

@@ -129,25 +129,15 @@ func (s *session) runPublish() (int, error) {
path, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{ path, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{
Author: s, Author: s,
AccessRequest: defs.PathAccessRequest{ AccessRequest: defs.PathAccessRequest{
Name: s.req.pathName, Name: s.req.pathName,
Query: s.req.query, Publish: true,
Publish: true, IP: net.ParseIP(ip),
IP: net.ParseIP(ip), Proto: auth.ProtocolWebRTC,
User: s.req.user, ID: &s.uuid,
Pass: s.req.pass, HTTPRequest: s.req.httpRequest,
Proto: auth.ProtocolWebRTC,
ID: &s.uuid,
}, },
}) })
if err != nil { if err != nil {
var terr auth.Error
if errors.As(err, &terr) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
return http.StatusUnauthorized, err
}
return http.StatusBadRequest, err return http.StatusBadRequest, err
} }
@@ -250,23 +240,14 @@ func (s *session) runRead() (int, error) {
path, stream, err := s.pathManager.AddReader(defs.PathAddReaderReq{ path, stream, err := s.pathManager.AddReader(defs.PathAddReaderReq{
Author: s, Author: s,
AccessRequest: defs.PathAccessRequest{ AccessRequest: defs.PathAccessRequest{
Name: s.req.pathName, Name: s.req.pathName,
Query: s.req.query, IP: net.ParseIP(ip),
IP: net.ParseIP(ip), Proto: auth.ProtocolWebRTC,
User: s.req.user, ID: &s.uuid,
Pass: s.req.pass, HTTPRequest: s.req.httpRequest,
Proto: auth.ProtocolWebRTC,
ID: &s.uuid,
}, },
}) })
if err != nil { if err != nil {
var terr1 auth.Error
if errors.As(err, &terr1) {
// wait some seconds to mitigate brute force attacks
<-time.After(auth.PauseAfterError)
return http.StatusUnauthorized, err
}
var terr2 defs.PathNoOnePublishingError var terr2 defs.PathNoOnePublishingError
if errors.As(err, &terr2) { if errors.As(err, &terr2) {
return http.StatusNotFound, err return http.StatusNotFound, err
@@ -338,7 +319,7 @@ func (s *session) runRead() (int, error) {
Conf: path.SafeConf(), Conf: path.SafeConf(),
ExternalCmdEnv: path.ExternalCmdEnv(), ExternalCmdEnv: path.ExternalCmdEnv(),
Reader: s.APIReaderDescribe(), Reader: s.APIReaderDescribe(),
Query: s.req.query, Query: s.req.httpRequest.URL.RawQuery,
}) })
defer onUnreadHook() defer onUnreadHook()
@@ -451,7 +432,7 @@ func (s *session) apiItem() *defs.APIWebRTCSession {
return defs.APIWebRTCSessionStateRead return defs.APIWebRTCSessionStateRead
}(), }(),
Path: s.req.pathName, Path: s.req.pathName,
Query: s.req.query, Query: s.req.httpRequest.URL.RawQuery,
BytesReceived: bytesReceived, BytesReceived: bytesReceived,
BytesSent: bytesSent, BytesSent: bytesSent,
} }

View File

@@ -4,17 +4,17 @@ import "github.com/bluenviron/mediamtx/internal/auth"
// AuthManager is a test auth manager. // AuthManager is a test auth manager.
type AuthManager struct { type AuthManager struct {
Func func(req *auth.Request) error fnc func(req *auth.Request) error
} }
// Authenticate replicates auth.Manager.Replicate // Authenticate replicates auth.Manager.Replicate
func (m *AuthManager) Authenticate(req *auth.Request) error { func (m *AuthManager) Authenticate(req *auth.Request) error {
return m.Func(req) return m.fnc(req)
} }
// NilAuthManager is an auth manager that accepts everything. // NilAuthManager is an auth manager that accepts everything.
var NilAuthManager = &AuthManager{ var NilAuthManager = &AuthManager{
Func: func(_ *auth.Request) error { fnc: func(_ *auth.Request) error {
return nil return nil
}, },
} }