From 6d8c168f72402584d3f27753007b6ee7aa739798 Mon Sep 17 00:00:00 2001 From: David Date: Sat, 7 Apr 2018 08:37:48 -0500 Subject: [PATCH] Add Authenticator interface and Digest auth support --- basicAuth.go | 33 ++++++++++++++++++++ client.go | 60 +++++++++++++++++++++++++++++-------- digestAuth.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++ requests.go | 3 ++ 4 files changed, 166 insertions(+), 13 deletions(-) create mode 100644 basicAuth.go create mode 100644 digestAuth.go diff --git a/basicAuth.go b/basicAuth.go new file mode 100644 index 0000000..5a69113 --- /dev/null +++ b/basicAuth.go @@ -0,0 +1,33 @@ +package gowebdav + +import ( + "encoding/base64" +) + +// BasicAuth structure holds our credentials +type BasicAuth struct { + user string + pw string +} + +// Type identifies the BasicAuthenticator +func (b *BasicAuth) Type() string { + return "BasicAuth" +} + +// User holds the BasicAuth username +func (b *BasicAuth) User() string { + return b.user +} + +// Pass holds the BasicAuth password +func (b *BasicAuth) Pass() string { + return b.pw +} + +// Authorize the current request +func (b *BasicAuth) Authorize(c *Client, method string, path string) { + a := b.user + ":" + b.pw + auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a)) + c.headers.Set("Authorization", auth) +} diff --git a/client.go b/client.go index 988fd42..65414ea 100644 --- a/client.go +++ b/client.go @@ -3,7 +3,6 @@ package gowebdav import ( "bytes" - "encoding/base64" "encoding/xml" "io" "net/http" @@ -19,21 +18,45 @@ type Client struct { root string headers http.Header c *http.Client + auth Authenticator +} + +// Authenticator stub +type Authenticator interface { + Type() string + User() string + Pass() string + Authorize(*Client, string, string) +} + +// NoAuth structure holds our credentials +type NoAuth struct { + user string + pw string +} + +// Type identifies the authenticator +func (n *NoAuth) Type() string { + return "NoAuth" +} + +// User returns the current user +func (n *NoAuth) User() string { + return n.user +} + +// Pass returns the current password +func (n *NoAuth) Pass() string { + return n.pw +} + +// Authorize the current request +func (n *NoAuth) Authorize(c *Client, method string, path string) { } // NewClient creates a new instance of client func NewClient(uri, user, pw string) *Client { - c := &Client{uri, make(http.Header), &http.Client{}} - - if len(user) > 0 && len(pw) > 0 { - a := user + ":" + pw - auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(a)) - c.headers.Add("Authorization", auth) - } - - c.root = FixSlash(c.root) - - return c + return &Client{FixSlash(uri), make(http.Header), &http.Client{}, &NoAuth{user, pw}} } // SetHeader lets us set arbitrary headers for a given client @@ -63,7 +86,18 @@ func (c *Client) Connect() error { return err } - if rs.StatusCode != 200 || (rs.Header.Get("Dav") == "" && rs.Header.Get("DAV") == "") { + if rs.StatusCode == 401 && c.auth.Type() == "NoAuth" { + if strings.Index(rs.Header.Get("Www-Authenticate"), "Digest") > -1 { + c.auth = &DigestAuth{c.auth.User(), c.auth.Pass(), digestParts(rs)} + } else if strings.Index(rs.Header.Get("Www-Authenticate"), "Basic") > -1 { + c.auth = &BasicAuth{c.auth.User(), c.auth.Pass()} + } else { + return newPathError("Authorize", c.root, rs.StatusCode) + } + return c.Connect() + } else if rs.StatusCode == 401 { + return newPathError("Authorize", c.root, rs.StatusCode) + } else if rs.StatusCode != 200 || (rs.Header.Get("Dav") == "" && rs.Header.Get("DAV") == "") { return newPathError("Connect", c.root, rs.StatusCode) } diff --git a/digestAuth.go b/digestAuth.go new file mode 100644 index 0000000..868a2c6 --- /dev/null +++ b/digestAuth.go @@ -0,0 +1,83 @@ +package gowebdav + +import ( + "crypto/md5" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "net/http" + "strings" +) + +// DigestAuth structure holds our credentials +type DigestAuth struct { + user string + pw string + digestParts map[string]string +} + +// Type identifies the DigestAuthenticator +func (d *DigestAuth) Type() string { + return "DigestAuth" +} + +// User holds the DigestAuth username +func (d *DigestAuth) User() string { + return d.user +} + +// Pass holds the DigestAuth password +func (d *DigestAuth) Pass() string { + return d.pw +} + +// Authorize the current request +func (d *DigestAuth) Authorize(c *Client, method string, path string) { + d.digestParts["uri"] = path + d.digestParts["method"] = method + d.digestParts["username"] = d.user + d.digestParts["password"] = d.pw + c.headers.Set("Authorization", getDigestAuthorization(d.digestParts)) +} + +func digestParts(resp *http.Response) map[string]string { + result := map[string]string{} + if len(resp.Header["Www-Authenticate"]) > 0 { + wantedHeaders := []string{"nonce", "realm", "qop", "opaque"} + responseHeaders := strings.Split(resp.Header["Www-Authenticate"][0], ",") + for _, r := range responseHeaders { + for _, w := range wantedHeaders { + if strings.Contains(r, w) { + result[w] = strings.Split(r, `"`)[1] + } + } + } + } + return result +} + +func getMD5(text string) string { + hasher := md5.New() + hasher.Write([]byte(text)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +func getCnonce() string { + b := make([]byte, 8) + io.ReadFull(rand.Reader, b) + return fmt.Sprintf("%x", b)[:16] +} + +func getDigestAuthorization(digestParts map[string]string) string { + d := digestParts + // These are the correct ha1 and ha2 for qop=auth. We should probably check for other types of qop. + ha1 := getMD5(d["username"] + ":" + d["realm"] + ":" + d["password"]) + ha2 := getMD5(d["method"] + ":" + d["uri"]) + nonceCount := 00000001 + cnonce := getCnonce() + response := getMD5(fmt.Sprintf("%s:%s:%v:%s:%s:%s", ha1, d["nonce"], nonceCount, cnonce, d["qop"], ha2)) + authorization := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", cnonce="%s", nc="%v", qop="%s", response="%s", opaque="%s"`, + d["username"], d["realm"], d["nonce"], d["uri"], cnonce, nonceCount, d["qop"], response, d["opaque"]) + return authorization +} diff --git a/requests.go b/requests.go index 206dda8..fd7bbea 100644 --- a/requests.go +++ b/requests.go @@ -12,12 +12,15 @@ func (c *Client) req(method, path string, body io.Reader, intercept func(*http.R if err != nil { return nil, err } + for k, vals := range c.headers { for _, v := range vals { r.Header.Add(k, v) } } + c.auth.Authorize(c, method, path) + if intercept != nil { intercept(r) }