From f5dfe3f5a6a78f3a998d2a80c3d5811202dd7acf Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 2 Apr 2025 18:46:31 +0100 Subject: [PATCH] serve ftp: add serve rc interface --- cmd/serve/ftp/ftp.go | 88 +++++++++++++++++++++++++++++++++------ cmd/serve/ftp/ftp_test.go | 28 +++++++++---- 2 files changed, 94 insertions(+), 22 deletions(-) diff --git a/cmd/serve/ftp/ftp.go b/cmd/serve/ftp/ftp.go index 6e2122a2a..98d48db1a 100644 --- a/cmd/serve/ftp/ftp.go +++ b/cmd/serve/ftp/ftp.go @@ -23,9 +23,11 @@ import ( "github.com/rclone/rclone/cmd/serve/proxy/proxyflags" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/accounting" + "github.com/rclone/rclone/fs/config/configstruct" "github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/config/obscure" "github.com/rclone/rclone/fs/log" + "github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/vfs" "github.com/rclone/rclone/vfs/vfscommon" "github.com/rclone/rclone/vfs/vfsflags" @@ -71,8 +73,8 @@ type Options struct { ListenAddr string `config:"addr"` // Port to listen on PublicIP string `config:"public_ip"` // Passive ports range PassivePorts string `config:"passive_port"` // Passive ports range - BasicUser string `config:"user"` // single username for basic auth if not using Htpasswd - BasicPass string `config:"pass"` // password for BasicUser + User string `config:"user"` // single username for basic auth if not using Htpasswd + Pass string `config:"pass"` // password for User TLSCert string `config:"cert"` // TLS PEM key (concatenation of certificate and CA certificate) TLSKey string `config:"key"` // TLS PEM Private key } @@ -90,6 +92,28 @@ func init() { proxyflags.AddFlags(Command.Flags()) AddFlags(Command.Flags()) serve.Command.AddCommand(Command) + serve.AddRc("ftp", 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 @@ -130,11 +154,11 @@ You can set a single username and password with the --user and --pass flags. cmd.CheckArgs(0, 0, command, args) } cmd.Run(false, false, command, func() error { - s, err := newServer(context.Background(), f, &Opt) + s, err := newServer(context.Background(), f, &Opt, &vfscommon.Opt, &proxy.Opt) if err != nil { return err } - return s.serve() + return s.Serve() }) }, } @@ -159,7 +183,7 @@ func init() { var passivePortsRe = regexp.MustCompile(`^\s*\d+\s*-\s*\d+\s*$`) // Make a new FTP to serve the remote -func newServer(ctx context.Context, f fs.Fs, opt *Options) (*driver, error) { +func newServer(ctx context.Context, f fs.Fs, opt *Options, vfsOpt *vfscommon.Options, proxyOpt *proxy.Options) (*driver, error) { host, port, err := net.SplitHostPort(opt.ListenAddr) if err != nil { return nil, fmt.Errorf("failed to parse host:port from %q", opt.ListenAddr) @@ -175,10 +199,10 @@ func newServer(ctx context.Context, f fs.Fs, opt *Options) (*driver, error) { opt: *opt, } if proxy.Opt.AuthProxy != "" { - d.proxy = proxy.New(ctx, &proxy.Opt, &vfscommon.Opt) + d.proxy = proxy.New(ctx, proxyOpt, vfsOpt) d.userPass = make(map[string]string, 16) } else { - d.globalVFS = vfs.New(f, &vfscommon.Opt) + d.globalVFS = vfs.New(f, vfsOpt) } d.useTLS = d.opt.TLSKey != "" @@ -210,20 +234,58 @@ func newServer(ctx context.Context, f fs.Fs, opt *Options) (*driver, error) { return d, nil } -// serve runs the ftp server -func (d *driver) serve() error { +// Serve runs the FTP server until it is shutdown +func (d *driver) Serve() error { fs.Logf(d.f, "Serving FTP on %s", d.srv.Hostname+":"+strconv.Itoa(d.srv.Port)) - return d.srv.ListenAndServe() + err := d.srv.ListenAndServe() + if err == ftp.ErrServerClosed { + err = nil + } + return err } -// close stops the ftp server +// Shutdown stops the ftp server // //lint:ignore U1000 unused when not building linux -func (d *driver) close() error { +func (d *driver) Shutdown() error { fs.Logf(d.f, "Stopping FTP on %s", d.srv.Hostname+":"+strconv.Itoa(d.srv.Port)) return d.srv.Shutdown() } +// Return the first address of the server +func (d *driver) Addr() net.Addr { + // The FTP server doesn't let us read the listener + // so we have to synthesize the net.Addr here. + // On errors we'll return a zero item or zero parts. + addr := &net.TCPAddr{} + + // Split host and port + host, port, err := net.SplitHostPort(d.opt.ListenAddr) + if err != nil { + fs.Errorf(nil, "ftp: addr: invalid address format: %v", err) + return addr + } + + // Parse port + addr.Port, err = strconv.Atoi(port) + if err != nil { + fs.Errorf(nil, "ftp: addr: invalid port number: %v", err) + } + + // Resolve the host to an IP address. + ipAddrs, err := net.LookupIP(host) + if err != nil { + fs.Errorf(nil, "ftp: addr: failed to resolve host: %v", err) + } else if len(ipAddrs) == 0 { + fs.Errorf(nil, "ftp: addr: no IP addresses found for host: %s", host) + } else { + // Choose the first IP address. + addr.IP = ipAddrs[0] + } + + return addr +} + // Logger ftp logger output formatted message type Logger struct{} @@ -271,7 +333,7 @@ func (d *driver) CheckPasswd(sctx *ftp.Context, user, pass string) (ok bool, err d.userPass[user] = oPass d.userPassMu.Unlock() } else { - ok = d.opt.BasicUser == user && (d.opt.BasicPass == "" || d.opt.BasicPass == pass) + ok = d.opt.User == user && (d.opt.Pass == "" || d.opt.Pass == pass) if !ok { fs.Infof(nil, "login failed: bad credentials") return false, nil diff --git a/cmd/serve/ftp/ftp_test.go b/cmd/serve/ftp/ftp_test.go index b886069d9..83255fc77 100644 --- a/cmd/serve/ftp/ftp_test.go +++ b/cmd/serve/ftp/ftp_test.go @@ -12,12 +12,15 @@ import ( "testing" _ "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/lib/israce" + "github.com/rclone/rclone/vfs/vfscommon" "github.com/stretchr/testify/assert" - ftp "goftp.io/server/v2" ) const ( @@ -36,19 +39,16 @@ func TestFTP(t *testing.T) { opt := Opt opt.ListenAddr = testHOST + ":" + testPORT opt.PassivePorts = testPASSIVEPORTRANGE - opt.BasicUser = testUSER - opt.BasicPass = testPASS + opt.User = testUSER + opt.Pass = testPASS - w, err := newServer(context.Background(), f, &opt) + w, err := newServer(context.Background(), f, &opt, &vfscommon.Opt, &proxy.Opt) assert.NoError(t, err) quit := make(chan struct{}) go func() { - err := w.serve() + assert.NoError(t, w.Serve()) close(quit) - if err != ftp.ErrServerClosed { - assert.NoError(t, err) - } }() // Config for the backend we'll use to connect to the server @@ -61,7 +61,7 @@ func TestFTP(t *testing.T) { } return config, func() { - err := w.close() + err := w.Shutdown() assert.NoError(t, err) <-quit } @@ -69,3 +69,13 @@ func TestFTP(t *testing.T) { servetest.Run(t, "ftp", start) } + +func TestRc(t *testing.T) { + if israce.Enabled { + t.Skip("Skipping under race detector as underlying library is racy") + } + servetest.TestRc(t, rc.Params{ + "type": "ftp", + "vfs_cache_mode": "off", + }) +}