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 }