github.com/stffabi/git-lfs@v2.3.5-0.20180214015214-8eeaa8d88902+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/bgentry/go-netrc/netrc" 13 "github.com/git-lfs/git-lfs/errors" 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 req.Header = c.extraHeadersFor(req) 28 29 apiEndpoint, access, credHelper, credsURL, creds, err := c.getCreds(remote, req) 30 if err != nil { 31 return nil, err 32 } 33 34 res, err := c.doWithCreds(req, credHelper, creds, credsURL, access) 35 if err != nil { 36 if errors.IsAuthError(err) { 37 newAccess := getAuthAccess(res) 38 if newAccess != access { 39 c.Endpoints.SetAccess(apiEndpoint.Url, newAccess) 40 } 41 42 if creds != nil || (access == NoneAccess && len(req.Header.Get("Authorization")) == 0) { 43 tracerx.Printf("api: http response indicates %q authentication. Resubmitting...", newAccess) 44 if creds != nil { 45 req.Header.Del("Authorization") 46 credHelper.Reject(creds) 47 } 48 return c.DoWithAuth(remote, req) 49 } 50 } 51 } 52 53 if res != nil && res.StatusCode < 300 && res.StatusCode > 199 { 54 credHelper.Approve(creds) 55 } 56 57 return res, err 58 } 59 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) 63 } 64 return c.do(req) 65 } 66 67 // getCreds fills the authorization header for the given request if possible, 68 // from the following sources: 69 // 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. 75 // 76 // There are three URLs in play, that make this a little confusing. 77 // 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) { 88 ef := c.Endpoints 89 if ef == nil { 90 ef = defaultEndpointFinder 91 } 92 93 netrcFinder := c.Netrc 94 if netrcFinder == nil { 95 netrcFinder = defaultNetrcFinder 96 } 97 98 operation := getReqOperation(req) 99 apiEndpoint := ef.Endpoint(operation, remote) 100 access := ef.AccessFor(apiEndpoint.Url) 101 102 if access != NTLMAccess { 103 if requestHasAuth(req) || setAuthFromNetrc(netrcFinder, req) || access == NoneAccess { 104 return apiEndpoint, access, nullCreds, nil, nil, nil 105 } 106 107 credsURL, err := getCredURLForAPI(ef, operation, remote, apiEndpoint, req) 108 if err != nil { 109 return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds") 110 } 111 112 if credsURL == nil { 113 return apiEndpoint, access, nullCreds, nil, nil, nil 114 } 115 116 credHelper, creds, err := c.getGitCreds(ef, req, credsURL) 117 if err == nil { 118 tracerx.Printf("Filled credentials for %s", credsURL) 119 setRequestAuth(req, creds["username"], creds["password"]) 120 } 121 return apiEndpoint, access, credHelper, credsURL, creds, err 122 } 123 124 // NTLM ONLY 125 126 credsURL, err := url.Parse(apiEndpoint.Url) 127 if err != nil { 128 return apiEndpoint, access, nullCreds, nil, nil, errors.Wrap(err, "creds") 129 } 130 131 if netrcMachine := getAuthFromNetrc(netrcFinder, req); netrcMachine != nil { 132 creds := Creds{ 133 "protocol": credsURL.Scheme, 134 "host": credsURL.Host, 135 "username": netrcMachine.Login, 136 "password": netrcMachine.Password, 137 "source": "netrc", 138 } 139 140 return apiEndpoint, access, nullCreds, credsURL, creds, nil 141 } 142 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 146 } 147 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) 153 if err != nil { 154 errmsg = errmsg + ":\n" + err.Error() 155 } else { 156 errmsg = errmsg + "." 157 } 158 err = errors.New(errmsg) 159 } 160 161 return credHelper, creds, err 162 } 163 164 func getAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) *netrc.Machine { 165 hostname := req.URL.Host 166 var host string 167 168 if strings.Contains(hostname, ":") { 169 var err error 170 host, _, err = net.SplitHostPort(hostname) 171 if err != nil { 172 tracerx.Printf("netrc: error parsing %q: %s", hostname, err) 173 return nil 174 } 175 } else { 176 host = hostname 177 } 178 179 return netrcFinder.FindMachine(host) 180 } 181 182 func setAuthFromNetrc(netrcFinder NetrcFinder, req *http.Request) bool { 183 if machine := getAuthFromNetrc(netrcFinder, req); machine != nil { 184 setRequestAuth(req, machine.Login, machine.Password) 185 return true 186 } 187 188 return false 189 } 190 191 func getCredURLForAPI(ef EndpointFinder, operation, remote string, apiEndpoint Endpoint, req *http.Request) (*url.URL, error) { 192 apiURL, err := url.Parse(apiEndpoint.Url) 193 if err != nil { 194 return nil, err 195 } 196 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 { 201 return req.URL, nil 202 } 203 204 if setRequestAuthFromURL(req, apiURL) { 205 return nil, nil 206 } 207 208 if len(remote) > 0 { 209 if u := ef.GitRemoteURL(remote, operation == "upload"); u != "" { 210 schemedUrl, _ := prependEmptySchemeIfAbsent(u) 211 212 gitRemoteURL, err := url.Parse(schemedUrl) 213 if err != nil { 214 return nil, err 215 } 216 217 if gitRemoteURL.Scheme == apiURL.Scheme && 218 gitRemoteURL.Host == apiURL.Host { 219 220 if setRequestAuthFromURL(req, gitRemoteURL) { 221 return nil, nil 222 } 223 224 return gitRemoteURL, nil 225 } 226 } 227 } 228 229 return apiURL, nil 230 } 231 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()`. 234 // 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) { 238 if hasScheme(u) { 239 return u, false 240 } 241 242 colon := strings.Index(u, ":") 243 slash := strings.Index(u, "/") 244 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 250 } 251 return u, true 252 } 253 254 var ( 255 // supportedSchemes is the list of URL schemes the `lfsapi` package 256 // supports. 257 supportedSchemes = []string{"ssh", "http", "https"} 258 ) 259 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)) { 265 return true 266 } 267 } 268 269 return false 270 } 271 272 func requestHasAuth(req *http.Request) bool { 273 if len(req.Header.Get("Authorization")) > 0 { 274 return true 275 } 276 277 return len(req.URL.Query().Get("token")) > 0 278 } 279 280 func setRequestAuthFromURL(req *http.Request, u *url.URL) bool { 281 if u.User == nil { 282 return false 283 } 284 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) 288 return true 289 } 290 291 return false 292 } 293 294 func setRequestAuth(req *http.Request, user, pass string) { 295 // better not be NTLM! 296 if len(user) == 0 && len(pass) == 0 { 297 return 298 } 299 300 token := fmt.Sprintf("%s:%s", user, pass) 301 auth := "Basic " + strings.TrimSpace(base64.StdEncoding.EncodeToString([]byte(token))) 302 req.Header.Set("Authorization", auth) 303 } 304 305 func getReqOperation(req *http.Request) string { 306 operation := "download" 307 if req.Method == "POST" || req.Method == "PUT" { 308 operation = "upload" 309 } 310 return operation 311 } 312 313 var ( 314 authenticateHeaders = []string{"Lfs-Authenticate", "Www-Authenticate"} 315 ) 316 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 { 322 continue 323 } 324 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. 329 return NTLMAccess 330 } 331 } 332 } 333 334 return BasicAccess 335 }