зеркало из https://github.com/golang/build.git
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:
Родитель
d289b909b3
Коммит
ff20af95f9
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче