2018-09-06 21:42:41 +00:00
|
|
|
package lfshttp
|
2017-01-04 23:23:46 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2019-04-24 20:26:51 +00:00
|
|
|
"os"
|
2017-01-04 23:23:46 +00:00
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
2017-05-19 15:25:22 +00:00
|
|
|
"regexp"
|
2017-01-04 23:23:46 +00:00
|
|
|
"strings"
|
2017-03-24 18:37:00 +00:00
|
|
|
"time"
|
2017-01-04 23:23:46 +00:00
|
|
|
|
2017-10-25 21:33:20 +00:00
|
|
|
"github.com/git-lfs/git-lfs/config"
|
2018-09-14 21:50:09 +00:00
|
|
|
"github.com/git-lfs/git-lfs/subprocess"
|
2017-02-18 22:46:22 +00:00
|
|
|
"github.com/git-lfs/git-lfs/tools"
|
2017-01-04 23:23:46 +00:00
|
|
|
"github.com/rubyist/tracerx"
|
|
|
|
)
|
|
|
|
|
2017-03-23 19:48:52 +00:00
|
|
|
type SSHResolver interface {
|
2017-03-23 19:44:51 +00:00
|
|
|
Resolve(Endpoint, string) (sshAuthResponse, error)
|
|
|
|
}
|
|
|
|
|
2017-03-23 19:58:15 +00:00
|
|
|
func withSSHCache(ssh SSHResolver) SSHResolver {
|
|
|
|
return &sshCache{
|
|
|
|
endpoints: make(map[string]*sshAuthResponse),
|
|
|
|
ssh: ssh,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type sshCache struct {
|
|
|
|
endpoints map[string]*sshAuthResponse
|
|
|
|
ssh SSHResolver
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshCache) Resolve(e Endpoint, method string) (sshAuthResponse, error) {
|
2017-03-24 17:27:44 +00:00
|
|
|
if len(e.SshUserAndHost) == 0 {
|
|
|
|
return sshAuthResponse{}, nil
|
|
|
|
}
|
|
|
|
|
2017-03-23 19:58:15 +00:00
|
|
|
key := strings.Join([]string{e.SshUserAndHost, e.SshPort, e.SshPath, method}, "//")
|
2017-04-05 19:59:52 +00:00
|
|
|
if res, ok := c.endpoints[key]; ok {
|
|
|
|
if _, expired := res.IsExpiredWithin(5 * time.Second); !expired {
|
|
|
|
tracerx.Printf("ssh cache: %s git-lfs-authenticate %s %s",
|
|
|
|
e.SshUserAndHost, e.SshPath, endpointOperation(e, method))
|
|
|
|
return *res, nil
|
|
|
|
} else {
|
|
|
|
tracerx.Printf("ssh cache expired: %s git-lfs-authenticate %s %s",
|
|
|
|
e.SshUserAndHost, e.SshPath, endpointOperation(e, method))
|
|
|
|
}
|
2017-03-23 19:58:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
res, err := c.ssh.Resolve(e, method)
|
|
|
|
if err == nil {
|
|
|
|
c.endpoints[key] = &res
|
|
|
|
}
|
|
|
|
return res, err
|
|
|
|
}
|
|
|
|
|
2017-03-23 19:44:51 +00:00
|
|
|
type sshAuthResponse struct {
|
|
|
|
Message string `json:"-"`
|
|
|
|
Href string `json:"href"`
|
|
|
|
Header map[string]string `json:"header"`
|
lfsapi/ssh.go: use zero-value sentinels
Instead of storing the ExpiresAt and ExpiresIn fields as a *time.Time,
and *int, respectively, use a time.Time and int instead.
Later, in the zero-value comparison, change from != nil to != 0 and
time.Time.IsZero(), respectively.
This has the effect of ridding the API of setting expires_in to zero,
and will fall-back to the default expiration offset, after which it will
retry, or fail the underlying request. We don't have precise
determinations on how many API requests this will affect, but it's
certainly not common usage of the specification, so the behavior change
is OK.
2018-07-02 18:21:39 +00:00
|
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
|
|
ExpiresIn int `json:"expires_in"`
|
2017-04-05 22:43:00 +00:00
|
|
|
|
|
|
|
createdAt time.Time
|
2017-04-05 19:59:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *sshAuthResponse) IsExpiredWithin(d time.Duration) (time.Time, bool) {
|
lfsapi/ssh.go: use zero-value sentinels
Instead of storing the ExpiresAt and ExpiresIn fields as a *time.Time,
and *int, respectively, use a time.Time and int instead.
Later, in the zero-value comparison, change from != nil to != 0 and
time.Time.IsZero(), respectively.
This has the effect of ridding the API of setting expires_in to zero,
and will fall-back to the default expiration offset, after which it will
retry, or fail the underlying request. We don't have precise
determinations on how many API requests this will affect, but it's
certainly not common usage of the specification, so the behavior change
is OK.
2018-07-02 18:21:39 +00:00
|
|
|
return tools.IsExpiredAtOrIn(r.createdAt, d, r.ExpiresAt,
|
|
|
|
time.Duration(r.ExpiresIn)*time.Second)
|
2017-03-23 19:44:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type sshAuthClient struct {
|
2018-02-14 22:26:47 +00:00
|
|
|
os config.Environment
|
2018-02-14 21:58:59 +00:00
|
|
|
git config.Environment
|
2017-03-23 19:44:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *sshAuthClient) Resolve(e Endpoint, method string) (sshAuthResponse, error) {
|
2017-01-04 23:23:46 +00:00
|
|
|
res := sshAuthResponse{}
|
|
|
|
if len(e.SshUserAndHost) == 0 {
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2018-09-07 14:46:21 +00:00
|
|
|
exe, args := sshGetLFSExeAndArgs(c.os, c.git, e, method)
|
2017-01-04 23:23:46 +00:00
|
|
|
cmd := exec.Command(exe, args...)
|
|
|
|
|
|
|
|
// Save stdout and stderr in separate buffers
|
|
|
|
var outbuf, errbuf bytes.Buffer
|
2019-04-24 20:26:51 +00:00
|
|
|
cmd.Stdin = os.Stdin
|
2017-01-04 23:23:46 +00:00
|
|
|
cmd.Stdout = &outbuf
|
|
|
|
cmd.Stderr = &errbuf
|
|
|
|
|
2017-04-05 22:43:00 +00:00
|
|
|
now := time.Now()
|
|
|
|
|
2017-01-04 23:23:46 +00:00
|
|
|
// Execute command
|
|
|
|
err := cmd.Start()
|
|
|
|
if err == nil {
|
|
|
|
err = cmd.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Processing result
|
|
|
|
if err != nil {
|
|
|
|
res.Message = strings.TrimSpace(errbuf.String())
|
|
|
|
} else {
|
|
|
|
err = json.Unmarshal(outbuf.Bytes(), &res)
|
lfsapi/ssh.go: use zero-value sentinels
Instead of storing the ExpiresAt and ExpiresIn fields as a *time.Time,
and *int, respectively, use a time.Time and int instead.
Later, in the zero-value comparison, change from != nil to != 0 and
time.Time.IsZero(), respectively.
This has the effect of ridding the API of setting expires_in to zero,
and will fall-back to the default expiration offset, after which it will
retry, or fail the underlying request. We don't have precise
determinations on how many API requests this will affect, but it's
certainly not common usage of the specification, so the behavior change
is OK.
2018-07-02 18:21:39 +00:00
|
|
|
if res.ExpiresIn == 0 && res.ExpiresAt.IsZero() {
|
2018-02-14 21:58:59 +00:00
|
|
|
ttl := c.git.Int("lfs.defaulttokenttl", 0)
|
2018-02-15 21:25:55 +00:00
|
|
|
if ttl < 0 {
|
|
|
|
ttl = 0
|
2018-02-14 21:58:59 +00:00
|
|
|
}
|
lfsapi/ssh.go: use zero-value sentinels
Instead of storing the ExpiresAt and ExpiresIn fields as a *time.Time,
and *int, respectively, use a time.Time and int instead.
Later, in the zero-value comparison, change from != nil to != 0 and
time.Time.IsZero(), respectively.
This has the effect of ridding the API of setting expires_in to zero,
and will fall-back to the default expiration offset, after which it will
retry, or fail the underlying request. We don't have precise
determinations on how many API requests this will affect, but it's
certainly not common usage of the specification, so the behavior change
is OK.
2018-07-02 18:21:39 +00:00
|
|
|
res.ExpiresIn = ttl
|
2018-02-14 21:58:59 +00:00
|
|
|
}
|
2017-04-05 22:43:00 +00:00
|
|
|
res.createdAt = now
|
2017-01-04 23:23:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return res, err
|
|
|
|
}
|
|
|
|
|
2018-08-27 17:39:07 +00:00
|
|
|
func sshFormatArgs(cmd string, args []string, needShell bool) (string, []string) {
|
|
|
|
if !needShell {
|
|
|
|
return cmd, args
|
|
|
|
}
|
|
|
|
|
2018-09-14 22:31:39 +00:00
|
|
|
return subprocess.FormatForShellQuotedArgs(cmd, args)
|
2018-08-27 17:39:07 +00:00
|
|
|
}
|
|
|
|
|
2018-09-07 14:46:21 +00:00
|
|
|
func sshGetLFSExeAndArgs(osEnv config.Environment, gitEnv config.Environment, e Endpoint, method string) (string, []string) {
|
|
|
|
exe, args, needShell := sshGetExeAndArgs(osEnv, gitEnv, e)
|
2017-05-19 15:25:22 +00:00
|
|
|
operation := endpointOperation(e, method)
|
|
|
|
args = append(args, fmt.Sprintf("git-lfs-authenticate %s %s", e.SshPath, operation))
|
2018-08-27 17:39:07 +00:00
|
|
|
exe, args = sshFormatArgs(exe, args, needShell)
|
2017-05-19 15:25:22 +00:00
|
|
|
tracerx.Printf("run_command: %s %s", exe, strings.Join(args, " "))
|
|
|
|
return exe, args
|
2017-01-25 15:59:31 +00:00
|
|
|
}
|
|
|
|
|
2018-09-07 15:21:22 +00:00
|
|
|
// Parse command, and if it looks like a valid command, return the ssh binary
|
|
|
|
// name, the command to run, and whether we need a shell. If not, return
|
|
|
|
// existing as the ssh binary name.
|
|
|
|
func sshParseShellCommand(command string, existing string) (ssh string, cmd string, needShell bool) {
|
|
|
|
ssh = existing
|
|
|
|
if cmdArgs := tools.QuotedFields(command); len(cmdArgs) > 0 {
|
|
|
|
needShell = true
|
|
|
|
ssh = cmdArgs[0]
|
|
|
|
cmd = command
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-01-04 23:23:46 +00:00
|
|
|
// Return the executable name for ssh on this machine and the base args
|
|
|
|
// Base args includes port settings, user/host, everything pre the command to execute
|
2018-09-07 14:46:21 +00:00
|
|
|
func sshGetExeAndArgs(osEnv config.Environment, gitEnv config.Environment, e Endpoint) (exe string, baseargs []string, needShell bool) {
|
2018-08-27 17:39:07 +00:00
|
|
|
var cmd string
|
|
|
|
|
2017-01-04 23:23:46 +00:00
|
|
|
isPlink := false
|
|
|
|
isTortoise := false
|
|
|
|
|
|
|
|
ssh, _ := osEnv.Get("GIT_SSH")
|
|
|
|
sshCmd, _ := osEnv.Get("GIT_SSH_COMMAND")
|
2018-09-07 15:21:22 +00:00
|
|
|
ssh, cmd, needShell = sshParseShellCommand(sshCmd, ssh)
|
2017-01-04 23:23:46 +00:00
|
|
|
|
|
|
|
if ssh == "" {
|
2018-09-07 17:06:30 +00:00
|
|
|
sshCmd, _ := gitEnv.Get("core.sshcommand")
|
|
|
|
ssh, cmd, needShell = sshParseShellCommand(sshCmd, defaultSSHCmd)
|
2017-05-19 15:25:22 +00:00
|
|
|
}
|
|
|
|
|
2018-08-27 17:39:07 +00:00
|
|
|
if cmd == "" {
|
|
|
|
cmd = ssh
|
|
|
|
}
|
|
|
|
|
2017-05-19 15:25:22 +00:00
|
|
|
basessh := filepath.Base(ssh)
|
|
|
|
|
|
|
|
if basessh != defaultSSHCmd {
|
2017-01-04 23:23:46 +00:00
|
|
|
// Strip extension for easier comparison
|
|
|
|
if ext := filepath.Ext(basessh); len(ext) > 0 {
|
|
|
|
basessh = basessh[:len(basessh)-len(ext)]
|
|
|
|
}
|
|
|
|
isPlink = strings.EqualFold(basessh, "plink")
|
|
|
|
isTortoise = strings.EqualFold(basessh, "tortoiseplink")
|
|
|
|
}
|
|
|
|
|
2018-08-27 17:39:07 +00:00
|
|
|
args := make([]string, 0, 7)
|
2017-01-04 23:23:46 +00:00
|
|
|
|
|
|
|
if isTortoise {
|
|
|
|
// TortoisePlink requires the -batch argument to behave like ssh/plink
|
|
|
|
args = append(args, "-batch")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(e.SshPort) > 0 {
|
|
|
|
if isPlink || isTortoise {
|
|
|
|
args = append(args, "-P")
|
|
|
|
} else {
|
|
|
|
args = append(args, "-p")
|
|
|
|
}
|
|
|
|
args = append(args, e.SshPort)
|
|
|
|
}
|
2017-05-19 15:25:22 +00:00
|
|
|
|
|
|
|
if sep, ok := sshSeparators[basessh]; ok {
|
|
|
|
// inserts a separator between cli -options and host/cmd commands
|
|
|
|
// example: $ ssh -p 12345 -- user@host.com git-lfs-authenticate ...
|
|
|
|
args = append(args, sep, e.SshUserAndHost)
|
|
|
|
} else {
|
|
|
|
// no prefix supported, strip leading - off host to prevent cmd like:
|
|
|
|
// $ git config lfs.url ssh://-proxycmd=whatever
|
|
|
|
// $ plink -P 12345 -proxycmd=foo git-lfs-authenticate ...
|
|
|
|
//
|
|
|
|
// Instead, it'll attempt this, and eventually return an error
|
|
|
|
// $ plink -P 12345 proxycmd=foo git-lfs-authenticate ...
|
|
|
|
args = append(args, sshOptPrefixRE.ReplaceAllString(e.SshUserAndHost, ""))
|
|
|
|
}
|
2017-01-04 23:23:46 +00:00
|
|
|
|
2018-08-27 17:39:07 +00:00
|
|
|
return cmd, args, needShell
|
2017-01-04 23:23:46 +00:00
|
|
|
}
|
2017-05-19 15:25:22 +00:00
|
|
|
|
|
|
|
const defaultSSHCmd = "ssh"
|
|
|
|
|
|
|
|
var (
|
|
|
|
sshOptPrefixRE = regexp.MustCompile(`\A\-+`)
|
|
|
|
sshSeparators = map[string]string{
|
|
|
|
"ssh": "--",
|
|
|
|
"lfs-ssh-echo": "--", // used in lfs integration tests only
|
|
|
|
}
|
|
|
|
)
|