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