github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/sftp/ssh_external.go (about)

     1  //go:build !plan9
     2  
     3  package sftp
     4  
     5  import (
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"os/exec"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/rclone/rclone/fs"
    15  )
    16  
    17  // Implement the sshClient interface for external ssh programs
    18  type sshClientExternal struct {
    19  	f       *Fs
    20  	session *sshSessionExternal
    21  }
    22  
    23  func (f *Fs) newSSHClientExternal() (sshClient, error) {
    24  	return &sshClientExternal{f: f}, nil
    25  }
    26  
    27  // Wait for connection to close
    28  func (s *sshClientExternal) Wait() error {
    29  	if s.session == nil {
    30  		return nil
    31  	}
    32  	return s.session.Wait()
    33  }
    34  
    35  // Send a keepalive over the ssh connection
    36  func (s *sshClientExternal) SendKeepAlive() {
    37  	// Up to the user to configure -o ServerAliveInterval=20 on their ssh connections
    38  }
    39  
    40  // Close the connection
    41  func (s *sshClientExternal) Close() error {
    42  	if s.session == nil {
    43  		return nil
    44  	}
    45  	return s.session.Close()
    46  }
    47  
    48  // NewSession makes a new external SSH connection
    49  func (s *sshClientExternal) NewSession() (sshSession, error) {
    50  	session := s.f.newSSHSessionExternal()
    51  	if s.session == nil {
    52  		fs.Debugf(s.f, "ssh external: creating additional session")
    53  	}
    54  	return session, nil
    55  }
    56  
    57  // CanReuse indicates if this client can be reused
    58  func (s *sshClientExternal) CanReuse() bool {
    59  	if s.session == nil {
    60  		return true
    61  	}
    62  	exited := s.session.exited()
    63  	canReuse := !exited && s.session.runningSFTP
    64  	// fs.Debugf(s.f, "ssh external: CanReuse %v, exited=%v runningSFTP=%v", canReuse, exited, s.session.runningSFTP)
    65  	return canReuse
    66  }
    67  
    68  // Check interfaces
    69  var _ sshClient = &sshClientExternal{}
    70  
    71  // implement the sshSession interface for external ssh binary
    72  type sshSessionExternal struct {
    73  	f           *Fs
    74  	cmd         *exec.Cmd
    75  	cancel      func()
    76  	startCalled bool
    77  	runningSFTP bool
    78  }
    79  
    80  func (f *Fs) newSSHSessionExternal() *sshSessionExternal {
    81  	s := &sshSessionExternal{
    82  		f: f,
    83  	}
    84  
    85  	// Make a cancellation function for this to call in Close()
    86  	ctx, cancel := context.WithCancel(context.Background())
    87  	s.cancel = cancel
    88  
    89  	// Connect to a remote host and request the sftp subsystem via
    90  	// the 'ssh' command. This assumes that passwordless login is
    91  	// correctly configured.
    92  	ssh := append([]string(nil), s.f.opt.SSH...)
    93  	s.cmd = exec.CommandContext(ctx, ssh[0], ssh[1:]...)
    94  
    95  	// Allow the command a short time only to shut down
    96  	s.cmd.WaitDelay = time.Second
    97  
    98  	return s
    99  }
   100  
   101  // Setenv sets an environment variable that will be applied to any
   102  // command executed by Shell or Run.
   103  func (s *sshSessionExternal) Setenv(name, value string) error {
   104  	return errors.New("ssh external: can't set environment variables")
   105  }
   106  
   107  const requestSubsystem = "***Subsystem***:"
   108  
   109  // Start runs cmd on the remote host. Typically, the remote
   110  // server passes cmd to the shell for interpretation.
   111  // A Session only accepts one call to Run, Start or Shell.
   112  func (s *sshSessionExternal) Start(cmd string) error {
   113  	if s.startCalled {
   114  		return errors.New("internal error: ssh external: command already running")
   115  	}
   116  	s.startCalled = true
   117  
   118  	// Adjust the args
   119  	if strings.HasPrefix(cmd, requestSubsystem) {
   120  		s.cmd.Args = append(s.cmd.Args, "-s", cmd[len(requestSubsystem):])
   121  		s.runningSFTP = true
   122  	} else {
   123  		s.cmd.Args = append(s.cmd.Args, cmd)
   124  		s.runningSFTP = false
   125  	}
   126  
   127  	fs.Debugf(s.f, "ssh external: running: %v", fs.SpaceSepList(s.cmd.Args))
   128  
   129  	// start the process
   130  	err := s.cmd.Start()
   131  	if err != nil {
   132  		return fmt.Errorf("ssh external: start process: %w", err)
   133  	}
   134  
   135  	return nil
   136  }
   137  
   138  // RequestSubsystem requests the association of a subsystem
   139  // with the session on the remote host. A subsystem is a
   140  // predefined command that runs in the background when the ssh
   141  // session is initiated
   142  func (s *sshSessionExternal) RequestSubsystem(subsystem string) error {
   143  	return s.Start(requestSubsystem + subsystem)
   144  }
   145  
   146  // StdinPipe returns a pipe that will be connected to the
   147  // remote command's standard input when the command starts.
   148  func (s *sshSessionExternal) StdinPipe() (io.WriteCloser, error) {
   149  	rd, err := s.cmd.StdinPipe()
   150  	if err != nil {
   151  		return nil, fmt.Errorf("ssh external: stdin pipe: %w", err)
   152  	}
   153  	return rd, nil
   154  }
   155  
   156  // StdoutPipe returns a pipe that will be connected to the
   157  // remote command's standard output when the command starts.
   158  // There is a fixed amount of buffering that is shared between
   159  // stdout and stderr streams. If the StdoutPipe reader is
   160  // not serviced fast enough it may eventually cause the
   161  // remote command to block.
   162  func (s *sshSessionExternal) StdoutPipe() (io.Reader, error) {
   163  	wr, err := s.cmd.StdoutPipe()
   164  	if err != nil {
   165  		return nil, fmt.Errorf("ssh external: stdout pipe: %w", err)
   166  	}
   167  	return wr, nil
   168  }
   169  
   170  // Return whether the command has finished or not
   171  func (s *sshSessionExternal) exited() bool {
   172  	return s.cmd.ProcessState != nil
   173  }
   174  
   175  // Wait for the command to exit
   176  func (s *sshSessionExternal) Wait() error {
   177  	if s.exited() {
   178  		return nil
   179  	}
   180  	err := s.cmd.Wait()
   181  	if err == nil {
   182  		fs.Debugf(s.f, "ssh external: command exited OK")
   183  	} else {
   184  		fs.Debugf(s.f, "ssh external: command exited with error: %v", err)
   185  	}
   186  	return err
   187  }
   188  
   189  // Run runs cmd on the remote host. Typically, the remote
   190  // server passes cmd to the shell for interpretation.
   191  // A Session only accepts one call to Run, Start, Shell, Output,
   192  // or CombinedOutput.
   193  func (s *sshSessionExternal) Run(cmd string) error {
   194  	err := s.Start(cmd)
   195  	if err != nil {
   196  		return err
   197  	}
   198  	return s.Wait()
   199  }
   200  
   201  // Close the external ssh
   202  func (s *sshSessionExternal) Close() error {
   203  	fs.Debugf(s.f, "ssh external: close")
   204  	// Cancel the context which kills the process
   205  	s.cancel()
   206  	// Wait for it to finish
   207  	_ = s.Wait()
   208  	return nil
   209  }
   210  
   211  // Set the stdout
   212  func (s *sshSessionExternal) SetStdout(wr io.Writer) {
   213  	s.cmd.Stdout = wr
   214  }
   215  
   216  // Set the stderr
   217  func (s *sshSessionExternal) SetStderr(wr io.Writer) {
   218  	s.cmd.Stderr = wr
   219  }
   220  
   221  // Check interfaces
   222  var _ sshSession = &sshSessionExternal{}