split headers.Authenticate and headers.Authorization (#523)

This commit is contained in:
Alessandro Ros
2024-02-22 13:19:31 +01:00
committed by GitHub
parent c93d5c54d9
commit c10f7aaedb
21 changed files with 369 additions and 424 deletions

View File

@@ -1691,12 +1691,13 @@ func TestClientPlayRedirect(t *testing.T) {
authOpaque := "exampleOpaque" authOpaque := "exampleOpaque"
authStale := "FALSE" authStale := "FALSE"
authAlg := "MD5" authAlg := "MD5"
err = conn.WriteResponse(&base.Response{ err = conn.WriteResponse(&base.Response{
Header: base.Header{ Header: base.Header{
"WWW-Authenticate": headers.Authenticate{ "WWW-Authenticate": headers.Authenticate{
Method: headers.AuthDigest, Method: headers.AuthDigest,
Realm: &authRealm, Realm: authRealm,
Nonce: &authNonce, Nonce: authNonce,
Opaque: &authOpaque, Opaque: &authOpaque,
Stale: &authStale, Stale: &authStale,
Algorithm: &authAlg, Algorithm: &authAlg,
@@ -1706,13 +1707,16 @@ func TestClientPlayRedirect(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
} }
req, err = conn.ReadRequest() req, err = conn.ReadRequest()
require.NoError(t, err) require.NoError(t, err)
authHeaderVal, exists := req.Header["Authorization"] authHeaderVal, exists := req.Header["Authorization"]
require.True(t, exists) require.True(t, exists)
var authHeader headers.Authenticate
var authHeader headers.Authorization
require.NoError(t, authHeader.Unmarshal(authHeaderVal)) 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) require.Equal(t, base.Describe, req.Method)
} }

View File

@@ -21,9 +21,7 @@ func findHeader(v base.HeaderValue, prefix string) string {
type Sender struct { type Sender struct {
user string user string
pass string pass string
method headers.AuthMethod authenticateHeader *headers.Authenticate
realm string
nonce string
} }
// NewSender allocates a Sender. // NewSender allocates a Sender.
@@ -38,20 +36,10 @@ func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) {
return nil, err 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{ return &Sender{
user: user, user: user,
pass: pass, pass: pass,
method: headers.AuthDigest, authenticateHeader: &auth,
realm: *auth.Realm,
nonce: *auth.Nonce,
}, nil }, nil
} }
@@ -62,15 +50,10 @@ func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) {
return nil, err return nil, err
} }
if auth.Realm == nil {
return nil, fmt.Errorf("realm is missing")
}
return &Sender{ return &Sender{
user: user, user: user,
pass: pass, pass: pass,
method: headers.AuthBasic, authenticateHeader: &auth,
realm: *auth.Realm,
}, nil }, nil
} }
@@ -82,26 +65,19 @@ 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.method, Method: se.authenticateHeader.Method,
} }
switch se.method { if se.authenticateHeader.Method == headers.AuthBasic {
case headers.AuthBasic:
h.BasicUser = se.user h.BasicUser = se.user
h.BasicPass = se.pass h.BasicPass = se.pass
} else { // digest
default: // headers.AuthDigest h.Username = se.user
response := md5Hex(md5Hex(se.user+":"+se.realm+":"+se.pass) + ":" + h.Realm = se.authenticateHeader.Realm
se.nonce + ":" + md5Hex(string(req.Method)+":"+urStr)) h.Nonce = se.authenticateHeader.Nonce
h.URI = urStr
h.DigestValues = headers.Authenticate{ h.Response = md5Hex(md5Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
Method: headers.AuthDigest, se.authenticateHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
Username: &se.user,
Realm: &se.realm,
Nonce: &se.nonce,
URI: &urStr,
Response: &response,
}
} }
if req.Header == nil { if req.Header == nil {

View File

@@ -3,51 +3,18 @@ package auth
import ( import (
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/base"
) )
func TestSenderErrors(t *testing.T) { func FuzzSender(f *testing.F) {
for _, ca := range []struct { f.Add(`Invalid`)
name string f.Add(`Digest`)
hv base.HeaderValue f.Add(`Digest nonce=123`)
err string f.Add(`Digest realm=123`)
}{ f.Add(`Basic`)
{ f.Add(`Basic nonce=123`)
"invalid method",
base.HeaderValue{`Invalid`}, f.Fuzz(func(t *testing.T, a string) {
"no authentication methods available", NewSender(base.HeaderValue{a}, "myuser", "mypass") //nolint:errcheck
},
{
"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)
}) })
} }
}

View File

@@ -39,14 +39,14 @@ func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce s
case headers.AuthBasic: case headers.AuthBasic:
ret = append(ret, (&headers.Authenticate{ ret = append(ret, (&headers.Authenticate{
Method: headers.AuthBasic, Method: headers.AuthBasic,
Realm: &realm, Realm: realm,
}).Marshal()...) }).Marshal()...)
case headers.AuthDigest: case headers.AuthDigest:
ret = append(ret, headers.Authenticate{ ret = append(ret, headers.Authenticate{
Method: headers.AuthDigest, Method: headers.AuthDigest,
Realm: &realm, Realm: realm,
Nonce: &nonce, Nonce: nonce,
}.Marshal()...) }.Marshal()...)
} }
} }
@@ -92,46 +92,26 @@ func Validate(
return fmt.Errorf("authentication failed") return fmt.Errorf("authentication failed")
} }
case auth.Method == headers.AuthDigest && contains(methods, headers.AuthDigest): case auth.Method == headers.AuthDigest && contains(methods, headers.AuthDigest):
if auth.DigestValues.Realm == nil { if auth.Nonce != nonce {
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 {
return fmt.Errorf("wrong nonce") return fmt.Errorf("wrong nonce")
} }
if *auth.DigestValues.Realm != realm { if auth.Realm != realm {
return fmt.Errorf("wrong realm") return fmt.Errorf("wrong realm")
} }
if *auth.DigestValues.Username != user { if auth.Username != user {
return fmt.Errorf("authentication failed") return fmt.Errorf("authentication failed")
} }
ur := req.URL ur := req.URL
if *auth.DigestValues.URI != ur.String() { if auth.URI != ur.String() {
// in SETUP requests, VLC strips the control attribute. // in SETUP requests, VLC strips the control attribute.
// try again with the base URL. // try again with the base URL.
if baseURL != nil { if baseURL != nil {
ur = baseURL ur = baseURL
if *auth.DigestValues.URI != ur.String() { if auth.URI != ur.String() {
return fmt.Errorf("wrong URL") return fmt.Errorf("wrong URL")
} }
} else { } else {
@@ -142,7 +122,7 @@ func Validate(
response := md5Hex(md5Hex(user+":"+realm+":"+pass) + response := md5Hex(md5Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String())) ":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String()))
if *auth.DigestValues.Response != response { if auth.Response != response {
return fmt.Errorf("authentication failed") return fmt.Errorf("authentication failed")
} }
default: default:

View File

@@ -3,65 +3,28 @@ package auth
import ( import (
"testing" "testing"
"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"
"github.com/stretchr/testify/require"
) )
func TestValidateErrors(t *testing.T) { func FuzzValidate(f *testing.F) {
for _, ca := range []struct { f.Add(`Invalid`)
name string f.Add(`Digest `)
hv base.HeaderValue f.Add(`Digest realm=123`)
err string 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`)
"invalid auth", f.Add(`Digest realm=123,nonce=123,username=123,uri=123,response=123`)
base.HeaderValue{`Invalid`}, f.Add(`Digest realm=123,nonce=abcde,username=123,uri=123,response=123`)
"invalid authorization header",
}, f.Fuzz(func(t *testing.T, a string) {
{ Validate( //nolint:errcheck
"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{ &base.Request{
Method: base.Describe, Method: base.Describe,
URL: nil, URL: nil,
Header: base.Header{ Header: base.Header{
"Authorization": ca.hv, "Authorization": base.HeaderValue{a},
}, },
}, },
"myuser", "myuser",
@@ -71,7 +34,24 @@ func TestValidateErrors(t *testing.T) {
"IPCAM", "IPCAM",
"abcde", "abcde",
) )
require.EqualError(t, err, ca.err)
}) })
} }
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)
} }

View File

@@ -19,37 +19,32 @@ const (
AuthDigest AuthDigest
) )
// Authenticate is an Authenticate or a WWW-Authenticate header. // Authenticate is a WWW-Authenticate header.
type Authenticate struct { type Authenticate struct {
// authentication method // authentication method
Method AuthMethod Method AuthMethod
// (optional) username // realm
Username *string Realm string
// (optional) realm //
Realm *string // Digest authentication fields
//
// (optional) nonce // nonce
Nonce *string Nonce string
// (optional) uri // opaque
URI *string
// (optional) response
Response *string
// (optional) opaque
Opaque *string Opaque *string
// (optional) stale // stale
Stale *string Stale *string
// (optional) algorithm // algorithm
Algorithm *string 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 { func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
if len(v) == 0 { if len(v) == 0 {
return fmt.Errorf("value not provided") return fmt.Errorf("value not provided")
@@ -78,29 +73,46 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
return fmt.Errorf("invalid method (%s)", method) return fmt.Errorf("invalid method (%s)", method)
} }
if h.Method == AuthBasic {
kvs, err := keyValParse(v0, ',') kvs, err := keyValParse(v0, ',')
if err != nil { if err != nil {
return err return err
} }
realmReceived := false
for k, rv := range kvs {
v := rv
if k == "realm" {
h.Realm = v
realmReceived = true
}
}
if !realmReceived {
return fmt.Errorf("realm is missing")
}
} else { // digest
kvs, err := keyValParse(v0, ',')
if err != nil {
return err
}
realmReceived := false
nonceReceived := false
for k, rv := range kvs { for k, rv := range kvs {
v := rv v := rv
switch k { switch k {
case "username":
h.Username = &v
case "realm": case "realm":
h.Realm = &v h.Realm = v
realmReceived = true
case "nonce": case "nonce":
h.Nonce = &v h.Nonce = v
nonceReceived = true
case "uri":
h.URI = &v
case "response":
h.Response = &v
case "opaque": case "opaque":
h.Opaque = &v h.Opaque = &v
@@ -113,58 +125,34 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
} }
} }
if !realmReceived || !nonceReceived {
return fmt.Errorf("one or more digest fields are missing")
}
}
return nil return nil
} }
// Marshal encodes an Authenticate or a WWW-Authenticate header. // Marshal encodes a WWW-Authenticate header.
func (h Authenticate) Marshal() base.HeaderValue { func (h Authenticate) Marshal() base.HeaderValue {
ret := "" if h.Method == AuthBasic {
return base.HeaderValue{"Basic " +
switch h.Method { "realm=\"" + h.Realm + "\""}
case AuthBasic:
ret += "Basic"
case AuthDigest:
ret += "Digest"
} }
ret += " " ret := "Digest realm=\"" + h.Realm + "\", nonce=\"" + h.Nonce + "\""
var rets []string
if h.Username != nil {
rets = append(rets, "username=\""+*h.Username+"\"")
}
if h.Realm != nil {
rets = append(rets, "realm=\""+*h.Realm+"\"")
}
if h.Nonce != nil {
rets = append(rets, "nonce=\""+*h.Nonce+"\"")
}
if h.URI != nil {
rets = append(rets, "uri=\""+*h.URI+"\"")
}
if h.Response != nil {
rets = append(rets, "response=\""+*h.Response+"\"")
}
if h.Opaque != nil { if h.Opaque != nil {
rets = append(rets, "opaque=\""+*h.Opaque+"\"") ret += ", opaque=\"" + *h.Opaque + "\""
} }
if h.Stale != nil { if h.Stale != nil {
rets = append(rets, "stale=\""+*h.Stale+"\"") ret += ", stale=\"" + *h.Stale + "\""
} }
if h.Algorithm != nil { if h.Algorithm != nil {
rets = append(rets, "algorithm=\""+*h.Algorithm+"\"") ret += ", algorithm=\"" + *h.Algorithm + "\""
} }
ret += strings.Join(rets, ", ")
return base.HeaderValue{ret} return base.HeaderValue{ret}
} }

View File

@@ -24,82 +24,52 @@ var casesAuthenticate = []struct {
base.HeaderValue{`Basic realm="4419b63f5e51"`}, base.HeaderValue{`Basic realm="4419b63f5e51"`},
Authenticate{ Authenticate{
Method: AuthBasic, 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"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
Authenticate{ Authenticate{
Method: AuthDigest, Method: AuthDigest,
Realm: stringPtr("4419b63f5e51"), Realm: "4419b63f5e51",
Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"), Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"), 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`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
Authenticate{ Authenticate{
Method: AuthDigest, Method: AuthDigest,
Realm: stringPtr("4419b63f5e51"), Realm: "4419b63f5e51",
Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"), Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"), 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"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", stale="FALSE"`}, base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", stale="FALSE"`},
Authenticate{ Authenticate{
Method: AuthDigest, Method: AuthDigest,
Realm: stringPtr("4419b63f5e51"), Realm: "4419b63f5e51",
Nonce: stringPtr("133767111917411116111311118211673010032"), Nonce: "133767111917411116111311118211673010032",
Stale: stringPtr("FALSE"), Stale: stringPtr("FALSE"),
}, },
}, },
{ {
"digest response generic", "digest after failed auth",
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",
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,algorithm=MD5`},
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", algorithm="MD5"`},
Authenticate{ Authenticate{
Method: AuthDigest, Method: AuthDigest,
Realm: stringPtr("Please log in with a valid username"), Realm: "Please log in with a valid username",
Nonce: stringPtr("752a62306daf32b401a41004555c7663"), Nonce: "752a62306daf32b401a41004555c7663",
Opaque: stringPtr(""), Opaque: stringPtr(""),
Stale: stringPtr("FALSE"), Stale: stringPtr("FALSE"),
Algorithm: stringPtr("MD5"), 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) { func TestAuthenticateMarshal(t *testing.T) {
for _, ca := range casesAuthenticate { for _, ca := range casesAuthenticate {
t.Run(ca.name, func(t *testing.T) { 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)
}()
}

View File

@@ -13,14 +13,37 @@ type Authorization struct {
// authentication method // authentication method
Method AuthMethod Method AuthMethod
// basic user //
// Basic authentication fields
//
// user
BasicUser string BasicUser string
// basic password // password
BasicPass string 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. // Unmarshal decodes an Authorization header.
@@ -35,12 +58,24 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
v0 := v[0] v0 := v[0]
switch { i := strings.IndexByte(v0, ' ')
case strings.HasPrefix(v0, "Basic "): 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 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) tmp, err := base64.StdEncoding.DecodeString(v0)
if err != nil { if err != nil {
return fmt.Errorf("invalid value") 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] h.BasicUser, h.BasicPass = tmp2[0], tmp2[1]
} else { // digest
case strings.HasPrefix(v0, "Digest "): kvs, err := keyValParse(v0, ',')
h.Method = AuthDigest
var vals Authenticate
err := vals.Unmarshal(base.HeaderValue{v0})
if err != nil { if err != nil {
return err return err
} }
h.DigestValues = vals realmReceived := false
usernameReceived := false
nonceReceived := false
uriReceived := false
responseReceived := false
default: for k, rv := range kvs {
return fmt.Errorf("invalid authorization header") 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 return nil
@@ -73,13 +138,18 @@ 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 {
switch h.Method { if h.Method == AuthBasic {
case AuthBasic: return base.HeaderValue{"Basic " +
response := base64.StdEncoding.EncodeToString([]byte(h.BasicUser + ":" + h.BasicPass)) base64.StdEncoding.EncodeToString([]byte(h.BasicUser+":"+h.BasicPass))}
return base.HeaderValue{"Basic " + response}
default: // AuthDigest
return h.DigestValues.Marshal()
} }
ret := "Digest " +
"username=\"" + h.Username + "\", realm=\"" + h.Realm + "\", " +
"nonce=\"" + h.Nonce + "\", uri=\"" + h.URI + "\", response=\"" + h.Response + "\""
if h.Opaque != nil {
ret += ", opaque=\"" + *h.Opaque + "\""
}
return base.HeaderValue{ret}
} }

View File

@@ -26,17 +26,38 @@ var casesAuthorization = []struct {
}, },
{ {
"digest", "digest",
base.HeaderValue{"Digest realm=\"4419b63f5e51\", nonce=\"8b84a3b789283a8bea8da7fa7d41f08b\", stale=\"FALSE\""}, base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
base.HeaderValue{"Digest realm=\"4419b63f5e51\", nonce=\"8b84a3b789283a8bea8da7fa7d41f08b\", stale=\"FALSE\""}, `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{ Authorization{
Method: AuthDigest, Method: AuthDigest,
DigestValues: Authenticate{ Username: "Mufasa",
Method: AuthDigest, Realm: "testrealm@host.com",
Realm: stringPtr("4419b63f5e51"), Nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093",
Nonce: stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"), URI: "/dir/index.html",
Stale: stringPtr("FALSE"), 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) { func TestAuthorizationMarshal(t *testing.T) {
for _, ca := range casesAuthorization { for _, ca := range casesAuthorization {
t.Run(ca.name, func(t *testing.T) { 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)
}()
}

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Basic =\"")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("0")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string(" ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest =\"")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Basic ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("0")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string(" ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest =\"")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Basic ")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Basic 0")