From 5702b7578cb99dc57d1f8798ab06e36aff36d694 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 2 Apr 2025 18:05:23 +0100 Subject: [PATCH] serve sftp: add serve rc interface --- cmd/serve/sftp/server.go | 66 ++++++++++++++++++------------------- cmd/serve/sftp/sftp.go | 34 ++++++++++++++++--- cmd/serve/sftp/sftp_test.go | 25 +++++++++++--- 3 files changed, 82 insertions(+), 43 deletions(-) diff --git a/cmd/serve/sftp/server.go b/cmd/serve/sftp/server.go index 5ee8d0c68..17889503d 100644 --- a/cmd/serve/sftp/server.go +++ b/cmd/serve/sftp/server.go @@ -16,6 +16,7 @@ import ( "encoding/pem" "errors" "fmt" + "io" "net" "os" "path/filepath" @@ -40,23 +41,27 @@ type server struct { ctx context.Context // for global config config *ssh.ServerConfig listener net.Listener - waitChan chan struct{} // for waiting on the listener to close + stopped chan struct{} // for waiting on the listener to stop proxy *proxy.Proxy } -func newServer(ctx context.Context, f fs.Fs, opt *Options) *server { +func newServer(ctx context.Context, f fs.Fs, opt *Options, vfsOpt *vfscommon.Options, proxyOpt *proxy.Options) (*server, error) { s := &server{ - f: f, - ctx: ctx, - opt: *opt, - waitChan: make(chan struct{}), + f: f, + ctx: ctx, + opt: *opt, + stopped: make(chan struct{}), } if proxy.Opt.AuthProxy != "" { - s.proxy = proxy.New(ctx, &proxy.Opt, &vfscommon.Opt) + s.proxy = proxy.New(ctx, proxyOpt, vfsOpt) } else { - s.vfs = vfs.New(f, &vfscommon.Opt) + s.vfs = vfs.New(f, vfsOpt) } - return s + err := s.configure() + if err != nil { + return nil, fmt.Errorf("sftp configuration failed: %w", err) + } + return s, nil } // getVFS gets the vfs from s or the proxy @@ -128,8 +133,10 @@ func (s *server) acceptConnections() { } } +// configure the server +// // Based on example server code from golang.org/x/crypto/ssh and server_standalone -func (s *server) serve() (err error) { +func (s *server) configure() (err error) { var authorizedKeysMap map[string]struct{} // ensure the user isn't trying to use conflicting flags @@ -292,42 +299,35 @@ func (s *server) serve() (err error) { } } s.listener = listener + return nil +} + +// Serve SFTP until the server is Shutdown +func (s *server) Serve() (err error) { fs.Logf(nil, "SFTP server listening on %v\n", s.listener.Addr()) - - go s.acceptConnections() - + s.acceptConnections() + close(s.stopped) return nil } // Addr returns the address the server is listening on -func (s *server) Addr() string { - return s.listener.Addr().String() -} - -// Serve runs the sftp server in the background. -// -// Use s.Close() and s.Wait() to shutdown server -func (s *server) Serve() error { - err := s.serve() - if err != nil { - return err - } - return nil +func (s *server) Addr() net.Addr { + return s.listener.Addr() } // Wait blocks while the listener is open. func (s *server) Wait() { - <-s.waitChan + <-s.stopped } -// Close shuts the running server down -func (s *server) Close() { +// Shutdown shuts the running server down +func (s *server) Shutdown() error { err := s.listener.Close() - if err != nil { - fs.Errorf(nil, "Error on closing SFTP server: %v", err) - return + if errors.Is(err, io.ErrUnexpectedEOF) { + err = nil } - close(s.waitChan) + s.Wait() + return err } func loadPrivateKey(keyPath string) (ssh.Signer, error) { diff --git a/cmd/serve/sftp/sftp.go b/cmd/serve/sftp/sftp.go index c39f31dd8..4f641874c 100644 --- a/cmd/serve/sftp/sftp.go +++ b/cmd/serve/sftp/sftp.go @@ -5,15 +5,19 @@ package sftp import ( "context" + "fmt" "github.com/rclone/rclone/cmd" "github.com/rclone/rclone/cmd/serve" "github.com/rclone/rclone/cmd/serve/proxy" "github.com/rclone/rclone/cmd/serve/proxy/proxyflags" "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/config/configstruct" "github.com/rclone/rclone/fs/config/flags" + "github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/lib/systemd" "github.com/rclone/rclone/vfs" + "github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfsflags" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -78,6 +82,28 @@ func init() { proxyflags.AddFlags(Command.Flags()) AddFlags(Command.Flags(), &Opt) serve.Command.AddCommand(Command) + serve.AddRc("sftp", func(ctx context.Context, f fs.Fs, in rc.Params) (serve.Handle, error) { + // Read VFS Opts + var vfsOpt = vfscommon.Opt // set default opts + err := configstruct.SetAny(in, &vfsOpt) + if err != nil { + return nil, err + } + // Read Proxy Opts + var proxyOpt = proxy.Opt // set default opts + err = configstruct.SetAny(in, &proxyOpt) + if err != nil { + return nil, err + } + // Read opts + var opt = Opt // set default opts + err = configstruct.SetAny(in, &opt) + if err != nil { + return nil, err + } + // Create server + return newServer(ctx, f, &opt, &vfsOpt, &proxyOpt) + }) } // Command definition for cobra @@ -164,14 +190,12 @@ provided by OpenSSH in this case. if Opt.Stdio { return serveStdio(f) } - s := newServer(context.Background(), f, &Opt) - err := s.Serve() + s, err := newServer(context.Background(), f, &Opt, &vfscommon.Opt, &proxy.Opt) if err != nil { - return err + fs.Fatal(nil, fmt.Sprint(err)) } defer systemd.Notify()() - s.Wait() - return nil + return s.Serve() }) }, } diff --git a/cmd/serve/sftp/sftp_test.go b/cmd/serve/sftp/sftp_test.go index 538e2caab..e18d6b86d 100644 --- a/cmd/serve/sftp/sftp_test.go +++ b/cmd/serve/sftp/sftp_test.go @@ -14,10 +14,14 @@ import ( "github.com/pkg/sftp" _ "github.com/rclone/rclone/backend/local" + "github.com/rclone/rclone/cmd/serve/proxy" "github.com/rclone/rclone/cmd/serve/servetest" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/config/obscure" + "github.com/rclone/rclone/fs/rc" + "github.com/rclone/rclone/vfs/vfscommon" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -45,11 +49,14 @@ func TestSftp(t *testing.T) { opt.User = testUser opt.Pass = testPass - w := newServer(context.Background(), f, &opt) - require.NoError(t, w.serve()) + w, err := newServer(context.Background(), f, &opt, &vfscommon.Opt, &proxy.Opt) + require.NoError(t, err) + go func() { + require.NoError(t, w.Serve()) + }() // Read the host and port we started on - addr := w.Addr() + addr := w.Addr().String() colon := strings.LastIndex(addr, ":") // Config for the backend we'll use to connect to the server @@ -63,10 +70,18 @@ func TestSftp(t *testing.T) { // return a stop function return config, func() { - w.Close() - w.Wait() + assert.NoError(t, w.Shutdown()) } } servetest.Run(t, "sftp", start) } + +func TestRc(t *testing.T) { + servetest.TestRc(t, rc.Params{ + "type": "sftp", + "user": "test", + "pass": obscure.MustObscure("test"), + "vfs_cache_mode": "off", + }) +}