blob: f11cd2fbf057d728569b092d472b03d1d9eb8db4 [file] [edit]
// 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.SplitSeq(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
}