From 425dd682321c8d3c54f6c7e947ae4b8a84eb6779 Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Fri, 3 May 2024 23:37:59 +0200 Subject: [PATCH] keep original algorithm in auth responses (https://github.com/bluenviron/mediamtx/issues/3116) This fixes authentication issues with some TP-LINK cameras. --- client_play_test.go | 2 +- pkg/auth/auth_test.go | 49 ++-------------- pkg/auth/sender.go | 85 ++++++++++----------------- pkg/auth/sender_test.go | 90 +++++++++++++++++++++++++++++ pkg/auth/validate.go | 33 +++++++---- pkg/auth/validate_test.go | 95 ++++++++++++++++++++++++------- pkg/auth/www_authenticate.go | 40 ++++++++++--- pkg/headers/authenticate.go | 68 +++++++++++----------- pkg/headers/authenticate_test.go | 43 +++++++++----- pkg/headers/authorization.go | 35 ++++++------ pkg/headers/authorization_test.go | 47 +++++++++++---- 11 files changed, 374 insertions(+), 213 deletions(-) diff --git a/client_play_test.go b/client_play_test.go index cc69a8f8..9b5ece83 100644 --- a/client_play_test.go +++ b/client_play_test.go @@ -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, diff --git a/pkg/auth/auth_test.go b/pkg/auth/auth_test.go index 61b8b3b4..e0d5c36b 100644 --- a/pkg/auth/auth_test.go +++ b/pkg/auth/auth_test.go @@ -1,13 +1,11 @@ package auth import ( - "fmt" "testing" "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 { @@ -18,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", @@ -94,40 +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) - - fmt.Println(req.URL, req.Header) - - err = Validate(req, "testuser", "testpass", nil, "IPCAM", nonce) - require.NoError(t, err) - } -} diff --git a/pkg/auth/sender.go b/pkg/auth/sender.go index 0fd14c88..de88227a 100644 --- a/pkg/auth/sender.go +++ b/pkg/auth/sender.go @@ -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 } @@ -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 { diff --git a/pkg/auth/sender_test.go b/pkg/auth/sender_test.go index 83defa6c..d72b0c1b 100644 --- a/pkg/auth/sender_test.go +++ b/pkg/auth/sender_test.go @@ -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`) diff --git a/pkg/auth/validate.go b/pkg/auth/validate.go index dca2bf81..86691292 100644 --- a/pkg/auth/validate.go +++ b/pkg/auth/validate.go @@ -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 @@ -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 @@ -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") } @@ -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") } diff --git a/pkg/auth/validate_test.go b/pkg/auth/validate_test.go index 2484682f..b75f200f 100644 --- a/pkg/auth/validate_test.go +++ b/pkg/auth/validate_test.go @@ -1,13 +1,88 @@ package auth import ( + "fmt" "testing" "github.com/bluenviron/gortsplib/v4/pkg/base" - "github.com/bluenviron/gortsplib/v4/pkg/headers" "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) { f.Add(`Invalid`) 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) -} diff --git a/pkg/auth/www_authenticate.go b/pkg/auth/www_authenticate.go index 5ee0a904..06e25f0e 100644 --- a/pkg/auth/www_authenticate.go +++ b/pkg/auth/www_authenticate.go @@ -6,18 +6,44 @@ import ( ) // 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 { - methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic} + methods = []ValidateMethod{ValidateMethodBasic, ValidateMethodDigestMD5, ValidateMethodSHA256} } var ret base.HeaderValue + for _, m := range methods { - ret = append(ret, headers.Authenticate{ - Method: m, - Realm: realm, - Nonce: nonce, // used only by digest - }.Marshal()...) + var a base.HeaderValue + + switch m { + case ValidateMethodBasic: + 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 } diff --git a/pkg/headers/authenticate.go b/pkg/headers/authenticate.go index 0b85c8cf..3a010a93 100644 --- a/pkg/headers/authenticate.go +++ b/pkg/headers/authenticate.go @@ -11,34 +11,31 @@ import ( // AuthMethod is an authentication method. type AuthMethod int +// authentication methods. const ( - // AuthBasic is the Basic authentication method - AuthBasic AuthMethod = iota - - // AuthDigestMD5 is the Digest authentication method with the MD5 hash - AuthDigestMD5 - - // AuthDigestSHA256 is the Digest authentication method with the SHA-256 hash - AuthDigestSHA256 + AuthMethodBasic AuthMethod = iota + AuthMethodDigest ) +// AuthAlgorithm is a digest algorithm. +type AuthAlgorithm int + +// digest algorithms. const ( - // AuthDigest is an alias for AuthDigestMD5 - // - // Deprecated: replaced by AuthDigestMD5 - AuthDigest = AuthDigestMD5 + AuthAlgorithmMD5 AuthAlgorithm = iota + AuthAlgorithmSHA256 ) -func algorithmToMethod(v *string) (AuthMethod, error) { +func parseAuthAlgorithm(v string) (AuthAlgorithm, error) { switch { - case v == nil, strings.ToLower(*v) == "md5": - return AuthDigestMD5, nil + case strings.ToLower(v) == "md5": + return AuthAlgorithmMD5, nil - case strings.ToLower(*v) == "sha-256": - return AuthDigestSHA256, nil + case strings.ToLower(v) == "sha-256": + return AuthAlgorithmSHA256, nil 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 *string + + // algorithm + Algorithm *AuthAlgorithm } // 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:] - isDigest := false - switch method { case "Basic": - h.Method = AuthBasic + h.Method = AuthMethodBasic case "Digest": - isDigest = true + h.Method = AuthMethodDigest default: return fmt.Errorf("invalid method (%s)", method) } - if !isDigest { + if h.Method == AuthMethodBasic { kvs, err := keyValParse(v0, ',') if err != nil { return err @@ -123,7 +121,6 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error { realmReceived := false nonceReceived := false - var algorithm *string for k, rv := range kvs { v := rv @@ -144,18 +141,17 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error { h.Stale = &v case "algorithm": - algorithm = &v + a, err := parseAuthAlgorithm(v) + if err != nil { + return err + } + h.Algorithm = &a } } if !realmReceived || !nonceReceived { return fmt.Errorf("one or more digest fields are missing") } - - h.Method, err = algorithmToMethod(algorithm) - if err != nil { - return err - } } return nil @@ -163,7 +159,7 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error { // Marshal encodes a WWW-Authenticate header. func (h Authenticate) Marshal() base.HeaderValue { - if h.Method == AuthBasic { + if h.Method == AuthMethodBasic { return base.HeaderValue{"Basic " + "realm=\"" + h.Realm + "\""} } @@ -178,10 +174,12 @@ func (h Authenticate) Marshal() base.HeaderValue { ret += ", stale=\"" + *h.Stale + "\"" } - if h.Method == AuthDigestMD5 { - ret += ", algorithm=\"MD5\"" - } else { - ret += ", algorithm=\"SHA-256\"" + if h.Algorithm != nil { + if *h.Algorithm == AuthAlgorithmMD5 { + ret += ", algorithm=\"MD5\"" + } else { + ret += ", algorithm=\"SHA-256\"" + } } return base.HeaderValue{ret} diff --git a/pkg/headers/authenticate_test.go b/pkg/headers/authenticate_test.go index e9932156..22041749 100644 --- a/pkg/headers/authenticate_test.go +++ b/pkg/headers/authenticate_test.go @@ -23,7 +23,7 @@ var casesAuthenticate = []struct { base.HeaderValue{`Basic realm="4419b63f5e51"`}, base.HeaderValue{`Basic realm="4419b63f5e51"`}, Authenticate{ - Method: AuthBasic, + Method: AuthMethodBasic, Realm: "4419b63f5e51", }, }, @@ -31,9 +31,9 @@ var casesAuthenticate = []struct { "digest 1", base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` + - `stale="FALSE", algorithm="MD5"`}, + `stale="FALSE"`}, Authenticate{ - Method: AuthDigestMD5, + Method: AuthMethodDigest, Realm: "4419b63f5e51", Nonce: "8b84a3b789283a8bea8da7fa7d41f08b", Stale: stringPtr("FALSE"), @@ -43,9 +43,9 @@ var casesAuthenticate = []struct { "digest 2", base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` + - `stale="FALSE", algorithm="MD5"`}, + `stale="FALSE"`}, Authenticate{ - Method: AuthDigestMD5, + Method: AuthMethodDigest, Realm: "4419b63f5e51", Nonce: "8b84a3b789283a8bea8da7fa7d41f08b", Stale: stringPtr("FALSE"), @@ -55,9 +55,9 @@ var casesAuthenticate = []struct { "digest 3", base.HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", ` + - `stale="FALSE", algorithm="MD5"`}, + `stale="FALSE"`}, Authenticate{ - Method: AuthDigestMD5, + Method: AuthMethodDigest, Realm: "4419b63f5e51", Nonce: "133767111917411116111311118211673010032", Stale: stringPtr("FALSE"), @@ -66,17 +66,31 @@ var casesAuthenticate = []struct { { "digest after failed auth", 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", ` + - `nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE", algorithm="MD5"`}, + `nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE"`}, Authenticate{ - Method: AuthDigestMD5, + Method: AuthMethodDigest, Realm: "Please log in with a valid username", Nonce: "752a62306daf32b401a41004555c7663", Opaque: stringPtr(""), 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", base.HeaderValue{`Digest realm="IP Camera(AB705)", ` + @@ -84,10 +98,11 @@ var casesAuthenticate = []struct { base.HeaderValue{`Digest realm="IP Camera(AB705)", ` + `nonce="fcc86deace979a488b2bfb89f4d0812c", stale="FALSE", algorithm="SHA-256"`}, Authenticate{ - Method: AuthDigestSHA256, - Realm: "IP Camera(AB705)", - Nonce: "fcc86deace979a488b2bfb89f4d0812c", - Stale: stringPtr("FALSE"), + Method: AuthMethodDigest, + Realm: "IP Camera(AB705)", + Nonce: "fcc86deace979a488b2bfb89f4d0812c", + Stale: stringPtr("FALSE"), + Algorithm: algorithmPtr(AuthAlgorithmSHA256), }, }, } diff --git a/pkg/headers/authorization.go b/pkg/headers/authorization.go index b65761bc..e20dd97e 100644 --- a/pkg/headers/authorization.go +++ b/pkg/headers/authorization.go @@ -44,6 +44,9 @@ type Authorization struct { // opaque Opaque *string + + // algorithm + Algorithm *AuthAlgorithm } // Unmarshal decodes an Authorization header. @@ -64,20 +67,18 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error { } method, v0 := v0[:i], v0[i+1:] - isDigest := false - switch method { case "Basic": - h.Method = AuthBasic + h.Method = AuthMethodBasic case "Digest": - isDigest = true + h.Method = AuthMethodDigest default: return fmt.Errorf("invalid method (%s)", method) } - if !isDigest { + if h.Method == AuthMethodBasic { tmp, err := base64.StdEncoding.DecodeString(v0) if err != nil { return fmt.Errorf("invalid value") @@ -100,7 +101,6 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error { nonceReceived := false uriReceived := false responseReceived := false - var algorithm *string for k, rv := range kvs { v := rv @@ -130,18 +130,17 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error { h.Opaque = &v case "algorithm": - algorithm = &v + a, err := parseAuthAlgorithm(v) + if err != nil { + return err + } + h.Algorithm = &a } } if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived { return fmt.Errorf("one or more digest fields are missing") } - - h.Method, err = algorithmToMethod(algorithm) - if err != nil { - return err - } } return nil @@ -149,7 +148,7 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error { // Marshal encodes an Authorization header. func (h Authorization) Marshal() base.HeaderValue { - if h.Method == AuthBasic { + if h.Method == AuthMethodBasic { return base.HeaderValue{"Basic " + base64.StdEncoding.EncodeToString([]byte(h.BasicUser+":"+h.BasicPass))} } @@ -162,10 +161,12 @@ func (h Authorization) Marshal() base.HeaderValue { ret += ", opaque=\"" + *h.Opaque + "\"" } - if h.Method == AuthDigestMD5 { - ret += ", algorithm=\"MD5\"" - } else { - ret += ", algorithm=\"SHA-256\"" + if h.Algorithm != nil { + if *h.Algorithm == AuthAlgorithmMD5 { + ret += ", algorithm=\"MD5\"" + } else { + ret += ", algorithm=\"SHA-256\"" + } } return base.HeaderValue{ret} diff --git a/pkg/headers/authorization_test.go b/pkg/headers/authorization_test.go index a7556224..9ba6696e 100644 --- a/pkg/headers/authorization_test.go +++ b/pkg/headers/authorization_test.go @@ -8,6 +8,10 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/base" ) +func algorithmPtr(v AuthAlgorithm) *AuthAlgorithm { + return &v +} + var casesAuthorization = []struct { name string vin base.HeaderValue @@ -19,7 +23,7 @@ var casesAuthorization = []struct { base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="}, base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="}, Authorization{ - Method: AuthBasic, + Method: AuthMethodBasic, BasicUser: "myuser", BasicPass: "mypass", }, @@ -31,9 +35,9 @@ var casesAuthorization = []struct { `uri="/dir/index.html", response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`}, base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` + `nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", ` + - `response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm="MD5"`}, + `response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`}, Authorization{ - Method: AuthDigestMD5, + Method: AuthMethodDigest, Username: "Mufasa", Realm: "testrealm@host.com", Nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093", @@ -49,9 +53,9 @@ var casesAuthorization = []struct { `response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, base.HeaderValue{`Digest username="", realm="IPCAM", ` + `nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` + - `response="c072ae90eb4a27f4cdcb90d62266b2a1", algorithm="MD5"`}, + `response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, Authorization{ - Method: AuthDigestMD5, + Method: AuthMethodDigest, Username: "", Realm: "IPCAM", Nonce: "5d17cd12b9fa8a85ac5ceef0926ea5a6", @@ -59,6 +63,26 @@ var casesAuthorization = []struct { 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", base.HeaderValue{`Digest username="admin", realm="IP Camera(AB705)", ` + @@ -68,12 +92,13 @@ var casesAuthorization = []struct { `nonce="1ad195c2b2ca5a03784e53f88e16f579", uri="rtsp://192.168.80.76/", ` + `response="9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", algorithm="SHA-256"`}, Authorization{ - Method: AuthDigestSHA256, - Username: "admin", - Realm: "IP Camera(AB705)", - Nonce: "1ad195c2b2ca5a03784e53f88e16f579", - URI: "rtsp://192.168.80.76/", - Response: "9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", + Method: AuthMethodDigest, + Username: "admin", + Realm: "IP Camera(AB705)", + Nonce: "1ad195c2b2ca5a03784e53f88e16f579", + URI: "rtsp://192.168.80.76/", + Response: "9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", + Algorithm: algorithmPtr(AuthAlgorithmSHA256), }, }, }