gerrit: Add support for Digest Authorization

The existing implementation doesn't support the HTTP Digest Authorization
that widely used in docker-based Gerrit configuration

Proposed code is based on http://play.golang.org/p/ABoHSHoTmu

Change-Id: Ia01d03cc849a4fcd538b05a60b83ac7e18809d5a
Reviewed-on: https://go-review.googlesource.com/29295
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/gerrit/auth.go b/gerrit/auth.go
index 527249e..5718c6e 100644
--- a/gerrit/auth.go
+++ b/gerrit/auth.go
@@ -5,6 +5,9 @@
 package gerrit
 
 import (
+	"bytes"
+	"crypto/md5"
+	"encoding/hex"
 	"fmt"
 	"io/ioutil"
 	"log"
@@ -171,3 +174,90 @@
 type noAuth struct{}
 
 func (noAuth) setAuth(c *Client, r *http.Request) {}
+
+type digestAuth struct {
+	Username, Password, Realm, NONCE, QOP, Opaque, Algorithm string
+}
+
+func getDigestAuth(username, password string, resp *http.Response) *digestAuth {
+	header := resp.Header.Get("www-authenticate")
+	parts := strings.SplitN(header, " ", 2)
+	parts = strings.Split(parts[1], ", ")
+	opts := make(map[string]string)
+
+	for _, part := range parts {
+		vals := strings.SplitN(part, "=", 2)
+		key := vals[0]
+		val := strings.Trim(vals[1], "\",")
+		opts[key] = val
+	}
+
+	auth := digestAuth{
+		username, password,
+		opts["realm"], opts["nonce"], opts["qop"], opts["opaque"], opts["algorithm"],
+	}
+	return &auth
+}
+
+func setDigestAuth(r *http.Request, username, password string, resp *http.Response, nc int) {
+	auth := getDigestAuth(username, password, resp)
+	authStr := getDigestAuthString(auth, r.URL, r.Method, nc)
+	r.Header.Add("Authorization", authStr)
+}
+
+func getDigestAuthString(auth *digestAuth, url *url.URL, method string, nc int) string {
+	var buf bytes.Buffer
+	h := md5.New()
+	fmt.Fprintf(&buf, "%s:%s:%s", auth.Username, auth.Realm, auth.Password)
+	buf.WriteTo(h)
+	ha1 := hex.EncodeToString(h.Sum(nil))
+
+	h = md5.New()
+	fmt.Fprintf(&buf, "%s:%s", method, url.Path)
+	buf.WriteTo(h)
+	ha2 := hex.EncodeToString(h.Sum(nil))
+
+	ncStr := fmt.Sprintf("%08x", nc)
+	hnc := "MTM3MDgw"
+
+	h = md5.New()
+	fmt.Fprintf(&buf, "%s:%s:%s:%s:%s:%s", ha1, auth.NONCE, ncStr, hnc, auth.QOP, ha2)
+	buf.WriteTo(h)
+	respdig := hex.EncodeToString(h.Sum(nil))
+
+	buf.Write([]byte("Digest "))
+	fmt.Fprintf(&buf,
+		`username="%s", realm="%s", nonce="%s", uri="%s", response="%s"`,
+		auth.Username, auth.Realm, auth.NONCE, url.Path, respdig,
+	)
+
+	if auth.Opaque != "" {
+		fmt.Fprintf(&buf, `, opaque="%s"`, auth.Opaque)
+	}
+	if auth.QOP != "" {
+		fmt.Fprintf(&buf, `, qop="%s", nc=%s, cnonce="%s"`, auth.QOP, ncStr, hnc)
+	}
+	if auth.Algorithm != "" {
+		fmt.Fprintf(&buf, `, algorithm="%s"`, auth.Algorithm)
+	}
+
+	return buf.String()
+}
+
+func (a digestAuth) setAuth(c *Client, r *http.Request) {
+	resp, err := http.Get(r.URL.String())
+	if err != nil {
+		return
+	}
+	setDigestAuth(r, a.Username, a.Password, resp, 1)
+}
+
+// DigestAuth returns an Auth implementation which sends
+// the provided username and password using HTTP Digest Authentication
+// (RFC 2617)
+func DigestAuth(username, password string) Auth {
+	return digestAuth{
+		Username: username,
+		Password: password,
+	}
+}
diff --git a/gerrit/auth_test.go b/gerrit/auth_test.go
new file mode 100644
index 0000000..fcff2d5
--- /dev/null
+++ b/gerrit/auth_test.go
@@ -0,0 +1,113 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package gerrit
+
+import (
+	"context"
+	"crypto/md5"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+func md5str(text string) string {
+	h := md5.Sum([]byte(text))
+	return hex.EncodeToString(h[:])
+}
+
+func TestBasicAuth(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		expected := "User Password true"
+		u, p, ok := r.BasicAuth()
+		if expected != fmt.Sprintf("%s %s %t", u, p, ok) {
+			t.Errorf("Expected %s, got %s %s %t", expected, u, p, ok)
+			w.WriteHeader(http.StatusUnauthorized)
+		} else {
+			w.Header().Set("Content-Type", "application/json; charset=UTF-8")
+			// The JSON response begins with an XSRF-defeating header ")]}\n"
+			fmt.Fprintln(w, ")]}")
+			json.NewEncoder(w).Encode(AccountInfo{})
+		}
+	}))
+	defer ts.Close()
+
+	_, err := NewClient(
+		ts.URL,
+		BasicAuth("User", "Password"),
+	).GetAccountInfo(context.Background(), "self")
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestDigestAuth(t *testing.T) {
+	const (
+		user   = "User"
+		pass   = "Password"
+		nonce  = "dcd98b7102dd2f0e8b11d0f600bfb0c093"
+		opaque = "5ccc069c403ebaf9f0171e9517f40e41"
+		realm  = "Gerrit Code Review"
+		qop    = "auth"
+	)
+
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		header := r.Header.Get("Authorization")
+		if header == "" {
+			w.Header().Set("WWW-Authenticate", fmt.Sprintf(
+				`Digest realm="%s", qop="%s", nonce="%s", opaque="%s"`,
+				realm, qop, nonce, opaque,
+			))
+			w.WriteHeader(http.StatusUnauthorized)
+		} else {
+			parts := strings.SplitN(header, " ", 2)
+			parts = strings.Split(parts[1], ", ")
+			opts := make(map[string]string)
+
+			for _, part := range parts {
+				vals := strings.SplitN(part, "=", 2)
+				key := vals[0]
+				val := strings.Trim(vals[1], "\",")
+				opts[key] = val
+			}
+
+			// https://en.wikipedia.org/wiki/Digest_access_authentication#Example_with_explanation
+			// The "response" value is calculated in three steps, as follows.
+			//   Where values are combined, they are delimited by colons.
+			// 1. The MD5 hash of the combined username, authentication realm and password is calculated.
+			//    The result is referred to as HA1.
+			// 2. The MD5 hash of the combined method and digest URI is calculated, e.g. of "GET" and "/index.html".
+			//    The result is referred to as HA2.
+			// 3. The MD5 hash of the combined HA1 result, server nonce (nonce), request counter (nc),
+			//    client nonce (cnonce), quality of protection code (qop) and HA2 result is calculated.
+			//    The result is the "response" value provided by the client.
+			ha1 := md5str(fmt.Sprintf("%s:%s:%s", user, realm, pass))
+			ha2 := md5str("GET:/a/accounts/self")
+			expected := md5str(fmt.Sprintf("%s:%s:%s:%s:%s:%s", ha1, nonce, opts["nc"], opts["cnonce"], qop, ha2))
+
+			if expected != opts["response"] {
+				t.Errorf("Expected %s, got %s", expected, opts["response"])
+				w.WriteHeader(http.StatusUnauthorized)
+			} else {
+				w.Header().Set("Content-Type", "application/json; charset=UTF-8")
+				// The JSON response begins with an XSRF-defeating header ")]}\n"
+				fmt.Fprintln(w, ")]}")
+				json.NewEncoder(w).Encode(AccountInfo{})
+			}
+		}
+	}))
+	defer ts.Close()
+
+	_, err := NewClient(
+		ts.URL,
+		DigestAuth(user, pass),
+	).GetAccountInfo(context.Background(), "self")
+	if err != nil {
+		t.Error(err)
+	}
+}