golang.org/x/tools@v0.21.0/cmd/auth/gitauth/gitauth.go (about)

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // gitauth uses 'git credential' to implement the GOAUTH protocol described in
     6  // https://golang.org/issue/26232. It expects an absolute path to the working
     7  // directory for the 'git' command as the first command-line argument.
     8  //
     9  // Example GOAUTH usage:
    10  //
    11  //	export GOAUTH="gitauth $HOME"
    12  //
    13  // See https://git-scm.com/docs/gitcredentials or run 'man gitcredentials' for
    14  // information on how to configure 'git credential'.
    15  package main
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"log"
    21  	"net/http"
    22  	"net/url"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"strings"
    27  )
    28  
    29  func main() {
    30  	if len(os.Args) < 2 || !filepath.IsAbs(os.Args[1]) {
    31  		fmt.Fprintf(os.Stderr, "usage: %s WORKDIR [URL]", os.Args[0])
    32  		os.Exit(2)
    33  	}
    34  
    35  	log.SetPrefix("gitauth: ")
    36  
    37  	if len(os.Args) != 3 {
    38  		// No explicit URL was passed on the command line, but 'git credential'
    39  		// provides no way to enumerate existing credentials.
    40  		// Wait for a request for a specific URL.
    41  		return
    42  	}
    43  
    44  	u, err := url.ParseRequestURI(os.Args[2])
    45  	if err != nil {
    46  		log.Fatalf("invalid request URI (%v): %q\n", err, os.Args[1])
    47  	}
    48  
    49  	var (
    50  		prefix     *url.URL
    51  		lastHeader http.Header
    52  		lastStatus = http.StatusUnauthorized
    53  	)
    54  	for lastStatus == http.StatusUnauthorized {
    55  		cmd := exec.Command("git", "credential", "fill")
    56  
    57  		// We don't want to execute a 'git' command in an arbitrary directory, since
    58  		// that opens up a number of config-injection attacks (for example,
    59  		// https://golang.org/issue/29230). Instead, we have the user configure a
    60  		// directory explicitly on the command line.
    61  		cmd.Dir = os.Args[1]
    62  
    63  		cmd.Stdin = strings.NewReader(fmt.Sprintf("url=%s\n", u))
    64  		cmd.Stderr = os.Stderr
    65  		out, err := cmd.Output()
    66  		if err != nil {
    67  			log.Fatalf("'git credential fill' failed: %v\n", err)
    68  		}
    69  
    70  		prefix = new(url.URL)
    71  		var username, password string
    72  		lines := strings.Split(string(out), "\n")
    73  		for _, line := range lines {
    74  			frags := strings.SplitN(line, "=", 2)
    75  			if len(frags) != 2 {
    76  				continue // Ignore unrecognized response lines.
    77  			}
    78  			switch strings.TrimSpace(frags[0]) {
    79  			case "protocol":
    80  				prefix.Scheme = frags[1]
    81  			case "host":
    82  				prefix.Host = frags[1]
    83  			case "path":
    84  				prefix.Path = frags[1]
    85  			case "username":
    86  				username = frags[1]
    87  			case "password":
    88  				password = frags[1]
    89  			case "url":
    90  				// Write to a local variable instead of updating prefix directly:
    91  				// if the url field is malformed, we don't want to invalidate
    92  				// information parsed from the protocol, host, and path fields.
    93  				u, err := url.ParseRequestURI(frags[1])
    94  				if err == nil {
    95  					prefix = u
    96  				} else {
    97  					log.Printf("malformed URL from 'git credential fill' (%v): %q\n", err, frags[1])
    98  					// Proceed anyway: we might be able to parse the prefix from other fields of the response.
    99  				}
   100  			}
   101  		}
   102  
   103  		// Double-check that the URL Git gave us is a prefix of the one we requested.
   104  		if !strings.HasPrefix(u.String(), prefix.String()) {
   105  			log.Fatalf("requested a credential for %q, but 'git credential fill' provided one for %q\n", u, prefix)
   106  		}
   107  
   108  		// Send a HEAD request to try to detect whether the credential is valid.
   109  		// If the user just typed in a correct password and has caching enabled,
   110  		// we don't want to nag them for it again the next time they run a 'go' command.
   111  		req, err := http.NewRequest("HEAD", u.String(), nil)
   112  		if err != nil {
   113  			log.Fatalf("internal error constructing HTTP HEAD request: %v\n", err)
   114  		}
   115  		req.SetBasicAuth(username, password)
   116  		lastHeader = req.Header
   117  		resp, err := http.DefaultClient.Do(req)
   118  		if err != nil {
   119  			log.Printf("HTTPS HEAD request failed to connect: %v\n", err)
   120  			// Couldn't verify the credential, but we have no evidence that it is invalid either.
   121  			// Proceed, but don't update git's credential cache.
   122  			break
   123  		}
   124  		lastStatus = resp.StatusCode
   125  
   126  		if resp.StatusCode != http.StatusOK {
   127  			log.Printf("%s: %v %s\n", u, resp.StatusCode, http.StatusText(resp.StatusCode))
   128  		}
   129  
   130  		if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized {
   131  			// We learned something about the credential: it either worked or it was invalid.
   132  			// Approve or reject the credential (on a best-effort basis)
   133  			// so that the git credential helper can update its cache as appropriate.
   134  			action := "approve"
   135  			if resp.StatusCode != http.StatusOK {
   136  				action = "reject"
   137  			}
   138  			cmd = exec.Command("git", "credential", action)
   139  			cmd.Stderr = os.Stderr
   140  			cmd.Stdout = os.Stderr
   141  			cmd.Stdin = bytes.NewReader(out)
   142  			_ = cmd.Run()
   143  		}
   144  	}
   145  
   146  	// Write out the credential in the format expected by the 'go' command.
   147  	fmt.Printf("%s\n\n", prefix)
   148  	lastHeader.Write(os.Stdout)
   149  	fmt.Println()
   150  }