// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // gitauth uses 'git credential' to implement the GOAUTH protocol. // // See https://git-scm.com/docs/gitcredentials or run 'man gitcredentials' for // information on how to configure 'git credential'. package auth import ( "bytes" "cmd/go/internal/base" "cmd/go/internal/cfg" "cmd/go/internal/web/intercept" "fmt" "log" "net/http" "net/url" "os/exec" "strings" ) const maxTries = 3 // runGitAuth retrieves credentials for the given url using // 'git credential fill', validates them with a HEAD request // (using the provided client) and updates the credential helper's cache. // It returns the matching credential prefix, the http.Header with the // Basic Authentication header set, or an error. // The caller must not mutate the header. func runGitAuth(client *http.Client, dir, url string) (string, http.Header, error) { if url == "" { // No explicit url was passed, but 'git credential' // provides no way to enumerate existing credentials. // Wait for a request for a specific url. return "", nil, fmt.Errorf("no explicit url was passed") } if dir == "" { // Prevent config-injection attacks by requiring an explicit working directory. // See https://golang.org/issue/29230 for details. panic("'git' invoked in an arbitrary directory") // this should be caught earlier. } cmd := exec.Command("git", "credential", "fill") cmd.Dir = dir cmd.Stdin = strings.NewReader(fmt.Sprintf("url=%s\n", url)) out, err := cmd.CombinedOutput() if err != nil { return "", nil, fmt.Errorf("'git credential fill' failed (url=%s): %w\n%s", url, err, out) } parsedPrefix, username, password := parseGitAuth(out) if parsedPrefix == "" { return "", nil, fmt.Errorf("'git credential fill' failed for url=%s, could not parse url\n", url) } // Check that the URL Git gave us is a prefix of the one we requested. if !strings.HasPrefix(url, parsedPrefix) { return "", nil, fmt.Errorf("requested a credential for %s, but 'git credential fill' provided one for %s\n", url, parsedPrefix) } req, err := http.NewRequest("HEAD", parsedPrefix, nil) if err != nil { return "", nil, fmt.Errorf("internal error constructing HTTP HEAD request: %v\n", err) } req.SetBasicAuth(username, password) // Asynchronously validate the provided credentials using a HEAD request, // allowing the git credential helper to update its cache without blocking. // This avoids repeatedly prompting the user for valid credentials. // This is a best-effort update; the primary validation will still occur // with the caller's client. // The request is intercepted for testing purposes to simulate interactions // with the credential helper. intercept.Request(req) go updateGitCredentialHelper(client, req, out) // Return the parsed prefix and headers, even if credential validation fails. // The caller is responsible for the primary validation. return parsedPrefix, req.Header, nil } // parseGitAuth parses the output of 'git credential fill', extracting // the URL prefix, user, and password. // Any of these values may be empty if parsing fails. func parseGitAuth(data []byte) (parsedPrefix, username, password string) { prefix := new(url.URL) for _, line := range strings.Split(string(data), "\n") { key, value, ok := strings.Cut(strings.TrimSpace(line), "=") if !ok { continue } switch key { case "protocol": prefix.Scheme = value case "host": prefix.Host = value case "path": prefix.Path = value case "username": username = value case "password": password = value case "url": // Write to a local variable instead of updating prefix directly: // if the url field is malformed, we don't want to invalidate // information parsed from the protocol, host, and path fields. u, err := url.ParseRequestURI(value) if err != nil { if cfg.BuildX { log.Printf("malformed URL from 'git credential fill' (%v): %q\n", err, value) // Proceed anyway: we might be able to parse the prefix from other fields of the response. } continue } prefix = u } } return prefix.String(), username, password } // updateGitCredentialHelper validates the given credentials by sending a HEAD request // and updates the git credential helper's cache accordingly. It retries the // request up to maxTries times. func updateGitCredentialHelper(client *http.Client, req *http.Request, credentialOutput []byte) { for range maxTries { release, err := base.AcquireNet() if err != nil { return } res, err := client.Do(req) if err != nil { release() continue } res.Body.Close() release() if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusUnauthorized { approveOrRejectCredential(credentialOutput, res.StatusCode == http.StatusOK) break } } } // approveOrRejectCredential approves or rejects the provided credential using // 'git credential approve/reject'. func approveOrRejectCredential(credentialOutput []byte, approve bool) { action := "reject" if approve { action = "approve" } cmd := exec.Command("git", "credential", action) cmd.Stdin = bytes.NewReader(credentialOutput) cmd.Run() // ignore error }