GoPLS Viewer

Home|gopls/cmd/auth/gitauth/gitauth.go
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'.
15package main
16
17import (
18    "bytes"
19    "fmt"
20    exec "golang.org/x/sys/execabs"
21    "log"
22    "net/http"
23    "net/url"
24    "os"
25    "path/filepath"
26    "strings"
27)
28
29func 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    uerr := url.ParseRequestURI(os.Args[2])
45    if err != nil {
46        log.Fatalf("invalid request URI (%v): %q\n"erros.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        outerr := 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 usernamepassword 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                uerr := 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"errfrags[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"uprefix)
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        reqerr := 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(usernamepassword)
116        lastHeader = req.Header
117        resperr := 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"uresp.StatusCodehttp.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}
151
MembersX
main.prefix
main.BlockStmt.RangeStmt_2027.BlockStmt.BlockStmt.u
bytes
log
url
main.BlockStmt.err
main.BlockStmt.password
exec
main.err
main.lastHeader
main.BlockStmt.cmd
strings
main
main.BlockStmt.lines
main.BlockStmt.RangeStmt_2027.line
fmt
filepath
main.u
main.BlockStmt.RangeStmt_2027.BlockStmt.frags
main.BlockStmt.req
main.BlockStmt.BlockStmt.action
os
main.BlockStmt.out
main.BlockStmt.username
main.BlockStmt.RangeStmt_2027.BlockStmt.BlockStmt.err
main.BlockStmt.resp
http
main.lastStatus
Members
X