mirror of
https://github.com/rclone/rclone.git
synced 2025-08-17 17:11:37 +02:00
serve ftp: add serve rc interface
This commit is contained in:
@ -23,9 +23,11 @@ import (
|
|||||||
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
|
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/accounting"
|
"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/flags"
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"github.com/rclone/rclone/fs/config/obscure"
|
||||||
"github.com/rclone/rclone/fs/log"
|
"github.com/rclone/rclone/fs/log"
|
||||||
|
"github.com/rclone/rclone/fs/rc"
|
||||||
"github.com/rclone/rclone/vfs"
|
"github.com/rclone/rclone/vfs"
|
||||||
"github.com/rclone/rclone/vfs/vfscommon"
|
"github.com/rclone/rclone/vfs/vfscommon"
|
||||||
"github.com/rclone/rclone/vfs/vfsflags"
|
"github.com/rclone/rclone/vfs/vfsflags"
|
||||||
@ -71,8 +73,8 @@ type Options struct {
|
|||||||
ListenAddr string `config:"addr"` // Port to listen on
|
ListenAddr string `config:"addr"` // Port to listen on
|
||||||
PublicIP string `config:"public_ip"` // Passive ports range
|
PublicIP string `config:"public_ip"` // Passive ports range
|
||||||
PassivePorts string `config:"passive_port"` // Passive ports range
|
PassivePorts string `config:"passive_port"` // Passive ports range
|
||||||
BasicUser string `config:"user"` // single username for basic auth if not using Htpasswd
|
User string `config:"user"` // single username for basic auth if not using Htpasswd
|
||||||
BasicPass string `config:"pass"` // password for BasicUser
|
Pass string `config:"pass"` // password for User
|
||||||
TLSCert string `config:"cert"` // TLS PEM key (concatenation of certificate and CA certificate)
|
TLSCert string `config:"cert"` // TLS PEM key (concatenation of certificate and CA certificate)
|
||||||
TLSKey string `config:"key"` // TLS PEM Private key
|
TLSKey string `config:"key"` // TLS PEM Private key
|
||||||
}
|
}
|
||||||
@ -90,6 +92,28 @@ func init() {
|
|||||||
proxyflags.AddFlags(Command.Flags())
|
proxyflags.AddFlags(Command.Flags())
|
||||||
AddFlags(Command.Flags())
|
AddFlags(Command.Flags())
|
||||||
serve.Command.AddCommand(Command)
|
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
|
// 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.CheckArgs(0, 0, command, args)
|
||||||
}
|
}
|
||||||
cmd.Run(false, false, command, func() error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.serve()
|
return s.Serve()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -159,7 +183,7 @@ func init() {
|
|||||||
var passivePortsRe = regexp.MustCompile(`^\s*\d+\s*-\s*\d+\s*$`)
|
var passivePortsRe = regexp.MustCompile(`^\s*\d+\s*-\s*\d+\s*$`)
|
||||||
|
|
||||||
// Make a new FTP to serve the remote
|
// 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)
|
host, port, err := net.SplitHostPort(opt.ListenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse host:port from %q", opt.ListenAddr)
|
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,
|
opt: *opt,
|
||||||
}
|
}
|
||||||
if proxy.Opt.AuthProxy != "" {
|
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)
|
d.userPass = make(map[string]string, 16)
|
||||||
} else {
|
} else {
|
||||||
d.globalVFS = vfs.New(f, &vfscommon.Opt)
|
d.globalVFS = vfs.New(f, vfsOpt)
|
||||||
}
|
}
|
||||||
d.useTLS = d.opt.TLSKey != ""
|
d.useTLS = d.opt.TLSKey != ""
|
||||||
|
|
||||||
@ -210,20 +234,58 @@ func newServer(ctx context.Context, f fs.Fs, opt *Options) (*driver, error) {
|
|||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve runs the ftp server
|
// Serve runs the FTP server until it is shutdown
|
||||||
func (d *driver) serve() error {
|
func (d *driver) Serve() error {
|
||||||
fs.Logf(d.f, "Serving FTP on %s", d.srv.Hostname+":"+strconv.Itoa(d.srv.Port))
|
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
|
//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))
|
fs.Logf(d.f, "Stopping FTP on %s", d.srv.Hostname+":"+strconv.Itoa(d.srv.Port))
|
||||||
return d.srv.Shutdown()
|
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
|
// Logger ftp logger output formatted message
|
||||||
type Logger struct{}
|
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.userPass[user] = oPass
|
||||||
d.userPassMu.Unlock()
|
d.userPassMu.Unlock()
|
||||||
} else {
|
} 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 {
|
if !ok {
|
||||||
fs.Infof(nil, "login failed: bad credentials")
|
fs.Infof(nil, "login failed: bad credentials")
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -12,12 +12,15 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
_ "github.com/rclone/rclone/backend/local"
|
_ "github.com/rclone/rclone/backend/local"
|
||||||
|
"github.com/rclone/rclone/cmd/serve/proxy"
|
||||||
"github.com/rclone/rclone/cmd/serve/servetest"
|
"github.com/rclone/rclone/cmd/serve/servetest"
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
"github.com/rclone/rclone/fs/config/configmap"
|
"github.com/rclone/rclone/fs/config/configmap"
|
||||||
"github.com/rclone/rclone/fs/config/obscure"
|
"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"
|
"github.com/stretchr/testify/assert"
|
||||||
ftp "goftp.io/server/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -36,19 +39,16 @@ func TestFTP(t *testing.T) {
|
|||||||
opt := Opt
|
opt := Opt
|
||||||
opt.ListenAddr = testHOST + ":" + testPORT
|
opt.ListenAddr = testHOST + ":" + testPORT
|
||||||
opt.PassivePorts = testPASSIVEPORTRANGE
|
opt.PassivePorts = testPASSIVEPORTRANGE
|
||||||
opt.BasicUser = testUSER
|
opt.User = testUSER
|
||||||
opt.BasicPass = testPASS
|
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)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
quit := make(chan struct{})
|
quit := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
err := w.serve()
|
assert.NoError(t, w.Serve())
|
||||||
close(quit)
|
close(quit)
|
||||||
if err != ftp.ErrServerClosed {
|
|
||||||
assert.NoError(t, err)
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Config for the backend we'll use to connect to the server
|
// Config for the backend we'll use to connect to the server
|
||||||
@ -61,7 +61,7 @@ func TestFTP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return config, func() {
|
return config, func() {
|
||||||
err := w.close()
|
err := w.Shutdown()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
<-quit
|
<-quit
|
||||||
}
|
}
|
||||||
@ -69,3 +69,13 @@ func TestFTP(t *testing.T) {
|
|||||||
|
|
||||||
servetest.Run(t, "ftp", start)
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user