Imported Upstream version 2.5.0
[scm/test.git] / lfsapi / ssh.go
1 package lfsapi
2
3 import (
4         "bytes"
5         "encoding/json"
6         "fmt"
7         "os/exec"
8         "path/filepath"
9         "regexp"
10         "strings"
11         "time"
12
13         "github.com/git-lfs/git-lfs/config"
14         "github.com/git-lfs/git-lfs/tools"
15         "github.com/rubyist/tracerx"
16 )
17
18 type SSHResolver interface {
19         Resolve(Endpoint, string) (sshAuthResponse, error)
20 }
21
22 func withSSHCache(ssh SSHResolver) SSHResolver {
23         return &sshCache{
24                 endpoints: make(map[string]*sshAuthResponse),
25                 ssh:       ssh,
26         }
27 }
28
29 type sshCache struct {
30         endpoints map[string]*sshAuthResponse
31         ssh       SSHResolver
32 }
33
34 func (c *sshCache) Resolve(e Endpoint, method string) (sshAuthResponse, error) {
35         if len(e.SshUserAndHost) == 0 {
36                 return sshAuthResponse{}, nil
37         }
38
39         key := strings.Join([]string{e.SshUserAndHost, e.SshPort, e.SshPath, method}, "//")
40         if res, ok := c.endpoints[key]; ok {
41                 if _, expired := res.IsExpiredWithin(5 * time.Second); !expired {
42                         tracerx.Printf("ssh cache: %s git-lfs-authenticate %s %s",
43                                 e.SshUserAndHost, e.SshPath, endpointOperation(e, method))
44                         return *res, nil
45                 } else {
46                         tracerx.Printf("ssh cache expired: %s git-lfs-authenticate %s %s",
47                                 e.SshUserAndHost, e.SshPath, endpointOperation(e, method))
48                 }
49         }
50
51         res, err := c.ssh.Resolve(e, method)
52         if err == nil {
53                 c.endpoints[key] = &res
54         }
55         return res, err
56 }
57
58 type sshAuthResponse struct {
59         Message   string            `json:"-"`
60         Href      string            `json:"href"`
61         Header    map[string]string `json:"header"`
62         ExpiresAt time.Time         `json:"expires_at"`
63         ExpiresIn int               `json:"expires_in"`
64
65         createdAt time.Time
66 }
67
68 func (r *sshAuthResponse) IsExpiredWithin(d time.Duration) (time.Time, bool) {
69         return tools.IsExpiredAtOrIn(r.createdAt, d, r.ExpiresAt,
70                 time.Duration(r.ExpiresIn)*time.Second)
71 }
72
73 type sshAuthClient struct {
74         os  config.Environment
75         git config.Environment
76 }
77
78 func (c *sshAuthClient) Resolve(e Endpoint, method string) (sshAuthResponse, error) {
79         res := sshAuthResponse{}
80         if len(e.SshUserAndHost) == 0 {
81                 return res, nil
82         }
83
84         exe, args := sshGetLFSExeAndArgs(c.os, e, method)
85         cmd := exec.Command(exe, args...)
86
87         // Save stdout and stderr in separate buffers
88         var outbuf, errbuf bytes.Buffer
89         cmd.Stdout = &outbuf
90         cmd.Stderr = &errbuf
91
92         now := time.Now()
93
94         // Execute command
95         err := cmd.Start()
96         if err == nil {
97                 err = cmd.Wait()
98         }
99
100         // Processing result
101         if err != nil {
102                 res.Message = strings.TrimSpace(errbuf.String())
103         } else {
104                 err = json.Unmarshal(outbuf.Bytes(), &res)
105                 if res.ExpiresIn == 0 && res.ExpiresAt.IsZero() {
106                         ttl := c.git.Int("lfs.defaulttokenttl", 0)
107                         if ttl < 0 {
108                                 ttl = 0
109                         }
110                         res.ExpiresIn = ttl
111                 }
112                 res.createdAt = now
113         }
114
115         return res, err
116 }
117
118 func sshGetLFSExeAndArgs(osEnv config.Environment, e Endpoint, method string) (string, []string) {
119         exe, args := sshGetExeAndArgs(osEnv, e)
120         operation := endpointOperation(e, method)
121         args = append(args, fmt.Sprintf("git-lfs-authenticate %s %s", e.SshPath, operation))
122         tracerx.Printf("run_command: %s %s", exe, strings.Join(args, " "))
123         return exe, args
124 }
125
126 // Return the executable name for ssh on this machine and the base args
127 // Base args includes port settings, user/host, everything pre the command to execute
128 func sshGetExeAndArgs(osEnv config.Environment, e Endpoint) (exe string, baseargs []string) {
129         isPlink := false
130         isTortoise := false
131
132         ssh, _ := osEnv.Get("GIT_SSH")
133         sshCmd, _ := osEnv.Get("GIT_SSH_COMMAND")
134         cmdArgs := tools.QuotedFields(sshCmd)
135         if len(cmdArgs) > 0 {
136                 ssh = cmdArgs[0]
137                 cmdArgs = cmdArgs[1:]
138         }
139
140         if ssh == "" {
141                 ssh = defaultSSHCmd
142         }
143
144         basessh := filepath.Base(ssh)
145
146         if basessh != defaultSSHCmd {
147                 // Strip extension for easier comparison
148                 if ext := filepath.Ext(basessh); len(ext) > 0 {
149                         basessh = basessh[:len(basessh)-len(ext)]
150                 }
151                 isPlink = strings.EqualFold(basessh, "plink")
152                 isTortoise = strings.EqualFold(basessh, "tortoiseplink")
153         }
154
155         args := make([]string, 0, 5+len(cmdArgs))
156         if len(cmdArgs) > 0 {
157                 args = append(args, cmdArgs...)
158         }
159
160         if isTortoise {
161                 // TortoisePlink requires the -batch argument to behave like ssh/plink
162                 args = append(args, "-batch")
163         }
164
165         if len(e.SshPort) > 0 {
166                 if isPlink || isTortoise {
167                         args = append(args, "-P")
168                 } else {
169                         args = append(args, "-p")
170                 }
171                 args = append(args, e.SshPort)
172         }
173
174         if sep, ok := sshSeparators[basessh]; ok {
175                 // inserts a separator between cli -options and host/cmd commands
176                 // example: $ ssh -p 12345 -- user@host.com git-lfs-authenticate ...
177                 args = append(args, sep, e.SshUserAndHost)
178         } else {
179                 // no prefix supported, strip leading - off host to prevent cmd like:
180                 // $ git config lfs.url ssh://-proxycmd=whatever
181                 // $ plink -P 12345 -proxycmd=foo git-lfs-authenticate ...
182                 //
183                 // Instead, it'll attempt this, and eventually return an error
184                 // $ plink -P 12345 proxycmd=foo git-lfs-authenticate ...
185                 args = append(args, sshOptPrefixRE.ReplaceAllString(e.SshUserAndHost, ""))
186         }
187
188         return ssh, args
189 }
190
191 const defaultSSHCmd = "ssh"
192
193 var (
194         sshOptPrefixRE = regexp.MustCompile(`\A\-+`)
195         sshSeparators  = map[string]string{
196                 "ssh":          "--",
197                 "lfs-ssh-echo": "--", // used in lfs integration tests only
198         }
199 )