Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix authentication when algorithm field is not supported #558

Merged
merged 1 commit into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading