Merge pull request #3028 from git-lfs/lfsapi-authenticate-redirects

lfsapi: re-authenticate HTTP redirects when needed
This commit is contained in:
Taylor Blau 2018-05-28 18:35:13 -07:00 committed by GitHub
commit 37bc00aa3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 103 additions and 10 deletions

@ -24,6 +24,10 @@ var (
// authentication from netrc or git's credential helpers if necessary,
// supporting basic and ntlm authentication.
func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, error) {
return c.doWithAuth(remote, req, nil)
}
func (c *Client) doWithAuth(remote string, req *http.Request, via []*http.Request) (*http.Response, error) {
req.Header = c.extraHeadersFor(req)
apiEndpoint, access, credHelper, credsURL, creds, err := c.getCreds(remote, req)
@ -31,7 +35,7 @@ func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, e
return nil, err
}
res, err := c.doWithCreds(req, credHelper, creds, credsURL, access)
res, err := c.doWithCreds(req, credHelper, creds, credsURL, access, via)
if err != nil {
if errors.IsAuthError(err) {
newAccess := getAuthAccess(res)
@ -45,6 +49,12 @@ func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, e
req.Header.Del("Authorization")
credHelper.Reject(creds)
}
// This case represents a rejected request that
// should have been authenticated but wasn't. Do
// not count this against our redirection
// maximum, so do not recur through doWithAuth
// and instead call DoWithAuth.
return c.DoWithAuth(remote, req)
}
}
@ -57,11 +67,11 @@ func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, e
return res, err
}
func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL, access Access) (*http.Response, error) {
func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL, access Access, via []*http.Request) (*http.Response, error) {
if access == NTLMAccess {
return c.doWithNTLM(req, credHelper, creds, credsURL)
}
return c.do(req)
return c.do(req, "", via)
}
// getCreds fills the authorization header for the given request if possible,

@ -96,16 +96,16 @@ func joinURL(prefix, suffix string) string {
func (c *Client) Do(req *http.Request) (*http.Response, error) {
req.Header = c.extraHeadersFor(req)
return c.do(req)
return c.do(req, "", nil)
}
// do performs an *http.Request respecting redirects, and handles the response
// as defined in c.handleResponse. Notably, it does not alter the headers for
// the request argument in any way.
func (c *Client) do(req *http.Request) (*http.Response, error) {
func (c *Client) do(req *http.Request, remote string, via []*http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", UserAgent)
res, err := c.doWithRedirects(c.httpClient(req.Host), req, nil)
res, err := c.doWithRedirects(c.httpClient(req.Host), req, remote, via)
if err != nil {
return res, err
}
@ -161,7 +161,7 @@ func (c *Client) extraHeaders(u *url.URL) map[string][]string {
return m
}
func (c *Client) doWithRedirects(cli *http.Client, req *http.Request, via []*http.Request) (*http.Response, error) {
func (c *Client) doWithRedirects(cli *http.Client, req *http.Request, remote string, via []*http.Request) (*http.Response, error) {
tracedReq, err := c.traceRequest(req)
if err != nil {
return nil, err
@ -231,7 +231,14 @@ func (c *Client) doWithRedirects(cli *http.Client, req *http.Request, via []*htt
return res, err
}
return c.doWithRedirects(cli, redirectedReq, via)
if len(req.Header.Get("Authorization")) > 0 {
// If the original request was authenticated (noted by the
// presence of the Authorization header), then recur through
// doWithAuth, retaining the requests via but only after
// authenticating the redirected request.
return c.doWithAuth(remote, redirectedReq, via)
}
return c.doWithRedirects(cli, redirectedReq, remote, via)
}
func (c *Client) httpClient(host string) *http.Client {

@ -1,11 +1,13 @@
package lfsapi
import (
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
@ -175,6 +177,80 @@ func TestClientRedirect(t *testing.T) {
assert.EqualError(t, err, "lfsapi/client: refusing insecure redirect, https->http")
}
func TestClientRedirectReauthenticate(t *testing.T) {
var srv1, srv2 *httptest.Server
var called1, called2 uint32
var creds1, creds2 Creds
srv1 = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddUint32(&called1, 1)
if hdr := r.Header.Get("Authorization"); len(hdr) > 0 {
parts := strings.SplitN(hdr, " ", 2)
typ, b64 := parts[0], parts[1]
auth, err := base64.URLEncoding.DecodeString(b64)
assert.Nil(t, err)
assert.Equal(t, "Basic", typ)
assert.Equal(t, "user1:pass1", string(auth))
http.Redirect(w, r, srv2.URL+r.URL.Path, http.StatusMovedPermanently)
return
}
w.WriteHeader(http.StatusUnauthorized)
}))
srv2 = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddUint32(&called2, 1)
parts := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
typ, b64 := parts[0], parts[1]
auth, err := base64.URLEncoding.DecodeString(b64)
assert.Nil(t, err)
assert.Equal(t, "Basic", typ)
assert.Equal(t, "user2:pass2", string(auth))
}))
// Change the URL of srv2 to make it appears as if it is a different
// host.
srv2.URL = strings.Replace(srv2.URL, "127.0.0.1", "0.0.0.0", 1)
creds1 = Creds(map[string]string{
"protocol": "http",
"host": strings.TrimPrefix(srv1.URL, "http://"),
"username": "user1",
"password": "pass1",
})
creds2 = Creds(map[string]string{
"protocol": "http",
"host": strings.TrimPrefix(srv2.URL, "http://"),
"username": "user2",
"password": "pass2",
})
defer srv1.Close()
defer srv2.Close()
c, err := NewClient(NewContext(nil, nil, nil))
creds := newCredentialCacher()
creds.Approve(creds1)
creds.Approve(creds2)
c.Credentials = creds
req, err := http.NewRequest("GET", srv1.URL, nil)
require.Nil(t, err)
_, err = c.DoWithAuth("", req)
assert.Nil(t, err)
// called1 is 2 since LFS tries an unauthenticated request first
assert.EqualValues(t, 2, called1)
assert.EqualValues(t, 1, called2)
}
func TestNewClient(t *testing.T) {
c, err := NewClient(NewContext(nil, nil, map[string]string{
"lfs.dialtimeout": "151",

@ -19,7 +19,7 @@ type ntmlCredentials struct {
}
func (c *Client) doWithNTLM(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL) (*http.Response, error) {
res, err := c.do(req)
res, err := c.do(req, "", nil)
if err != nil && !errors.IsAuthError(err) {
return res, err
}
@ -86,7 +86,7 @@ func (c *Client) ntlmSendMessage(req *http.Request, message []byte) (*http.Respo
msg := base64.StdEncoding.EncodeToString(message)
req.Header.Set("Authorization", "NTLM "+msg)
return c.do(req)
return c.do(req, "", nil)
}
func parseChallengeResponse(res *http.Response) ([]byte, error) {