Skip to content

Commit

Permalink
fix authentication when algorithm field is not supported (#558)
Browse files Browse the repository at this point in the history
(bluenviron/mediamtx#3116)

This fixes authentication issues with some TP-LINK cameras.
  • Loading branch information
aler9 authored May 15, 2024
1 parent 9f6428b commit f283abc
Show file tree
Hide file tree
Showing 11 changed files with 374 additions and 210 deletions.
2 changes: 1 addition & 1 deletion client_play_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1705,7 +1705,7 @@ func TestClientPlayRedirect(t *testing.T) {
err2 = conn.WriteResponse(&base.Response{
Header: base.Header{
"WWW-Authenticate": headers.Authenticate{
Method: headers.AuthDigestMD5,
Method: headers.AuthMethodDigest,
Realm: authRealm,
Nonce: authNonce,
Opaque: &authOpaque,
Expand Down
46 changes: 5 additions & 41 deletions pkg/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/require"

"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
)

func mustParseURL(s string) *base.URL {
Expand All @@ -17,22 +16,22 @@ func mustParseURL(s string) *base.URL {
return u
}

func TestAuth(t *testing.T) {
func TestCombined(t *testing.T) {
for _, c1 := range []struct {
name string
methods []headers.AuthMethod
methods []ValidateMethod
}{
{
"basic",
[]headers.AuthMethod{headers.AuthBasic},
[]ValidateMethod{ValidateMethodBasic},
},
{
"digest md5",
[]headers.AuthMethod{headers.AuthDigestMD5},
[]ValidateMethod{ValidateMethodDigestMD5},
},
{
"digest sha256",
[]headers.AuthMethod{headers.AuthDigestSHA256},
[]ValidateMethod{ValidateMethodSHA256},
},
{
"all",
Expand Down Expand Up @@ -93,38 +92,3 @@ func TestAuth(t *testing.T) {
}
}
}

func TestAuthVLC(t *testing.T) {
for _, ca := range []struct {
baseURL string
mediaURL string
}{
{
"rtsp://myhost/mypath/",
"rtsp://myhost/mypath/trackID=0",
},
{
"rtsp://myhost/mypath/test?testing/",
"rtsp://myhost/mypath/test?testing/trackID=0",
},
} {
nonce, err := GenerateNonce()
require.NoError(t, err)

se, err := NewSender(
GenerateWWWAuthenticate(nil, "IPCAM", nonce),
"testuser",
"testpass")
require.NoError(t, err)

req := &base.Request{
Method: base.Setup,
URL: mustParseURL(ca.baseURL),
}
se.AddAuthorization(req)
req.URL = mustParseURL(ca.mediaURL)

err = Validate(req, "testuser", "testpass", nil, "IPCAM", nonce)
require.NoError(t, err)
}
}
85 changes: 30 additions & 55 deletions pkg/auth/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,41 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/headers"
)

func findAuthenticateHeader(auths []headers.Authenticate, method headers.AuthMethod) *headers.Authenticate {
for _, auth := range auths {
if auth.Method == method {
return &auth
}
}
return nil
}

func pickAuthenticateHeader(auths []headers.Authenticate) (*headers.Authenticate, error) {
if auth := findAuthenticateHeader(auths, headers.AuthDigestSHA256); auth != nil {
return auth, nil
}

if auth := findAuthenticateHeader(auths, headers.AuthDigestMD5); auth != nil {
return auth, nil
}

if auth := findAuthenticateHeader(auths, headers.AuthBasic); auth != nil {
return auth, nil
}

return nil, fmt.Errorf("no authentication methods available")
}

// Sender allows to send credentials.
type Sender struct {
user string
pass string
authenticateHeader *headers.Authenticate
user string
pass string
authHeader *headers.Authenticate
}

// NewSender allocates a Sender.
// It requires a WWW-Authenticate header (provided by the server)
// and a set of credentials.
func NewSender(vals base.HeaderValue, user string, pass string) (*Sender, error) {
var auths []headers.Authenticate //nolint:prealloc
func NewSender(wwwAuth base.HeaderValue, user string, pass string) (*Sender, error) {
var bestAuthHeader *headers.Authenticate

for _, v := range vals {
for _, v := range wwwAuth {
var auth headers.Authenticate
err := auth.Unmarshal(base.HeaderValue{v})
if err != nil {
continue // ignore unrecognized headers
}

auths = append(auths, auth)
if bestAuthHeader == nil ||
(auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256) ||
(bestAuthHeader.Method == headers.AuthMethodBasic) {
bestAuthHeader = &auth
}
}

auth, err := pickAuthenticateHeader(auths)
if err != nil {
return nil, err
if bestAuthHeader == nil {
return nil, fmt.Errorf("no authentication methods available")
}

return &Sender{
user: user,
pass: pass,
authenticateHeader: auth,
user: user,
pass: pass,
authHeader: bestAuthHeader,
}, nil
}

Expand All @@ -72,29 +50,26 @@ func (se *Sender) AddAuthorization(req *base.Request) {
urStr := req.URL.CloneWithoutCredentials().String()

h := headers.Authorization{
Method: se.authenticateHeader.Method,
Method: se.authHeader.Method,
}

switch se.authenticateHeader.Method {
case headers.AuthBasic:
if se.authHeader.Method == headers.AuthMethodBasic {
h.BasicUser = se.user
h.BasicPass = se.pass

case headers.AuthDigestMD5:
} else { // digest
h.Username = se.user
h.Realm = se.authenticateHeader.Realm
h.Nonce = se.authenticateHeader.Nonce
h.Realm = se.authHeader.Realm
h.Nonce = se.authHeader.Nonce
h.URI = urStr
h.Response = md5Hex(md5Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
se.authenticateHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))

default: // digest SHA-256
h.Username = se.user
h.Realm = se.authenticateHeader.Realm
h.Nonce = se.authenticateHeader.Nonce
h.URI = urStr
h.Response = sha256Hex(sha256Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
se.authenticateHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr))
h.Algorithm = se.authHeader.Algorithm

if se.authHeader.Algorithm == nil || *se.authHeader.Algorithm == headers.AuthAlgorithmMD5 {
h.Response = md5Hex(md5Hex(se.user+":"+se.authHeader.Realm+":"+se.pass) + ":" +
se.authHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
} else { // sha256
h.Response = sha256Hex(sha256Hex(se.user+":"+se.authHeader.Realm+":"+se.pass) + ":" +
se.authHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr))
}
}

if req.Header == nil {
Expand Down
90 changes: 90 additions & 0 deletions pkg/auth/sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,98 @@ import (
"testing"

"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/stretchr/testify/require"
)

func TestSender(t *testing.T) {
for _, ca := range []struct {
name string
wwwAuthenticate base.HeaderValue
authorization base.HeaderValue
}{
{
"basic",
base.HeaderValue{
"Basic realm=testrealm",
},
base.HeaderValue{
"Basic bXl1c2VyOm15cGFzcw==",
},
},
{
"digest md5 implicit",
base.HeaderValue{
`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce"`,
},
base.HeaderValue{
"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\"",
},
},
{
"digest md5 explicit",
base.HeaderValue{
`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce", algorithm="MD5"`,
},
base.HeaderValue{
"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\", " +
"algorithm=\"MD5\"",
},
},
{
"digest sha256",
base.HeaderValue{
`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce", algorithm="SHA-256"`,
},
base.HeaderValue{
"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", " +
"response=\"e298296ce35c9ab79699c8f3f9508944c1be9395e892f8205b6d66f1b8e663ee\", " +
"algorithm=\"SHA-256\"",
},
},
{
"multiple 1",
base.HeaderValue{
"Basic realm=testrealm",
`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce"`,
},
base.HeaderValue{
"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\"",
},
},
{
"multiple 2",
base.HeaderValue{
"Basic realm=testrealm",
`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce", algorithm="MD5"`,
`Digest realm="myrealm", nonce="f49ac6dd0ba708d4becddc9692d1f2ce", algorithm="SHA-256"`,
},
base.HeaderValue{
"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", " +
"response=\"e298296ce35c9ab79699c8f3f9508944c1be9395e892f8205b6d66f1b8e663ee\", " +
"algorithm=\"SHA-256\"",
},
},
} {
t.Run(ca.name, func(t *testing.T) {
se, err := NewSender(ca.wwwAuthenticate, "myuser", "mypass")
require.NoError(t, err)

req := &base.Request{
Method: base.Setup,
URL: mustParseURL("rtsp://myhost/mypath?key=val/trackID=3"),
}
se.AddAuthorization(req)

require.Equal(t, ca.authorization, req.Header["Authorization"])
})
}
}

func FuzzSender(f *testing.F) {
f.Add(`Invalid`)
f.Add(`Digest`)
Expand Down
33 changes: 23 additions & 10 deletions pkg/auth/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func sha256Hex(in string) string {
return hex.EncodeToString(h.Sum(nil))
}

func contains(list []headers.AuthMethod, item headers.AuthMethod) bool {
func contains(list []ValidateMethod, item ValidateMethod) bool {
for _, i := range list {
if i == item {
return true
Expand All @@ -51,17 +51,27 @@ func urlMatches(expected string, received string, isSetup bool) bool {
return false
}

// ValidateMethod is a validation method.
type ValidateMethod int

// validation methods.
const (
ValidateMethodBasic ValidateMethod = iota
ValidateMethodDigestMD5
ValidateMethodSHA256
)

// Validate validates a request sent by a client.
func Validate(
req *base.Request,
user string,
pass string,
methods []headers.AuthMethod,
methods []ValidateMethod,
realm string,
nonce string,
) error {
if methods == nil {
methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic}
methods = []ValidateMethod{ValidateMethodBasic, ValidateMethodDigestMD5, ValidateMethodSHA256}
}

var auth headers.Authorization
Expand All @@ -71,8 +81,11 @@ func Validate(
}

switch {
case (auth.Method == headers.AuthDigestSHA256 && contains(methods, headers.AuthDigestSHA256)) ||
(auth.Method == headers.AuthDigestMD5 && contains(methods, headers.AuthDigestMD5)):
case auth.Method == headers.AuthMethodDigest &&
(contains(methods, ValidateMethodDigestMD5) &&
(auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5) ||
contains(methods, ValidateMethodSHA256) &&
auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256):
if auth.Nonce != nonce {
return fmt.Errorf("wrong nonce")
}
Expand All @@ -91,19 +104,19 @@ func Validate(

var response string

if auth.Method == headers.AuthDigestSHA256 {
response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + sha256Hex(string(req.Method)+":"+auth.URI))
} else {
if auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5 {
response = md5Hex(md5Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + md5Hex(string(req.Method)+":"+auth.URI))
} else { // sha256
response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + sha256Hex(string(req.Method)+":"+auth.URI))
}

if auth.Response != response {
return fmt.Errorf("authentication failed")
}

case auth.Method == headers.AuthBasic && contains(methods, headers.AuthBasic):
case auth.Method == headers.AuthMethodBasic && contains(methods, ValidateMethodBasic):
if auth.BasicUser != user {
return fmt.Errorf("authentication failed")
}
Expand Down
Loading

0 comments on commit f283abc

Please sign in to comment.