rtsp: rewrite authentication around ServerConn.VerifyCredentials (#4267)
This commit is contained in:
@@ -10,7 +10,7 @@ require (
|
||||
github.com/alecthomas/kong v1.8.1
|
||||
github.com/asticode/go-astits v1.13.0
|
||||
github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc
|
||||
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10
|
||||
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218163904-55556f1ecfa2
|
||||
github.com/bluenviron/mediacommon/v2 v2.0.0
|
||||
github.com/datarhei/gosrt v0.8.0
|
||||
github.com/fsnotify/fsnotify v1.8.0
|
||||
|
||||
@@ -33,8 +33,8 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh
|
||||
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
|
||||
github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc h1:t1i9foTQ+RfFT5Ke9HV845zWtz2vtWQCWV8ZXvpzM4g=
|
||||
github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc/go.mod h1:soTVqoidOT+L08hUSDreM7DebNyjjViUiEvpWlr7EIs=
|
||||
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10 h1:nQI+hp8j2uSSHTumlqG4JcgwdpK+jy8Hj19Z96HRmPo=
|
||||
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10/go.mod h1:87/zjOqku9cRSk7q6tZ4R8N7evB29E11GnwLVUk7sAQ=
|
||||
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218163904-55556f1ecfa2 h1:GgPHJTUYTwKBVu/bf0SVSCdXxRz9AMcLnV7tDmiVYko=
|
||||
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218163904-55556f1ecfa2/go.mod h1:87/zjOqku9cRSk7q6tZ4R8N7evB29E11GnwLVUk7sAQ=
|
||||
github.com/bluenviron/mediacommon/v2 v2.0.0 h1:JinZ9v2x6QeAOzA0cDA6aFe8vQuCrU8OyWEhG2iNzwY=
|
||||
github.com/bluenviron/mediacommon/v2 v2.0.0/go.mod h1:iHEz1SFIet6zBwAQoh1a92vTQ3dV3LpVFbom6/SLz3k=
|
||||
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
|
||||
|
||||
+1
-1
@@ -294,7 +294,7 @@ func (a *API) middlewareAuth(ctx *gin.Context) {
|
||||
|
||||
err := a.AuthManager.Authenticate(req)
|
||||
if err != nil {
|
||||
if err.(*auth.Error).AskCredentials { //nolint:errorlint
|
||||
if err.(auth.Error).AskCredentials { //nolint:errorlint
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
|
||||
+24
-46
@@ -14,8 +14,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/MicahParks/keyfunc/v3"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -26,19 +24,19 @@ const (
|
||||
// PauseAfterError is the pause to apply after an authentication failure.
|
||||
PauseAfterError = 2 * time.Second
|
||||
|
||||
rtspAuthRealm = "IPCAM"
|
||||
jwtRefreshPeriod = 60 * 60 * time.Second
|
||||
)
|
||||
|
||||
// Error is a authentication error.
|
||||
type Error struct {
|
||||
Wrapped error
|
||||
Message string
|
||||
AskCredentials bool
|
||||
}
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *Error) Error() string {
|
||||
return "authentication failed: " + e.Message
|
||||
func (e Error) Error() string {
|
||||
return "authentication failed: " + e.Wrapped.Error()
|
||||
}
|
||||
|
||||
func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bool {
|
||||
@@ -102,14 +100,13 @@ func (c *customClaims) UnmarshalJSON(b []byte) error {
|
||||
|
||||
// Manager is the authentication manager.
|
||||
type Manager struct {
|
||||
Method conf.AuthMethod
|
||||
InternalUsers []conf.AuthInternalUser
|
||||
HTTPAddress string
|
||||
HTTPExclude []conf.AuthInternalUserPermission
|
||||
JWTJWKS string
|
||||
JWTClaimKey string
|
||||
ReadTimeout time.Duration
|
||||
RTSPAuthMethods []auth.VerifyMethod
|
||||
Method conf.AuthMethod
|
||||
InternalUsers []conf.AuthInternalUser
|
||||
HTTPAddress string
|
||||
HTTPExclude []conf.AuthInternalUserPermission
|
||||
JWTJWKS string
|
||||
JWTClaimKey string
|
||||
ReadTimeout time.Duration
|
||||
|
||||
mutex sync.RWMutex
|
||||
jwtHTTPClient *http.Client
|
||||
@@ -140,8 +137,8 @@ func (m *Manager) Authenticate(req *Request) error {
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return &Error{
|
||||
Message: err.Error(),
|
||||
return Error{
|
||||
Wrapped: err,
|
||||
AskCredentials: (req.User == "" && req.Pass == ""),
|
||||
}
|
||||
}
|
||||
@@ -150,20 +147,11 @@ func (m *Manager) Authenticate(req *Request) error {
|
||||
}
|
||||
|
||||
func (m *Manager) authenticateInternal(req *Request) error {
|
||||
var rtspAuthHeader *headers.Authorization
|
||||
if req.RTSPRequest != nil {
|
||||
var tmp headers.Authorization
|
||||
err := tmp.Unmarshal(req.RTSPRequest.Header["Authorization"])
|
||||
if err == nil {
|
||||
rtspAuthHeader = &tmp
|
||||
}
|
||||
}
|
||||
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
|
||||
for _, u := range m.InternalUsers {
|
||||
if err := m.authenticateWithUser(req, rtspAuthHeader, &u); err == nil {
|
||||
if ok := m.authenticateWithUser(req, &u); ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -173,39 +161,29 @@ func (m *Manager) authenticateInternal(req *Request) error {
|
||||
|
||||
func (m *Manager) authenticateWithUser(
|
||||
req *Request,
|
||||
rtspAuthHeader *headers.Authorization,
|
||||
u *conf.AuthInternalUser,
|
||||
) error {
|
||||
if u.User != "any" && !u.User.Check(req.User) {
|
||||
return fmt.Errorf("wrong user")
|
||||
}
|
||||
|
||||
) bool {
|
||||
if len(u.IPs) != 0 && !u.IPs.Contains(req.IP) {
|
||||
return fmt.Errorf("IP not allowed")
|
||||
return false
|
||||
}
|
||||
|
||||
if !matchesPermission(u.Permissions, req) {
|
||||
return fmt.Errorf("user doesn't have permission to perform action")
|
||||
return false
|
||||
}
|
||||
|
||||
if u.User != "any" {
|
||||
if req.RTSPRequest != nil && rtspAuthHeader != nil && rtspAuthHeader.Method == headers.AuthMethodDigest {
|
||||
err := auth.Verify(
|
||||
req.RTSPRequest,
|
||||
string(u.User),
|
||||
string(u.Pass),
|
||||
m.RTSPAuthMethods,
|
||||
rtspAuthRealm,
|
||||
req.RTSPNonce)
|
||||
if err != nil {
|
||||
return err
|
||||
if req.CustomVerifyFunc != nil {
|
||||
if ok := req.CustomVerifyFunc(string(u.User), string(u.Pass)); !ok {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if !u.User.Check(req.User) || !u.Pass.Check(req.Pass) {
|
||||
return false
|
||||
}
|
||||
} else if !u.Pass.Check(req.Pass) {
|
||||
return fmt.Errorf("invalid credentials")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Manager) authenticateHTTP(req *Request) error {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/MicahParks/jwkset"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||
"github.com/bluenviron/mediamtx/internal/conf"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -56,8 +55,6 @@ func TestAuthInternal(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
},
|
||||
HTTPAddress: "",
|
||||
RTSPAuthMethods: nil,
|
||||
}
|
||||
|
||||
switch encryption {
|
||||
@@ -142,7 +139,7 @@ func TestAuthInternal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthInternalRTSPDigest(t *testing.T) {
|
||||
func TestAuthInternalCustomVerifyFunc(t *testing.T) {
|
||||
for _, ca := range []string{"ok", "invalid"} {
|
||||
t.Run(ca, func(t *testing.T) {
|
||||
m := Manager{
|
||||
@@ -158,8 +155,6 @@ func TestAuthInternalRTSPDigest(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
},
|
||||
HTTPAddress: "",
|
||||
RTSPAuthMethods: []auth.VerifyMethod{auth.VerifyMethodDigestMD5},
|
||||
}
|
||||
|
||||
u, err := base.ParseURL("rtsp://127.0.0.1:8554/mypath")
|
||||
@@ -170,25 +165,15 @@ func TestAuthInternalRTSPDigest(t *testing.T) {
|
||||
URL: u,
|
||||
}
|
||||
|
||||
if ca == "ok" {
|
||||
var s *auth.Sender
|
||||
s, err = auth.NewSender(
|
||||
auth.GenerateWWWAuthenticate([]auth.VerifyMethod{auth.VerifyMethodDigestMD5}, "IPCAM", "mynonce"),
|
||||
"myuser",
|
||||
"mypass",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
s.AddAuthorization(req)
|
||||
} else {
|
||||
req.Header = base.Header{"Authorization": base.HeaderValue{"garbage"}}
|
||||
}
|
||||
|
||||
req1 := &Request{
|
||||
IP: net.ParseIP("127.1.1.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
RTSPRequest: req,
|
||||
RTSPNonce: "mynonce",
|
||||
IP: net.ParseIP("127.1.1.1"),
|
||||
Action: conf.AuthActionPublish,
|
||||
Path: "mypath",
|
||||
CustomVerifyFunc: func(expectedUser, expectedPass string) bool {
|
||||
require.Equal(t, "myuser", expectedUser)
|
||||
require.Equal(t, "mypass", expectedPass)
|
||||
return (ca == "ok")
|
||||
},
|
||||
}
|
||||
req1.FillFromRTSPRequest(req)
|
||||
err = m.Authenticate(req1)
|
||||
@@ -216,8 +201,6 @@ func TestAuthInternalCredentialsInBearer(t *testing.T) {
|
||||
}},
|
||||
},
|
||||
},
|
||||
HTTPAddress: "",
|
||||
RTSPAuthMethods: []auth.VerifyMethod{auth.VerifyMethodDigestMD5},
|
||||
}
|
||||
|
||||
req := &Request{
|
||||
@@ -282,9 +265,8 @@ func TestAuthHTTP(t *testing.T) {
|
||||
defer httpServ.Shutdown(context.Background())
|
||||
|
||||
m := Manager{
|
||||
Method: conf.AuthMethodHTTP,
|
||||
HTTPAddress: "http://127.0.0.1:9120/auth",
|
||||
RTSPAuthMethods: nil,
|
||||
Method: conf.AuthMethodHTTP,
|
||||
HTTPAddress: "http://127.0.0.1:9120/auth",
|
||||
}
|
||||
|
||||
if outcome == "ok" {
|
||||
@@ -321,7 +303,6 @@ func TestAuthHTTPExclude(t *testing.T) {
|
||||
HTTPExclude: []conf.AuthInternalUserPermission{{
|
||||
Action: conf.AuthActionPublish,
|
||||
}},
|
||||
RTSPAuthMethods: nil,
|
||||
}
|
||||
|
||||
err := m.Authenticate(&Request{
|
||||
|
||||
@@ -37,20 +37,17 @@ const (
|
||||
|
||||
// Request is an authentication request.
|
||||
type Request struct {
|
||||
User string
|
||||
Pass string
|
||||
IP net.IP
|
||||
Action conf.AuthAction
|
||||
User string
|
||||
Pass string
|
||||
IP net.IP
|
||||
Action conf.AuthAction
|
||||
CustomVerifyFunc func(expectedUser string, expectedPass string) bool
|
||||
|
||||
// only for ActionPublish, ActionRead, ActionPlayback
|
||||
Path string
|
||||
Protocol Protocol
|
||||
ID *uuid.UUID
|
||||
Query string
|
||||
|
||||
// RTSP only
|
||||
RTSPRequest *base.Request
|
||||
RTSPNonce string
|
||||
}
|
||||
|
||||
// FillFromRTSPRequest fills User and Pass from a RTSP request.
|
||||
@@ -58,11 +55,9 @@ func (r *Request) FillFromRTSPRequest(rt *base.Request) {
|
||||
var rtspAuthHeader headers.Authorization
|
||||
err := rtspAuthHeader.Unmarshal(rt.Header["Authorization"])
|
||||
if err == nil {
|
||||
r.User = rtspAuthHeader.Username
|
||||
if rtspAuthHeader.Method == headers.AuthMethodBasic {
|
||||
r.User = rtspAuthHeader.BasicUser
|
||||
r.Pass = rtspAuthHeader.BasicPass
|
||||
} else {
|
||||
r.User = rtspAuthHeader.Username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
|
||||
)
|
||||
|
||||
@@ -18,6 +20,25 @@ type AuthInternalUser struct {
|
||||
Permissions []AuthInternalUserPermission `json:"permissions"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
func (d *AuthInternalUser) UnmarshalJSON(b []byte) error {
|
||||
type alias AuthInternalUser
|
||||
if err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// https://github.com/bluenviron/gortsplib/blob/55556f1ecfa2bd51b29fe14eddd70512a0361cbd/server_conn.go#L155-L156
|
||||
if d.User == "" {
|
||||
return fmt.Errorf("empty usernames are not supported")
|
||||
}
|
||||
|
||||
if d.User == "any" && d.Pass != "" {
|
||||
return fmt.Errorf("using a password with 'any' user is not supported")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AuthInternalUsers is a list of AuthInternalUser
|
||||
type AuthInternalUsers []AuthInternalUser
|
||||
|
||||
|
||||
@@ -407,6 +407,24 @@ func TestConfErrors(t *testing.T) {
|
||||
" sourceRedirect: invalid://invalid",
|
||||
`'sourceRedirect' is useless when source is not 'redirect'`,
|
||||
},
|
||||
{
|
||||
"invalid user",
|
||||
"authInternalUsers:\n" +
|
||||
"- user:\n" +
|
||||
" pass: test\n" +
|
||||
" permissions:\n" +
|
||||
" - action: publish\n",
|
||||
"empty usernames are not supported",
|
||||
},
|
||||
{
|
||||
"invalid pass",
|
||||
"authInternalUsers:\n" +
|
||||
"- user: any\n" +
|
||||
" pass: test\n" +
|
||||
" permissions:\n" +
|
||||
" - action: publish\n",
|
||||
`using a password with 'any' user is not supported`,
|
||||
},
|
||||
} {
|
||||
t.Run(ca.name, func(t *testing.T) {
|
||||
tmpf, err := createTempFile([]byte(ca.conf))
|
||||
|
||||
+8
-10
@@ -264,14 +264,13 @@ func (p *Core) createResources(initial bool) error {
|
||||
|
||||
if p.authManager == nil {
|
||||
p.authManager = &auth.Manager{
|
||||
Method: p.conf.AuthMethod,
|
||||
InternalUsers: p.conf.AuthInternalUsers,
|
||||
HTTPAddress: p.conf.AuthHTTPAddress,
|
||||
HTTPExclude: p.conf.AuthHTTPExclude,
|
||||
JWTJWKS: p.conf.AuthJWTJWKS,
|
||||
JWTClaimKey: p.conf.AuthJWTClaimKey,
|
||||
ReadTimeout: time.Duration(p.conf.ReadTimeout),
|
||||
RTSPAuthMethods: p.conf.RTSPAuthMethods,
|
||||
Method: p.conf.AuthMethod,
|
||||
InternalUsers: p.conf.AuthInternalUsers,
|
||||
HTTPAddress: p.conf.AuthHTTPAddress,
|
||||
HTTPExclude: p.conf.AuthHTTPExclude,
|
||||
JWTJWKS: p.conf.AuthJWTJWKS,
|
||||
JWTClaimKey: p.conf.AuthJWTClaimKey,
|
||||
ReadTimeout: time.Duration(p.conf.ReadTimeout),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,8 +652,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
|
||||
!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||
|
||||
newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||
|
||||
newConf.AuthJWTClaimKey != p.conf.AuthJWTClaimKey ||
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout ||
|
||||
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods)
|
||||
newConf.ReadTimeout != p.conf.ReadTimeout
|
||||
if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) {
|
||||
p.authManager.ReloadInternalUsers(newConf.AuthInternalUsers)
|
||||
}
|
||||
|
||||
@@ -32,15 +32,12 @@ type PathAccessRequest struct {
|
||||
SkipAuth bool
|
||||
|
||||
// only if skipAuth = false
|
||||
User string
|
||||
Pass string
|
||||
IP net.IP
|
||||
Proto auth.Protocol
|
||||
ID *uuid.UUID
|
||||
|
||||
// RTSP only
|
||||
RTSPRequest *base.Request
|
||||
RTSPNonce string
|
||||
User string
|
||||
Pass string
|
||||
IP net.IP
|
||||
CustomVerifyFunc func(expectedUser string, expectedPass string) bool
|
||||
Proto auth.Protocol
|
||||
ID *uuid.UUID
|
||||
}
|
||||
|
||||
// ToAuthRequest converts a path access request into an authentication request.
|
||||
@@ -55,12 +52,11 @@ func (r *PathAccessRequest) ToAuthRequest() *auth.Request {
|
||||
}
|
||||
return conf.AuthActionRead
|
||||
}(),
|
||||
Path: r.Name,
|
||||
Protocol: r.Proto,
|
||||
ID: r.ID,
|
||||
Query: r.Query,
|
||||
RTSPRequest: r.RTSPRequest,
|
||||
RTSPNonce: r.RTSPNonce,
|
||||
CustomVerifyFunc: r.CustomVerifyFunc,
|
||||
Path: r.Name,
|
||||
Protocol: r.Proto,
|
||||
ID: r.ID,
|
||||
Query: r.Query,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +65,9 @@ func (r *PathAccessRequest) FillFromRTSPRequest(rt *base.Request) {
|
||||
var rtspAuthHeader headers.Authorization
|
||||
err := rtspAuthHeader.Unmarshal(rt.Header["Authorization"])
|
||||
if err == nil {
|
||||
r.User = rtspAuthHeader.Username
|
||||
if rtspAuthHeader.Method == headers.AuthMethodBasic {
|
||||
r.User = rtspAuthHeader.BasicUser
|
||||
r.Pass = rtspAuthHeader.BasicPass
|
||||
} else {
|
||||
r.User = rtspAuthHeader.Username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) {
|
||||
|
||||
err := m.AuthManager.Authenticate(req)
|
||||
if err != nil {
|
||||
if err.(*auth.Error).AskCredentials { //nolint:errorlint
|
||||
if err.(auth.Error).AskCredentials { //nolint:errorlint
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
|
||||
@@ -126,7 +126,7 @@ func (s *Server) doAuth(ctx *gin.Context, pathName string) bool {
|
||||
|
||||
err := s.AuthManager.Authenticate(req)
|
||||
if err != nil {
|
||||
if err.(*auth.Error).AskCredentials { //nolint:errorlint
|
||||
if err.(auth.Error).AskCredentials { //nolint:errorlint
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
||||
return false
|
||||
|
||||
@@ -105,7 +105,7 @@ func (pp *PPROF) middlewareAuth(ctx *gin.Context) {
|
||||
|
||||
err := pp.AuthManager.Authenticate(req)
|
||||
if err != nil {
|
||||
if err.(*auth.Error).AskCredentials { //nolint:errorlint
|
||||
if err.(auth.Error).AskCredentials { //nolint:errorlint
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
ctx.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
|
||||
@@ -159,7 +159,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) {
|
||||
AccessRequest: req,
|
||||
})
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
if terr.AskCredentials {
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
|
||||
@@ -168,7 +168,7 @@ func (c *conn) runRead(conn *rtmp.Conn, u *url.URL) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
@@ -235,7 +235,7 @@ func (c *conn) runPublish(conn *rtmp.Conn, u *url.URL) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/bluenviron/gortsplib/v4"
|
||||
rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/headers"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/liberrors"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/bluenviron/mediamtx/internal/auth"
|
||||
@@ -20,10 +22,6 @@ import (
|
||||
"github.com/bluenviron/mediamtx/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
rtspAuthRealm = "IPCAM"
|
||||
)
|
||||
|
||||
func absoluteURL(req *base.Request, v string) string {
|
||||
if strings.HasPrefix(v, "/") {
|
||||
ur := base.URL{
|
||||
@@ -37,6 +35,12 @@ func absoluteURL(req *base.Request, v string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
func credentialsProvided(req *base.Request) bool {
|
||||
var auth headers.Authorization
|
||||
err := auth.Unmarshal(req.Header["Authorization"])
|
||||
return err == nil && auth.Username != ""
|
||||
}
|
||||
|
||||
type connParent interface {
|
||||
logger.Writer
|
||||
findSessionByRSessionUnsafe(rsession *gortsplib.ServerSession) *session
|
||||
@@ -59,8 +63,6 @@ type conn struct {
|
||||
uuid uuid.UUID
|
||||
created time.Time
|
||||
onDisconnectHook func()
|
||||
authNonce string
|
||||
authFailures int
|
||||
}
|
||||
|
||||
func (c *conn) initialize() {
|
||||
@@ -135,24 +137,15 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
||||
}
|
||||
ctx.Path = ctx.Path[1:]
|
||||
|
||||
if c.authNonce == "" {
|
||||
var err error
|
||||
c.authNonce, err = rtspauth.GenerateNonce()
|
||||
if err != nil {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusInternalServerError,
|
||||
}, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req := defs.PathAccessRequest{
|
||||
Name: ctx.Path,
|
||||
Query: ctx.Query,
|
||||
IP: c.ip(),
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
RTSPRequest: ctx.Request,
|
||||
RTSPNonce: c.authNonce,
|
||||
Name: ctx.Path,
|
||||
Query: ctx.Query,
|
||||
IP: c.ip(),
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
CustomVerifyFunc: func(expectedUser, expectedPass string) bool {
|
||||
return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)
|
||||
},
|
||||
}
|
||||
req.FillFromRTSPRequest(ctx.Request)
|
||||
|
||||
@@ -161,10 +154,10 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
||||
})
|
||||
|
||||
if res.Err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(res.Err, &terr) {
|
||||
res, err := c.handleAuthError(terr)
|
||||
return res, nil, err
|
||||
res, err2 := c.handleAuthError(ctx.Request)
|
||||
return res, nil, err2
|
||||
}
|
||||
|
||||
var terr2 defs.PathNoOnePublishingError
|
||||
@@ -200,30 +193,17 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,
|
||||
}, stream, nil
|
||||
}
|
||||
|
||||
func (c *conn) handleAuthError(authErr error) (*base.Response, error) {
|
||||
c.authFailures++
|
||||
|
||||
// VLC with login prompt sends 4 requests:
|
||||
// 1) without credentials
|
||||
// 2) with password but without username
|
||||
// 3) without credentials
|
||||
// 4) with password and username
|
||||
// therefore we must allow up to 3 failures
|
||||
if c.authFailures <= 3 {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusUnauthorized,
|
||||
Header: base.Header{
|
||||
"WWW-Authenticate": rtspauth.GenerateWWWAuthenticate(c.authMethods, rtspAuthRealm, c.authNonce),
|
||||
},
|
||||
}, nil
|
||||
func (c *conn) handleAuthError(req *base.Request) (*base.Response, error) {
|
||||
if credentialsProvided(req) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
}
|
||||
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
// let gortsplib decide whether connection should be terminated,
|
||||
// depending on whether credentials have been provided or not.
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusUnauthorized,
|
||||
}, authErr
|
||||
}, liberrors.ErrServerAuth{}
|
||||
}
|
||||
|
||||
func (c *conn) apiItem() *defs.APIRTSPConn {
|
||||
|
||||
@@ -106,6 +106,7 @@ func (s *Server) Initialize() error {
|
||||
WriteTimeout: time.Duration(s.WriteTimeout),
|
||||
WriteQueueSize: s.WriteQueueSize,
|
||||
RTSPAddress: s.Address,
|
||||
AuthMethods: s.AuthMethods,
|
||||
}
|
||||
|
||||
if s.UseUDP {
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestServerPublish(t *testing.T) {
|
||||
pathManager := &test.PathManager{
|
||||
AddPublisherImpl: func(req defs.PathAddPublisherReq) (defs.Path, error) {
|
||||
if req.AccessRequest.User == "" && req.AccessRequest.Pass == "" {
|
||||
return nil, &auth.Error{Message: "", AskCredentials: true}
|
||||
return nil, auth.Error{Message: "", AskCredentials: true}
|
||||
}
|
||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
||||
require.Equal(t, "param=value", req.AccessRequest.Query)
|
||||
@@ -162,7 +162,7 @@ func TestServerRead(t *testing.T) {
|
||||
pathManager := &test.PathManager{
|
||||
DescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {
|
||||
if req.AccessRequest.User == "" && req.AccessRequest.Pass == "" {
|
||||
return defs.PathDescribeRes{Err: &auth.Error{Message: "", AskCredentials: true}}
|
||||
return defs.PathDescribeRes{Err: auth.Error{Message: "", AskCredentials: true}}
|
||||
}
|
||||
require.Equal(t, "teststream", req.AccessRequest.Name)
|
||||
require.Equal(t, "param=value", req.AccessRequest.Query)
|
||||
@@ -282,7 +282,7 @@ func TestServerRedirect(t *testing.T) {
|
||||
}
|
||||
|
||||
if req.AccessRequest.User == "" && req.AccessRequest.Pass == "" {
|
||||
return defs.PathDescribeRes{Err: &auth.Error{Message: "", AskCredentials: true}}
|
||||
return defs.PathDescribeRes{Err: auth.Error{Message: "", AskCredentials: true}}
|
||||
}
|
||||
|
||||
require.Equal(t, "path2", req.AccessRequest.Name)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/bluenviron/gortsplib/v4"
|
||||
rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth"
|
||||
"github.com/bluenviron/gortsplib/v4/pkg/base"
|
||||
"github.com/google/uuid"
|
||||
"github.com/pion/rtp"
|
||||
@@ -101,25 +100,16 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
|
||||
}
|
||||
ctx.Path = ctx.Path[1:]
|
||||
|
||||
if c.authNonce == "" {
|
||||
var err error
|
||||
c.authNonce, err = rtspauth.GenerateNonce()
|
||||
if err != nil {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusInternalServerError,
|
||||
}, err
|
||||
}
|
||||
}
|
||||
|
||||
req := defs.PathAccessRequest{
|
||||
Name: ctx.Path,
|
||||
Query: ctx.Query,
|
||||
Publish: true,
|
||||
IP: c.ip(),
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
RTSPRequest: ctx.Request,
|
||||
RTSPNonce: c.authNonce,
|
||||
Name: ctx.Path,
|
||||
Query: ctx.Query,
|
||||
Publish: true,
|
||||
IP: c.ip(),
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
CustomVerifyFunc: func(expectedUser, expectedPass string) bool {
|
||||
return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)
|
||||
},
|
||||
}
|
||||
req.FillFromRTSPRequest(ctx.Request)
|
||||
|
||||
@@ -128,9 +118,9 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx)
|
||||
AccessRequest: req,
|
||||
})
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
return c.handleAuthError(terr)
|
||||
return c.handleAuthError(ctx.Request)
|
||||
}
|
||||
|
||||
return &base.Response{
|
||||
@@ -175,24 +165,15 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
|
||||
|
||||
switch s.rsession.State() {
|
||||
case gortsplib.ServerSessionStateInitial, gortsplib.ServerSessionStatePrePlay: // play
|
||||
if c.authNonce == "" {
|
||||
var err error
|
||||
c.authNonce, err = rtspauth.GenerateNonce()
|
||||
if err != nil {
|
||||
return &base.Response{
|
||||
StatusCode: base.StatusInternalServerError,
|
||||
}, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
req := defs.PathAccessRequest{
|
||||
Name: ctx.Path,
|
||||
Query: ctx.Query,
|
||||
IP: c.ip(),
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
RTSPRequest: ctx.Request,
|
||||
RTSPNonce: c.authNonce,
|
||||
Name: ctx.Path,
|
||||
Query: ctx.Query,
|
||||
IP: c.ip(),
|
||||
Proto: auth.ProtocolRTSP,
|
||||
ID: &c.uuid,
|
||||
CustomVerifyFunc: func(expectedUser, expectedPass string) bool {
|
||||
return c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)
|
||||
},
|
||||
}
|
||||
req.FillFromRTSPRequest(ctx.Request)
|
||||
|
||||
@@ -201,9 +182,9 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,
|
||||
AccessRequest: req,
|
||||
})
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
res, err2 := c.handleAuthError(terr)
|
||||
res, err2 := c.handleAuthError(ctx.Request)
|
||||
return res, nil, err2
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ func (c *conn) runPublish(streamID *streamID) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
@@ -250,7 +250,7 @@ func (c *conn) runRead(streamID *streamID) error {
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
// wait some seconds to mitigate brute force attacks
|
||||
<-time.After(auth.PauseAfterError)
|
||||
|
||||
@@ -136,7 +136,7 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string,
|
||||
AccessRequest: req,
|
||||
})
|
||||
if err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
if terr.AskCredentials {
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
@@ -198,7 +198,7 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool)
|
||||
httpRequest: ctx.Request,
|
||||
})
|
||||
if res.err != nil {
|
||||
var terr *auth.Error
|
||||
var terr auth.Error
|
||||
if errors.As(err, &terr) {
|
||||
if terr.AskCredentials {
|
||||
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
||||
|
||||
Reference in New Issue
Block a user