2016-12-19 21:38:06 +00:00
|
|
|
package lfsapi
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/base64"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
|
2018-09-19 15:05:48 +00:00
|
|
|
"github.com/git-lfs/git-lfs/creds"
|
2016-12-19 21:38:06 +00:00
|
|
|
"github.com/git-lfs/git-lfs/errors"
|
2018-09-06 21:42:41 +00:00
|
|
|
"github.com/git-lfs/git-lfs/lfshttp"
|
2016-12-19 21:38:06 +00:00
|
|
|
"github.com/rubyist/tracerx"
|
|
|
|
)
|
|
|
|
|
2016-12-19 22:05:35 +00:00
|
|
|
var (
|
2018-09-19 14:39:01 +00:00
|
|
|
defaultEndpointFinder = NewEndpointFinder(nil)
|
2016-12-19 22:05:35 +00:00
|
|
|
)
|
|
|
|
|
2017-10-27 20:10:46 +00:00
|
|
|
// DoWithAuth sends an HTTP request to get an HTTP response. It attempts to add
|
|
|
|
// authentication from netrc or git's credential helpers if necessary,
|
|
|
|
// supporting basic and ntlm authentication.
|
2018-10-02 23:47:10 +00:00
|
|
|
func (c *Client) DoWithAuth(remote string, access Access, req *http.Request) (*http.Response, error) {
|
2018-09-27 23:46:56 +00:00
|
|
|
res, err := c.doWithAuth(remote, access, req, nil)
|
|
|
|
|
2018-10-02 23:47:10 +00:00
|
|
|
if errors.IsAuthError(err) {
|
2018-09-27 23:46:56 +00:00
|
|
|
if len(req.Header.Get("Authorization")) == 0 {
|
|
|
|
// This case represents a rejected request that
|
|
|
|
// should have been authenticated but wasn't. Do
|
|
|
|
// not count this against our redirection
|
|
|
|
// maximum.
|
|
|
|
newAccess := c.Endpoints.AccessFor(access.url)
|
2018-10-02 23:35:54 +00:00
|
|
|
tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", newAccess.Mode())
|
2018-10-02 23:47:10 +00:00
|
|
|
return c.DoWithAuth(remote, newAccess, req)
|
2018-09-27 23:46:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return res, err
|
2018-05-25 16:24:36 +00:00
|
|
|
}
|
|
|
|
|
2018-10-02 23:47:10 +00:00
|
|
|
// DoWithAuthNoRetry sends an HTTP request to get an HTTP response. It works in
|
|
|
|
// the same way as DoWithAuth, but will not retry the request if it fails with
|
|
|
|
// an authorization error.
|
|
|
|
func (c *Client) DoWithAuthNoRetry(remote string, access Access, req *http.Request) (*http.Response, error) {
|
|
|
|
return c.doWithAuth(remote, access, req, nil)
|
|
|
|
}
|
|
|
|
|
2018-09-24 23:45:32 +00:00
|
|
|
// DoAPIRequestWithAuth sends an HTTP request to get an HTTP response similarly
|
|
|
|
// to DoWithAuth, but using the LFS API endpoint for the provided remote and
|
|
|
|
// operation to determine the access mode.
|
|
|
|
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)
|
2018-10-02 23:47:10 +00:00
|
|
|
return c.DoWithAuth(remote, access, req)
|
2018-09-24 23:45:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) doWithAuth(remote string, access Access, req *http.Request, via []*http.Request) (*http.Response, error) {
|
2018-09-06 21:42:41 +00:00
|
|
|
req.Header = c.client.ExtraHeadersFor(req)
|
2017-11-16 20:04:42 +00:00
|
|
|
|
2019-04-18 14:41:55 +00:00
|
|
|
credWrapper, err := c.getCreds(remote, access, req)
|
2016-12-20 00:14:03 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-18 14:41:55 +00:00
|
|
|
res, err := c.doWithCreds(req, credWrapper, access, via)
|
2016-12-20 00:14:03 +00:00
|
|
|
if err != nil {
|
|
|
|
if errors.IsAuthError(err) {
|
2018-09-24 23:45:32 +00:00
|
|
|
newAccess := access.Upgrade(getAuthAccess(res))
|
2018-10-02 23:35:54 +00:00
|
|
|
if newAccess.Mode() != access.Mode() {
|
2018-09-24 23:45:32 +00:00
|
|
|
c.Endpoints.SetAccess(newAccess)
|
2016-12-20 00:14:03 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 14:41:55 +00:00
|
|
|
if credWrapper.Creds != nil {
|
2018-09-27 23:46:56 +00:00
|
|
|
req.Header.Del("Authorization")
|
2019-04-18 14:41:55 +00:00
|
|
|
credWrapper.CredentialHelper.Reject(credWrapper.Creds)
|
2016-12-20 00:14:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-27 17:28:12 +00:00
|
|
|
if res != nil && res.StatusCode < 300 && res.StatusCode > 199 {
|
2019-04-18 14:41:55 +00:00
|
|
|
credWrapper.CredentialHelper.Approve(credWrapper.Creds)
|
2016-12-20 00:14:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return res, err
|
2016-12-19 22:05:35 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 14:41:55 +00:00
|
|
|
func (c *Client) doWithCreds(req *http.Request, credWrapper creds.CredentialHelperWrapper, access Access, via []*http.Request) (*http.Response, error) {
|
2018-10-02 23:35:54 +00:00
|
|
|
if access.Mode() == NTLMAccess {
|
2019-04-18 14:41:55 +00:00
|
|
|
return c.doWithNTLM(req, credWrapper)
|
2016-12-20 19:07:16 +00:00
|
|
|
}
|
2018-09-11 21:14:37 +00:00
|
|
|
|
|
|
|
req.Header.Set("User-Agent", lfshttp.UserAgent)
|
|
|
|
|
|
|
|
redirectedReq, res, err := c.client.DoWithRedirect(c.client.HttpClient(req.Host), req, "", via)
|
|
|
|
if err != nil || res != nil {
|
|
|
|
return res, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if redirectedReq == nil {
|
|
|
|
return res, errors.New("failed to redirect request")
|
|
|
|
}
|
|
|
|
|
2018-09-24 23:45:32 +00:00
|
|
|
return c.doWithAuth("", access, redirectedReq, via)
|
2016-12-20 19:07:16 +00:00
|
|
|
}
|
|
|
|
|
2016-12-23 18:13:42 +00:00
|
|
|
// getCreds fills the authorization header for the given request if possible,
|
|
|
|
// from the following sources:
|
|
|
|
//
|
|
|
|
// 1. NTLM access is handled elsewhere.
|
|
|
|
// 2. Existing Authorization or ?token query tells LFS that the request is ready.
|
|
|
|
// 3. Netrc based on the hostname.
|
|
|
|
// 4. URL authentication on the Endpoint URL or the Git Remote URL.
|
|
|
|
// 5. Git Credential Helper, potentially prompting the user.
|
|
|
|
//
|
|
|
|
// There are three URLs in play, that make this a little confusing.
|
|
|
|
//
|
|
|
|
// 1. The request URL, which should be something like "https://git.com/repo.git/info/lfs/objects/batch"
|
|
|
|
// 2. The LFS API URL, which should be something like "https://git.com/repo.git/info/lfs"
|
|
|
|
// This URL used for the "lfs.URL.access" git config key, which determines
|
|
|
|
// what kind of auth the LFS server expects. Could be BasicAccess, NTLMAccess,
|
|
|
|
// or NoneAccess, in which the Git Credential Helper step is skipped. We do
|
|
|
|
// not want to prompt the user for a password to fetch public repository data.
|
|
|
|
// 3. The Git Remote URL, which should be something like "https://git.com/repo.git"
|
|
|
|
// This URL is used for the Git Credential Helper. This way existing https
|
|
|
|
// Git remote credentials can be re-used for LFS.
|
2019-04-18 14:41:55 +00:00
|
|
|
func (c *Client) getCreds(remote string, access Access, req *http.Request) (creds.CredentialHelperWrapper, error) {
|
2017-10-26 22:09:37 +00:00
|
|
|
ef := c.Endpoints
|
|
|
|
if ef == nil {
|
|
|
|
ef = defaultEndpointFinder
|
|
|
|
}
|
|
|
|
|
2016-12-19 21:38:06 +00:00
|
|
|
operation := getReqOperation(req)
|
|
|
|
apiEndpoint := ef.Endpoint(operation, remote)
|
2017-01-05 21:13:27 +00:00
|
|
|
|
2018-10-02 23:35:54 +00:00
|
|
|
if access.Mode() != NTLMAccess {
|
2018-10-08 22:47:17 +00:00
|
|
|
if requestHasAuth(req) || access.Mode() == NoneAccess {
|
2019-05-01 16:45:21 +00:00
|
|
|
return creds.CredentialHelperWrapper{CredentialHelper: creds.NullCreds, Input: nil, Url: nil, Creds: nil}, nil
|
2017-01-05 21:13:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req)
|
|
|
|
if err != nil {
|
2019-05-01 16:45:21 +00:00
|
|
|
return creds.CredentialHelperWrapper{CredentialHelper: creds.NullCreds, Input: nil, Url: nil, Creds: nil}, errors.Wrap(err, "creds")
|
2017-01-05 21:13:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if credsURL == nil {
|
2019-05-01 16:45:21 +00:00
|
|
|
return creds.CredentialHelperWrapper{CredentialHelper: creds.NullCreds, Input: nil, Url: nil, Creds: nil}, nil
|
2017-01-05 21:13:27 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 14:41:55 +00:00
|
|
|
credWrapper := c.getGitCredsWrapper(ef, req, credsURL)
|
|
|
|
err = credWrapper.FillCreds()
|
2017-10-26 22:09:37 +00:00
|
|
|
if err == nil {
|
|
|
|
tracerx.Printf("Filled credentials for %s", credsURL)
|
2019-04-18 14:41:55 +00:00
|
|
|
setRequestAuth(req, credWrapper.Creds["username"], credWrapper.Creds["password"])
|
2017-10-26 22:09:37 +00:00
|
|
|
}
|
2019-04-18 14:41:55 +00:00
|
|
|
return credWrapper, err
|
2016-12-19 21:38:06 +00:00
|
|
|
}
|
|
|
|
|
2017-10-26 22:09:37 +00:00
|
|
|
// NTLM ONLY
|
|
|
|
|
2017-01-05 21:13:27 +00:00
|
|
|
credsURL, err := url.Parse(apiEndpoint.Url)
|
2016-12-23 03:06:51 +00:00
|
|
|
if err != nil {
|
2019-05-01 16:45:21 +00:00
|
|
|
return creds.CredentialHelperWrapper{CredentialHelper: creds.NullCreds, Input: nil, Url: nil, Creds: nil}, errors.Wrap(err, "creds")
|
2016-12-19 21:38:06 +00:00
|
|
|
}
|
|
|
|
|
2017-10-26 22:09:37 +00:00
|
|
|
// NTLM uses creds to create the session
|
2019-04-18 14:41:55 +00:00
|
|
|
credWrapper := c.getGitCredsWrapper(ef, req, credsURL)
|
|
|
|
return credWrapper, err
|
2016-12-19 21:38:06 +00:00
|
|
|
}
|
|
|
|
|
2019-04-18 14:41:55 +00:00
|
|
|
func (c *Client) getGitCredsWrapper(ef EndpointFinder, req *http.Request, u *url.URL) creds.CredentialHelperWrapper {
|
2019-05-01 16:45:21 +00:00
|
|
|
return c.credContext.GetCredentialHelper(c.Credentials, u)
|
2016-12-19 21:38:06 +00:00
|
|
|
}
|
|
|
|
|
2018-09-06 21:42:41 +00:00
|
|
|
func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint lfshttp.Endpoint, req *http.Request) (*url.URL, error) {
|
2016-12-23 03:06:51 +00:00
|
|
|
apiURL, err := url.Parse(apiEndpoint.Url)
|
2016-12-19 21:38:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the LFS request doesn't match the current LFS url, don't bother
|
|
|
|
// attempting to set the Authorization header from the LFS or Git remote URLs.
|
2016-12-23 03:06:51 +00:00
|
|
|
if req.URL.Scheme != apiURL.Scheme ||
|
|
|
|
req.URL.Host != apiURL.Host {
|
2016-12-19 21:38:06 +00:00
|
|
|
return req.URL, nil
|
|
|
|
}
|
|
|
|
|
2016-12-23 03:06:51 +00:00
|
|
|
if setRequestAuthFromURL(req, apiURL) {
|
2016-12-19 21:38:06 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(remote) > 0 {
|
|
|
|
if u := ef.GitRemoteURL(remote, operation == "upload"); u != "" {
|
2017-03-27 22:38:04 +00:00
|
|
|
schemedUrl, _ := prependEmptySchemeIfAbsent(u)
|
|
|
|
|
|
|
|
gitRemoteURL, err := url.Parse(schemedUrl)
|
2016-12-19 21:38:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2016-12-23 03:06:51 +00:00
|
|
|
if gitRemoteURL.Scheme == apiURL.Scheme &&
|
|
|
|
gitRemoteURL.Host == apiURL.Host {
|
2016-12-19 21:38:06 +00:00
|
|
|
|
2016-12-23 03:06:51 +00:00
|
|
|
if setRequestAuthFromURL(req, gitRemoteURL) {
|
2016-12-19 21:38:06 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2016-12-23 03:06:51 +00:00
|
|
|
return gitRemoteURL, nil
|
2016-12-19 21:38:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-23 03:06:51 +00:00
|
|
|
return apiURL, nil
|
2016-12-19 21:38:06 +00:00
|
|
|
}
|
|
|
|
|
2017-03-27 22:38:04 +00:00
|
|
|
// prependEmptySchemeIfAbsent prepends an empty scheme "//" if none was found in
|
|
|
|
// the URL in order to satisfy RFC 3986 §3.3, and `net/url.Parse()`.
|
|
|
|
//
|
|
|
|
// It returns a string parse-able with `net/url.Parse()` and a boolean whether
|
|
|
|
// or not an empty scheme was added.
|
|
|
|
func prependEmptySchemeIfAbsent(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) {
|
|
|
|
// First path segment has a colon, assumed that it's a
|
|
|
|
// scheme-less URL. Append an empty scheme on top to
|
|
|
|
// satisfy RFC 3986 §3.3, and `net/url.Parse()`.
|
|
|
|
return fmt.Sprintf("//%s", u), true
|
|
|
|
}
|
|
|
|
return u, true
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
// supportedSchemes is the list of URL schemes the `lfsapi` package
|
|
|
|
// supports.
|
|
|
|
supportedSchemes = []string{"ssh", "http", "https"}
|
|
|
|
)
|
|
|
|
|
|
|
|
// hasScheme returns whether or not a given string (taken to represent a RFC
|
|
|
|
// 3986 URL) has a scheme that is supported by the `lfsapi` package.
|
|
|
|
func hasScheme(what string) bool {
|
|
|
|
for _, scheme := range supportedSchemes {
|
|
|
|
if strings.HasPrefix(what, fmt.Sprintf("%s://", scheme)) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2016-12-23 03:06:51 +00:00
|
|
|
func requestHasAuth(req *http.Request) bool {
|
2018-05-17 03:48:46 +00:00
|
|
|
// The "Authorization" string constant is safe, since we assume that all
|
|
|
|
// request headers have been canonicalized.
|
2016-12-19 21:38:06 +00:00
|
|
|
if len(req.Header.Get("Authorization")) > 0 {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return len(req.URL.Query().Get("token")) > 0
|
|
|
|
}
|
|
|
|
|
2016-12-23 03:06:51 +00:00
|
|
|
func setRequestAuthFromURL(req *http.Request, u *url.URL) bool {
|
|
|
|
if u.User == nil {
|
2016-12-19 21:38:06 +00:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if pass, ok := u.User.Password(); ok {
|
|
|
|
fmt.Fprintln(os.Stderr, "warning: current Git remote contains credentials")
|
|
|
|
setRequestAuth(req, u.User.Username(), pass)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func setRequestAuth(req *http.Request, user, pass string) {
|
|
|
|
// better not be NTLM!
|
|
|
|
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 getReqOperation(req *http.Request) string {
|
|
|
|
operation := "download"
|
|
|
|
if req.Method == "POST" || req.Method == "PUT" {
|
|
|
|
operation = "upload"
|
|
|
|
}
|
|
|
|
return operation
|
|
|
|
}
|
2016-12-20 00:14:03 +00:00
|
|
|
|
|
|
|
var (
|
|
|
|
authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
|
|
|
|
)
|
|
|
|
|
2018-09-20 17:21:42 +00:00
|
|
|
func getAuthAccess(res *http.Response) AccessMode {
|
2016-12-20 00:14:03 +00:00
|
|
|
for _, headerName := range authenticateHeaders {
|
|
|
|
for _, auth := range res.Header[headerName] {
|
2016-12-20 16:40:58 +00:00
|
|
|
pieces := strings.SplitN(strings.ToLower(auth), " ", 2)
|
|
|
|
if len(pieces) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-09-20 17:21:42 +00:00
|
|
|
switch AccessMode(pieces[0]) {
|
2016-12-20 00:14:03 +00:00
|
|
|
case NegotiateAccess, NTLMAccess:
|
|
|
|
// When server sends Www-Authentication: Negotiate, it supports both Kerberos and NTLM.
|
|
|
|
// Since git-lfs current does not support Kerberos, we will return NTLM in this case.
|
|
|
|
return NTLMAccess
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return BasicAccess
|
|
|
|
}
|