fix authentication when algorithm field is not supported (#558)

(https://github.com/bluenviron/mediamtx/issues/3116)

This fixes authentication issues with some TP-LINK cameras.
This commit is contained in:
Alessandro Ros
2024-05-15 10:21:30 +02:00
committed by GitHub
parent 9f6428bdb8
commit f283abc2e7
11 changed files with 373 additions and 209 deletions

View File

@@ -1705,7 +1705,7 @@ func TestClientPlayRedirect(t *testing.T) {
err2 = conn.WriteResponse(&base.Response{ err2 = conn.WriteResponse(&base.Response{
Header: base.Header{ Header: base.Header{
"WWW-Authenticate": headers.Authenticate{ "WWW-Authenticate": headers.Authenticate{
Method: headers.AuthDigestMD5, Method: headers.AuthMethodDigest,
Realm: authRealm, Realm: authRealm,
Nonce: authNonce, Nonce: authNonce,
Opaque: &authOpaque, Opaque: &authOpaque,

View File

@@ -6,7 +6,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
) )
func mustParseURL(s string) *base.URL { func mustParseURL(s string) *base.URL {
@@ -17,22 +16,22 @@ func mustParseURL(s string) *base.URL {
return u return u
} }
func TestAuth(t *testing.T) { func TestCombined(t *testing.T) {
for _, c1 := range []struct { for _, c1 := range []struct {
name string name string
methods []headers.AuthMethod methods []ValidateMethod
}{ }{
{ {
"basic", "basic",
[]headers.AuthMethod{headers.AuthBasic}, []ValidateMethod{ValidateMethodBasic},
}, },
{ {
"digest md5", "digest md5",
[]headers.AuthMethod{headers.AuthDigestMD5}, []ValidateMethod{ValidateMethodDigestMD5},
}, },
{ {
"digest sha256", "digest sha256",
[]headers.AuthMethod{headers.AuthDigestSHA256}, []ValidateMethod{ValidateMethodSHA256},
}, },
{ {
"all", "all",
@@ -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)
}
}

View File

@@ -7,63 +7,41 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/headers" "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. // Sender allows to send credentials.
type Sender struct { type Sender struct {
user string user string
pass string pass string
authenticateHeader *headers.Authenticate authHeader *headers.Authenticate
} }
// NewSender allocates a Sender. // NewSender allocates a Sender.
// It requires a WWW-Authenticate header (provided by the server) // It requires a WWW-Authenticate header (provided by the server)
// and a set of credentials. // and a set of credentials.
func NewSender(vals base.HeaderValue, user string, pass string) (*Sender, error) { func NewSender(wwwAuth base.HeaderValue, user string, pass string) (*Sender, error) {
var auths []headers.Authenticate //nolint:prealloc var bestAuthHeader *headers.Authenticate
for _, v := range vals { for _, v := range wwwAuth {
var auth headers.Authenticate var auth headers.Authenticate
err := auth.Unmarshal(base.HeaderValue{v}) err := auth.Unmarshal(base.HeaderValue{v})
if err != nil { if err != nil {
continue // ignore unrecognized headers 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 bestAuthHeader == nil {
if err != nil { return nil, fmt.Errorf("no authentication methods available")
return nil, err
} }
return &Sender{ return &Sender{
user: user, user: user,
pass: pass, pass: pass,
authenticateHeader: auth, authHeader: bestAuthHeader,
}, nil }, nil
} }
@@ -72,29 +50,26 @@ func (se *Sender) AddAuthorization(req *base.Request) {
urStr := req.URL.CloneWithoutCredentials().String() urStr := req.URL.CloneWithoutCredentials().String()
h := headers.Authorization{ h := headers.Authorization{
Method: se.authenticateHeader.Method, Method: se.authHeader.Method,
} }
switch se.authenticateHeader.Method { if se.authHeader.Method == headers.AuthMethodBasic {
case headers.AuthBasic:
h.BasicUser = se.user h.BasicUser = se.user
h.BasicPass = se.pass h.BasicPass = se.pass
} else { // digest
case headers.AuthDigestMD5:
h.Username = se.user h.Username = se.user
h.Realm = se.authenticateHeader.Realm h.Realm = se.authHeader.Realm
h.Nonce = se.authenticateHeader.Nonce h.Nonce = se.authHeader.Nonce
h.URI = urStr h.URI = urStr
h.Response = md5Hex(md5Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" + h.Algorithm = se.authHeader.Algorithm
se.authenticateHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
default: // digest SHA-256 if se.authHeader.Algorithm == nil || *se.authHeader.Algorithm == headers.AuthAlgorithmMD5 {
h.Username = se.user h.Response = md5Hex(md5Hex(se.user+":"+se.authHeader.Realm+":"+se.pass) + ":" +
h.Realm = se.authenticateHeader.Realm se.authHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
h.Nonce = se.authenticateHeader.Nonce } else { // sha256
h.URI = urStr h.Response = sha256Hex(sha256Hex(se.user+":"+se.authHeader.Realm+":"+se.pass) + ":" +
h.Response = sha256Hex(sha256Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" + se.authHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr))
se.authenticateHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr)) }
} }
if req.Header == nil { if req.Header == nil {

View File

@@ -4,8 +4,98 @@ import (
"testing" "testing"
"github.com/bluenviron/gortsplib/v4/pkg/base" "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) { func FuzzSender(f *testing.F) {
f.Add(`Invalid`) f.Add(`Invalid`)
f.Add(`Digest`) f.Add(`Digest`)

View File

@@ -25,7 +25,7 @@ func sha256Hex(in string) string {
return hex.EncodeToString(h.Sum(nil)) 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 { for _, i := range list {
if i == item { if i == item {
return true return true
@@ -51,17 +51,27 @@ func urlMatches(expected string, received string, isSetup bool) bool {
return false 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. // Validate validates a request sent by a client.
func Validate( func Validate(
req *base.Request, req *base.Request,
user string, user string,
pass string, pass string,
methods []headers.AuthMethod, methods []ValidateMethod,
realm string, realm string,
nonce string, nonce string,
) error { ) error {
if methods == nil { if methods == nil {
methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic} methods = []ValidateMethod{ValidateMethodBasic, ValidateMethodDigestMD5, ValidateMethodSHA256}
} }
var auth headers.Authorization var auth headers.Authorization
@@ -71,8 +81,11 @@ func Validate(
} }
switch { switch {
case (auth.Method == headers.AuthDigestSHA256 && contains(methods, headers.AuthDigestSHA256)) || case auth.Method == headers.AuthMethodDigest &&
(auth.Method == headers.AuthDigestMD5 && contains(methods, headers.AuthDigestMD5)): (contains(methods, ValidateMethodDigestMD5) &&
(auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5) ||
contains(methods, ValidateMethodSHA256) &&
auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256):
if auth.Nonce != nonce { if auth.Nonce != nonce {
return fmt.Errorf("wrong nonce") return fmt.Errorf("wrong nonce")
} }
@@ -91,19 +104,19 @@ func Validate(
var response string var response string
if auth.Method == headers.AuthDigestSHA256 { if auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5 {
response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + sha256Hex(string(req.Method)+":"+auth.URI))
} else {
response = md5Hex(md5Hex(user+":"+realm+":"+pass) + response = md5Hex(md5Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + md5Hex(string(req.Method)+":"+auth.URI)) ":" + 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 { if auth.Response != response {
return fmt.Errorf("authentication failed") 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 { if auth.BasicUser != user {
return fmt.Errorf("authentication failed") return fmt.Errorf("authentication failed")
} }

View File

@@ -1,13 +1,88 @@
package auth package auth
import ( import (
"fmt"
"testing" "testing"
"github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestValidate(t *testing.T) {
for _, ca := range []struct {
name string
authorization base.HeaderValue
}{
{
"basic",
base.HeaderValue{
"Basic bXl1c2VyOm15cGFzcw==",
},
},
{
"digest md5 implicit",
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 username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", response=\"ba6e9cccbfeb38db775378a0a9067ba5\", " +
"algorithm=\"MD5\"",
},
},
{
"digest sha256",
base.HeaderValue{
"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
"uri=\"rtsp://myhost/mypath?key=val/trackID=3\", " +
"response=\"e298296ce35c9ab79699c8f3f9508944c1be9395e892f8205b6d66f1b8e663ee\", " +
"algorithm=\"SHA-256\"",
},
},
{
"digest vlc",
base.HeaderValue{
"Digest username=\"myuser\", realm=\"myrealm\", nonce=\"f49ac6dd0ba708d4becddc9692d1f2ce\", " +
"uri=\"rtsp://myhost/mypath?key=val/\", response=\"5ca5ceeca20a05e9a3f49ecde4b42655\"",
},
},
} {
t.Run(ca.name, func(t *testing.T) {
se, err := NewSender(
GenerateWWWAuthenticate([]ValidateMethod{ValidateMethodDigestMD5}, "myrealm", "f49ac6dd0ba708d4becddc9692d1f2ce"),
"myuser",
"mypass")
require.NoError(t, err)
req1 := &base.Request{
Method: base.Setup,
URL: mustParseURL("rtsp://myhost/mypath?key=val/"),
}
se.AddAuthorization(req1)
fmt.Println(req1.Header)
req := &base.Request{
Method: base.Setup,
URL: mustParseURL("rtsp://myhost/mypath?key=val/trackID=3"),
Header: base.Header{
"Authorization": ca.authorization,
},
}
err = Validate(
req,
"myuser",
"mypass",
nil,
"myrealm",
"f49ac6dd0ba708d4becddc9692d1f2ce")
require.NoError(t, err)
})
}
}
func FuzzValidate(f *testing.F) { func FuzzValidate(f *testing.F) {
f.Add(`Invalid`) f.Add(`Invalid`)
f.Add(`Digest `) f.Add(`Digest `)
@@ -35,21 +110,3 @@ func FuzzValidate(f *testing.F) {
) )
}) })
} }
func TestValidateAdditionalErrors(t *testing.T) {
err := Validate(
&base.Request{
Method: base.Describe,
URL: nil,
Header: base.Header{
"Authorization": base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="},
},
},
"myuser",
"mypass",
[]headers.AuthMethod{headers.AuthDigestMD5},
"IPCAM",
"abcde",
)
require.Error(t, err)
}

View File

@@ -6,18 +6,44 @@ import (
) )
// GenerateWWWAuthenticate generates a WWW-Authenticate header. // GenerateWWWAuthenticate generates a WWW-Authenticate header.
func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce string) base.HeaderValue { func GenerateWWWAuthenticate(methods []ValidateMethod, realm string, nonce string) base.HeaderValue {
if methods == nil { if methods == nil {
methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic} methods = []ValidateMethod{ValidateMethodBasic, ValidateMethodDigestMD5, ValidateMethodSHA256}
} }
var ret base.HeaderValue var ret base.HeaderValue
for _, m := range methods { for _, m := range methods {
ret = append(ret, headers.Authenticate{ var a base.HeaderValue
Method: m,
Realm: realm, switch m {
Nonce: nonce, // used only by digest case ValidateMethodBasic:
}.Marshal()...) a = headers.Authenticate{
Method: headers.AuthMethodBasic,
Realm: realm,
}.Marshal()
case ValidateMethodDigestMD5:
aa := headers.AuthAlgorithmMD5
a = headers.Authenticate{
Method: headers.AuthMethodDigest,
Realm: realm,
Nonce: nonce,
Algorithm: &aa,
}.Marshal()
default: // sha256
aa := headers.AuthAlgorithmSHA256
a = headers.Authenticate{
Method: headers.AuthMethodDigest,
Realm: realm,
Nonce: nonce,
Algorithm: &aa,
}.Marshal()
}
ret = append(ret, a...)
} }
return ret return ret
} }

View File

@@ -11,34 +11,31 @@ import (
// AuthMethod is an authentication method. // AuthMethod is an authentication method.
type AuthMethod int type AuthMethod int
// authentication methods.
const ( const (
// AuthBasic is the Basic authentication method AuthMethodBasic AuthMethod = iota
AuthBasic AuthMethod = iota AuthMethodDigest
// AuthDigestMD5 is the Digest authentication method with the MD5 hash
AuthDigestMD5
// AuthDigestSHA256 is the Digest authentication method with the SHA-256 hash
AuthDigestSHA256
) )
// AuthAlgorithm is a digest algorithm.
type AuthAlgorithm int
// digest algorithms.
const ( const (
// AuthDigest is an alias for AuthDigestMD5 AuthAlgorithmMD5 AuthAlgorithm = iota
// AuthAlgorithmSHA256
// Deprecated: replaced by AuthDigestMD5
AuthDigest = AuthDigestMD5
) )
func algorithmToMethod(v *string) (AuthMethod, error) { func parseAuthAlgorithm(v string) (AuthAlgorithm, error) {
switch { switch {
case v == nil, strings.ToLower(*v) == "md5": case strings.ToLower(v) == "md5":
return AuthDigestMD5, nil return AuthAlgorithmMD5, nil
case strings.ToLower(*v) == "sha-256": case strings.ToLower(v) == "sha-256":
return AuthDigestSHA256, nil return AuthAlgorithmSHA256, nil
default: default:
return 0, fmt.Errorf("unrecognized algorithm: %v", *v) return 0, fmt.Errorf("unrecognized algorithm: %v", v)
} }
} }
@@ -62,6 +59,9 @@ type Authenticate struct {
// stale // stale
Stale *string Stale *string
// algorithm
Algorithm *AuthAlgorithm
} }
// Unmarshal decodes a WWW-Authenticate header. // Unmarshal decodes a WWW-Authenticate header.
@@ -82,20 +82,18 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
} }
method, v0 := v0[:i], v0[i+1:] method, v0 := v0[:i], v0[i+1:]
isDigest := false
switch method { switch method {
case "Basic": case "Basic":
h.Method = AuthBasic h.Method = AuthMethodBasic
case "Digest": case "Digest":
isDigest = true h.Method = AuthMethodDigest
default: default:
return fmt.Errorf("invalid method (%s)", method) return fmt.Errorf("invalid method (%s)", method)
} }
if !isDigest { if h.Method == AuthMethodBasic {
kvs, err := keyValParse(v0, ',') kvs, err := keyValParse(v0, ',')
if err != nil { if err != nil {
return err return err
@@ -123,7 +121,6 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
realmReceived := false realmReceived := false
nonceReceived := false nonceReceived := false
var algorithm *string
for k, rv := range kvs { for k, rv := range kvs {
v := rv v := rv
@@ -144,18 +141,17 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
h.Stale = &v h.Stale = &v
case "algorithm": case "algorithm":
algorithm = &v a, err := parseAuthAlgorithm(v)
if err != nil {
return err
}
h.Algorithm = &a
} }
} }
if !realmReceived || !nonceReceived { if !realmReceived || !nonceReceived {
return fmt.Errorf("one or more digest fields are missing") return fmt.Errorf("one or more digest fields are missing")
} }
h.Method, err = algorithmToMethod(algorithm)
if err != nil {
return err
}
} }
return nil return nil
@@ -163,7 +159,7 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
// Marshal encodes a WWW-Authenticate header. // Marshal encodes a WWW-Authenticate header.
func (h Authenticate) Marshal() base.HeaderValue { func (h Authenticate) Marshal() base.HeaderValue {
if h.Method == AuthBasic { if h.Method == AuthMethodBasic {
return base.HeaderValue{"Basic " + return base.HeaderValue{"Basic " +
"realm=\"" + h.Realm + "\""} "realm=\"" + h.Realm + "\""}
} }
@@ -178,10 +174,12 @@ func (h Authenticate) Marshal() base.HeaderValue {
ret += ", stale=\"" + *h.Stale + "\"" ret += ", stale=\"" + *h.Stale + "\""
} }
if h.Method == AuthDigestMD5 { if h.Algorithm != nil {
ret += ", algorithm=\"MD5\"" if *h.Algorithm == AuthAlgorithmMD5 {
} else { ret += ", algorithm=\"MD5\""
ret += ", algorithm=\"SHA-256\"" } else {
ret += ", algorithm=\"SHA-256\""
}
} }
return base.HeaderValue{ret} return base.HeaderValue{ret}

View File

@@ -23,7 +23,7 @@ var casesAuthenticate = []struct {
base.HeaderValue{`Basic realm="4419b63f5e51"`}, base.HeaderValue{`Basic realm="4419b63f5e51"`},
base.HeaderValue{`Basic realm="4419b63f5e51"`}, base.HeaderValue{`Basic realm="4419b63f5e51"`},
Authenticate{ Authenticate{
Method: AuthBasic, Method: AuthMethodBasic,
Realm: "4419b63f5e51", Realm: "4419b63f5e51",
}, },
}, },
@@ -31,9 +31,9 @@ var casesAuthenticate = []struct {
"digest 1", "digest 1",
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` + base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
`stale="FALSE", algorithm="MD5"`}, `stale="FALSE"`},
Authenticate{ Authenticate{
Method: AuthDigestMD5, Method: AuthMethodDigest,
Realm: "4419b63f5e51", Realm: "4419b63f5e51",
Nonce: "8b84a3b789283a8bea8da7fa7d41f08b", Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"), Stale: stringPtr("FALSE"),
@@ -43,9 +43,9 @@ var casesAuthenticate = []struct {
"digest 2", "digest 2",
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` + base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
`stale="FALSE", algorithm="MD5"`}, `stale="FALSE"`},
Authenticate{ Authenticate{
Method: AuthDigestMD5, Method: AuthMethodDigest,
Realm: "4419b63f5e51", Realm: "4419b63f5e51",
Nonce: "8b84a3b789283a8bea8da7fa7d41f08b", Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"), Stale: stringPtr("FALSE"),
@@ -55,9 +55,9 @@ var casesAuthenticate = []struct {
"digest 3", "digest 3",
base.HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", ` + base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", ` +
`stale="FALSE", algorithm="MD5"`}, `stale="FALSE"`},
Authenticate{ Authenticate{
Method: AuthDigestMD5, Method: AuthMethodDigest,
Realm: "4419b63f5e51", Realm: "4419b63f5e51",
Nonce: "133767111917411116111311118211673010032", Nonce: "133767111917411116111311118211673010032",
Stale: stringPtr("FALSE"), Stale: stringPtr("FALSE"),
@@ -66,17 +66,31 @@ var casesAuthenticate = []struct {
{ {
"digest after failed auth", "digest after failed auth",
base.HeaderValue{`Digest realm="Please log in with a valid username",` + base.HeaderValue{`Digest realm="Please log in with a valid username",` +
`nonce="752a62306daf32b401a41004555c7663",opaque="",stale=FALSE,algorithm=MD5`}, `nonce="752a62306daf32b401a41004555c7663",opaque="",stale=FALSE`},
base.HeaderValue{`Digest realm="Please log in with a valid username", ` + base.HeaderValue{`Digest realm="Please log in with a valid username", ` +
`nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE", algorithm="MD5"`}, `nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE"`},
Authenticate{ Authenticate{
Method: AuthDigestMD5, Method: AuthMethodDigest,
Realm: "Please log in with a valid username", Realm: "Please log in with a valid username",
Nonce: "752a62306daf32b401a41004555c7663", Nonce: "752a62306daf32b401a41004555c7663",
Opaque: stringPtr(""), Opaque: stringPtr(""),
Stale: stringPtr("FALSE"), Stale: stringPtr("FALSE"),
}, },
}, },
{
"digest md5 explicit",
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
`stale="FALSE", algorithm="MD5"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
`stale="FALSE", algorithm="MD5"`},
Authenticate{
Method: AuthMethodDigest,
Realm: "4419b63f5e51",
Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"),
Algorithm: algorithmPtr(AuthAlgorithmMD5),
},
},
{ {
"digest sha256", "digest sha256",
base.HeaderValue{`Digest realm="IP Camera(AB705)", ` + base.HeaderValue{`Digest realm="IP Camera(AB705)", ` +
@@ -84,10 +98,11 @@ var casesAuthenticate = []struct {
base.HeaderValue{`Digest realm="IP Camera(AB705)", ` + base.HeaderValue{`Digest realm="IP Camera(AB705)", ` +
`nonce="fcc86deace979a488b2bfb89f4d0812c", stale="FALSE", algorithm="SHA-256"`}, `nonce="fcc86deace979a488b2bfb89f4d0812c", stale="FALSE", algorithm="SHA-256"`},
Authenticate{ Authenticate{
Method: AuthDigestSHA256, Method: AuthMethodDigest,
Realm: "IP Camera(AB705)", Realm: "IP Camera(AB705)",
Nonce: "fcc86deace979a488b2bfb89f4d0812c", Nonce: "fcc86deace979a488b2bfb89f4d0812c",
Stale: stringPtr("FALSE"), Stale: stringPtr("FALSE"),
Algorithm: algorithmPtr(AuthAlgorithmSHA256),
}, },
}, },
} }

View File

@@ -44,6 +44,9 @@ type Authorization struct {
// opaque // opaque
Opaque *string Opaque *string
// algorithm
Algorithm *AuthAlgorithm
} }
// Unmarshal decodes an Authorization header. // Unmarshal decodes an Authorization header.
@@ -64,20 +67,18 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
} }
method, v0 := v0[:i], v0[i+1:] method, v0 := v0[:i], v0[i+1:]
isDigest := false
switch method { switch method {
case "Basic": case "Basic":
h.Method = AuthBasic h.Method = AuthMethodBasic
case "Digest": case "Digest":
isDigest = true h.Method = AuthMethodDigest
default: default:
return fmt.Errorf("invalid method (%s)", method) return fmt.Errorf("invalid method (%s)", method)
} }
if !isDigest { if h.Method == AuthMethodBasic {
tmp, err := base64.StdEncoding.DecodeString(v0) tmp, err := base64.StdEncoding.DecodeString(v0)
if err != nil { if err != nil {
return fmt.Errorf("invalid value") return fmt.Errorf("invalid value")
@@ -100,7 +101,6 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
nonceReceived := false nonceReceived := false
uriReceived := false uriReceived := false
responseReceived := false responseReceived := false
var algorithm *string
for k, rv := range kvs { for k, rv := range kvs {
v := rv v := rv
@@ -130,18 +130,17 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
h.Opaque = &v h.Opaque = &v
case "algorithm": case "algorithm":
algorithm = &v a, err := parseAuthAlgorithm(v)
if err != nil {
return err
}
h.Algorithm = &a
} }
} }
if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived { if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived {
return fmt.Errorf("one or more digest fields are missing") return fmt.Errorf("one or more digest fields are missing")
} }
h.Method, err = algorithmToMethod(algorithm)
if err != nil {
return err
}
} }
return nil return nil
@@ -149,7 +148,7 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
// Marshal encodes an Authorization header. // Marshal encodes an Authorization header.
func (h Authorization) Marshal() base.HeaderValue { func (h Authorization) Marshal() base.HeaderValue {
if h.Method == AuthBasic { if h.Method == AuthMethodBasic {
return base.HeaderValue{"Basic " + return base.HeaderValue{"Basic " +
base64.StdEncoding.EncodeToString([]byte(h.BasicUser+":"+h.BasicPass))} base64.StdEncoding.EncodeToString([]byte(h.BasicUser+":"+h.BasicPass))}
} }
@@ -162,10 +161,12 @@ func (h Authorization) Marshal() base.HeaderValue {
ret += ", opaque=\"" + *h.Opaque + "\"" ret += ", opaque=\"" + *h.Opaque + "\""
} }
if h.Method == AuthDigestMD5 { if h.Algorithm != nil {
ret += ", algorithm=\"MD5\"" if *h.Algorithm == AuthAlgorithmMD5 {
} else { ret += ", algorithm=\"MD5\""
ret += ", algorithm=\"SHA-256\"" } else {
ret += ", algorithm=\"SHA-256\""
}
} }
return base.HeaderValue{ret} return base.HeaderValue{ret}

View File

@@ -8,6 +8,10 @@ import (
"github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/base"
) )
func algorithmPtr(v AuthAlgorithm) *AuthAlgorithm {
return &v
}
var casesAuthorization = []struct { var casesAuthorization = []struct {
name string name string
vin base.HeaderValue vin base.HeaderValue
@@ -19,7 +23,7 @@ var casesAuthorization = []struct {
base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="}, base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="},
base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="}, base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="},
Authorization{ Authorization{
Method: AuthBasic, Method: AuthMethodBasic,
BasicUser: "myuser", BasicUser: "myuser",
BasicPass: "mypass", BasicPass: "mypass",
}, },
@@ -31,9 +35,9 @@ var casesAuthorization = []struct {
`uri="/dir/index.html", response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`}, `uri="/dir/index.html", response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`},
base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` + base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", ` + `nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", ` +
`response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm="MD5"`}, `response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`},
Authorization{ Authorization{
Method: AuthDigestMD5, Method: AuthMethodDigest,
Username: "Mufasa", Username: "Mufasa",
Realm: "testrealm@host.com", Realm: "testrealm@host.com",
Nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093", Nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093",
@@ -49,9 +53,9 @@ var casesAuthorization = []struct {
`response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, `response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
base.HeaderValue{`Digest username="", realm="IPCAM", ` + base.HeaderValue{`Digest username="", realm="IPCAM", ` +
`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` + `nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
`response="c072ae90eb4a27f4cdcb90d62266b2a1", algorithm="MD5"`}, `response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
Authorization{ Authorization{
Method: AuthDigestMD5, Method: AuthMethodDigest,
Username: "", Username: "",
Realm: "IPCAM", Realm: "IPCAM",
Nonce: "5d17cd12b9fa8a85ac5ceef0926ea5a6", Nonce: "5d17cd12b9fa8a85ac5ceef0926ea5a6",
@@ -59,6 +63,26 @@ var casesAuthorization = []struct {
Response: "c072ae90eb4a27f4cdcb90d62266b2a1", Response: "c072ae90eb4a27f4cdcb90d62266b2a1",
}, },
}, },
{
"digest explicit md5",
base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", ` +
`uri="/dir/index.html", response="e966c932a9242554e42c8ee200cec7f6", ` +
`opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm="MD5"`},
base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", ` +
`response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm="MD5"`},
Authorization{
Method: AuthMethodDigest,
Username: "Mufasa",
Realm: "testrealm@host.com",
Nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093",
URI: "/dir/index.html",
Response: "e966c932a9242554e42c8ee200cec7f6",
Opaque: stringPtr("5ccc069c403ebaf9f0171e9517f40e41"),
Algorithm: algorithmPtr(AuthAlgorithmMD5),
},
},
{ {
"digest sha256", "digest sha256",
base.HeaderValue{`Digest username="admin", realm="IP Camera(AB705)", ` + base.HeaderValue{`Digest username="admin", realm="IP Camera(AB705)", ` +
@@ -68,12 +92,13 @@ var casesAuthorization = []struct {
`nonce="1ad195c2b2ca5a03784e53f88e16f579", uri="rtsp://192.168.80.76/", ` + `nonce="1ad195c2b2ca5a03784e53f88e16f579", uri="rtsp://192.168.80.76/", ` +
`response="9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", algorithm="SHA-256"`}, `response="9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", algorithm="SHA-256"`},
Authorization{ Authorization{
Method: AuthDigestSHA256, Method: AuthMethodDigest,
Username: "admin", Username: "admin",
Realm: "IP Camera(AB705)", Realm: "IP Camera(AB705)",
Nonce: "1ad195c2b2ca5a03784e53f88e16f579", Nonce: "1ad195c2b2ca5a03784e53f88e16f579",
URI: "rtsp://192.168.80.76/", URI: "rtsp://192.168.80.76/",
Response: "9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", Response: "9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913",
Algorithm: algorithmPtr(AuthAlgorithmSHA256),
}, },
}, },
} }