Skip to content

Commit 2f08bef

Browse files
committed
rtsp: rewrite authentication around ServerConn.VerifyCredentials
1 parent 7ade289 commit 2f08bef

File tree

19 files changed

+126
-218
lines changed

19 files changed

+126
-218
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/alecthomas/kong v1.8.1
1111
github.com/asticode/go-astits v1.13.0
1212
github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc
13-
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10
13+
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218155728-d8df3689feb2
1414
github.com/bluenviron/mediacommon/v2 v2.0.0
1515
github.com/datarhei/gosrt v0.8.0
1616
github.com/fsnotify/fsnotify v1.8.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh
3333
github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=
3434
github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc h1:t1i9foTQ+RfFT5Ke9HV845zWtz2vtWQCWV8ZXvpzM4g=
3535
github.com/bluenviron/gohlslib/v2 v2.1.4-0.20250210133907-d3dddacbb9fc/go.mod h1:soTVqoidOT+L08hUSDreM7DebNyjjViUiEvpWlr7EIs=
36-
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10 h1:nQI+hp8j2uSSHTumlqG4JcgwdpK+jy8Hj19Z96HRmPo=
37-
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250214103455-885a9975ef10/go.mod h1:87/zjOqku9cRSk7q6tZ4R8N7evB29E11GnwLVUk7sAQ=
36+
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218155728-d8df3689feb2 h1:bjxOlJln0gIAjzmqLgCSNwixBlTmD7ldOPnfNS316S4=
37+
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218155728-d8df3689feb2/go.mod h1:87/zjOqku9cRSk7q6tZ4R8N7evB29E11GnwLVUk7sAQ=
3838
github.com/bluenviron/mediacommon/v2 v2.0.0 h1:JinZ9v2x6QeAOzA0cDA6aFe8vQuCrU8OyWEhG2iNzwY=
3939
github.com/bluenviron/mediacommon/v2 v2.0.0/go.mod h1:iHEz1SFIet6zBwAQoh1a92vTQ3dV3LpVFbom6/SLz3k=
4040
github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=

internal/api/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ func (a *API) middlewareAuth(ctx *gin.Context) {
294294

295295
err := a.AuthManager.Authenticate(req)
296296
if err != nil {
297-
if err.(*auth.Error).AskCredentials { //nolint:errorlint
297+
if err.(auth.Error).AskCredentials { //nolint:errorlint
298298
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
299299
ctx.AbortWithStatus(http.StatusUnauthorized)
300300
return

internal/auth/manager.go

Lines changed: 24 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import (
1414
"time"
1515

1616
"github.com/MicahParks/keyfunc/v3"
17-
"github.com/bluenviron/gortsplib/v4/pkg/auth"
18-
"github.com/bluenviron/gortsplib/v4/pkg/headers"
1917
"github.com/bluenviron/mediamtx/internal/conf"
2018
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
2119
"github.com/golang-jwt/jwt/v5"
@@ -26,19 +24,19 @@ const (
2624
// PauseAfterError is the pause to apply after an authentication failure.
2725
PauseAfterError = 2 * time.Second
2826

29-
rtspAuthRealm = "IPCAM"
3027
jwtRefreshPeriod = 60 * 60 * time.Second
3128
)
3229

3330
// Error is a authentication error.
3431
type Error struct {
32+
Wrapped error
3533
Message string
3634
AskCredentials bool
3735
}
3836

3937
// Error implements the error interface.
40-
func (e *Error) Error() string {
41-
return "authentication failed: " + e.Message
38+
func (e Error) Error() string {
39+
return "authentication failed: " + e.Wrapped.Error()
4240
}
4341

4442
func matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bool {
@@ -102,14 +100,13 @@ func (c *customClaims) UnmarshalJSON(b []byte) error {
102100

103101
// Manager is the authentication manager.
104102
type Manager struct {
105-
Method conf.AuthMethod
106-
InternalUsers []conf.AuthInternalUser
107-
HTTPAddress string
108-
HTTPExclude []conf.AuthInternalUserPermission
109-
JWTJWKS string
110-
JWTClaimKey string
111-
ReadTimeout time.Duration
112-
RTSPAuthMethods []auth.VerifyMethod
103+
Method conf.AuthMethod
104+
InternalUsers []conf.AuthInternalUser
105+
HTTPAddress string
106+
HTTPExclude []conf.AuthInternalUserPermission
107+
JWTJWKS string
108+
JWTClaimKey string
109+
ReadTimeout time.Duration
113110

114111
mutex sync.RWMutex
115112
jwtHTTPClient *http.Client
@@ -140,8 +137,8 @@ func (m *Manager) Authenticate(req *Request) error {
140137
}
141138

142139
if err != nil {
143-
return &Error{
144-
Message: err.Error(),
140+
return Error{
141+
Wrapped: err,
145142
AskCredentials: (req.User == "" && req.Pass == ""),
146143
}
147144
}
@@ -150,20 +147,11 @@ func (m *Manager) Authenticate(req *Request) error {
150147
}
151148

152149
func (m *Manager) authenticateInternal(req *Request) error {
153-
var rtspAuthHeader *headers.Authorization
154-
if req.RTSPRequest != nil {
155-
var tmp headers.Authorization
156-
err := tmp.Unmarshal(req.RTSPRequest.Header["Authorization"])
157-
if err == nil {
158-
rtspAuthHeader = &tmp
159-
}
160-
}
161-
162150
m.mutex.RLock()
163151
defer m.mutex.RUnlock()
164152

165153
for _, u := range m.InternalUsers {
166-
if err := m.authenticateWithUser(req, rtspAuthHeader, &u); err == nil {
154+
if ok := m.authenticateWithUser(req, &u); ok {
167155
return nil
168156
}
169157
}
@@ -173,39 +161,29 @@ func (m *Manager) authenticateInternal(req *Request) error {
173161

174162
func (m *Manager) authenticateWithUser(
175163
req *Request,
176-
rtspAuthHeader *headers.Authorization,
177164
u *conf.AuthInternalUser,
178-
) error {
179-
if u.User != "any" && !u.User.Check(req.User) {
180-
return fmt.Errorf("wrong user")
181-
}
182-
165+
) bool {
183166
if len(u.IPs) != 0 && !u.IPs.Contains(req.IP) {
184-
return fmt.Errorf("IP not allowed")
167+
return false
185168
}
186169

187170
if !matchesPermission(u.Permissions, req) {
188-
return fmt.Errorf("user doesn't have permission to perform action")
171+
return false
189172
}
190173

191174
if u.User != "any" {
192-
if req.RTSPRequest != nil && rtspAuthHeader != nil && rtspAuthHeader.Method == headers.AuthMethodDigest {
193-
err := auth.Verify(
194-
req.RTSPRequest,
195-
string(u.User),
196-
string(u.Pass),
197-
m.RTSPAuthMethods,
198-
rtspAuthRealm,
199-
req.RTSPNonce)
200-
if err != nil {
201-
return err
175+
if req.CustomVerifyFunc != nil {
176+
if ok := req.CustomVerifyFunc(string(u.User), string(u.Pass)); !ok {
177+
return false
178+
}
179+
} else {
180+
if !u.User.Check(req.User) || !u.Pass.Check(req.Pass) {
181+
return false
202182
}
203-
} else if !u.Pass.Check(req.Pass) {
204-
return fmt.Errorf("invalid credentials")
205183
}
206184
}
207185

208-
return nil
186+
return true
209187
}
210188

211189
func (m *Manager) authenticateHTTP(req *Request) error {

internal/auth/manager_test.go

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"time"
1313

1414
"github.com/MicahParks/jwkset"
15-
"github.com/bluenviron/gortsplib/v4/pkg/auth"
1615
"github.com/bluenviron/gortsplib/v4/pkg/base"
1716
"github.com/bluenviron/mediamtx/internal/conf"
1817
"github.com/golang-jwt/jwt/v5"
@@ -56,8 +55,6 @@ func TestAuthInternal(t *testing.T) {
5655
}},
5756
},
5857
},
59-
HTTPAddress: "",
60-
RTSPAuthMethods: nil,
6158
}
6259

6360
switch encryption {
@@ -142,7 +139,7 @@ func TestAuthInternal(t *testing.T) {
142139
}
143140
}
144141

145-
func TestAuthInternalRTSPDigest(t *testing.T) {
142+
func TestAuthInternalCustomVerifyFunc(t *testing.T) {
146143
for _, ca := range []string{"ok", "invalid"} {
147144
t.Run(ca, func(t *testing.T) {
148145
m := Manager{
@@ -158,8 +155,6 @@ func TestAuthInternalRTSPDigest(t *testing.T) {
158155
}},
159156
},
160157
},
161-
HTTPAddress: "",
162-
RTSPAuthMethods: []auth.VerifyMethod{auth.VerifyMethodDigestMD5},
163158
}
164159

165160
u, err := base.ParseURL("rtsp://127.0.0.1:8554/mypath")
@@ -170,25 +165,15 @@ func TestAuthInternalRTSPDigest(t *testing.T) {
170165
URL: u,
171166
}
172167

173-
if ca == "ok" {
174-
var s *auth.Sender
175-
s, err = auth.NewSender(
176-
auth.GenerateWWWAuthenticate([]auth.VerifyMethod{auth.VerifyMethodDigestMD5}, "IPCAM", "mynonce"),
177-
"myuser",
178-
"mypass",
179-
)
180-
require.NoError(t, err)
181-
s.AddAuthorization(req)
182-
} else {
183-
req.Header = base.Header{"Authorization": base.HeaderValue{"garbage"}}
184-
}
185-
186168
req1 := &Request{
187-
IP: net.ParseIP("127.1.1.1"),
188-
Action: conf.AuthActionPublish,
189-
Path: "mypath",
190-
RTSPRequest: req,
191-
RTSPNonce: "mynonce",
169+
IP: net.ParseIP("127.1.1.1"),
170+
Action: conf.AuthActionPublish,
171+
Path: "mypath",
172+
CustomVerifyFunc: func(expectedUser, expectedPass string) bool {
173+
require.Equal(t, "myuser", expectedUser)
174+
require.Equal(t, "mypass", expectedPass)
175+
return (ca == "ok")
176+
},
192177
}
193178
req1.FillFromRTSPRequest(req)
194179
err = m.Authenticate(req1)
@@ -216,8 +201,6 @@ func TestAuthInternalCredentialsInBearer(t *testing.T) {
216201
}},
217202
},
218203
},
219-
HTTPAddress: "",
220-
RTSPAuthMethods: []auth.VerifyMethod{auth.VerifyMethodDigestMD5},
221204
}
222205

223206
req := &Request{
@@ -282,9 +265,8 @@ func TestAuthHTTP(t *testing.T) {
282265
defer httpServ.Shutdown(context.Background())
283266

284267
m := Manager{
285-
Method: conf.AuthMethodHTTP,
286-
HTTPAddress: "http://127.0.0.1:9120/auth",
287-
RTSPAuthMethods: nil,
268+
Method: conf.AuthMethodHTTP,
269+
HTTPAddress: "http://127.0.0.1:9120/auth",
288270
}
289271

290272
if outcome == "ok" {
@@ -321,7 +303,6 @@ func TestAuthHTTPExclude(t *testing.T) {
321303
HTTPExclude: []conf.AuthInternalUserPermission{{
322304
Action: conf.AuthActionPublish,
323305
}},
324-
RTSPAuthMethods: nil,
325306
}
326307

327308
err := m.Authenticate(&Request{

internal/auth/request.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,32 +37,27 @@ const (
3737

3838
// Request is an authentication request.
3939
type Request struct {
40-
User string
41-
Pass string
42-
IP net.IP
43-
Action conf.AuthAction
40+
User string
41+
Pass string
42+
IP net.IP
43+
Action conf.AuthAction
44+
CustomVerifyFunc func(expectedUser string, expectedPass string) bool
4445

4546
// only for ActionPublish, ActionRead, ActionPlayback
4647
Path string
4748
Protocol Protocol
4849
ID *uuid.UUID
4950
Query string
50-
51-
// RTSP only
52-
RTSPRequest *base.Request
53-
RTSPNonce string
5451
}
5552

5653
// FillFromRTSPRequest fills User and Pass from a RTSP request.
5754
func (r *Request) FillFromRTSPRequest(rt *base.Request) {
5855
var rtspAuthHeader headers.Authorization
5956
err := rtspAuthHeader.Unmarshal(rt.Header["Authorization"])
6057
if err == nil {
58+
r.User = rtspAuthHeader.Username
6159
if rtspAuthHeader.Method == headers.AuthMethodBasic {
62-
r.User = rtspAuthHeader.BasicUser
6360
r.Pass = rtspAuthHeader.BasicPass
64-
} else {
65-
r.User = rtspAuthHeader.Username
6661
}
6762
}
6863
}

internal/core/core.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -264,14 +264,13 @@ func (p *Core) createResources(initial bool) error {
264264

265265
if p.authManager == nil {
266266
p.authManager = &auth.Manager{
267-
Method: p.conf.AuthMethod,
268-
InternalUsers: p.conf.AuthInternalUsers,
269-
HTTPAddress: p.conf.AuthHTTPAddress,
270-
HTTPExclude: p.conf.AuthHTTPExclude,
271-
JWTJWKS: p.conf.AuthJWTJWKS,
272-
JWTClaimKey: p.conf.AuthJWTClaimKey,
273-
ReadTimeout: time.Duration(p.conf.ReadTimeout),
274-
RTSPAuthMethods: p.conf.RTSPAuthMethods,
267+
Method: p.conf.AuthMethod,
268+
InternalUsers: p.conf.AuthInternalUsers,
269+
HTTPAddress: p.conf.AuthHTTPAddress,
270+
HTTPExclude: p.conf.AuthHTTPExclude,
271+
JWTJWKS: p.conf.AuthJWTJWKS,
272+
JWTClaimKey: p.conf.AuthJWTClaimKey,
273+
ReadTimeout: time.Duration(p.conf.ReadTimeout),
275274
}
276275
}
277276

@@ -653,8 +652,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
653652
!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||
654653
newConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||
655654
newConf.AuthJWTClaimKey != p.conf.AuthJWTClaimKey ||
656-
newConf.ReadTimeout != p.conf.ReadTimeout ||
657-
!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods)
655+
newConf.ReadTimeout != p.conf.ReadTimeout
658656
if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) {
659657
p.authManager.ReloadInternalUsers(newConf.AuthInternalUsers)
660658
}

internal/defs/path_access_request.go

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,12 @@ type PathAccessRequest struct {
3232
SkipAuth bool
3333

3434
// only if skipAuth = false
35-
User string
36-
Pass string
37-
IP net.IP
38-
Proto auth.Protocol
39-
ID *uuid.UUID
40-
41-
// RTSP only
42-
RTSPRequest *base.Request
43-
RTSPNonce string
35+
User string
36+
Pass string
37+
IP net.IP
38+
CustomVerifyFunc func(expectedUser string, expectedPass string) bool
39+
Proto auth.Protocol
40+
ID *uuid.UUID
4441
}
4542

4643
// ToAuthRequest converts a path access request into an authentication request.
@@ -55,12 +52,11 @@ func (r *PathAccessRequest) ToAuthRequest() *auth.Request {
5552
}
5653
return conf.AuthActionRead
5754
}(),
58-
Path: r.Name,
59-
Protocol: r.Proto,
60-
ID: r.ID,
61-
Query: r.Query,
62-
RTSPRequest: r.RTSPRequest,
63-
RTSPNonce: r.RTSPNonce,
55+
CustomVerifyFunc: r.CustomVerifyFunc,
56+
Path: r.Name,
57+
Protocol: r.Proto,
58+
ID: r.ID,
59+
Query: r.Query,
6460
}
6561
}
6662

@@ -69,11 +65,9 @@ func (r *PathAccessRequest) FillFromRTSPRequest(rt *base.Request) {
6965
var rtspAuthHeader headers.Authorization
7066
err := rtspAuthHeader.Unmarshal(rt.Header["Authorization"])
7167
if err == nil {
68+
r.User = rtspAuthHeader.Username
7269
if rtspAuthHeader.Method == headers.AuthMethodBasic {
73-
r.User = rtspAuthHeader.BasicUser
7470
r.Pass = rtspAuthHeader.BasicPass
75-
} else {
76-
r.User = rtspAuthHeader.Username
7771
}
7872
}
7973
}

internal/metrics/metrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func (m *Metrics) middlewareAuth(ctx *gin.Context) {
130130

131131
err := m.AuthManager.Authenticate(req)
132132
if err != nil {
133-
if err.(*auth.Error).AskCredentials { //nolint:errorlint
133+
if err.(auth.Error).AskCredentials { //nolint:errorlint
134134
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
135135
ctx.AbortWithStatus(http.StatusUnauthorized)
136136
return

0 commit comments

Comments
 (0)