github.com/2lambda123/git-lfs@v2.5.2+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, 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 )