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{}