github.com/saracen/git-lfs@v2.5.2+incompatible/lfsapi/client.go (about)

     1  package lfsapi
     2  
     3  import (
     4  	"context"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/textproto"
    11  	"net/url"
    12  	"os"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/git-lfs/git-lfs/config"
    19  	"github.com/git-lfs/git-lfs/errors"
    20  	"github.com/git-lfs/git-lfs/tools"
    21  	"github.com/rubyist/tracerx"
    22  )
    23  
    24  const MediaType = "application/vnd.git-lfs+json; charset=utf-8"
    25  
    26  var (
    27  	UserAgent = "git-lfs"
    28  	httpRE    = regexp.MustCompile(`\Ahttps?://`)
    29  )
    30  
    31  var hintFileUrl = strings.TrimSpace(`
    32  hint: The remote resolves to a file:// URL, which can only work with a
    33  hint: standalone transfer agent.  See section "Using a Custom Transfer Type
    34  hint: without the API server" in custom-transfers.md for details.
    35  `)
    36  
    37  func (c *Client) NewRequest(method string, e Endpoint, suffix string, body interface{}) (*http.Request, error) {
    38  	if strings.HasPrefix(e.Url, "file://") {
    39  		// Initial `\n` to avoid overprinting `Downloading LFS...`.
    40  		fmt.Fprintf(os.Stderr, "\n%s\n", hintFileUrl)
    41  	}
    42  
    43  	sshRes, err := c.sshResolveWithRetries(e, method)
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  
    48  	prefix := e.Url
    49  	if len(sshRes.Href) > 0 {
    50  		prefix = sshRes.Href
    51  	}
    52  
    53  	if !httpRE.MatchString(prefix) {
    54  		urlfragment := strings.SplitN(prefix, "?", 2)[0]
    55  		return nil, fmt.Errorf("missing protocol: %q", urlfragment)
    56  	}
    57  
    58  	req, err := http.NewRequest(method, joinURL(prefix, suffix), nil)
    59  	if err != nil {
    60  		return req, err
    61  	}
    62  
    63  	for key, value := range sshRes.Header {
    64  		req.Header.Set(key, value)
    65  	}
    66  	req.Header.Set("Accept", MediaType)
    67  
    68  	if body != nil {
    69  		if merr := MarshalToRequest(req, body); merr != nil {
    70  			return req, merr
    71  		}
    72  		req.Header.Set("Content-Type", MediaType)
    73  	}
    74  
    75  	return req, err
    76  }
    77  
    78  const slash = "/"
    79  
    80  func joinURL(prefix, suffix string) string {
    81  	if strings.HasSuffix(prefix, slash) {
    82  		return prefix + suffix
    83  	}
    84  	return prefix + slash + suffix
    85  }
    86  
    87  // Do sends an HTTP request to get an HTTP response. It wraps net/http, adding
    88  // extra headers, redirection handling, and error reporting.
    89  func (c *Client) Do(req *http.Request) (*http.Response, error) {
    90  	req.Header = c.extraHeadersFor(req)
    91  
    92  	return c.do(req, "", nil)
    93  }
    94  
    95  // do performs an *http.Request respecting redirects, and handles the response
    96  // as defined in c.handleResponse. Notably, it does not alter the headers for
    97  // the request argument in any way.
    98  func (c *Client) do(req *http.Request, remote string, via []*http.Request) (*http.Response, error) {
    99  	req.Header.Set("User-Agent", UserAgent)
   100  
   101  	res, err := c.doWithRedirects(c.httpClient(req.Host), req, remote, via)
   102  	if err != nil {
   103  		return res, err
   104  	}
   105  
   106  	return res, c.handleResponse(res)
   107  }
   108  
   109  // Close closes any resources that this client opened.
   110  func (c *Client) Close() error {
   111  	return c.httpLogger.Close()
   112  }
   113  
   114  func (c *Client) sshResolveWithRetries(e Endpoint, method string) (*sshAuthResponse, error) {
   115  	var sshRes sshAuthResponse
   116  	var err error
   117  
   118  	requests := tools.MaxInt(0, c.sshTries) + 1
   119  	for i := 0; i < requests; i++ {
   120  		sshRes, err = c.SSH.Resolve(e, method)
   121  		if err == nil {
   122  			return &sshRes, nil
   123  		}
   124  
   125  		tracerx.Printf(
   126  			"ssh: %s failed, error: %s, message: %s (try: %d/%d)",
   127  			e.SshUserAndHost, err.Error(), sshRes.Message, i,
   128  			requests,
   129  		)
   130  	}
   131  
   132  	if len(sshRes.Message) > 0 {
   133  		return nil, errors.Wrap(err, sshRes.Message)
   134  	}
   135  	return nil, err
   136  }
   137  
   138  func (c *Client) extraHeadersFor(req *http.Request) http.Header {
   139  	extraHeaders := c.extraHeaders(req.URL)
   140  	if len(extraHeaders) == 0 {
   141  		return req.Header
   142  	}
   143  
   144  	copy := make(http.Header, len(req.Header))
   145  	for k, vs := range req.Header {
   146  		copy[k] = vs
   147  	}
   148  
   149  	for k, vs := range extraHeaders {
   150  		for _, v := range vs {
   151  			copy[k] = append(copy[k], v)
   152  		}
   153  	}
   154  	return copy
   155  }
   156  
   157  func (c *Client) extraHeaders(u *url.URL) map[string][]string {
   158  	hdrs := c.uc.GetAll("http", u.String(), "extraHeader")
   159  	m := make(map[string][]string, len(hdrs))
   160  
   161  	for _, hdr := range hdrs {
   162  		parts := strings.SplitN(hdr, ":", 2)
   163  		if len(parts) < 2 {
   164  			continue
   165  		}
   166  
   167  		k, v := parts[0], strings.TrimSpace(parts[1])
   168  		// If header keys are given in non-canonicalized form (e.g.,
   169  		// "AUTHORIZATION" as opposed to "Authorization") they will not
   170  		// be returned in calls to net/http.Header.Get().
   171  		//
   172  		// So, we avoid this problem by first canonicalizing header keys
   173  		// for extra headers.
   174  		k = textproto.CanonicalMIMEHeaderKey(k)
   175  
   176  		m[k] = append(m[k], v)
   177  	}
   178  	return m
   179  }
   180  
   181  func (c *Client) doWithRedirects(cli *http.Client, req *http.Request, remote string, via []*http.Request) (*http.Response, error) {
   182  	tracedReq, err := c.traceRequest(req)
   183  	if err != nil {
   184  		return nil, err
   185  	}
   186  
   187  	var retries int
   188  	if n, ok := Retries(req); ok {
   189  		retries = n
   190  	} else {
   191  		retries = defaultRequestRetries
   192  	}
   193  
   194  	var res *http.Response
   195  
   196  	requests := tools.MaxInt(0, retries) + 1
   197  	for i := 0; i < requests; i++ {
   198  		res, err = cli.Do(req)
   199  		if err == nil {
   200  			break
   201  		}
   202  
   203  		if seek, ok := req.Body.(io.Seeker); ok {
   204  			seek.Seek(0, io.SeekStart)
   205  		}
   206  
   207  		c.traceResponse(req, tracedReq, nil)
   208  	}
   209  
   210  	if err != nil {
   211  		c.traceResponse(req, tracedReq, nil)
   212  		return nil, err
   213  	}
   214  
   215  	if res == nil {
   216  		return nil, nil
   217  	}
   218  
   219  	c.traceResponse(req, tracedReq, res)
   220  
   221  	if res.StatusCode != 301 &&
   222  		res.StatusCode != 302 &&
   223  		res.StatusCode != 303 &&
   224  		res.StatusCode != 307 &&
   225  		res.StatusCode != 308 {
   226  
   227  		// Above are the list of 3xx status codes that we know
   228  		// how to handle below. If the status code contained in
   229  		// the HTTP response was none of them, return the (res,
   230  		// err) tuple as-is, otherwise handle the redirect.
   231  		return res, err
   232  	}
   233  
   234  	redirectTo := res.Header.Get("Location")
   235  	locurl, err := url.Parse(redirectTo)
   236  	if err == nil && !locurl.IsAbs() {
   237  		locurl = req.URL.ResolveReference(locurl)
   238  		redirectTo = locurl.String()
   239  	}
   240  
   241  	via = append(via, req)
   242  	if len(via) >= 3 {
   243  		return res, errors.New("too many redirects")
   244  	}
   245  
   246  	redirectedReq, err := newRequestForRetry(req, redirectTo)
   247  	if err != nil {
   248  		return res, err
   249  	}
   250  
   251  	if len(req.Header.Get("Authorization")) > 0 {
   252  		// If the original request was authenticated (noted by the
   253  		// presence of the Authorization header), then recur through
   254  		// doWithAuth, retaining the requests via but only after
   255  		// authenticating the redirected request.
   256  		return c.doWithAuth(remote, redirectedReq, via)
   257  	}
   258  	return c.doWithRedirects(cli, redirectedReq, remote, via)
   259  }
   260  
   261  func (c *Client) httpClient(host string) *http.Client {
   262  	c.clientMu.Lock()
   263  	defer c.clientMu.Unlock()
   264  
   265  	if c.gitEnv == nil {
   266  		c.gitEnv = make(testEnv)
   267  	}
   268  
   269  	if c.osEnv == nil {
   270  		c.osEnv = make(testEnv)
   271  	}
   272  
   273  	if c.hostClients == nil {
   274  		c.hostClients = make(map[string]*http.Client)
   275  	}
   276  
   277  	if client, ok := c.hostClients[host]; ok {
   278  		return client
   279  	}
   280  
   281  	concurrentTransfers := c.ConcurrentTransfers
   282  	if concurrentTransfers < 1 {
   283  		concurrentTransfers = 8
   284  	}
   285  
   286  	dialtime := c.DialTimeout
   287  	if dialtime < 1 {
   288  		dialtime = 30
   289  	}
   290  
   291  	keepalivetime := c.KeepaliveTimeout
   292  	if keepalivetime < 1 {
   293  		keepalivetime = 1800
   294  	}
   295  
   296  	tlstime := c.TLSTimeout
   297  	if tlstime < 1 {
   298  		tlstime = 30
   299  	}
   300  
   301  	tr := &http.Transport{
   302  		Proxy:               proxyFromClient(c),
   303  		TLSHandshakeTimeout: time.Duration(tlstime) * time.Second,
   304  		MaxIdleConnsPerHost: concurrentTransfers,
   305  	}
   306  
   307  	activityTimeout := 30
   308  	if v, ok := c.uc.Get("lfs", fmt.Sprintf("https://%v", host), "activitytimeout"); ok {
   309  		if i, err := strconv.Atoi(v); err == nil {
   310  			activityTimeout = i
   311  		} else {
   312  			activityTimeout = 0
   313  		}
   314  	}
   315  
   316  	dialer := &net.Dialer{
   317  		Timeout:   time.Duration(dialtime) * time.Second,
   318  		KeepAlive: time.Duration(keepalivetime) * time.Second,
   319  		DualStack: true,
   320  	}
   321  
   322  	if activityTimeout > 0 {
   323  		activityDuration := time.Duration(activityTimeout) * time.Second
   324  		tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
   325  			c, err := dialer.DialContext(ctx, network, addr)
   326  			if c == nil {
   327  				return c, err
   328  			}
   329  			if tc, ok := c.(*net.TCPConn); ok {
   330  				tc.SetKeepAlive(true)
   331  				tc.SetKeepAlivePeriod(dialer.KeepAlive)
   332  			}
   333  			return &deadlineConn{Timeout: activityDuration, Conn: c}, err
   334  		}
   335  	} else {
   336  		tr.DialContext = dialer.DialContext
   337  	}
   338  
   339  	tr.TLSClientConfig = &tls.Config{}
   340  
   341  	if isClientCertEnabledForHost(c, host) {
   342  		tracerx.Printf("http: client cert for %s", host)
   343  		tr.TLSClientConfig.Certificates = []tls.Certificate{getClientCertForHost(c, host)}
   344  		tr.TLSClientConfig.BuildNameToCertificate()
   345  	}
   346  
   347  	if isCertVerificationDisabledForHost(c, host) {
   348  		tr.TLSClientConfig.InsecureSkipVerify = true
   349  	} else {
   350  		tr.TLSClientConfig.RootCAs = getRootCAsForHost(c, host)
   351  	}
   352  
   353  	httpClient := &http.Client{
   354  		Transport: tr,
   355  		CheckRedirect: func(*http.Request, []*http.Request) error {
   356  			return http.ErrUseLastResponse
   357  		},
   358  	}
   359  
   360  	c.hostClients[host] = httpClient
   361  	if c.VerboseOut == nil {
   362  		c.VerboseOut = os.Stderr
   363  	}
   364  
   365  	return httpClient
   366  }
   367  
   368  func (c *Client) CurrentUser() (string, string) {
   369  	userName, _ := c.gitEnv.Get("user.name")
   370  	userEmail, _ := c.gitEnv.Get("user.email")
   371  	return userName, userEmail
   372  }
   373  
   374  func newRequestForRetry(req *http.Request, location string) (*http.Request, error) {
   375  	newReq, err := http.NewRequest(req.Method, location, nil)
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  
   380  	if req.URL.Scheme == "https" && newReq.URL.Scheme == "http" {
   381  		return nil, errors.New("lfsapi/client: refusing insecure redirect, https->http")
   382  	}
   383  
   384  	sameHost := req.URL.Host == newReq.URL.Host
   385  	for key := range req.Header {
   386  		if key == "Authorization" {
   387  			if !sameHost {
   388  				continue
   389  			}
   390  		}
   391  		newReq.Header.Set(key, req.Header.Get(key))
   392  	}
   393  
   394  	oldestURL := strings.SplitN(req.URL.String(), "?", 2)[0]
   395  	newURL := strings.SplitN(newReq.URL.String(), "?", 2)[0]
   396  	tracerx.Printf("api: redirect %s %s to %s", req.Method, oldestURL, newURL)
   397  
   398  	// This body will have already been rewound from a call to
   399  	// lfsapi.Client.traceRequest().
   400  	newReq.Body = req.Body
   401  	newReq.ContentLength = req.ContentLength
   402  
   403  	// Copy the request's context.Context, if any.
   404  	newReq = newReq.WithContext(req.Context())
   405  
   406  	return newReq, nil
   407  }
   408  
   409  type deadlineConn struct {
   410  	Timeout time.Duration
   411  	net.Conn
   412  }
   413  
   414  func (c *deadlineConn) Read(b []byte) (int, error) {
   415  	if err := c.Conn.SetDeadline(time.Now().Add(c.Timeout)); err != nil {
   416  		return 0, err
   417  	}
   418  	return c.Conn.Read(b)
   419  }
   420  
   421  func (c *deadlineConn) Write(b []byte) (int, error) {
   422  	if err := c.Conn.SetDeadline(time.Now().Add(c.Timeout)); err != nil {
   423  		return 0, err
   424  	}
   425  
   426  	return c.Conn.Write(b)
   427  }
   428  
   429  func init() {
   430  	UserAgent = config.VersionDesc
   431  }