From b46a92cc96f6b75c1733a141f911a9190a249bba Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Wed, 21 Feb 2024 23:29:42 +0100 Subject: [PATCH] split headers.Authenticate and headers.Authorization --- client_play_test.go | 12 +- pkg/auth/sender.go | 60 +++----- pkg/auth/sender_test.go | 55 ++----- pkg/auth/validate.go | 38 ++--- pkg/auth/validate_test.go | 114 ++++++-------- pkg/headers/authenticate.go | 142 ++++++++---------- pkg/headers/authenticate_test.go | 121 +++++---------- pkg/headers/authorization.go | 114 +++++++++++--- pkg/headers/authorization_test.go | 109 +++++++------- .../593a6affc7d1fcee | 2 + .../5fb42e6dcbfb41cb | 2 + .../771e938e4458e983 | 2 + .../8643a79bab153d1b | 2 + .../9a1c51bbe1216ed5 | 2 + .../c44cf6083afc355b | 2 + .../5fb42e6dcbfb41cb | 2 + .../771e938e4458e983 | 2 + .../8643a79bab153d1b | 2 + .../9a1c51bbe1216ed5 | 2 + .../c44cf6083afc355b | 2 + .../e77c46eef8802889 | 2 + 21 files changed, 367 insertions(+), 422 deletions(-) create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/593a6affc7d1fcee create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/5fb42e6dcbfb41cb create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/771e938e4458e983 create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/8643a79bab153d1b create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/9a1c51bbe1216ed5 create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/c44cf6083afc355b create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/5fb42e6dcbfb41cb create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/771e938e4458e983 create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/8643a79bab153d1b create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/9a1c51bbe1216ed5 create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/c44cf6083afc355b create mode 100644 pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/e77c46eef8802889 diff --git a/client_play_test.go b/client_play_test.go index cbd5c20a..889d49c6 100644 --- a/client_play_test.go +++ b/client_play_test.go @@ -1691,12 +1691,13 @@ func TestClientPlayRedirect(t *testing.T) { authOpaque := "exampleOpaque" authStale := "FALSE" authAlg := "MD5" + err = conn.WriteResponse(&base.Response{ Header: base.Header{ "WWW-Authenticate": headers.Authenticate{ Method: headers.AuthDigest, - Realm: &authRealm, - Nonce: &authNonce, + Realm: authRealm, + Nonce: authNonce, Opaque: &authOpaque, Stale: &authStale, Algorithm: &authAlg, @@ -1706,13 +1707,16 @@ func TestClientPlayRedirect(t *testing.T) { }) require.NoError(t, err) } + req, err = conn.ReadRequest() require.NoError(t, err) + authHeaderVal, exists := req.Header["Authorization"] require.True(t, exists) - var authHeader headers.Authenticate + + var authHeader headers.Authorization require.NoError(t, authHeader.Unmarshal(authHeaderVal)) - require.Equal(t, *authHeader.Username, "testusr") + require.Equal(t, authHeader.Username, "testusr") require.Equal(t, base.Describe, req.Method) } diff --git a/pkg/auth/sender.go b/pkg/auth/sender.go index e5d95004..7a662b64 100644 --- a/pkg/auth/sender.go +++ b/pkg/auth/sender.go @@ -19,11 +19,9 @@ func findHeader(v base.HeaderValue, prefix string) string { // Sender allows to send credentials. type Sender struct { - user string - pass string - method headers.AuthMethod - realm string - nonce string + user string + pass string + authenticateHeader *headers.Authenticate } // NewSender allocates a Sender. @@ -38,20 +36,10 @@ func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) { return nil, err } - if auth.Realm == nil { - return nil, fmt.Errorf("realm is missing") - } - - if auth.Nonce == nil { - return nil, fmt.Errorf("nonce is missing") - } - return &Sender{ - user: user, - pass: pass, - method: headers.AuthDigest, - realm: *auth.Realm, - nonce: *auth.Nonce, + user: user, + pass: pass, + authenticateHeader: &auth, }, nil } @@ -62,15 +50,10 @@ func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) { return nil, err } - if auth.Realm == nil { - return nil, fmt.Errorf("realm is missing") - } - return &Sender{ - user: user, - pass: pass, - method: headers.AuthBasic, - realm: *auth.Realm, + user: user, + pass: pass, + authenticateHeader: &auth, }, nil } @@ -82,26 +65,19 @@ func (se *Sender) AddAuthorization(req *base.Request) { urStr := req.URL.CloneWithoutCredentials().String() h := headers.Authorization{ - Method: se.method, + Method: se.authenticateHeader.Method, } - switch se.method { - case headers.AuthBasic: + if se.authenticateHeader.Method == headers.AuthBasic { h.BasicUser = se.user h.BasicPass = se.pass - - default: // headers.AuthDigest - response := md5Hex(md5Hex(se.user+":"+se.realm+":"+se.pass) + ":" + - se.nonce + ":" + md5Hex(string(req.Method)+":"+urStr)) - - h.DigestValues = headers.Authenticate{ - Method: headers.AuthDigest, - Username: &se.user, - Realm: &se.realm, - Nonce: &se.nonce, - URI: &urStr, - Response: &response, - } + } else { // digest + h.Username = se.user + h.Realm = se.authenticateHeader.Realm + h.Nonce = se.authenticateHeader.Nonce + h.URI = urStr + h.Response = md5Hex(md5Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" + + se.authenticateHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr)) } if req.Header == nil { diff --git a/pkg/auth/sender_test.go b/pkg/auth/sender_test.go index b0506877..c4eca4fb 100644 --- a/pkg/auth/sender_test.go +++ b/pkg/auth/sender_test.go @@ -3,51 +3,18 @@ package auth import ( "testing" - "github.com/stretchr/testify/require" - "github.com/bluenviron/gortsplib/v4/pkg/base" ) -func TestSenderErrors(t *testing.T) { - for _, ca := range []struct { - name string - hv base.HeaderValue - err string - }{ - { - "invalid method", - base.HeaderValue{`Invalid`}, - "no authentication methods available", - }, - { - "digest invalid", - base.HeaderValue{`Digest`}, - "unable to split between method and keys (Digest)", - }, - { - "digest, missing realm", - base.HeaderValue{`Digest nonce=123`}, - "realm is missing", - }, - { - "digest, missing nonce", - base.HeaderValue{`Digest realm=123`}, - "nonce is missing", - }, - { - "basic invalid", - base.HeaderValue{`Basic`}, - "unable to split between method and keys (Basic)", - }, - { - "basic, missing realm", - base.HeaderValue{`Basic nonce=123`}, - "realm is missing", - }, - } { - t.Run(ca.name, func(t *testing.T) { - _, err := NewSender(ca.hv, "myuser", "mypass") - require.EqualError(t, err, ca.err) - }) - } +func FuzzSender(f *testing.F) { + f.Add(`Invalid`) + f.Add(`Digest`) + f.Add(`Digest nonce=123`) + f.Add(`Digest realm=123`) + f.Add(`Basic`) + f.Add(`Basic nonce=123`) + + f.Fuzz(func(t *testing.T, a string) { + NewSender(base.HeaderValue{a}, "myuser", "mypass") //nolint:errcheck + }) } diff --git a/pkg/auth/validate.go b/pkg/auth/validate.go index e43d52b9..3e20cd27 100644 --- a/pkg/auth/validate.go +++ b/pkg/auth/validate.go @@ -39,14 +39,14 @@ func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce s case headers.AuthBasic: ret = append(ret, (&headers.Authenticate{ Method: headers.AuthBasic, - Realm: &realm, + Realm: realm, }).Marshal()...) case headers.AuthDigest: ret = append(ret, headers.Authenticate{ Method: headers.AuthDigest, - Realm: &realm, - Nonce: &nonce, + Realm: realm, + Nonce: nonce, }.Marshal()...) } } @@ -92,46 +92,26 @@ func Validate( return fmt.Errorf("authentication failed") } case auth.Method == headers.AuthDigest && contains(methods, headers.AuthDigest): - if auth.DigestValues.Realm == nil { - return fmt.Errorf("realm is missing") - } - - if auth.DigestValues.Nonce == nil { - return fmt.Errorf("nonce is missing") - } - - if auth.DigestValues.Username == nil { - return fmt.Errorf("username is missing") - } - - if auth.DigestValues.URI == nil { - return fmt.Errorf("uri is missing") - } - - if auth.DigestValues.Response == nil { - return fmt.Errorf("response is missing") - } - - if *auth.DigestValues.Nonce != nonce { + if auth.Nonce != nonce { return fmt.Errorf("wrong nonce") } - if *auth.DigestValues.Realm != realm { + if auth.Realm != realm { return fmt.Errorf("wrong realm") } - if *auth.DigestValues.Username != user { + if auth.Username != user { return fmt.Errorf("authentication failed") } ur := req.URL - if *auth.DigestValues.URI != ur.String() { + if auth.URI != ur.String() { // in SETUP requests, VLC strips the control attribute. // try again with the base URL. if baseURL != nil { ur = baseURL - if *auth.DigestValues.URI != ur.String() { + if auth.URI != ur.String() { return fmt.Errorf("wrong URL") } } else { @@ -142,7 +122,7 @@ func Validate( response := md5Hex(md5Hex(user+":"+realm+":"+pass) + ":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String())) - if *auth.DigestValues.Response != response { + if auth.Response != response { return fmt.Errorf("authentication failed") } default: diff --git a/pkg/auth/validate_test.go b/pkg/auth/validate_test.go index 2abb255e..956fa5fb 100644 --- a/pkg/auth/validate_test.go +++ b/pkg/auth/validate_test.go @@ -3,75 +3,55 @@ package auth import ( "testing" - "github.com/stretchr/testify/require" - "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/stretchr/testify/require" ) -func TestValidateErrors(t *testing.T) { - for _, ca := range []struct { - name string - hv base.HeaderValue - err string - }{ - { - "invalid auth", - base.HeaderValue{`Invalid`}, - "invalid authorization header", - }, - { - "digest missing realm", - base.HeaderValue{`Digest `}, - "realm is missing", - }, - { - "digest missing nonce", - base.HeaderValue{`Digest realm=123`}, - "nonce is missing", - }, - { - "digest missing username", - base.HeaderValue{`Digest realm=123,nonce=123`}, - "username is missing", - }, - { - "digest missing uri", - base.HeaderValue{`Digest realm=123,nonce=123,username=123`}, - "uri is missing", - }, - { - "digest missing response", - base.HeaderValue{`Digest realm=123,nonce=123,username=123,uri=123`}, - "response is missing", - }, - { - "digest wrong nonce", - base.HeaderValue{`Digest realm=123,nonce=123,username=123,uri=123,response=123`}, - "wrong nonce", - }, - { - "digest wrong realm", - base.HeaderValue{`Digest realm=123,nonce=abcde,username=123,uri=123,response=123`}, - "wrong realm", - }, - } { - t.Run(ca.name, func(t *testing.T) { - err := Validate( - &base.Request{ - Method: base.Describe, - URL: nil, - Header: base.Header{ - "Authorization": ca.hv, - }, +func FuzzValidate(f *testing.F) { + f.Add(`Invalid`) + f.Add(`Digest `) + f.Add(`Digest realm=123`) + f.Add(`Digest realm=123,nonce=123`) + f.Add(`Digest realm=123,nonce=123,username=123`) + f.Add(`Digest realm=123,nonce=123,username=123,uri=123`) + f.Add(`Digest realm=123,nonce=123,username=123,uri=123,response=123`) + f.Add(`Digest realm=123,nonce=abcde,username=123,uri=123,response=123`) + + f.Fuzz(func(t *testing.T, a string) { + Validate( //nolint:errcheck + &base.Request{ + Method: base.Describe, + URL: nil, + Header: base.Header{ + "Authorization": base.HeaderValue{a}, }, - "myuser", - "mypass", - nil, - nil, - "IPCAM", - "abcde", - ) - require.EqualError(t, err, ca.err) - }) - } + }, + "myuser", + "mypass", + nil, + nil, + "IPCAM", + "abcde", + ) + }) +} + +func TestValidateAdditionalErrors(t *testing.T) { + err := Validate( + &base.Request{ + Method: base.Describe, + URL: nil, + Header: base.Header{ + "Authorization": base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="}, + }, + }, + "myuser", + "mypass", + nil, + []headers.AuthMethod{headers.AuthDigest}, + "IPCAM", + "abcde", + ) + require.Error(t, err) } diff --git a/pkg/headers/authenticate.go b/pkg/headers/authenticate.go index 5049b124..d4d37763 100644 --- a/pkg/headers/authenticate.go +++ b/pkg/headers/authenticate.go @@ -19,37 +19,32 @@ const ( AuthDigest ) -// Authenticate is an Authenticate or a WWW-Authenticate header. +// Authenticate is a WWW-Authenticate header. type Authenticate struct { // authentication method Method AuthMethod - // (optional) username - Username *string + // realm + Realm string - // (optional) realm - Realm *string + // + // Digest authentication fields + // - // (optional) nonce - Nonce *string + // nonce + Nonce string - // (optional) uri - URI *string - - // (optional) response - Response *string - - // (optional) opaque + // opaque Opaque *string - // (optional) stale + // stale Stale *string - // (optional) algorithm + // algorithm Algorithm *string } -// Unmarshal decodes an Authenticate or a WWW-Authenticate header. +// Unmarshal decodes a WWW-Authenticate header. func (h *Authenticate) Unmarshal(v base.HeaderValue) error { if len(v) == 0 { return fmt.Errorf("value not provided") @@ -78,93 +73,86 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error { return fmt.Errorf("invalid method (%s)", method) } - kvs, err := keyValParse(v0, ',') - if err != nil { - return err - } - - for k, rv := range kvs { - v := rv - - switch k { - case "username": - h.Username = &v - - case "realm": - h.Realm = &v - - case "nonce": - h.Nonce = &v - - case "uri": - h.URI = &v + if h.Method == AuthBasic { + kvs, err := keyValParse(v0, ',') + if err != nil { + return err + } - case "response": - h.Response = &v + realmReceived := false - case "opaque": - h.Opaque = &v + for k, rv := range kvs { + v := rv - case "stale": - h.Stale = &v + if k == "realm" { + h.Realm = v + realmReceived = true + } + } - case "algorithm": - h.Algorithm = &v + if !realmReceived { + return fmt.Errorf("realm is missing") + } + } else { // digest + kvs, err := keyValParse(v0, ',') + if err != nil { + return err } - } - return nil -} + realmReceived := false + nonceReceived := false -// Marshal encodes an Authenticate or a WWW-Authenticate header. -func (h Authenticate) Marshal() base.HeaderValue { - ret := "" + for k, rv := range kvs { + v := rv - switch h.Method { - case AuthBasic: - ret += "Basic" + switch k { + case "realm": + h.Realm = v + realmReceived = true - case AuthDigest: - ret += "Digest" - } + case "nonce": + h.Nonce = v + nonceReceived = true - ret += " " + case "opaque": + h.Opaque = &v - var rets []string + case "stale": + h.Stale = &v - if h.Username != nil { - rets = append(rets, "username=\""+*h.Username+"\"") - } + case "algorithm": + h.Algorithm = &v + } + } - if h.Realm != nil { - rets = append(rets, "realm=\""+*h.Realm+"\"") + if !realmReceived || !nonceReceived { + return fmt.Errorf("one or more digest fields are missing") + } } - if h.Nonce != nil { - rets = append(rets, "nonce=\""+*h.Nonce+"\"") - } + return nil +} - if h.URI != nil { - rets = append(rets, "uri=\""+*h.URI+"\"") +// Marshal encodes a WWW-Authenticate header. +func (h Authenticate) Marshal() base.HeaderValue { + if h.Method == AuthBasic { + return base.HeaderValue{"Basic " + + "realm=\"" + h.Realm + "\""} } - if h.Response != nil { - rets = append(rets, "response=\""+*h.Response+"\"") - } + ret := "Digest realm=\"" + h.Realm + "\", nonce=\"" + h.Nonce + "\"" if h.Opaque != nil { - rets = append(rets, "opaque=\""+*h.Opaque+"\"") + ret += ", opaque=\"" + *h.Opaque + "\"" } if h.Stale != nil { - rets = append(rets, "stale=\""+*h.Stale+"\"") + ret += ", stale=\"" + *h.Stale + "\"" } if h.Algorithm != nil { - rets = append(rets, "algorithm=\""+*h.Algorithm+"\"") + ret += ", algorithm=\"" + *h.Algorithm + "\"" } - ret += strings.Join(rets, ", ") - return base.HeaderValue{ret} } diff --git a/pkg/headers/authenticate_test.go b/pkg/headers/authenticate_test.go index 07c8469d..6d82eab2 100644 --- a/pkg/headers/authenticate_test.go +++ b/pkg/headers/authenticate_test.go @@ -24,82 +24,52 @@ var casesAuthenticate = []struct { base.HeaderValue{`Basic realm="4419b63f5e51"`}, Authenticate{ Method: AuthBasic, - Realm: stringPtr("4419b63f5e51"), + Realm: "4419b63f5e51", }, }, { - "digest request 1", + "digest 1", base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, Authenticate{ Method: AuthDigest, - Realm: stringPtr("4419b63f5e51"), - Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"), + Realm: "4419b63f5e51", + Nonce: "8b84a3b789283a8bea8da7fa7d41f08b", Stale: stringPtr("FALSE"), }, }, { - "digest request 2", + "digest 2", base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, Authenticate{ Method: AuthDigest, - Realm: stringPtr("4419b63f5e51"), - Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"), + Realm: "4419b63f5e51", + Nonce: "8b84a3b789283a8bea8da7fa7d41f08b", Stale: stringPtr("FALSE"), }, }, { - "digest request 3", + "digest 3", base.HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", stale="FALSE"`}, Authenticate{ Method: AuthDigest, - Realm: stringPtr("4419b63f5e51"), - Nonce: stringPtr("133767111917411116111311118211673010032"), + Realm: "4419b63f5e51", + Nonce: "133767111917411116111311118211673010032", Stale: stringPtr("FALSE"), }, }, { - "digest response generic", - base.HeaderValue{`Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`}, - base.HeaderValue{`Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`}, - Authenticate{ - Method: AuthDigest, - Username: stringPtr("aa"), - Realm: stringPtr("bb"), - Nonce: stringPtr("cc"), - URI: stringPtr("dd"), - Response: stringPtr("ee"), - }, - }, - { - "digest response with empty field", - base.HeaderValue{`Digest username="", realm="IPCAM", ` + - `nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` + - `response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, - base.HeaderValue{`Digest username="", realm="IPCAM", ` + - `nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` + - `response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, - Authenticate{ - Method: AuthDigest, - Username: stringPtr(""), - Realm: stringPtr("IPCAM"), - Nonce: stringPtr("5d17cd12b9fa8a85ac5ceef0926ea5a6"), - URI: stringPtr("rtsp://localhost:8554/mystream"), - Response: stringPtr("c072ae90eb4a27f4cdcb90d62266b2a1"), - }, - }, - { - "digest response with no spaces and additional fields", + "digest after failed auth", base.HeaderValue{`Digest realm="Please log in with a valid username",` + `nonce="752a62306daf32b401a41004555c7663",opaque="",stale=FALSE,algorithm=MD5`}, base.HeaderValue{`Digest realm="Please log in with a valid username", ` + `nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE", algorithm="MD5"`}, Authenticate{ Method: AuthDigest, - Realm: stringPtr("Please log in with a valid username"), - Nonce: stringPtr("752a62306daf32b401a41004555c7663"), + Realm: "Please log in with a valid username", + Nonce: "752a62306daf32b401a41004555c7663", Opaque: stringPtr(""), Stale: stringPtr("FALSE"), Algorithm: stringPtr("MD5"), @@ -118,46 +88,6 @@ func TestAuthenticateUnmarshal(t *testing.T) { } } -func TestAutenticatehUnmarshalErrors(t *testing.T) { - for _, ca := range []struct { - name string - hv base.HeaderValue - err string - }{ - { - "empty", - base.HeaderValue{}, - "value not provided", - }, - { - "2 values", - base.HeaderValue{"a", "b"}, - "value provided multiple times ([a b])", - }, - { - "no keys", - base.HeaderValue{"Basic"}, - "unable to split between method and keys (Basic)", - }, - { - "invalid keys", - base.HeaderValue{`Basic key1="k`}, - "apexes not closed (key1=\"k)", - }, - { - "invalid method", - base.HeaderValue{"Testing key1=val1"}, - "invalid method (Testing)", - }, - } { - t.Run(ca.name, func(t *testing.T) { - var h Authenticate - err := h.Unmarshal(ca.hv) - require.EqualError(t, err, ca.err) - }) - } -} - func TestAuthenticateMarshal(t *testing.T) { for _, ca := range casesAuthenticate { t.Run(ca.name, func(t *testing.T) { @@ -166,3 +96,28 @@ func TestAuthenticateMarshal(t *testing.T) { }) } } + +func FuzzAuthenticateUnmarshal(f *testing.F) { + for _, ca := range casesAuthenticate { + f.Add(ca.vin[0]) + } + + f.Fuzz(func(t *testing.T, b string) { + var h Authenticate + h.Unmarshal(base.HeaderValue{b}) //nolint:errcheck + }) +} + +func TestAuthenticateAdditionalErrors(t *testing.T) { + func() { + var h Authenticate + err := h.Unmarshal(base.HeaderValue{}) + require.Error(t, err) + }() + + func() { + var h Authenticate + err := h.Unmarshal(base.HeaderValue{"a", "b"}) + require.Error(t, err) + }() +} diff --git a/pkg/headers/authorization.go b/pkg/headers/authorization.go index 18e5e7e3..8fb18ccf 100644 --- a/pkg/headers/authorization.go +++ b/pkg/headers/authorization.go @@ -13,14 +13,37 @@ type Authorization struct { // authentication method Method AuthMethod - // basic user + // + // Basic authentication fields + // + + // user BasicUser string - // basic password + // password BasicPass string - // digest values - DigestValues Authenticate + // + // Digest authentication fields + // + + // username + Username string + + // realm + Realm string + + // nonce + Nonce string + + // URI + URI string + + // response + Response string + + // response + Opaque *string } // Unmarshal decodes an Authorization header. @@ -35,12 +58,24 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error { v0 := v[0] - switch { - case strings.HasPrefix(v0, "Basic "): + i := strings.IndexByte(v0, ' ') + if i < 0 { + return fmt.Errorf("unable to split between method and keys (%v)", v0) + } + method, v0 := v0[:i], v0[i+1:] + + switch method { + case "Basic": h.Method = AuthBasic - v0 = v0[len("Basic "):] + case "Digest": + h.Method = AuthDigest + default: + return fmt.Errorf("invalid method (%s)", method) + } + + if h.Method == AuthBasic { tmp, err := base64.StdEncoding.DecodeString(v0) if err != nil { return fmt.Errorf("invalid value") @@ -52,20 +87,50 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error { } h.BasicUser, h.BasicPass = tmp2[0], tmp2[1] - - case strings.HasPrefix(v0, "Digest "): - h.Method = AuthDigest - - var vals Authenticate - err := vals.Unmarshal(base.HeaderValue{v0}) + } else { // digest + kvs, err := keyValParse(v0, ',') if err != nil { return err } - h.DigestValues = vals + realmReceived := false + usernameReceived := false + nonceReceived := false + uriReceived := false + responseReceived := false - default: - return fmt.Errorf("invalid authorization header") + for k, rv := range kvs { + v := rv + + switch k { + case "realm": + h.Realm = v + realmReceived = true + + case "username": + h.Username = v + usernameReceived = true + + case "nonce": + h.Nonce = v + nonceReceived = true + + case "uri": + h.URI = v + uriReceived = true + + case "response": + h.Response = v + responseReceived = true + + case "opaque": + h.Opaque = &v + } + } + + if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived { + return fmt.Errorf("one or more digest fields are missing") + } } return nil @@ -73,13 +138,18 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error { // Marshal encodes an Authorization header. func (h Authorization) Marshal() base.HeaderValue { - switch h.Method { - case AuthBasic: - response := base64.StdEncoding.EncodeToString([]byte(h.BasicUser + ":" + h.BasicPass)) + if h.Method == AuthBasic { + return base.HeaderValue{"Basic " + + base64.StdEncoding.EncodeToString([]byte(h.BasicUser+":"+h.BasicPass))} + } - return base.HeaderValue{"Basic " + response} + ret := "Digest " + + "username=\"" + h.Username + "\", realm=\"" + h.Realm + "\", " + + "nonce=\"" + h.Nonce + "\", uri=\"" + h.URI + "\", response=\"" + h.Response + "\"" - default: // AuthDigest - return h.DigestValues.Marshal() + if h.Opaque != nil { + ret += ", opaque=\"" + *h.Opaque + "\"" } + + return base.HeaderValue{ret} } diff --git a/pkg/headers/authorization_test.go b/pkg/headers/authorization_test.go index 0f8a9cad..e86ea8e0 100644 --- a/pkg/headers/authorization_test.go +++ b/pkg/headers/authorization_test.go @@ -26,16 +26,37 @@ var casesAuthorization = []struct { }, { "digest", - base.HeaderValue{"Digest realm=\"4419b63f5e51\", nonce=\"8b84a3b789283a8bea8da7fa7d41f08b\", stale=\"FALSE\""}, - base.HeaderValue{"Digest realm=\"4419b63f5e51\", nonce=\"8b84a3b789283a8bea8da7fa7d41f08b\", stale=\"FALSE\""}, + base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` + + `nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", ` + + `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"`}, Authorization{ - Method: AuthDigest, - DigestValues: Authenticate{ - Method: AuthDigest, - Realm: stringPtr("4419b63f5e51"), - Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"), - Stale: stringPtr("FALSE"), - }, + Method: AuthDigest, + Username: "Mufasa", + Realm: "testrealm@host.com", + Nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093", + URI: "/dir/index.html", + Response: "e966c932a9242554e42c8ee200cec7f6", + Opaque: stringPtr("5ccc069c403ebaf9f0171e9517f40e41"), + }, + }, + { + "digest with empty field", + base.HeaderValue{`Digest username="", realm="IPCAM", ` + + `nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` + + `response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, + base.HeaderValue{`Digest username="", realm="IPCAM", ` + + `nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` + + `response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, + Authorization{ + Method: AuthDigest, + Username: "", + Realm: "IPCAM", + Nonce: "5d17cd12b9fa8a85ac5ceef0926ea5a6", + URI: "rtsp://localhost:8554/mystream", + Response: "c072ae90eb4a27f4cdcb90d62266b2a1", }, }, } @@ -51,51 +72,6 @@ func TestAuthorizationUnmarshal(t *testing.T) { } } -func TestAuthorizationUnmarshalErrors(t *testing.T) { - for _, ca := range []struct { - name string - hv base.HeaderValue - err string - }{ - { - "empty", - base.HeaderValue{}, - "value not provided", - }, - { - "2 values", - base.HeaderValue{"a", "b"}, - "value provided multiple times ([a b])", - }, - { - "invalid", - base.HeaderValue{`Invalid`}, - "invalid authorization header", - }, - { - "basic invalid 1", - base.HeaderValue{`Basic aaa`}, - "invalid value", - }, - { - "basic invalid 2", - base.HeaderValue{`Basic aW52YWxpZA==`}, - "invalid value", - }, - { - "digest invalid", - base.HeaderValue{`Digest test="v`}, - "apexes not closed (test=\"v)", - }, - } { - t.Run(ca.name, func(t *testing.T) { - var h Authorization - err := h.Unmarshal(ca.hv) - require.EqualError(t, err, ca.err) - }) - } -} - func TestAuthorizationMarshal(t *testing.T) { for _, ca := range casesAuthorization { t.Run(ca.name, func(t *testing.T) { @@ -104,3 +80,28 @@ func TestAuthorizationMarshal(t *testing.T) { }) } } + +func FuzzAuthorizationUnmarshal(f *testing.F) { + for _, ca := range casesAuthorization { + f.Add(ca.vin[0]) + } + + f.Fuzz(func(t *testing.T, b string) { + var h Authorization + h.Unmarshal(base.HeaderValue{b}) //nolint:errcheck + }) +} + +func TestAuthorizationAdditionalErrors(t *testing.T) { + func() { + var h Authorization + err := h.Unmarshal(base.HeaderValue{}) + require.Error(t, err) + }() + + func() { + var h Authorization + err := h.Unmarshal(base.HeaderValue{"a", "b"}) + require.Error(t, err) + }() +} diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/593a6affc7d1fcee b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/593a6affc7d1fcee new file mode 100644 index 00000000..21449f99 --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/593a6affc7d1fcee @@ -0,0 +1,2 @@ +go test fuzz v1 +string("Basic =\"") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/5fb42e6dcbfb41cb b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/5fb42e6dcbfb41cb new file mode 100644 index 00000000..c098300d --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/5fb42e6dcbfb41cb @@ -0,0 +1,2 @@ +go test fuzz v1 +string("Digest ") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/771e938e4458e983 b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/771e938e4458e983 new file mode 100644 index 00000000..ee3f3399 --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/771e938e4458e983 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("0") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/8643a79bab153d1b b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/8643a79bab153d1b new file mode 100644 index 00000000..037b4727 --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/8643a79bab153d1b @@ -0,0 +1,2 @@ +go test fuzz v1 +string(" ") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/9a1c51bbe1216ed5 b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/9a1c51bbe1216ed5 new file mode 100644 index 00000000..c460e4b4 --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/9a1c51bbe1216ed5 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("Digest =\"") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/c44cf6083afc355b b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/c44cf6083afc355b new file mode 100644 index 00000000..228e628d --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/c44cf6083afc355b @@ -0,0 +1,2 @@ +go test fuzz v1 +string("Basic ") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/5fb42e6dcbfb41cb b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/5fb42e6dcbfb41cb new file mode 100644 index 00000000..c098300d --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/5fb42e6dcbfb41cb @@ -0,0 +1,2 @@ +go test fuzz v1 +string("Digest ") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/771e938e4458e983 b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/771e938e4458e983 new file mode 100644 index 00000000..ee3f3399 --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/771e938e4458e983 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("0") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/8643a79bab153d1b b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/8643a79bab153d1b new file mode 100644 index 00000000..037b4727 --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/8643a79bab153d1b @@ -0,0 +1,2 @@ +go test fuzz v1 +string(" ") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/9a1c51bbe1216ed5 b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/9a1c51bbe1216ed5 new file mode 100644 index 00000000..c460e4b4 --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/9a1c51bbe1216ed5 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("Digest =\"") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/c44cf6083afc355b b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/c44cf6083afc355b new file mode 100644 index 00000000..228e628d --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/c44cf6083afc355b @@ -0,0 +1,2 @@ +go test fuzz v1 +string("Basic ") diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/e77c46eef8802889 b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/e77c46eef8802889 new file mode 100644 index 00000000..74941108 --- /dev/null +++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/e77c46eef8802889 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("Basic 0")