diff --git a/authclient.go b/authclient.go new file mode 100644 index 00000000..95097e1b --- /dev/null +++ b/authclient.go @@ -0,0 +1,71 @@ +package gortsplib + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "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. +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 path. +func (ac *AuthClient) GenerateHeader(method string, path string) []string { + ha1 := md5Hex(ac.user + ":" + ac.realm + ":" + ac.pass) + ha2 := md5Hex(method + ":" + path) + 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, path, response)} +} diff --git a/authclientprovider.go b/authclientprovider.go deleted file mode 100644 index 14223042..00000000 --- a/authclientprovider.go +++ /dev/null @@ -1,38 +0,0 @@ -package gortsplib - -import ( - "crypto/md5" - "encoding/hex" - "fmt" -) - -func md5Hex(in string) string { - h := md5.New() - h.Write([]byte(in)) - return hex.EncodeToString(h.Sum(nil)) -} - -type authClientProvider struct { - user string - pass string - realm string - nonce string -} - -func newAuthClientProvider(user string, pass string, realm string, nonce string) *authClientProvider { - return &authClientProvider{ - user: user, - pass: pass, - realm: realm, - nonce: nonce, - } -} - -func (ap *authClientProvider) generateHeader(method string, path string) string { - ha1 := md5Hex(ap.user + ":" + ap.realm + ":" + ap.pass) - ha2 := md5Hex(method + ":" + path) - response := md5Hex(ha1 + ":" + ap.nonce + ":" + ha2) - - return fmt.Sprintf("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"", - ap.user, ap.realm, ap.nonce, path, response) -} diff --git a/authserver.go b/authserver.go new file mode 100644 index 00000000..beacac95 --- /dev/null +++ b/authserver.go @@ -0,0 +1,98 @@ +package gortsplib + +import ( + "crypto/rand" + "encoding/hex" + "fmt" +) + +// AuthServer is an object that helps a server validating the credentials of a client. +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 string, path string) 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 != path { + return fmt.Errorf("wrong uri") + } + + ha1 := md5Hex(as.user + ":" + as.realm + ":" + as.pass) + ha2 := md5Hex(method + ":" + path) + response := md5Hex(ha1 + ":" + as.nonce + ":" + ha2) + + if inResponse != response { + return fmt.Errorf("wrong response") + } + + return nil +} diff --git a/authserver_test.go b/authserver_test.go new file mode 100644 index 00000000..e32d7526 --- /dev/null +++ b/authserver_test.go @@ -0,0 +1,19 @@ +package gortsplib + +import ( + "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", "rtsp://myhost/mypath") + + err = as.ValidateHeader(authorization, "ANNOUNCE", "rtsp://myhost/mypath") + require.NoError(t, err) +} diff --git a/connclient.go b/connclient.go index e74d7a11..17b11287 100644 --- a/connclient.go +++ b/connclient.go @@ -14,7 +14,7 @@ type ConnClient struct { session string cseqEnabled bool cseq int - authProv *authClientProvider + authProv *AuthClient } // NewConnClient allocates a ConnClient. @@ -44,8 +44,10 @@ func (c *ConnClient) EnableCseq() { // SetCredentials allows to automatically insert the Authenticate header into every outgoing request. // The content of the header is computed with the given user, password, realm and nonce. -func (c *ConnClient) SetCredentials(user string, pass string, realm string, nonce string) { - c.authProv = newAuthClientProvider(user, pass, realm, nonce) +func (c *ConnClient) SetCredentials(wwwAuthenticateHeader []string, user string, pass string) error { + var err error + c.authProv, err = NewAuthClient(wwwAuthenticateHeader, user, pass) + return err } // WriteRequest writes a Request. @@ -67,7 +69,7 @@ func (c *ConnClient) WriteRequest(req *Request) error { if req.Header == nil { req.Header = make(Header) } - req.Header["Authorization"] = []string{c.authProv.generateHeader(req.Method, req.Url)} + req.Header["Authorization"] = c.authProv.GenerateHeader(req.Method, req.Url) } return req.write(c.bw) }