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