github.com/catandhorse/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  )