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 }