github.com/stffabi/git-lfs@v2.3.5-0.20180214015214-8eeaa8d88902+incompatible/lfsapi/ssh.go (about) 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, time.Duration(r.ExpiresIn)*time.Second) 70 } 71 72 type sshAuthClient struct { 73 os config.Environment 74 } 75 76 func (c *sshAuthClient) Resolve(e Endpoint, method string) (sshAuthResponse, error) { 77 res := sshAuthResponse{} 78 if len(e.SshUserAndHost) == 0 { 79 return res, nil 80 } 81 82 exe, args := sshGetLFSExeAndArgs(c.os, e, method) 83 cmd := exec.Command(exe, args...) 84 85 // Save stdout and stderr in separate buffers 86 var outbuf, errbuf bytes.Buffer 87 cmd.Stdout = &outbuf 88 cmd.Stderr = &errbuf 89 90 now := time.Now() 91 92 // Execute command 93 err := cmd.Start() 94 if err == nil { 95 err = cmd.Wait() 96 } 97 98 // Processing result 99 if err != nil { 100 res.Message = strings.TrimSpace(errbuf.String()) 101 } else { 102 err = json.Unmarshal(outbuf.Bytes(), &res) 103 res.createdAt = now 104 } 105 106 return res, err 107 } 108 109 func sshGetLFSExeAndArgs(osEnv config.Environment, e Endpoint, method string) (string, []string) { 110 exe, args := sshGetExeAndArgs(osEnv, e) 111 operation := endpointOperation(e, method) 112 args = append(args, fmt.Sprintf("git-lfs-authenticate %s %s", e.SshPath, operation)) 113 tracerx.Printf("run_command: %s %s", exe, strings.Join(args, " ")) 114 return exe, args 115 } 116 117 // Return the executable name for ssh on this machine and the base args 118 // Base args includes port settings, user/host, everything pre the command to execute 119 func sshGetExeAndArgs(osEnv config.Environment, e Endpoint) (exe string, baseargs []string) { 120 isPlink := false 121 isTortoise := false 122 123 ssh, _ := osEnv.Get("GIT_SSH") 124 sshCmd, _ := osEnv.Get("GIT_SSH_COMMAND") 125 cmdArgs := tools.QuotedFields(sshCmd) 126 if len(cmdArgs) > 0 { 127 ssh = cmdArgs[0] 128 cmdArgs = cmdArgs[1:] 129 } 130 131 if ssh == "" { 132 ssh = defaultSSHCmd 133 } 134 135 basessh := filepath.Base(ssh) 136 137 if basessh != defaultSSHCmd { 138 // Strip extension for easier comparison 139 if ext := filepath.Ext(basessh); len(ext) > 0 { 140 basessh = basessh[:len(basessh)-len(ext)] 141 } 142 isPlink = strings.EqualFold(basessh, "plink") 143 isTortoise = strings.EqualFold(basessh, "tortoiseplink") 144 } 145 146 args := make([]string, 0, 5+len(cmdArgs)) 147 if len(cmdArgs) > 0 { 148 args = append(args, cmdArgs...) 149 } 150 151 if isTortoise { 152 // TortoisePlink requires the -batch argument to behave like ssh/plink 153 args = append(args, "-batch") 154 } 155 156 if len(e.SshPort) > 0 { 157 if isPlink || isTortoise { 158 args = append(args, "-P") 159 } else { 160 args = append(args, "-p") 161 } 162 args = append(args, e.SshPort) 163 } 164 165 if sep, ok := sshSeparators[basessh]; ok { 166 // inserts a separator between cli -options and host/cmd commands 167 // example: $ ssh -p 12345 -- user@host.com git-lfs-authenticate ... 168 args = append(args, sep, e.SshUserAndHost) 169 } else { 170 // no prefix supported, strip leading - off host to prevent cmd like: 171 // $ git config lfs.url ssh://-proxycmd=whatever 172 // $ plink -P 12345 -proxycmd=foo git-lfs-authenticate ... 173 // 174 // Instead, it'll attempt this, and eventually return an error 175 // $ plink -P 12345 proxycmd=foo git-lfs-authenticate ... 176 args = append(args, sshOptPrefixRE.ReplaceAllString(e.SshUserAndHost, "")) 177 } 178 179 return ssh, args 180 } 181 182 const defaultSSHCmd = "ssh" 183 184 var ( 185 sshOptPrefixRE = regexp.MustCompile(`\A\-+`) 186 sshSeparators = map[string]string{ 187 "ssh": "--", 188 "lfs-ssh-echo": "--", // used in lfs integration tests only 189 } 190 )