diff --git a/auth-client.go b/auth-client.go deleted file mode 100644 index 70af845e..00000000 --- a/auth-client.go +++ /dev/null @@ -1,73 +0,0 @@ -package gortsplib - -import ( - "crypto/md5" - "encoding/hex" - "fmt" - "net/url" - "strings" -) - -func md5Hex(in string) string { - h := md5.New() - h.Write([]byte(in)) - return hex.EncodeToString(h.Sum(nil)) -} - -// AuthClient is an object that helps a client sending its credentials to a -// server, with the Digest authentication method. -type AuthClient struct { - user string - pass string - realm string - nonce string -} - -// NewAuthClient allocates an AuthClient. -// header is the WWW-Authenticate header provided by the server. -func NewAuthClient(header []string, user string, pass string) (*AuthClient, error) { - headerAuthDigest := func() string { - for _, v := range header { - if strings.HasPrefix(v, "Digest ") { - return v - } - } - return "" - }() - if headerAuthDigest == "" { - return nil, fmt.Errorf("Authentication/Digest header not provided") - } - - auth, err := ReadHeaderAuth(headerAuthDigest) - if err != nil { - return nil, err - } - - nonce, ok := auth.Values["nonce"] - if !ok { - return nil, fmt.Errorf("nonce not provided") - } - - realm, ok := auth.Values["realm"] - if !ok { - return nil, fmt.Errorf("realm not provided") - } - - return &AuthClient{ - user: user, - pass: pass, - realm: realm, - nonce: nonce, - }, nil -} - -// GenerateHeader generates an Authorization Header that allows to authenticate a request with -// the given method and url. -func (ac *AuthClient) GenerateHeader(method Method, ur *url.URL) []string { - ha1 := md5Hex(ac.user + ":" + ac.realm + ":" + ac.pass) - ha2 := md5Hex(string(method) + ":" + ur.String()) - response := md5Hex(ha1 + ":" + ac.nonce + ":" + ha2) - - return []string{fmt.Sprintf("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"", - ac.user, ac.realm, ac.nonce, ur.String(), response)} -} diff --git a/auth-server.go b/auth-server.go deleted file mode 100644 index 8fc6bc79..00000000 --- a/auth-server.go +++ /dev/null @@ -1,100 +0,0 @@ -package gortsplib - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "net/url" -) - -// AuthServer is an object that helps a server validating the credentials of -// a client, sent with the Digest authentication method. -type AuthServer struct { - nonce string - realm string - user string - pass string -} - -// NewAuthServer allocates an AuthServer. -func NewAuthServer(user string, pass string) *AuthServer { - nonceByts := make([]byte, 16) - rand.Read(nonceByts) - nonce := hex.EncodeToString(nonceByts) - - return &AuthServer{ - nonce: nonce, - realm: "IPCAM", - user: user, - pass: pass, - } -} - -// GenerateHeader generates the WWW-Authenticate header needed by a client to log in. -func (as *AuthServer) GenerateHeader() []string { - return []string{fmt.Sprintf("Digest nonce=\"%s\", realm=\"%s\"", as.nonce, as.realm)} -} - -// ValidateHeader validates the Authorization header sent by a client after receiving the -// WWW-Authenticate header provided by GenerateHeader(). -func (as *AuthServer) ValidateHeader(header []string, method Method, ur *url.URL) error { - if len(header) != 1 { - return fmt.Errorf("Authorization header not provided") - } - - auth, err := ReadHeaderAuth(header[0]) - if err != nil { - return err - } - - inNonce, ok := auth.Values["nonce"] - if !ok { - return fmt.Errorf("nonce not provided") - } - - inRealm, ok := auth.Values["realm"] - if !ok { - return fmt.Errorf("realm not provided") - } - - inUsername, ok := auth.Values["username"] - if !ok { - return fmt.Errorf("username not provided") - } - - inUri, ok := auth.Values["uri"] - if !ok { - return fmt.Errorf("uri not provided") - } - - inResponse, ok := auth.Values["response"] - if !ok { - return fmt.Errorf("response not provided") - } - - if inNonce != as.nonce { - return fmt.Errorf("wrong nonce") - } - - if inRealm != as.realm { - return fmt.Errorf("wrong realm") - } - - if inUsername != as.user { - return fmt.Errorf("wrong username") - } - - if inUri != ur.String() { - return fmt.Errorf("wrong url") - } - - ha1 := md5Hex(as.user + ":" + as.realm + ":" + as.pass) - ha2 := md5Hex(string(method) + ":" + ur.String()) - response := md5Hex(ha1 + ":" + as.nonce + ":" + ha2) - - if inResponse != response { - return fmt.Errorf("wrong response") - } - - return nil -} diff --git a/auth-server_test.go b/auth-server_test.go deleted file mode 100644 index e408efdc..00000000 --- a/auth-server_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package gortsplib - -import ( - "net/url" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestAuthClientServer(t *testing.T) { - as := NewAuthServer("testuser", "testpass") - wwwAuthenticate := as.GenerateHeader() - - ac, err := NewAuthClient(wwwAuthenticate, "testuser", "testpass") - require.NoError(t, err) - authorization := ac.GenerateHeader(ANNOUNCE, - &url.URL{Scheme: "rtsp", Host: "myhost", Path: "mypath"}) - - err = as.ValidateHeader(authorization, ANNOUNCE, - &url.URL{Scheme: "rtsp", Host: "myhost", Path: "mypath"}) - require.NoError(t, err) -} diff --git a/auth.go b/auth.go new file mode 100644 index 00000000..707a06ff --- /dev/null +++ b/auth.go @@ -0,0 +1,265 @@ +package gortsplib + +import ( + "crypto/md5" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "net/url" + "strings" +) + +func md5Hex(in string) string { + h := md5.New() + h.Write([]byte(in)) + return hex.EncodeToString(h.Sum(nil)) +} + +// AuthMethod is an authentication method. +type AuthMethod int + +const ( + Basic AuthMethod = iota + Digest +) + +// AuthServer is an object that helps a server to validate the credentials of +// a client. +type AuthServer struct { + user string + pass string + methods []AuthMethod + realm string + nonce string +} + +// NewAuthServer allocates an AuthServer. +// If methods is nil, Basic and Digest authentications are used. +func NewAuthServer(user string, pass string, methods []AuthMethod) *AuthServer { + if methods == nil { + methods = []AuthMethod{Basic, Digest} + } + + nonceByts := make([]byte, 16) + rand.Read(nonceByts) + nonce := hex.EncodeToString(nonceByts) + + return &AuthServer{ + user: user, + pass: pass, + methods: methods, + realm: "IPCAM", + nonce: nonce, + } +} + +// GenerateHeader generates the WWW-Authenticate header needed by a client to log in. +func (as *AuthServer) GenerateHeader() []string { + var ret []string + for _, m := range as.methods { + switch m { + case Basic: + ret = append(ret, (&HeaderAuth{ + Prefix: "Basic", + Values: map[string]string{ + "realm": as.realm, + }, + }).Write()) + + case Digest: + ret = append(ret, (&HeaderAuth{ + Prefix: "Digest", + Values: map[string]string{ + "realm": as.realm, + "nonce": as.nonce, + }, + }).Write()) + } + } + return ret +} + +// ValidateHeader validates the Authorization header sent by a client after receiving the +// WWW-Authenticate header provided by GenerateHeader(). +func (as *AuthServer) ValidateHeader(header []string, method Method, ur *url.URL) error { + if len(header) != 1 { + return fmt.Errorf("authorization header not provided or provided multiple times") + } + + head := header[0] + + if strings.HasPrefix(head, "Basic ") { + inResponse := head[len("Basic "):] + + response := base64.StdEncoding.EncodeToString([]byte(as.user + ":" + as.pass)) + + if inResponse != response { + return fmt.Errorf("wrong response") + } + + } else if strings.HasPrefix(head, "Digest ") { + auth, err := ReadHeaderAuth(head) + if err != nil { + return err + } + + inRealm, ok := auth.Values["realm"] + if !ok { + return fmt.Errorf("realm not provided") + } + + inNonce, ok := auth.Values["nonce"] + if !ok { + return fmt.Errorf("nonce not provided") + } + + inUsername, ok := auth.Values["username"] + if !ok { + return fmt.Errorf("username not provided") + } + + inUri, ok := auth.Values["uri"] + if !ok { + return fmt.Errorf("uri not provided") + } + + inResponse, ok := auth.Values["response"] + if !ok { + return fmt.Errorf("response not provided") + } + + if inNonce != as.nonce { + return fmt.Errorf("wrong nonce") + } + + if inRealm != as.realm { + return fmt.Errorf("wrong realm") + } + + if inUsername != as.user { + return fmt.Errorf("wrong username") + } + + if inUri != ur.String() { + return fmt.Errorf("wrong url") + } + + response := md5Hex(md5Hex(as.user+":"+as.realm+":"+as.pass) + + ":" + as.nonce + ":" + md5Hex(string(method)+":"+ur.String())) + + if inResponse != response { + return fmt.Errorf("wrong response") + } + + } else { + return fmt.Errorf("unsupported authorization header") + } + + return nil +} + +// AuthClient is an object that helps a client to send its credentials to a +// server. +type AuthClient struct { + user string + pass string + method AuthMethod + realm string + nonce string +} + +// NewAuthClient allocates an AuthClient. +// header is the WWW-Authenticate header provided by the server. +func NewAuthClient(header []string, user string, pass string) (*AuthClient, error) { + // prefer digest + headerAuthDigest := func() string { + for _, v := range header { + if strings.HasPrefix(v, "Digest ") { + return v + } + } + return "" + }() + if headerAuthDigest != "" { + auth, err := ReadHeaderAuth(headerAuthDigest) + if err != nil { + return nil, err + } + + realm, ok := auth.Values["realm"] + if !ok { + return nil, fmt.Errorf("realm not provided") + } + + nonce, ok := auth.Values["nonce"] + if !ok { + return nil, fmt.Errorf("nonce not provided") + } + + return &AuthClient{ + user: user, + pass: pass, + method: Digest, + realm: realm, + nonce: nonce, + }, nil + } + + headerAuthBasic := func() string { + for _, v := range header { + if strings.HasPrefix(v, "Basic ") { + return v + } + } + return "" + }() + if headerAuthBasic != "" { + auth, err := ReadHeaderAuth(headerAuthBasic) + if err != nil { + return nil, err + } + + realm, ok := auth.Values["realm"] + if !ok { + return nil, fmt.Errorf("realm not provided") + } + + return &AuthClient{ + user: user, + pass: pass, + method: Basic, + realm: realm, + }, nil + } + + return nil, fmt.Errorf("there are no authentication methods available") +} + +// GenerateHeader generates an Authorization Header that allows to authenticate a request with +// the given method and url. +func (ac *AuthClient) GenerateHeader(method Method, ur *url.URL) []string { + switch ac.method { + case Basic: + response := base64.StdEncoding.EncodeToString([]byte(ac.user + ":" + ac.pass)) + + return []string{"Basic " + response} + + case Digest: + response := md5Hex(md5Hex(ac.user+":"+ac.realm+":"+ac.pass) + ":" + + ac.nonce + ":" + md5Hex(string(method)+":"+ur.String())) + + return []string{(&HeaderAuth{ + Prefix: "Digest", + Values: map[string]string{ + "username": ac.user, + "realm": ac.realm, + "nonce": ac.nonce, + "uri": ur.String(), + "response": response, + }, + }).Write()} + } + + return nil +} diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 00000000..2718c8b4 --- /dev/null +++ b/auth_test.go @@ -0,0 +1,44 @@ +package gortsplib + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +var casesAuth = []struct { + name string + methods []AuthMethod +}{ + { + "basic", + []AuthMethod{Basic}, + }, + { + "digest", + []AuthMethod{Digest}, + }, + { + "both", + []AuthMethod{Basic, Digest}, + }, +} + +func TestAuth(t *testing.T) { + for _, c := range casesAuth { + t.Run(c.name, func(t *testing.T) { + authServer := NewAuthServer("testuser", "testpass", c.methods) + wwwAuthenticate := authServer.GenerateHeader() + + ac, err := NewAuthClient(wwwAuthenticate, "testuser", "testpass") + require.NoError(t, err) + authorization := ac.GenerateHeader(ANNOUNCE, + &url.URL{Scheme: "rtsp", Host: "myhost", Path: "mypath"}) + + err = authServer.ValidateHeader(authorization, ANNOUNCE, + &url.URL{Scheme: "rtsp", Host: "myhost", Path: "mypath"}) + require.NoError(t, err) + }) + } +} diff --git a/header-auth.go b/header-auth.go index 85ca9af9..8647b70e 100644 --- a/header-auth.go +++ b/header-auth.go @@ -3,6 +3,7 @@ package gortsplib import ( "fmt" "regexp" + "sort" "strings" ) @@ -40,3 +41,22 @@ func ReadHeaderAuth(in string) (*HeaderAuth, error) { return ha, nil } + +// Write encodes an Authenticate or a WWW-Authenticate header. +func (ha *HeaderAuth) Write() string { + ret := ha.Prefix + " " + + var sortedKeys []string + for key := range ha.Values { + sortedKeys = append(sortedKeys, key) + } + sort.Strings(sortedKeys) + + var tmp []string + for _, key := range sortedKeys { + tmp = append(tmp, key+"=\""+ha.Values[key]+"\"") + } + ret += strings.Join(tmp, ", ") + + return ret +} diff --git a/header-auth_test.go b/header-auth_test.go index 59a21672..a7dd9ffa 100644 --- a/header-auth_test.go +++ b/header-auth_test.go @@ -8,12 +8,14 @@ import ( var casesHeaderAuth = []struct { name string - byts string + dec string + enc string ha *HeaderAuth }{ { "basic", `Basic realm="4419b63f5e51"`, + `Basic realm="4419b63f5e51"`, &HeaderAuth{ Prefix: "Basic", Values: map[string]string{ @@ -24,6 +26,7 @@ var casesHeaderAuth = []struct { { "digest request 1", `Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`, + `Digest nonce="8b84a3b789283a8bea8da7fa7d41f08b", realm="4419b63f5e51", stale="FALSE"`, &HeaderAuth{ Prefix: "Digest", Values: map[string]string{ @@ -36,6 +39,7 @@ var casesHeaderAuth = []struct { { "digest request 2", `Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`, + `Digest nonce="8b84a3b789283a8bea8da7fa7d41f08b", realm="4419b63f5e51", stale="FALSE"`, &HeaderAuth{ Prefix: "Digest", Values: map[string]string{ @@ -48,6 +52,7 @@ var casesHeaderAuth = []struct { { "digest response", `Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`, + `Digest nonce="cc", realm="bb", response="ee", uri="dd", username="aa"`, &HeaderAuth{ Prefix: "Digest", Values: map[string]string{ @@ -61,12 +66,21 @@ var casesHeaderAuth = []struct { }, } -func TestHeaderAuth(t *testing.T) { +func TestHeaderAuthRead(t *testing.T) { for _, c := range casesHeaderAuth { t.Run(c.name, func(t *testing.T) { - req, err := ReadHeaderAuth(c.byts) + req, err := ReadHeaderAuth(c.dec) require.NoError(t, err) require.Equal(t, c.ha, req) }) } } + +func TestHeaderAuthWrite(t *testing.T) { + for _, c := range casesHeaderAuth { + t.Run(c.name, func(t *testing.T) { + req := c.ha.Write() + require.Equal(t, c.enc, req) + }) + } +}