auth: add Validate() and deprecate Validator{} (#272)

This commit is contained in:
Alessandro Ros
2023-05-07 19:34:20 +02:00
committed by GitHub
parent 2170ef4b00
commit 7c67221494
11 changed files with 280 additions and 106 deletions

View File

@@ -1286,12 +1286,12 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
v := auth.NewValidator("myuser", "mypass", nil) nonce := auth.GenerateNonce()
err = conn.WriteResponse(&base.Response{ err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusUnauthorized, StatusCode: base.StatusUnauthorized,
Header: base.Header{ Header: base.Header{
"WWW-Authenticate": v.Header(), "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -1300,7 +1300,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
err = v.ValidateRequest(req, nil) err = auth.Validate(req, "myuser", "mypass", nil, nil, "IPCAM", nonce)
require.NoError(t, err) require.NoError(t, err)
err = conn.WriteResponse(&base.Response{ err = conn.WriteResponse(&base.Response{
@@ -1399,12 +1399,12 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Setup, req.Method) require.Equal(t, base.Setup, req.Method)
v := auth.NewValidator("myuser", "mypass", nil) nonce := auth.GenerateNonce()
err = conn.WriteResponse(&base.Response{ err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusUnauthorized, StatusCode: base.StatusUnauthorized,
Header: base.Header{ Header: base.Header{
"WWW-Authenticate": v.Header(), "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -1414,7 +1414,7 @@ func TestClientPlayAutomaticProtocol(t *testing.T) {
require.Equal(t, base.Setup, req.Method) require.Equal(t, base.Setup, req.Method)
require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/"+medias[0].Control), req.URL) require.Equal(t, mustParseURL("rtsp://localhost:8554/teststream/"+medias[0].Control), req.URL)
err = v.ValidateRequest(req, nil) err = auth.Validate(req, "myuser", "mypass", nil, nil, "IPCAM", nonce)
require.NoError(t, err) require.NoError(t, err)
var inTH headers.Transport var inTH headers.Transport

View File

@@ -165,12 +165,12 @@ func TestClientAuth(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
v := auth.NewValidator("myuser", "mypass", nil) nonce := auth.GenerateNonce()
err = conn.WriteResponse(&base.Response{ err = conn.WriteResponse(&base.Response{
StatusCode: base.StatusUnauthorized, StatusCode: base.StatusUnauthorized,
Header: base.Header{ Header: base.Header{
"WWW-Authenticate": v.Header(), "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
}, },
}) })
require.NoError(t, err) require.NoError(t, err)
@@ -179,7 +179,7 @@ func TestClientAuth(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, base.Describe, req.Method) require.Equal(t, base.Describe, req.Method)
err = v.ValidateRequest(req, nil) err = auth.Validate(req, "myuser", "mypass", nil, nil, "IPCAM", nonce)
require.NoError(t, err) require.NoError(t, err)
medias := media.Medias{testH264Media} medias := media.Medias{testH264Media}

View File

@@ -47,10 +47,10 @@ func TestAuth(t *testing.T) {
} }
t.Run(c1.name+"_"+conf, func(t *testing.T) { t.Run(c1.name+"_"+conf, func(t *testing.T) {
va := NewValidator("testuser", "testpass", c1.methods) nonce := GenerateNonce()
wwwAuthenticate := va.Header()
se, err := NewSender(wwwAuthenticate, se, err := NewSender(
GenerateWWWAuthenticate(c1.methods, "IPCAM", nonce),
func() string { func() string {
if conf == "wronguser" { if conf == "wronguser" {
return "test1user" return "test1user"
@@ -78,7 +78,7 @@ func TestAuth(t *testing.T) {
req.URL = mustParseURL("rtsp://myhost/mypath") req.URL = mustParseURL("rtsp://myhost/mypath")
err = va.ValidateRequest(req, nil) err = Validate(req, "testuser", "testpass", nil, c1.methods, "IPCAM", nonce)
if conf != "nofail" { if conf != "nofail" {
require.Error(t, err) require.Error(t, err)
@@ -104,10 +104,12 @@ func TestAuthVLC(t *testing.T) {
"rtsp://myhost/mypath/test?testing/trackID=0", "rtsp://myhost/mypath/test?testing/trackID=0",
}, },
} { } {
va := NewValidator("testuser", "testpass", nonce := GenerateNonce()
[]headers.AuthMethod{headers.AuthBasic, headers.AuthDigest})
se, err := NewSender(va.Header(), "testuser", "testpass") se, err := NewSender(
GenerateWWWAuthenticate(nil, "IPCAM", nonce),
"testuser",
"testpass")
require.NoError(t, err) require.NoError(t, err)
req := &base.Request{ req := &base.Request{
@@ -117,53 +119,10 @@ func TestAuthVLC(t *testing.T) {
se.AddAuthorization(req) se.AddAuthorization(req)
req.URL = mustParseURL(ca.mediaURL) req.URL = mustParseURL(ca.mediaURL)
err = va.ValidateRequest(req, mustParseURL(ca.clientURL)) err = Validate(req, "testuser", "testpass", mustParseURL(ca.clientURL), nil, "IPCAM", nonce)
require.NoError(t, err) require.NoError(t, err)
err = va.ValidateRequest(req, mustParseURL("rtsp://invalid")) err = Validate(req, "testuser", "testpass", mustParseURL("rtsp://invalid"), nil, "IPCAM", nonce)
require.Error(t, err) require.Error(t, err)
} }
} }
func TestAuthHashed(t *testing.T) {
for _, conf := range []string{
"nofail",
"wronguser",
"wrongpass",
} {
t.Run(conf, func(t *testing.T) {
se := NewValidator("sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=",
"sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=",
[]headers.AuthMethod{headers.AuthBasic, headers.AuthDigest})
va, err := NewSender(se.Header(),
func() string {
if conf == "wronguser" {
return "test1user"
}
return "testuser"
}(),
func() string {
if conf == "wrongpass" {
return "test1pass"
}
return "testpass"
}())
require.NoError(t, err)
req := &base.Request{
Method: base.Announce,
URL: mustParseURL("rtsp://myhost/mypath"),
}
va.AddAuthorization(req)
err = se.ValidateRequest(req, nil)
if conf != "nofail" {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -8,7 +8,16 @@ import (
"github.com/bluenviron/gortsplib/v3/pkg/headers" "github.com/bluenviron/gortsplib/v3/pkg/headers"
) )
// Sender allows to generate credentials for a Validator. func findHeader(v base.HeaderValue, prefix string) string {
for _, vi := range v {
if strings.HasPrefix(vi, prefix) {
return vi
}
}
return ""
}
// Sender allows to send credentials.
type Sender struct { type Sender struct {
user string user string
pass string pass string
@@ -17,18 +26,12 @@ type Sender struct {
nonce string nonce string
} }
// NewSender allocates a Sender with the WWW-Authenticate header provided by // NewSender allocates a Sender.
// a Validator and a set of credentials. // It requires a WWW-Authenticate header (provided by the server)
// and a set of credentials.
func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) { func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) {
// prefer digest // prefer digest
if v0 := func() string { if v0 := findHeader(v, "Digest"); v0 != "" {
for _, vi := range v {
if strings.HasPrefix(vi, "Digest") {
return vi
}
}
return ""
}(); v0 != "" {
var auth headers.Authenticate var auth headers.Authenticate
err := auth.Unmarshal(base.HeaderValue{v0}) err := auth.Unmarshal(base.HeaderValue{v0})
if err != nil { if err != nil {
@@ -52,14 +55,7 @@ func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) {
}, nil }, nil
} }
if v0 := func() string { if v0 := findHeader(v, "Basic"); v0 != "" {
for _, vi := range v {
if strings.HasPrefix(vi, "Basic") {
return vi
}
}
return ""
}(); v0 != "" {
var auth headers.Authenticate var auth headers.Authenticate
err := auth.Unmarshal(base.HeaderValue{v0}) err := auth.Unmarshal(base.HeaderValue{v0})
if err != nil { if err != nil {

View File

@@ -1,20 +0,0 @@
package auth
import (
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
)
func md5Hex(in string) string {
h := md5.New()
h.Write([]byte(in))
return hex.EncodeToString(h.Sum(nil))
}
func sha256Base64(in string) string {
h := sha256.New()
h.Write([]byte(in))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

143
pkg/auth/validate.go Normal file
View File

@@ -0,0 +1,143 @@
package auth
import (
"crypto/rand"
"encoding/hex"
"fmt"
"github.com/bluenviron/gortsplib/v3/pkg/base"
"github.com/bluenviron/gortsplib/v3/pkg/headers"
"github.com/bluenviron/gortsplib/v3/pkg/url"
)
// GenerateNonce generates a nonce that can be used in Validate().
func GenerateNonce() string {
byts := make([]byte, 16)
rand.Read(byts)
return hex.EncodeToString(byts)
}
// GenerateWWWAuthenticate generates a WWW-Authenticate header.
func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce string) base.HeaderValue {
if methods == nil {
methods = []headers.AuthMethod{headers.AuthBasic, headers.AuthDigest}
}
var ret base.HeaderValue
for _, m := range methods {
switch m {
case headers.AuthBasic:
ret = append(ret, (&headers.Authenticate{
Method: headers.AuthBasic,
Realm: &realm,
}).Marshal()...)
case headers.AuthDigest:
ret = append(ret, headers.Authenticate{
Method: headers.AuthDigest,
Realm: &realm,
Nonce: &nonce,
}.Marshal()...)
}
}
return ret
}
func contains(list []headers.AuthMethod, item headers.AuthMethod) bool {
for _, i := range list {
if i == item {
return true
}
}
return false
}
// Validate validates a request sent by a client.
func Validate(
req *base.Request,
user string,
pass string,
baseURL *url.URL,
methods []headers.AuthMethod,
realm string,
nonce string,
) error {
if methods == nil {
methods = []headers.AuthMethod{headers.AuthBasic, headers.AuthDigest}
}
var auth headers.Authorization
err := auth.Unmarshal(req.Header["Authorization"])
if err != nil {
return err
}
switch {
case auth.Method == headers.AuthBasic && contains(methods, headers.AuthBasic):
if auth.BasicUser != user {
return fmt.Errorf("authentication failed")
}
if auth.BasicPass != pass {
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 {
return fmt.Errorf("wrong nonce")
}
if *auth.DigestValues.Realm != realm {
return fmt.Errorf("wrong realm")
}
if *auth.DigestValues.Username != user {
return fmt.Errorf("authentication failed")
}
ur := req.URL
if *auth.DigestValues.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() {
return fmt.Errorf("wrong URL")
}
} else {
return fmt.Errorf("wrong URL")
}
}
response := md5Hex(md5Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String()))
if *auth.DigestValues.Response != response {
return fmt.Errorf("authentication failed")
}
default:
return fmt.Errorf("no supported authentication methods found")
}
return nil
}

77
pkg/auth/validate_test.go Normal file
View File

@@ -0,0 +1,77 @@
package auth
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/bluenviron/gortsplib/v3/pkg/base"
)
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,
},
},
"myuser",
"mypass",
nil,
nil,
"IPCAM",
"abcde",
)
require.EqualError(t, err, ca.err)
})
}
}

View File

@@ -1,7 +1,10 @@
package auth package auth
import ( import (
"crypto/md5"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"strings" "strings"
@@ -11,7 +14,20 @@ import (
"github.com/bluenviron/gortsplib/v3/pkg/url" "github.com/bluenviron/gortsplib/v3/pkg/url"
) )
func md5Hex(in string) string {
h := md5.New()
h.Write([]byte(in))
return hex.EncodeToString(h.Sum(nil))
}
func sha256Base64(in string) string {
h := sha256.New()
h.Write([]byte(in))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
// Validator allows to validate credentials generated by a Sender. // Validator allows to validate credentials generated by a Sender.
// Deprecated: Validator{} has been replaced by Validate()
type Validator struct { type Validator struct {
user string user string
userHashed bool userHashed bool
@@ -24,6 +40,7 @@ type Validator struct {
// NewValidator allocates a Validator. // NewValidator allocates a Validator.
// If methods is nil, the Basic and Digest methods are used. // If methods is nil, the Basic and Digest methods are used.
// Deprecated: Validator{} has been replaced by Validate()
func NewValidator(user string, pass string, methods []headers.AuthMethod) *Validator { func NewValidator(user string, pass string, methods []headers.AuthMethod) *Validator {
if methods == nil { if methods == nil {
methods = []headers.AuthMethod{headers.AuthBasic, headers.AuthDigest} methods = []headers.AuthMethod{headers.AuthBasic, headers.AuthDigest}
@@ -63,6 +80,7 @@ func NewValidator(user string, pass string, methods []headers.AuthMethod) *Valid
// Header generates the WWW-Authenticate header needed by a client to // Header generates the WWW-Authenticate header needed by a client to
// authenticate. // authenticate.
// Deprecated: Validator{} has been replaced by Validate()
func (va *Validator) Header() base.HeaderValue { func (va *Validator) Header() base.HeaderValue {
var ret base.HeaderValue var ret base.HeaderValue
for _, m := range va.methods { for _, m := range va.methods {
@@ -84,8 +102,9 @@ func (va *Validator) Header() base.HeaderValue {
return ret return ret
} }
// ValidateRequest validates a request sent by a client. // Validate validates a request sent by a client.
func (va *Validator) ValidateRequest(req *base.Request, baseURL *url.URL) error { // Deprecated: Validator{} has been replaced by Validate()
func (va *Validator) Validate(req *base.Request, baseURL *url.URL) error {
var auth headers.Authorization var auth headers.Authorization
err := auth.Unmarshal(req.Header["Authorization"]) err := auth.Unmarshal(req.Header["Authorization"])
if err != nil { if err != nil {

View File

@@ -58,7 +58,7 @@ func TestValidatorErrors(t *testing.T) {
t.Run(ca.name, func(t *testing.T) { t.Run(ca.name, func(t *testing.T) {
va := NewValidator("myuser", "mypass", nil) va := NewValidator("myuser", "mypass", nil)
va.nonce = "abcde" va.nonce = "abcde"
err := va.ValidateRequest(&base.Request{ err := va.Validate(&base.Request{
Method: base.Describe, Method: base.Describe,
URL: nil, URL: nil,
Header: base.Header{ Header: base.Header{

View File

@@ -1045,17 +1045,17 @@ func TestServerSessionTeardown(t *testing.T) {
} }
func TestServerAuth(t *testing.T) { func TestServerAuth(t *testing.T) {
authValidator := auth.NewValidator("myuser", "mypass", nil) nonce := auth.GenerateNonce()
s := &Server{ s := &Server{
Handler: &testServerHandler{ Handler: &testServerHandler{
onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) { onAnnounce: func(ctx *ServerHandlerOnAnnounceCtx) (*base.Response, error) {
err := authValidator.ValidateRequest(ctx.Request, nil) err := auth.Validate(ctx.Request, "myuser", "mypass", nil, nil, "IPCAM", nonce)
if err != nil { if err != nil {
return &base.Response{ return &base.Response{
StatusCode: base.StatusUnauthorized, StatusCode: base.StatusUnauthorized,
Header: base.Header{ Header: base.Header{
"WWW-Authenticate": authValidator.Header(), "WWW-Authenticate": auth.GenerateWWWAuthenticate(nil, "IPCAM", nonce),
}, },
}, nil }, nil
} }