Skip to content

Commit 386be42

Browse files
authored
rtsp: rewrite authentication around ServerConn.VerifyCredentials (#4267)
1 parent 7ade289 commit 386be42

File tree

21 files changed

+165
-218
lines changed

21 files changed

+165
-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.20250218163904-55556f1ecfa2
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.20250218163904-55556f1ecfa2 h1:GgPHJTUYTwKBVu/bf0SVSCdXxRz9AMcLnV7tDmiVYko=
37+
github.com/bluenviron/gortsplib/v4 v4.12.4-0.20250218163904-55556f1ecfa2/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/conf/auth_internal_users.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package conf
22

33
import (
4+
"fmt"
5+
46
"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper"
57
)
68

@@ -18,6 +20,25 @@ type AuthInternalUser struct {
1820
Permissions []AuthInternalUserPermission `json:"permissions"`
1921
}
2022

23+
// UnmarshalJSON implements json.Unmarshaler.
24+
func (d *AuthInternalUser) UnmarshalJSON(b []byte) error {
25+
type alias AuthInternalUser
26+
if err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {
27+
return err
28+
}
29+
30+
// https://github.com/bluenviron/gortsplib/blob/55556f1ecfa2bd51b29fe14eddd70512a0361cbd/server_conn.go#L155-L156
31+
if d.User == "" {
32+
return fmt.Errorf("empty usernames are not supported")
33+
}
34+
35+
if d.User == "any" && d.Pass != "" {
36+
return fmt.Errorf("using a password with 'any' user is not supported")
37+
}
38+
39+
return nil
40+
}
41+
2142
// AuthInternalUsers is a list of AuthInternalUser
2243
type AuthInternalUsers []AuthInternalUser
2344

internal/conf/conf_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,24 @@ func TestConfErrors(t *testing.T) {
407407
" sourceRedirect: invalid://invalid",
408408
`'sourceRedirect' is useless when source is not 'redirect'`,
409409
},
410+
{
411+
"invalid user",
412+
"authInternalUsers:\n" +
413+
"- user:\n" +
414+
" pass: test\n" +
415+
" permissions:\n" +
416+
" - action: publish\n",
417+
"empty usernames are not supported",
418+
},
419+
{
420+
"invalid pass",
421+
"authInternalUsers:\n" +
422+
"- user: any\n" +
423+
" pass: test\n" +
424+
" permissions:\n" +
425+
" - action: publish\n",
426+
`using a password with 'any' user is not supported`,
427+
},
410428
} {
411429
t.Run(ca.name, func(t *testing.T) {
412430
tmpf, err := createTempFile([]byte(ca.conf))

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
}

0 commit comments

Comments
 (0)