github.com/2lambda123/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 }