package lfsapi
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/git-lfs/git-lfs/v3/creds"
"github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/lfshttp"
"github.com/git-lfs/git-lfs/v3/tr"
"github.com/rubyist/tracerx"
)
var (
defaultEndpointFinder = NewEndpointFinder(nil)
defaultMaxAuthAttempts = 3
)
func (c *Client) DoWithAuth(remote string, access creds.Access, req *http.Request) (*http.Response, error) {
count := 0
res, err := c.doWithAuth(remote, &count, access, req, nil)
if errors.IsAuthError(err) {
if len(req.Header.Get("Authorization")) == 0 {
newAccess := c.Endpoints.AccessFor(access.URL())
tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", newAccess.Mode())
return c.DoWithAuth(remote, newAccess, req)
}
}
return res, err
}
func (c *Client) DoWithAuthNoRetry(remote string, access creds.Access, req *http.Request) (*http.Response, error) {
count := 0
return c.doWithAuth(remote, &count, access, req, nil)
}
func (c *Client) DoAPIRequestWithAuth(remote string, req *http.Request) (*http.Response, error) {
operation := getReqOperation(req)
apiEndpoint := c.Endpoints.Endpoint(operation, remote)
access := c.Endpoints.AccessFor(apiEndpoint.Url)
return c.DoWithAuth(remote, access, req)
}
func (c *Client) doWithAuth(remote string, count *int, access creds.Access, req *http.Request, via []*http.Request) (*http.Response, error) {
if *count == defaultMaxAuthAttempts {
return nil, fmt.Errorf("too many authentication attempts")
}
req.Header = c.client.ExtraHeadersFor(req)
credWrapper, err := c.getCreds(remote, access, req)
if err != nil {
return nil, err
}
c.credContext.SetStateFields(credWrapper.Creds["state[]"])
res, err := c.doWithCreds(req, count, credWrapper, access, via)
if err != nil {
if errors.IsAuthError(err) {
multistage := credWrapper.Creds.IsMultistage()
newMode, newModes, headers := getAuthAccess(res, access.Mode(), c.access, multistage)
newAccess := access.Upgrade(newMode)
if newAccess.Mode() != access.Mode() {
c.Endpoints.SetAccess(newAccess)
c.access = newModes
}
if credWrapper.Creds != nil {
req.Header.Del("Authorization")
if multistage && *count < defaultMaxAuthAttempts && res != nil && res.StatusCode == 401 {
*count++
} else {
credWrapper.CredentialHelper.Reject(credWrapper.Creds)
}
}
c.credContext.SetWWWAuthHeaders(headers)
}
}
if res != nil && res.StatusCode < 300 && res.StatusCode > 199 {
credWrapper.CredentialHelper.Approve(credWrapper.Creds)
}
return res, err
}
func (c *Client) doWithCreds(req *http.Request, count *int, credWrapper creds.CredentialHelperWrapper, access creds.Access, via []*http.Request) (*http.Response, error) {
if access.Mode() == creds.NegotiateAccess {
return c.doWithNegotiate(req, credWrapper)
}
req.Header.Set("User-Agent", lfshttp.UserAgent)
client, err := c.client.HttpClient(req.URL, access.Mode())
if err != nil {
return nil, err
}
redirectedReq, res, err := c.client.DoWithRedirect(client, req, "", via)
if err != nil || res != nil {
return res, err
}
if redirectedReq == nil {
return res, errors.New(tr.Tr.Get("failed to redirect request"))
}
return c.doWithAuth("", count, access, redirectedReq, via)
}
func (c *Client) getCreds(remote string, access creds.Access, req *http.Request) (creds.CredentialHelperWrapper, error) {
ef := c.Endpoints
if ef == nil {
ef = defaultEndpointFinder
}
operation := getReqOperation(req)
apiEndpoint := ef.Endpoint(operation, remote)
if access.Mode() != creds.NegotiateAccess {
if requestHasAuth(req) || access.Mode() == creds.NoneAccess {
return creds.CredentialHelperWrapper{CredentialHelper: creds.NullCreds, Input: nil, Url: nil, Creds: nil}, nil
}
credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req)
if err != nil {
return creds.CredentialHelperWrapper{CredentialHelper: creds.NullCreds, Input: nil, Url: nil, Creds: nil}, errors.Wrap(err, tr.Tr.Get("credentials"))
}
if credsURL == nil {
return creds.CredentialHelperWrapper{CredentialHelper: creds.NullCreds, Input: nil, Url: nil, Creds: nil}, nil
}
credWrapper := c.getGitCredsWrapper(ef, req, credsURL)
err = credWrapper.FillCreds()
if err == nil {
tracerx.Printf("Filled credentials for %s", credsURL)
setRequestAuthWithCreds(req, credWrapper.Creds)
}
return credWrapper, err
}
credsURL, err := url.Parse(apiEndpoint.Url)
if err != nil {
return creds.CredentialHelperWrapper{CredentialHelper: creds.NullCreds, Input: nil, Url: nil, Creds: nil}, errors.Wrap(err, tr.Tr.Get("credentials"))
}
credWrapper := c.getGitCredsWrapper(ef, req, credsURL)
return credWrapper, err
}
func (c *Client) getGitCredsWrapper(ef EndpointFinder, req *http.Request, u *url.URL) creds.CredentialHelperWrapper {
return c.credContext.GetCredentialHelper(c.Credentials, u)
}
func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint lfshttp.Endpoint, req *http.Request) (*url.URL, error) {
apiURL, err := url.Parse(apiEndpoint.Url)
if err != nil {
return nil, err
}
if req.URL.Scheme != apiURL.Scheme ||
req.URL.Host != apiURL.Host {
return req.URL, nil
}
if setRequestAuthFromURL(req, apiURL) {
return nil, nil
}
if len(remote) > 0 {
if u := ef.GitRemoteURL(remote, operation == "upload"); u != "" {
schemedUrl, _ := fixSchemelessURL(u)
gitRemoteURL, err := url.Parse(schemedUrl)
if err != nil {
return nil, err
}
if gitRemoteURL.Scheme == apiURL.Scheme &&
gitRemoteURL.Host == apiURL.Host {
if setRequestAuthFromURL(req, gitRemoteURL) {
return nil, nil
}
return gitRemoteURL, nil
}
}
}
return apiURL, nil
}
func fixSchemelessURL(u string) (string, bool) {
if hasScheme(u) {
return u, false
}
colon := strings.Index(u, ":")
slash := strings.Index(u, "/")
if colon >= 0 && (slash < 0 || colon < slash) {
return fmt.Sprintf("//%s", strings.Replace(u, ":", "/", 1)), true
}
return u, true
}
var (
supportedSchemes = []string{"ssh", "http", "https"}
)
func hasScheme(what string) bool {
for _, scheme := range supportedSchemes {
if strings.HasPrefix(what, fmt.Sprintf("%s://", scheme)) {
return true
}
}
return false
}
func requestHasAuth(req *http.Request) bool {
if len(req.Header.Get("Authorization")) > 0 {
return true
}
return len(req.URL.Query().Get("token")) > 0
}
func setRequestAuthFromURL(req *http.Request, u *url.URL) bool {
if u.User == nil {
return false
}
if pass, ok := u.User.Password(); ok {
fmt.Fprintln(os.Stderr, tr.Tr.Get("warning: current Git remote contains credentials"))
setRequestAuth(req, u.User.Username(), pass)
return true
}
return false
}
func setRequestAuth(req *http.Request, user, pass string) {
if len(user) == 0 && len(pass) == 0 {
return
}
token := fmt.Sprintf("%s:%s", user, pass)
auth := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token)))
req.Header.Set("Authorization", auth)
}
func setRequestAuthWithCreds(req *http.Request, c creds.Creds) {
authtype := creds.FirstEntryForKey(c, "authtype")
credential := creds.FirstEntryForKey(c, "credential")
if len(authtype) == 0 && len(credential) == 0 {
user := creds.FirstEntryForKey(c, "username")
pass := creds.FirstEntryForKey(c, "password")
setRequestAuth(req, user, pass)
return
}
auth := fmt.Sprintf("%s %s", authtype, credential)
req.Header.Set("Authorization", auth)
}
func getReqOperation(req *http.Request) string {
operation := "download"
if req.Method == "POST" || req.Method == "PUT" {
operation = "upload"
}
return operation
}
var (
authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
)
func getAuthAccess(res *http.Response, access creds.AccessMode, modes []creds.AccessMode, multistage bool) (creds.AccessMode, []creds.AccessMode, []string) {
newModes := make([]creds.AccessMode, 0, len(modes))
for _, mode := range modes {
if multistage || access != mode {
newModes = append(newModes, mode)
}
}
headers := make([]string, 0)
if res != nil {
for _, headerName := range authenticateHeaders {
headers = append(headers, res.Header[headerName]...)
}
supportedModes := make(map[creds.AccessMode]struct{})
for _, headerName := range authenticateHeaders {
for _, auth := range res.Header[headerName] {
pieces := strings.SplitN(strings.ToLower(auth), " ", 2)
if len(pieces) == 0 {
continue
}
supportedModes[creds.AccessMode(pieces[0])] = struct{}{}
}
}
for _, mode := range newModes {
if _, ok := supportedModes[mode]; ok {
return mode, newModes, headers
}
}
}
return creds.BasicAccess, newModes, headers
}