github.com/2lambda123/git-lfs@v2.5.2+incompatible/lfsapi/auth.go (about)

     1  package lfsapi
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"net"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"strings"
    11  
    12  	"github.com/git-lfs/git-lfs/errors"
    13  	"github.com/git-lfs/go-netrc/netrc"
    14  	"github.com/rubyist/tracerx"
    15  )
    16  
    17  var (
    18  	defaultCredentialHelper = &commandCredentialHelper{}
    19  	defaultNetrcFinder      = &noFinder{}
    20  	defaultEndpointFinder   = NewEndpointFinder(nil)
    21  )
    22  
    23  // DoWithAuth sends an HTTP request to get an HTTP response. It attempts to add
    24  // authentication from netrc or git's credential helpers if necessary,
    25  // supporting basic and ntlm authentication.
    26  func (c *Client) DoWithAuth(remote string, req *http.Request) (*http.Response, error) {
    27  	return c.doWithAuth(remote, req, nil)
    28  }
    29  
    30  func (c *Client) doWithAuth(remote string, req *http.Request, via []*http.Request) (*http.Response, error) {
    31  	req.Header = c.extraHeadersFor(req)
    32  
    33  	apiEndpoint, access, credHelper, credsURL, creds, err := c.getCreds(remote, req)
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  
    38  	res, err := c.doWithCreds(req, credHelper, creds, credsURL, access, via)
    39  	if err != nil {
    40  		if errors.IsAuthError(err) {
    41  			newAccess := getAuthAccess(res)
    42  			if newAccess != access {
    43  				c.Endpoints.SetAccess(apiEndpoint.Url, newAccess)
    44  			}
    45  
    46  			if creds != nil || (access == NoneAccess && len(req.Header.Get("Authorization")) == 0) {
    47  				tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", newAccess)
    48  				if creds != nil {
    49  					req.Header.Del("Authorization")
    50  					credHelper.Reject(creds)
    51  				}
    52  
    53  				// This case represents a rejected request that
    54  				// should have been authenticated but wasn't. Do
    55  				// not count this against our redirection
    56  				// maximum, so do not recur through doWithAuth
    57  				// and instead call DoWithAuth.
    58  				return c.DoWithAuth(remote, req)
    59  			}
    60  		}
    61  	}
    62  
    63  	if res != nil && res.StatusCode < 300 && res.StatusCode > 199 {
    64  		credHelper.Approve(creds)
    65  	}
    66  
    67  	return res, err
    68  }
    69  
    70  func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL, access Access, via []*http.Request) (*http.Response, error) {
    71  	if access == NTLMAccess {
    72  		return c.doWithNTLM(req, credHelper, creds, credsURL)
    73  	}
    74  	return c.do(req, "", via)
    75  }
    76  
    77  // getCreds fills the authorization header for the given request if possible,
    78  // from the following sources:
    79  //
    80  // 1. NTLM access is handled elsewhere.
    81  // 2. Existing Authorization or ?token query tells LFS that the request is ready.
    82  // 3. Netrc based on the hostname.
    83  // 4. URL authentication on the Endpoint URL or the Git Remote URL.
    84  // 5. Git Credential Helper, potentially prompting the user.
    85  //
    86  // There are three URLs in play, that make this a little confusing.
    87  //
    88  // 1. The request URL, which should be something like "https://git.com/repo.git/info/lfs/objects/batch"
    89  // 2. The LFS API URL, which should be something like "https://git.com/repo.git/info/lfs"
    90  //    This URL used for the "lfs.URL.access" git config key, which determines
    91  //    what kind of auth the LFS server expects. Could be BasicAccess, NTLMAccess,
    92  //    or NoneAccess, in which the Git Credential Helper step is skipped. We do
    93  //    not want to prompt the user for a password to fetch public repository data.
    94  // 3. The Git Remote URL, which should be something like "https://git.com/repo.git"
    95  //    This URL is used for the Git Credential Helper. This way existing https
    96  //    Git remote credentials can be re-used for LFS.
    97  func (c *Client) getCreds(remote string, req *http.Request) (Endpoint, Access, CredentialHelper, *url.URL, Creds, error) {
    98  	ef := c.Endpoints
    99  	if ef == nil {
   100  		ef = defaultEndpointFinder
   101  	}
   102  
   103  	netrcFinder := c.Netrc
   104  	if netrcFinder == nil {
   105  		netrcFinder = defaultNetrcFinder
   106  	}
   107  
   108  	operation := getReqOperation(req)
   109  	apiEndpoint := ef.Endpoint(operation, remote)
   110  	access := ef.AccessFor(apiEndpoint.Url)
   111  
   112  	if access != NTLMAccess {
   113  		if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access == NoneAccess {
   114  			return apiEndpoint, access, nullCreds, nil, nil, nil
   115  		}
   116  
   117  		credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req)
   118  		if err != nil {
   119  			return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
   120  		}
   121  
   122  		if credsURL == nil {
   123  			return apiEndpoint, access, nullCreds, nil, nil, nil
   124  		}
   125  
   126  		credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
   127  		if err == nil {
   128  			tracerx.Printf("Filled credentials for %s", credsURL)
   129  			setRequestAuth(req, creds["username"], creds["password"])
   130  		}
   131  		return apiEndpoint, access, credHelper, credsURL, creds, err
   132  	}
   133  
   134  	// NTLM ONLY
   135  
   136  	credsURL, err := url.Parse(apiEndpoint.Url)
   137  	if err != nil {
   138  		return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
   139  	}
   140  
   141  	if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil {
   142  		creds := Creds{
   143  			"protocol": credsURL.Scheme,
   144  			"host":     credsURL.Host,
   145  			"username": netrcMachine.Login,
   146  			"password": netrcMachine.Password,
   147  			"source":   "netrc",
   148  		}
   149  
   150  		return apiEndpoint, access, nullCreds, credsURL, creds, nil
   151  	}
   152  
   153  	// NTLM uses creds to create the session
   154  	credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
   155  	return apiEndpoint, access, credHelper, credsURL, creds, err
   156  }
   157  
   158  func (c *Client) getGitCreds(ef EndpointFinder, req *http.Request, u *url.URL) (CredentialHelper, Creds, error) {
   159  	credHelper, input := c.getCredentialHelper(u)
   160  	creds, err := credHelper.Fill(input)
   161  	if creds == nil || len(creds) < 1 {
   162  		errmsg := fmt.Sprintf("Git credentials for %s not found", u)
   163  		if err != nil {
   164  			errmsg = errmsg + ":\n" + err.Error()
   165  		} else {
   166  			errmsg = errmsg + "."
   167  		}
   168  		err = errors.New(errmsg)
   169  	}
   170  
   171  	return credHelper, creds, err
   172  }
   173  
   174  func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine {
   175  	hostname := req.URL.Host
   176  	var host string
   177  
   178  	if strings.Contains(hostname, ":") {
   179  		var err error
   180  		host, _, err = net.SplitHostPort(hostname)
   181  		if err != nil {
   182  			tracerx.Printf("netrc: error parsing %q: %s", hostname, err)
   183  			return nil
   184  		}
   185  	} else {
   186  		host = hostname
   187  	}
   188  
   189  	return netrcFinder.FindMachine(host)
   190  }
   191  
   192  func setAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) bool {
   193  	if machine := getAuthFromNetrc(netrcFinder, req); machine != nil {
   194  		setRequestAuth(req, machine.Login, machine.Password)
   195  		return true
   196  	}
   197  
   198  	return false
   199  }
   200  
   201  func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint Endpoint, req *http.Request) (*url.URL, error) {
   202  	apiURL, err := url.Parse(apiEndpoint.Url)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	// if the LFS request doesn't match the current LFS url, don't bother
   208  	// attempting to set the Authorization header from the LFS or Git remote URLs.
   209  	if req.URL.Scheme != apiURL.Scheme ||
   210  		req.URL.Host != apiURL.Host {
   211  		return req.URL, nil
   212  	}
   213  
   214  	if setRequestAuthFromURL(req, apiURL) {
   215  		return nil, nil
   216  	}
   217  
   218  	if len(remote) > 0 {
   219  		if u := ef.GitRemoteURL(remote, operation == "upload"); u != "" {
   220  			schemedUrl, _ := prependEmptySchemeIfAbsent(u)
   221  
   222  			gitRemoteURL, err := url.Parse(schemedUrl)
   223  			if err != nil {
   224  				return nil, err
   225  			}
   226  
   227  			if gitRemoteURL.Scheme == apiURL.Scheme &&
   228  				gitRemoteURL.Host == apiURL.Host {
   229  
   230  				if setRequestAuthFromURL(req, gitRemoteURL) {
   231  					return nil, nil
   232  				}
   233  
   234  				return gitRemoteURL, nil
   235  			}
   236  		}
   237  	}
   238  
   239  	return apiURL, nil
   240  }
   241  
   242  // prependEmptySchemeIfAbsent prepends an empty scheme "//" if none was found in
   243  // the URL in order to satisfy RFC 3986 §3.3, and `net/url.Parse()`.
   244  //
   245  // It returns a string parse-able with `net/url.Parse()` and a boolean whether
   246  // or not an empty scheme was added.
   247  func prependEmptySchemeIfAbsent(u string) (string, bool) {
   248  	if hasScheme(u) {
   249  		return u, false
   250  	}
   251  
   252  	colon := strings.Index(u, ":")
   253  	slash := strings.Index(u, "/")
   254  
   255  	if colon >= 0 && (slash < 0 || colon < slash) {
   256  		// First path segment has a colon, assumed that it's a
   257  		// scheme-less URL. Append an empty scheme on top to
   258  		// satisfy RFC 3986 §3.3, and `net/url.Parse()`.
   259  		return fmt.Sprintf("//%s", u), true
   260  	}
   261  	return u, true
   262  }
   263  
   264  var (
   265  	// supportedSchemes is the list of URL schemes the `lfsapi` package
   266  	// supports.
   267  	supportedSchemes = []string{"ssh", "http", "https"}
   268  )
   269  
   270  // hasScheme returns whether or not a given string (taken to represent a RFC
   271  // 3986 URL) has a scheme that is supported by the `lfsapi` package.
   272  func hasScheme(what string) bool {
   273  	for _, scheme := range supportedSchemes {
   274  		if strings.HasPrefix(what, fmt.Sprintf("%s://", scheme)) {
   275  			return true
   276  		}
   277  	}
   278  
   279  	return false
   280  }
   281  
   282  func requestHasAuth(req *http.Request) bool {
   283  	// The "Authorization" string constant is safe, since we assume that all
   284  	// request headers have been canonicalized.
   285  	if len(req.Header.Get("Authorization")) > 0 {
   286  		return true
   287  	}
   288  
   289  	return len(req.URL.Query().Get("token")) > 0
   290  }
   291  
   292  func setRequestAuthFromURL(req *http.Request, u *url.URL) bool {
   293  	if u.User == nil {
   294  		return false
   295  	}
   296  
   297  	if pass, ok := u.User.Password(); ok {
   298  		fmt.Fprintln(os.Stderr, "warning: current Git remote contains credentials")
   299  		setRequestAuth(req, u.User.Username(), pass)
   300  		return true
   301  	}
   302  
   303  	return false
   304  }
   305  
   306  func setRequestAuth(req *http.Request, user, pass string) {
   307  	// better not be NTLM!
   308  	if len(user) == 0 && len(pass) == 0 {
   309  		return
   310  	}
   311  
   312  	token := fmt.Sprintf("%s:%s", user, pass)
   313  	auth := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token)))
   314  	req.Header.Set("Authorization", auth)
   315  }
   316  
   317  func getReqOperation(req *http.Request) string {
   318  	operation := "download"
   319  	if req.Method == "POST" || req.Method == "PUT" {
   320  		operation = "upload"
   321  	}
   322  	return operation
   323  }
   324  
   325  var (
   326  	authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
   327  )
   328  
   329  func getAuthAccess(res *http.Response) Access {
   330  	for _, headerName := range authenticateHeaders {
   331  		for _, auth := range res.Header[headerName] {
   332  			pieces := strings.SplitN(strings.ToLower(auth), " ", 2)
   333  			if len(pieces) == 0 {
   334  				continue
   335  			}
   336  
   337  			switch Access(pieces[0]) {
   338  			case NegotiateAccess, NTLMAccess:
   339  				// When server sends Www-Authentication: Negotiate, it supports both Kerberos and NTLM.
   340  				// Since git-lfs current does not support Kerberos, we will return NTLM in this case.
   341  				return NTLMAccess
   342  			}
   343  		}
   344  	}
   345  
   346  	return BasicAccess
   347  }