12 "github.com/bgentry/go-netrc/netrc"
13 "github.com/git-lfs/git-lfs/errors"
14 "github.com/rubyist/tracerx"
18 defaultCredentialHelper = &commandCredentialHelper{}
19 defaultNetrcFinder = &noFinder{}
20 defaultEndpointFinder = NewEndpointFinder(nil)
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)
29 apiEndpoint, access, credHelper, credsURL, creds, err := c.getCreds(remote, req)
34 res, err := c.doWithCreds(req, credHelper, creds, credsURL, access)
36 if errors.IsAuthError(err) {
37 newAccess := getAuthAccess(res)
38 if newAccess != access {
39 c.Endpoints.SetAccess(apiEndpoint.Url, newAccess)
42 if creds != nil || (access == NoneAccess && len(req.Header.Get("Authorization")) == 0) {
43 tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", newAccess)
45 req.Header.Del("Authorization")
46 credHelper.Reject(creds)
48 return c.DoWithAuth(remote, req)
53 if res != nil && res.StatusCode < 300 && res.StatusCode > 199 {
54 credHelper.Approve(creds)
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)
67 // getCreds fills the authorization header for the given request if possible,
68 // from the following sources:
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.
76 // There are three URLs in play, that make this a little confusing.
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) {
90 ef = defaultEndpointFinder
93 netrcFinder := c.Netrc
94 if netrcFinder == nil {
95 netrcFinder = defaultNetrcFinder
98 operation := getReqOperation(req)
99 apiEndpoint := ef.Endpoint(operation, remote)
100 access := ef.AccessFor(apiEndpoint.Url)
102 if access != NTLMAccess {
103 if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access == NoneAccess {
104 return apiEndpoint, access, nullCreds, nil, nil, nil
107 credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req)
109 return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
113 return apiEndpoint, access, nullCreds, nil, nil, nil
116 credHelper, creds, err := c.getGitCreds(ef, req, credsURL)
118 tracerx.Printf("Filled credentials for %s", credsURL)
119 setRequestAuth(req, creds["username"], creds["password"])
121 return apiEndpoint, access, credHelper, credsURL, creds, err
126 credsURL, err := url.Parse(apiEndpoint.Url)
128 return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds")
131 if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil {
133 "protocol": credsURL.Scheme,
134 "host": credsURL.Host,
135 "username": netrcMachine.Login,
136 "password": netrcMachine.Password,
140 return apiEndpoint, access, nullCreds, credsURL, creds, nil
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
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)
154 errmsg = errmsg + ":\n" + err.Error()
156 errmsg = errmsg + "."
158 err = errors.New(errmsg)
161 return credHelper, creds, err
164 func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine {
165 hostname := req.URL.Host
168 if strings.Contains(hostname, ":") {
170 host, _, err = net.SplitHostPort(hostname)
172 tracerx.Printf("netrc: error parsing %q: %s", hostname, err)
179 return netrcFinder.FindMachine(host)
182 func setAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) bool {
183 if machine := getAuthFromNetrc(netrcFinder, req); machine != nil {
184 setRequestAuth(req, machine.Login, machine.Password)
191 func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint Endpoint, req *http.Request) (*url.URL, error) {
192 apiURL, err := url.Parse(apiEndpoint.Url)
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 {
204 if setRequestAuthFromURL(req, apiURL) {
209 if u := ef.GitRemoteURL(remote, operation == "upload"); u != "" {
210 schemedUrl, _ := prependEmptySchemeIfAbsent(u)
212 gitRemoteURL, err := url.Parse(schemedUrl)
217 if gitRemoteURL.Scheme == apiURL.Scheme &&
218 gitRemoteURL.Host == apiURL.Host {
220 if setRequestAuthFromURL(req, gitRemoteURL) {
224 return gitRemoteURL, nil
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()`.
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) {
242 colon := strings.Index(u, ":")
243 slash := strings.Index(u, "/")
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
255 // supportedSchemes is the list of URL schemes the `lfsapi` package
257 supportedSchemes = []string{"ssh", "http", "https"}
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)) {
272 func requestHasAuth(req *http.Request) bool {
273 if len(req.Header.Get("Authorization")) > 0 {
277 return len(req.URL.Query().Get("token")) > 0
280 func setRequestAuthFromURL(req *http.Request, u *url.URL) bool {
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)
294 func setRequestAuth(req *http.Request, user, pass string) {
295 // better not be NTLM!
296 if len(user) == 0 && len(pass) == 0 {
300 token := fmt.Sprintf("%s:%s", user, pass)
301 auth := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token)))
302 req.Header.Set("Authorization", auth)
305 func getReqOperation(req *http.Request) string {
306 operation := "download"
307 if req.Method == "POST" || req.Method == "PUT" {
314 authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"}
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 {
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.