github.com/2lambda123/git-lfs@v2.5.2+incompatible/lfsapi/auth.go (about) 1 package lfsapi 2 3 import ( 4 "encoding/base64" 5 "fmt" 6 "net" 7 "net/http" 8 "net/url" 9 "os" 10 "strings" 11 12 "github.com/git-lfs/git-lfs/errors" 13 "github.com/git-lfs/go-netrc/netrc" 14 "github.com/rubyist/tracerx" 15 ) 16 17 var ( 18 defaultCredentialHelper = &commandCredentialHelper{} 19 defaultNetrcFinder = &noFinder{} 20 defaultEndpointFinder = NewEndpointFinder(nil) 21 ) 22 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 return c.doWithAuth(remote, req, nil) 28 } 29 30 func (c *Client) doWithAuth(remote string, req *http.Request, via []*http.Request) (*http.Response, error) { 31 req.Header = c.extraHeadersFor(req) 32 33 apiEndpoint, access, credHelper, credsURL, creds, err := c.getCreds(remote, req) 34 if err != nil { 35 return nil, err 36 } 37 38 res, err := c.doWithCreds(req, credHelper, creds, credsURL, access, via) 39 if err != nil { 40 if errors.IsAuthError(err) { 41 newAccess := getAuthAccess(res) 42 if newAccess != access { 43 c.Endpoints.SetAccess(apiEndpoint.Url, newAccess) 44 } 45 46 if creds != nil || (access == NoneAccess && len(req.Header.Get("Authorization")) == 0) { 47 tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", newAccess) 48 if creds != nil { 49 req.Header.Del("Authorization") 50 credHelper.Reject(creds) 51 } 52 53 // This case represents a rejected request that 54 // should have been authenticated but wasn't. Do 55 // not count this against our redirection 56 // maximum, so do not recur through doWithAuth 57 // and instead call DoWithAuth. 58 return c.DoWithAuth(remote, req) 59 } 60 } 61 } 62 63 if res != nil && res.StatusCode < 300 && res.StatusCode > 199 { 64 credHelper.Approve(creds) 65 } 66 67 return res, err 68 } 69 70 func (c *Client) doWithCreds(req *http.Request, credHelper CredentialHelper, creds Creds, credsURL *url.URL, access Access, via []*http.Request) (*http.Response, error) { 71 if access == NTLMAccess { 72 return c.doWithNTLM(req, credHelper, creds, credsURL) 73 } 74 return c.do(req, "", via) 75 } 76 77 // getCreds fills the authorization header for the given request if possible, 78 // from the following sources: 79 // 80 // 1. NTLM access is handled elsewhere. 81 // 2. Existing Authorization or ?token query tells LFS that the request is ready. 82 // 3. Netrc based on the hostname. 83 // 4. URL authentication on the Endpoint URL or the Git Remote URL. 84 // 5. Git Credential Helper, potentially prompting the user. 85 // 86 // There are three URLs in play, that make this a little confusing. 87 // 88 // 1. The request URL, which should be something like "https://git.com/repo.git/info/lfs/objects/batch" 89 // 2. The LFS API URL, which should be something like "https://git.com/repo.git/info/lfs" 90 // This URL used for the "lfs.URL.access" git config key, which determines 91 // what kind of auth the LFS server expects. Could be BasicAccess, NTLMAccess, 92 // or NoneAccess, in which the Git Credential Helper step is skipped. We do 93 // not want to prompt the user for a password to fetch public repository data. 94 // 3. The Git Remote URL, which should be something like "https://git.com/repo.git" 95 // This URL is used for the Git Credential Helper. This way existing https 96 // Git remote credentials can be re-used for LFS. 97 func (c *Client) getCreds(remote string, req *http.Request) (Endpoint, Access, CredentialHelper, *url.URL, Creds, error) { 98 ef := c.Endpoints 99 if ef == nil { 100 ef = defaultEndpointFinder 101 } 102 103 netrcFinder := c.Netrc 104 if netrcFinder == nil { 105 netrcFinder = defaultNetrcFinder 106 } 107 108 operation := getReqOperation(req) 109 apiEndpoint := ef.Endpoint(operation, remote) 110 access := ef.AccessFor(apiEndpoint.Url) 111 112 if access != NTLMAccess { 113 if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access == NoneAccess { 114 return apiEndpoint, access, nullCreds, nil, nil, nil 115 } 116 117 credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req) 118 if err != nil { 119 return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds") 120 } 121 122 if credsURL == nil { 123 return apiEndpoint, access, nullCreds, nil, nil, nil 124 } 125 126 credHelper, creds, err := c.getGitCreds(ef, req, credsURL) 127 if err == nil { 128 tracerx.Printf("Filled credentials for %s", credsURL) 129 setRequestAuth(req, creds["username"], creds["password"]) 130 } 131 return apiEndpoint, access, credHelper, credsURL, creds, err 132 } 133 134 // NTLM ONLY 135 136 credsURL, err := url.Parse(apiEndpoint.Url) 137 if err != nil { 138 return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds") 139 } 140 141 if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil { 142 creds := Creds{ 143 "protocol": credsURL.Scheme, 144 "host": credsURL.Host, 145 "username": netrcMachine.Login, 146 "password": netrcMachine.Password, 147 "source": "netrc", 148 } 149 150 return apiEndpoint, access, nullCreds, credsURL, creds, nil 151 } 152 153 // NTLM uses creds to create the session 154 credHelper, creds, err := c.getGitCreds(ef, req, credsURL) 155 return apiEndpoint, access, credHelper, credsURL, creds, err 156 } 157 158 func (c *Client) getGitCreds(ef EndpointFinder, req *http.Request, u *url.URL) (CredentialHelper, Creds, error) { 159 credHelper, input := c.getCredentialHelper(u) 160 creds, err := credHelper.Fill(input) 161 if creds == nil || len(creds) < 1 { 162 errmsg := fmt.Sprintf("Git credentials for %s not found", u) 163 if err != nil { 164 errmsg = errmsg + ":\n" + err.Error() 165 } else { 166 errmsg = errmsg + "." 167 } 168 err = errors.New(errmsg) 169 } 170 171 return credHelper, creds, err 172 } 173 174 func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine { 175 hostname := req.URL.Host 176 var host string 177 178 if strings.Contains(hostname, ":") { 179 var err error 180 host, _, err = net.SplitHostPort(hostname) 181 if err != nil { 182 tracerx.Printf("netrc: error parsing %q: %s", hostname, err) 183 return nil 184 } 185 } else { 186 host = hostname 187 } 188 189 return netrcFinder.FindMachine(host) 190 } 191 192 func setAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) bool { 193 if machine := getAuthFromNetrc(netrcFinder, req); machine != nil { 194 setRequestAuth(req, machine.Login, machine.Password) 195 return true 196 } 197 198 return false 199 } 200 201 func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint Endpoint, req *http.Request) (*url.URL, error) { 202 apiURL, err := url.Parse(apiEndpoint.Url) 203 if err != nil { 204 return nil, err 205 } 206 207 // if the LFS request doesn't match the current LFS url, don't bother 208 // attempting to set the Authorization header from the LFS or Git remote URLs. 209 if req.URL.Scheme != apiURL.Scheme || 210 req.URL.Host != apiURL.Host { 211 return req.URL, nil 212 } 213 214 if setRequestAuthFromURL(req, apiURL) { 215 return nil, nil 216 } 217 218 if len(remote) > 0 { 219 if u := ef.GitRemoteURL(remote, operation == "upload"); u != "" { 220 schemedUrl, _ := prependEmptySchemeIfAbsent(u) 221 222 gitRemoteURL, err := url.Parse(schemedUrl) 223 if err != nil { 224 return nil, err 225 } 226 227 if gitRemoteURL.Scheme == apiURL.Scheme && 228 gitRemoteURL.Host == apiURL.Host { 229 230 if setRequestAuthFromURL(req, gitRemoteURL) { 231 return nil, nil 232 } 233 234 return gitRemoteURL, nil 235 } 236 } 237 } 238 239 return apiURL, nil 240 } 241 242 // prependEmptySchemeIfAbsent prepends an empty scheme "//" if none was found in 243 // the URL in order to satisfy RFC 3986 §3.3, and `net/url.Parse()`. 244 // 245 // It returns a string parse-able with `net/url.Parse()` and a boolean whether 246 // or not an empty scheme was added. 247 func prependEmptySchemeIfAbsent(u string) (string, bool) { 248 if hasScheme(u) { 249 return u, false 250 } 251 252 colon := strings.Index(u, ":") 253 slash := strings.Index(u, "/") 254 255 if colon >= 0 && (slash < 0 || colon < slash) { 256 // First path segment has a colon, assumed that it's a 257 // scheme-less URL. Append an empty scheme on top to 258 // satisfy RFC 3986 §3.3, and `net/url.Parse()`. 259 return fmt.Sprintf("//%s", u), true 260 } 261 return u, true 262 } 263 264 var ( 265 // supportedSchemes is the list of URL schemes the `lfsapi` package 266 // supports. 267 supportedSchemes = []string{"ssh", "http", "https"} 268 ) 269 270 // hasScheme returns whether or not a given string (taken to represent a RFC 271 // 3986 URL) has a scheme that is supported by the `lfsapi` package. 272 func hasScheme(what string) bool { 273 for _, scheme := range supportedSchemes { 274 if strings.HasPrefix(what, fmt.Sprintf("%s://", scheme)) { 275 return true 276 } 277 } 278 279 return false 280 } 281 282 func requestHasAuth(req *http.Request) bool { 283 // The "Authorization" string constant is safe, since we assume that all 284 // request headers have been canonicalized. 285 if len(req.Header.Get("Authorization")) > 0 { 286 return true 287 } 288 289 return len(req.URL.Query().Get("token")) > 0 290 } 291 292 func setRequestAuthFromURL(req *http.Request, u *url.URL) bool { 293 if u.User == nil { 294 return false 295 } 296 297 if pass, ok := u.User.Password(); ok { 298 fmt.Fprintln(os.Stderr, "warning: current Git remote contains credentials") 299 setRequestAuth(req, u.User.Username(), pass) 300 return true 301 } 302 303 return false 304 } 305 306 func setRequestAuth(req *http.Request, user, pass string) { 307 // better not be NTLM! 308 if len(user) == 0 && len(pass) == 0 { 309 return 310 } 311 312 token := fmt.Sprintf("%s:%s", user, pass) 313 auth := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token))) 314 req.Header.Set("Authorization", auth) 315 } 316 317 func getReqOperation(req *http.Request) string { 318 operation := "download" 319 if req.Method == "POST" || req.Method == "PUT" { 320 operation = "upload" 321 } 322 return operation 323 } 324 325 var ( 326 authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"} 327 ) 328 329 func getAuthAccess(res *http.Response) Access { 330 for _, headerName := range authenticateHeaders { 331 for _, auth := range res.Header[headerName] { 332 pieces := strings.SplitN(strings.ToLower(auth), " ", 2) 333 if len(pieces) == 0 { 334 continue 335 } 336 337 switch Access(pieces[0]) { 338 case NegotiateAccess, NTLMAccess: 339 // When server sends Www-Authentication: Negotiate, it supports both Kerberos and NTLM. 340 // Since git-lfs current does not support Kerberos, we will return NTLM in this case. 341 return NTLMAccess 342 } 343 } 344 } 345 346 return BasicAccess 347 }