git-lfs/httputil/request.go

199 lines
4.7 KiB
Go
Raw Normal View History

package httputil
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/github/git-lfs/auth"
"github.com/github/git-lfs/config"
2016-08-18 20:20:33 +00:00
"github.com/github/git-lfs/errors"
"github.com/rubyist/tracerx"
)
type ClientError struct {
Message string `json:"message"`
DocumentationUrl string `json:"documentation_url,omitempty"`
RequestId string `json:"request_id,omitempty"`
}
const (
2016-07-21 17:06:43 +00:00
basicAuthType = "basic"
ntlmAuthType = "ntlm"
negotiateAuthType = "negotiate"
2016-07-18 19:10:56 +00:00
)
var (
2016-07-21 17:06:43 +00:00
authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
)
func (e *ClientError) Error() string {
msg := e.Message
if len(e.DocumentationUrl) > 0 {
msg += "\nDocs: " + e.DocumentationUrl
}
if len(e.RequestId) > 0 {
msg += "\nRequest ID: " + e.RequestId
}
return msg
}
// Internal http request management
func doHttpRequest(cfg *config.Configuration, req *http.Request, creds auth.Creds) (*http.Response, error) {
var (
res *http.Response
err error
)
if cfg.NtlmAccess(auth.GetOperationForRequest(req)) {
res, err = doNTLMRequest(cfg, req, true)
} else {
res, err = NewHttpClient(cfg, req.Host).Do(req)
}
if res == nil {
res = &http.Response{
StatusCode: 0,
Header: make(http.Header),
Request: req,
Body: ioutil.NopCloser(bytes.NewBufferString("")),
}
}
if err != nil {
2016-08-18 20:20:33 +00:00
if errors.IsAuthError(err) {
SetAuthType(cfg, req, res)
doHttpRequest(cfg, req, creds)
} else {
2016-08-18 20:20:33 +00:00
err = errors.Error(err)
}
} else {
err = handleResponse(cfg, res, creds)
}
if err != nil {
if res != nil {
SetErrorResponseContext(cfg, err, res)
} else {
setErrorRequestContext(cfg, err, req)
}
}
return res, err
}
// DoHttpRequest performs a single HTTP request
func DoHttpRequest(cfg *config.Configuration, req *http.Request, useCreds bool) (*http.Response, error) {
var creds auth.Creds
if useCreds {
c, err := auth.GetCreds(cfg, req)
if err != nil {
return nil, err
}
creds = c
}
return doHttpRequest(cfg, req, creds)
}
// DoHttpRequestWithRedirects runs a HTTP request and responds to redirects
func DoHttpRequestWithRedirects(cfg *config.Configuration, req *http.Request, via []*http.Request, useCreds bool) (*http.Response, error) {
var creds auth.Creds
if useCreds {
c, err := auth.GetCreds(cfg, req)
if err != nil {
return nil, err
}
creds = c
}
res, err := doHttpRequest(cfg, req, creds)
if err != nil {
return res, err
}
if res.StatusCode == 307 {
redirectTo := res.Header.Get("Location")
locurl, err := url.Parse(redirectTo)
if err == nil && !locurl.IsAbs() {
locurl = req.URL.ResolveReference(locurl)
redirectTo = locurl.String()
}
redirectedReq, err := NewHttpRequest(req.Method, redirectTo, nil)
if err != nil {
2016-08-18 20:20:33 +00:00
return res, errors.Errorf(err, err.Error())
}
via = append(via, req)
// Avoid seeking and re-wrapping the CountingReadCloser, just get the "real" body
realBody := req.Body
if wrappedBody, ok := req.Body.(*CountingReadCloser); ok {
realBody = wrappedBody.ReadCloser
}
seeker, ok := realBody.(io.Seeker)
if !ok {
2016-08-18 20:20:33 +00:00
return res, errors.Errorf(nil, "Request body needs to be an io.Seeker to handle redirects.")
}
if _, err := seeker.Seek(0, 0); err != nil {
2016-08-18 20:20:33 +00:00
return res, errors.Error(err)
}
redirectedReq.Body = realBody
redirectedReq.ContentLength = req.ContentLength
if err = CheckRedirect(redirectedReq, via); err != nil {
2016-08-18 20:20:33 +00:00
return res, errors.Errorf(err, err.Error())
}
return DoHttpRequestWithRedirects(cfg, redirectedReq, via, useCreds)
}
return res, nil
}
// NewHttpRequest creates a template request, with the given headers & UserAgent supplied
func NewHttpRequest(method, rawurl string, header map[string]string) (*http.Request, error) {
req, err := http.NewRequest(method, rawurl, nil)
if err != nil {
return nil, err
}
for key, value := range header {
req.Header.Set(key, value)
}
req.Header.Set("User-Agent", UserAgent)
return req, nil
}
func SetAuthType(cfg *config.Configuration, req *http.Request, res *http.Response) {
authType := GetAuthType(res)
operation := auth.GetOperationForRequest(req)
cfg.SetAccess(operation, authType)
tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", authType)
}
func GetAuthType(res *http.Response) string {
2016-07-21 17:06:43 +00:00
for _, headerName := range authenticateHeaders {
2016-07-18 19:10:56 +00:00
for _, auth := range res.Header[headerName] {
authLower := strings.ToLower(auth)
2016-07-21 17:06:43 +00:00
// 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.
2016-07-21 17:06:43 +00:00
if strings.HasPrefix(authLower, ntlmAuthType) || strings.HasPrefix(authLower, negotiateAuthType) {
return ntlmAuthType
}
}
}
2016-07-21 17:06:43 +00:00
return basicAuthType
}