diff --git a/cmd/serve/sftp/connection.go b/cmd/serve/sftp/connection.go index 21c5c6dfa..93a80c60b 100644 --- a/cmd/serve/sftp/connection.go +++ b/cmd/serve/sftp/connection.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net" + "os" "regexp" "strings" @@ -14,7 +15,9 @@ import ( "github.com/pkg/sftp" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/hash" + "github.com/rclone/rclone/lib/terminal" "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfsflags" "golang.org/x/crypto/ssh" ) @@ -225,19 +228,8 @@ func (c *conn) handleChannel(newChannel ssh.NewChannel) { // Wait for either subsystem "sftp" or "exec" request if <-isSFTP { - fs.Debugf(c.what, "Starting SFTP server") - server := sftp.NewRequestServer(channel, c.handlers) - defer func() { - err := server.Close() - if err != nil && err != io.EOF { - fs.Debugf(c.what, "Failed to close server: %v", err) - } - }() - err = server.Serve() - if err == io.EOF || err == nil { - fs.Debugf(c.what, "exited session") - } else { - fs.Errorf(c.what, "completed with error: %v", err) + if err := serveChannel(channel, c.handlers, c.what); err != nil { + fs.Errorf(c.what, "Failed to serve SFPT: %v", err) } } else { var rc = uint32(0) @@ -263,3 +255,54 @@ func (c *conn) handleChannels(chans <-chan ssh.NewChannel) { go c.handleChannel(newChannel) } } + +func serveChannel(rwc io.ReadWriteCloser, h sftp.Handlers, what string) error { + fs.Debugf(what, "Starting SFTP server") + server := sftp.NewRequestServer(rwc, h) + defer func() { + err := server.Close() + if err != nil && err != io.EOF { + fs.Debugf(what, "Failed to close server: %v", err) + } + }() + err := server.Serve() + if err != nil && err != io.EOF { + return errors.Wrap(err, "completed with error") + } + fs.Debugf(what, "exited session") + return nil +} + +func serveStdio(f fs.Fs) error { + if terminal.IsTerminal(int(os.Stdout.Fd())) { + return errors.New("refusing to run SFTP server directly on a terminal. Please let sshd start rclone, by connecting with sftp or sshfs") + } + sshChannel := &stdioChannel{ + stdin: os.Stdin, + stdout: os.Stdout, + } + handlers := newVFSHandler(vfs.New(f, &vfsflags.Opt)) + return serveChannel(sshChannel, handlers, "stdio") +} + +type stdioChannel struct { + stdin *os.File + stdout *os.File +} + +func (c *stdioChannel) Read(data []byte) (int, error) { + return c.stdin.Read(data) +} + +func (c *stdioChannel) Write(data []byte) (int, error) { + return c.stdout.Write(data) +} + +func (c *stdioChannel) Close() error { + err1 := c.stdin.Close() + err2 := c.stdout.Close() + if err1 != nil { + return err1 + } + return err2 +} diff --git a/cmd/serve/sftp/sftp.go b/cmd/serve/sftp/sftp.go index 13ddef6bf..9182f38d8 100644 --- a/cmd/serve/sftp/sftp.go +++ b/cmd/serve/sftp/sftp.go @@ -27,6 +27,7 @@ type Options struct { User string // single username Pass string // password for user NoAuth bool // allow no authentication on connections + Stdio bool // serve on stdio } // DefaultOpt is the default values used for Options @@ -47,6 +48,7 @@ func AddFlags(flagSet *pflag.FlagSet, Opt *Options) { flags.StringVarP(flagSet, &Opt.User, "user", "", Opt.User, "User name for authentication.") flags.StringVarP(flagSet, &Opt.Pass, "pass", "", Opt.Pass, "Password for authentication.") flags.BoolVarP(flagSet, &Opt.NoAuth, "no-auth", "", Opt.NoAuth, "Allow connections with no authentication if set.") + flags.BoolVarP(flagSet, &Opt.Stdio, "stdio", "", Opt.Stdio, "Run an sftp server on run stdin/stdout") } func init() { @@ -90,6 +92,11 @@ reachable externally then supply "--addr :2022" for example. Note that the default of "--vfs-cache-mode off" is fine for the rclone sftp backend, but it may not be with other SFTP clients. +If --stdio is specified, rclone will serve SFTP over stdio, which can +be used with sshd via ~/.ssh/authorized_keys, for example: + + restrict,command="rclone serve sftp --stdio ./photos" ssh-rsa ... + ` + vfs.Help + proxy.Help, Run: func(command *cobra.Command, args []string) { var f fs.Fs @@ -100,6 +107,9 @@ sftp backend, but it may not be with other SFTP clients. cmd.CheckArgs(0, 0, command, args) } cmd.Run(false, true, command, func() error { + if Opt.Stdio { + return serveStdio(f) + } s := newServer(context.Background(), f, &Opt) err := s.Serve() if err != nil {