13 "github.com/git-lfs/git-lfs/config"
14 "github.com/git-lfs/git-lfs/tools"
15 "github.com/rubyist/tracerx"
18 type SSHResolver interface {
19 Resolve(Endpoint, string) (sshAuthResponse, error)
22 func withSSHCache(ssh SSHResolver) SSHResolver {
24 endpoints: make(map[string]*sshAuthResponse),
29 type sshCache struct {
30 endpoints map[string]*sshAuthResponse
34 func (c *sshCache) Resolve(e Endpoint, method string) (sshAuthResponse, error) {
35 if len(e.SshUserAndHost) == 0 {
36 return sshAuthResponse{}, nil
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))
46 tracerx.Printf("ssh cache expired: %s git-lfs-authenticate %s %s",
47 e.SshUserAndHost, e.SshPath, endpointOperation(e, method))
51 res, err := c.ssh.Resolve(e, method)
53 c.endpoints[key] = &res
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"`
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)
73 type sshAuthClient struct {
75 git config.Environment
78 func (c *sshAuthClient) Resolve(e Endpoint, method string) (sshAuthResponse, error) {
79 res := sshAuthResponse{}
80 if len(e.SshUserAndHost) == 0 {
84 exe, args := sshGetLFSExeAndArgs(c.os, e, method)
85 cmd := exec.Command(exe, args...)
87 // Save stdout and stderr in separate buffers
88 var outbuf, errbuf bytes.Buffer
102 res.Message = strings.TrimSpace(errbuf.String())
104 err = json.Unmarshal(outbuf.Bytes(), &res)
105 if res.ExpiresIn == 0 && res.ExpiresAt.IsZero() {
106 ttl := c.git.Int("lfs.defaulttokenttl", 0)
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, " "))
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) {
132 ssh, _ := osEnv.Get("GIT_SSH")
133 sshCmd, _ := osEnv.Get("GIT_SSH_COMMAND")
134 cmdArgs := tools.QuotedFields(sshCmd)
135 if len(cmdArgs) > 0 {
137 cmdArgs = cmdArgs[1:]
144 basessh := filepath.Base(ssh)
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)]
151 isPlink = strings.EqualFold(basessh, "plink")
152 isTortoise = strings.EqualFold(basessh, "tortoiseplink")
155 args := make([]string, 0, 5+len(cmdArgs))
156 if len(cmdArgs) > 0 {
157 args = append(args, cmdArgs...)
161 // TortoisePlink requires the -batch argument to behave like ssh/plink
162 args = append(args, "-batch")
165 if len(e.SshPort) > 0 {
166 if isPlink || isTortoise {
167 args = append(args, "-P")
169 args = append(args, "-p")
171 args = append(args, e.SshPort)
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)
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 ...
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, ""))
191 const defaultSSHCmd = "ssh"
194 sshOptPrefixRE = regexp.MustCompile(`\A\-+`)
195 sshSeparators = map[string]string{
197 "lfs-ssh-echo": "--", // used in lfs integration tests only