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>
This commit is contained in:
Pavlo Sumkin 2016-10-19 16:34:28 +03:00 коммит произвёл Brad Fitzpatrick
Родитель d289b909b3
Коммит ff20af95f9
2 изменённых файлов: 203 добавлений и 0 удалений

Просмотреть файл

@ -5,6 +5,9 @@
package gerrit
import (
"bytes"
"crypto/md5"
"encoding/hex"
"fmt"
"io/ioutil"
"log"
@ -171,3 +174,90 @@ var NoAuth = noAuth{}
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,
}
}

113
gerrit/auth_test.go Normal file
Просмотреть файл

@ -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)
}
}