github.com/artpar/rclone@v1.67.3/cmd/serve/ftp/ftp.go (about)

     1  //go:build !plan9
     2  
     3  // Package ftp implements an FTP server for rclone
     4  package ftp
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	iofs "io/fs"
    12  	"net"
    13  	"os"
    14  	"os/user"
    15  	"regexp"
    16  	"strconv"
    17  	"sync"
    18  	"time"
    19  
    20  	"github.com/artpar/rclone/cmd"
    21  	"github.com/artpar/rclone/cmd/serve/proxy"
    22  	"github.com/artpar/rclone/cmd/serve/proxy/proxyflags"
    23  	"github.com/artpar/rclone/fs"
    24  	"github.com/artpar/rclone/fs/accounting"
    25  	"github.com/artpar/rclone/fs/config/flags"
    26  	"github.com/artpar/rclone/fs/config/obscure"
    27  	"github.com/artpar/rclone/fs/log"
    28  	"github.com/artpar/rclone/fs/rc"
    29  	"github.com/artpar/rclone/vfs"
    30  	"github.com/artpar/rclone/vfs/vfsflags"
    31  	"github.com/spf13/cobra"
    32  	"github.com/spf13/pflag"
    33  	ftp "goftp.io/server/v2"
    34  )
    35  
    36  // Options contains options for the http Server
    37  type Options struct {
    38  	//TODO add more options
    39  	ListenAddr   string // Port to listen on
    40  	PublicIP     string // Passive ports range
    41  	PassivePorts string // Passive ports range
    42  	BasicUser    string // single username for basic auth if not using Htpasswd
    43  	BasicPass    string // password for BasicUser
    44  	TLSCert      string // TLS PEM key (concatenation of certificate and CA certificate)
    45  	TLSKey       string // TLS PEM Private key
    46  }
    47  
    48  // DefaultOpt is the default values used for Options
    49  var DefaultOpt = Options{
    50  	ListenAddr:   "localhost:2121",
    51  	PublicIP:     "",
    52  	PassivePorts: "30000-32000",
    53  	BasicUser:    "anonymous",
    54  	BasicPass:    "",
    55  }
    56  
    57  // Opt is options set by command line flags
    58  var Opt = DefaultOpt
    59  
    60  // AddFlags adds flags for ftp
    61  func AddFlags(flagSet *pflag.FlagSet) {
    62  	rc.AddOption("ftp", &Opt)
    63  	flags.StringVarP(flagSet, &Opt.ListenAddr, "addr", "", Opt.ListenAddr, "IPaddress:Port or :Port to bind server to", "")
    64  	flags.StringVarP(flagSet, &Opt.PublicIP, "public-ip", "", Opt.PublicIP, "Public IP address to advertise for passive connections", "")
    65  	flags.StringVarP(flagSet, &Opt.PassivePorts, "passive-port", "", Opt.PassivePorts, "Passive port range to use", "")
    66  	flags.StringVarP(flagSet, &Opt.BasicUser, "user", "", Opt.BasicUser, "User name for authentication", "")
    67  	flags.StringVarP(flagSet, &Opt.BasicPass, "pass", "", Opt.BasicPass, "Password for authentication (empty value allow every password)", "")
    68  	flags.StringVarP(flagSet, &Opt.TLSCert, "cert", "", Opt.TLSCert, "TLS PEM key (concatenation of certificate and CA certificate)", "")
    69  	flags.StringVarP(flagSet, &Opt.TLSKey, "key", "", Opt.TLSKey, "TLS PEM Private key", "")
    70  }
    71  
    72  func init() {
    73  	vfsflags.AddFlags(Command.Flags())
    74  	proxyflags.AddFlags(Command.Flags())
    75  	AddFlags(Command.Flags())
    76  }
    77  
    78  // Command definition for cobra
    79  var Command = &cobra.Command{
    80  	Use:   "ftp remote:path",
    81  	Short: `Serve remote:path over FTP.`,
    82  	Long: `Run a basic FTP server to serve a remote over FTP protocol.
    83  This can be viewed with a FTP client or you can make a remote of
    84  type FTP to read and write it.
    85  
    86  ### Server options
    87  
    88  Use --addr to specify which IP address and port the server should
    89  listen on, e.g. --addr 1.2.3.4:8000 or --addr :8080 to listen to all
    90  IPs.  By default it only listens on localhost.  You can use port
    91  :0 to let the OS choose an available port.
    92  
    93  If you set --addr to listen on a public or LAN accessible IP address
    94  then using Authentication is advised - see the next section for info.
    95  
    96  #### Authentication
    97  
    98  By default this will serve files without needing a login.
    99  
   100  You can set a single username and password with the --user and --pass flags.
   101  
   102  ` + vfs.Help() + proxy.Help,
   103  	Annotations: map[string]string{
   104  		"versionIntroduced": "v1.44",
   105  		"groups":            "Filter",
   106  	},
   107  	Run: func(command *cobra.Command, args []string) {
   108  		var f fs.Fs
   109  		if proxyflags.Opt.AuthProxy == "" {
   110  			cmd.CheckArgs(1, 1, command, args)
   111  			f = cmd.NewFsSrc(args)
   112  		} else {
   113  			cmd.CheckArgs(0, 0, command, args)
   114  		}
   115  		cmd.Run(false, false, command, func() error {
   116  			s, err := newServer(context.Background(), f, &Opt)
   117  			if err != nil {
   118  				return err
   119  			}
   120  			return s.serve()
   121  		})
   122  	},
   123  }
   124  
   125  // driver contains everything to run the driver for the FTP server
   126  type driver struct {
   127  	f          fs.Fs
   128  	srv        *ftp.Server
   129  	ctx        context.Context // for global config
   130  	opt        Options
   131  	globalVFS  *vfs.VFS     // the VFS if not using auth proxy
   132  	proxy      *proxy.Proxy // may be nil if not in use
   133  	useTLS     bool
   134  	userPassMu sync.Mutex        // to protect userPass
   135  	userPass   map[string]string // cache of username => password when using vfs proxy
   136  }
   137  
   138  var passivePortsRe = regexp.MustCompile(`^\s*\d+\s*-\s*\d+\s*$`)
   139  
   140  // Make a new FTP to serve the remote
   141  func newServer(ctx context.Context, f fs.Fs, opt *Options) (*driver, error) {
   142  	host, port, err := net.SplitHostPort(opt.ListenAddr)
   143  	if err != nil {
   144  		return nil, errors.New("failed to parse host:port")
   145  	}
   146  	portNum, err := strconv.Atoi(port)
   147  	if err != nil {
   148  		return nil, errors.New("failed to parse host:port")
   149  	}
   150  
   151  	d := &driver{
   152  		f:   f,
   153  		ctx: ctx,
   154  		opt: *opt,
   155  	}
   156  	if proxyflags.Opt.AuthProxy != "" {
   157  		d.proxy = proxy.New(ctx, &proxyflags.Opt)
   158  		d.userPass = make(map[string]string, 16)
   159  	} else {
   160  		d.globalVFS = vfs.New(f, &vfsflags.Opt)
   161  	}
   162  	d.useTLS = d.opt.TLSKey != ""
   163  
   164  	// Check PassivePorts format since the server library doesn't!
   165  	if !passivePortsRe.MatchString(opt.PassivePorts) {
   166  		return nil, fmt.Errorf("invalid format for passive ports %q", opt.PassivePorts)
   167  	}
   168  
   169  	ftpopt := &ftp.Options{
   170  		Name:           "Rclone FTP Server",
   171  		WelcomeMessage: "Welcome to Rclone " + fs.Version + " FTP Server",
   172  		Driver:         d,
   173  		Hostname:       host,
   174  		Port:           portNum,
   175  		PublicIP:       opt.PublicIP,
   176  		PassivePorts:   opt.PassivePorts,
   177  		Auth:           d,
   178  		Perm:           ftp.NewSimplePerm("ftp", "ftp"), // fake user and group
   179  		Logger:         &Logger{},
   180  		TLS:            d.useTLS,
   181  		CertFile:       d.opt.TLSCert,
   182  		KeyFile:        d.opt.TLSKey,
   183  		//TODO implement a maximum of https://godoc.org/goftp.io/server#ServerOpts
   184  	}
   185  	d.srv, err = ftp.NewServer(ftpopt)
   186  	if err != nil {
   187  		return nil, fmt.Errorf("failed to create new FTP server: %w", err)
   188  	}
   189  	return d, nil
   190  }
   191  
   192  // serve runs the ftp server
   193  func (d *driver) serve() error {
   194  	fs.Logf(d.f, "Serving FTP on %s", d.srv.Hostname+":"+strconv.Itoa(d.srv.Port))
   195  	return d.srv.ListenAndServe()
   196  }
   197  
   198  // close stops the ftp server
   199  //
   200  //lint:ignore U1000 unused when not building linux
   201  func (d *driver) close() error {
   202  	fs.Logf(d.f, "Stopping FTP on %s", d.srv.Hostname+":"+strconv.Itoa(d.srv.Port))
   203  	return d.srv.Shutdown()
   204  }
   205  
   206  // Logger ftp logger output formatted message
   207  type Logger struct{}
   208  
   209  // Print log simple text message
   210  func (l *Logger) Print(sessionID string, message interface{}) {
   211  	fs.Infof(sessionID, "%s", message)
   212  }
   213  
   214  // Printf log formatted text message
   215  func (l *Logger) Printf(sessionID string, format string, v ...interface{}) {
   216  	fs.Infof(sessionID, format, v...)
   217  }
   218  
   219  // PrintCommand log formatted command execution
   220  func (l *Logger) PrintCommand(sessionID string, command string, params string) {
   221  	if command == "PASS" {
   222  		fs.Infof(sessionID, "> PASS ****")
   223  	} else {
   224  		fs.Infof(sessionID, "> %s %s", command, params)
   225  	}
   226  }
   227  
   228  // PrintResponse log responses
   229  func (l *Logger) PrintResponse(sessionID string, code int, message string) {
   230  	fs.Infof(sessionID, "< %d %s", code, message)
   231  }
   232  
   233  // CheckPasswd handle auth based on configuration
   234  func (d *driver) CheckPasswd(sctx *ftp.Context, user, pass string) (ok bool, err error) {
   235  	if d.proxy != nil {
   236  		_, _, err = d.proxy.Call(user, pass, false)
   237  		if err != nil {
   238  			fs.Infof(nil, "proxy login failed: %v", err)
   239  			return false, nil
   240  		}
   241  		// Cache obscured password for later lookup.
   242  		//
   243  		// We don't cache the VFS directly in the driver as we want them
   244  		// to be expired and the auth proxy does that for us.
   245  		oPass, err := obscure.Obscure(pass)
   246  		if err != nil {
   247  			return false, err
   248  		}
   249  		d.userPassMu.Lock()
   250  		d.userPass[user] = oPass
   251  		d.userPassMu.Unlock()
   252  	} else {
   253  		ok = d.opt.BasicUser == user && (d.opt.BasicPass == "" || d.opt.BasicPass == pass)
   254  		if !ok {
   255  			fs.Infof(nil, "login failed: bad credentials")
   256  			return false, nil
   257  		}
   258  	}
   259  	return true, nil
   260  }
   261  
   262  // Get the VFS for this connection
   263  func (d *driver) getVFS(sctx *ftp.Context) (VFS *vfs.VFS, err error) {
   264  	if d.proxy == nil {
   265  		// If no proxy always use the same VFS
   266  		return d.globalVFS, nil
   267  	}
   268  	user := sctx.Sess.LoginUser()
   269  	d.userPassMu.Lock()
   270  	oPass, ok := d.userPass[user]
   271  	d.userPassMu.Unlock()
   272  	if !ok {
   273  		return nil, fmt.Errorf("proxy user not logged in")
   274  	}
   275  	pass, err := obscure.Reveal(oPass)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  	VFS, _, err = d.proxy.Call(user, pass, false)
   280  	if err != nil {
   281  		return nil, fmt.Errorf("proxy login failed: %w", err)
   282  	}
   283  	return VFS, nil
   284  }
   285  
   286  // Stat get information on file or folder
   287  func (d *driver) Stat(sctx *ftp.Context, path string) (fi iofs.FileInfo, err error) {
   288  	defer log.Trace(path, "")("fi=%+v, err = %v", &fi, &err)
   289  	VFS, err := d.getVFS(sctx)
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	n, err := VFS.Stat(path)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  	return &FileInfo{n, n.Mode(), VFS.Opt.UID, VFS.Opt.GID}, err
   298  }
   299  
   300  // ChangeDir move current folder
   301  func (d *driver) ChangeDir(sctx *ftp.Context, path string) (err error) {
   302  	defer log.Trace(path, "")("err = %v", &err)
   303  	VFS, err := d.getVFS(sctx)
   304  	if err != nil {
   305  		return err
   306  	}
   307  	n, err := VFS.Stat(path)
   308  	if err != nil {
   309  		return err
   310  	}
   311  	if !n.IsDir() {
   312  		return errors.New("not a directory")
   313  	}
   314  	return nil
   315  }
   316  
   317  // ListDir list content of a folder
   318  func (d *driver) ListDir(sctx *ftp.Context, path string, callback func(iofs.FileInfo) error) (err error) {
   319  	defer log.Trace(path, "")("err = %v", &err)
   320  	VFS, err := d.getVFS(sctx)
   321  	if err != nil {
   322  		return err
   323  	}
   324  	node, err := VFS.Stat(path)
   325  	if err == vfs.ENOENT {
   326  		return errors.New("directory not found")
   327  	} else if err != nil {
   328  		return err
   329  	}
   330  	if !node.IsDir() {
   331  		return errors.New("not a directory")
   332  	}
   333  
   334  	dir := node.(*vfs.Dir)
   335  	dirEntries, err := dir.ReadDirAll()
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	// Account the transfer
   341  	tr := accounting.GlobalStats().NewTransferRemoteSize(path, node.Size(), d.f, nil)
   342  	defer func() {
   343  		tr.Done(d.ctx, err)
   344  	}()
   345  
   346  	for _, file := range dirEntries {
   347  		err = callback(&FileInfo{file, file.Mode(), VFS.Opt.UID, VFS.Opt.GID})
   348  		if err != nil {
   349  			return err
   350  		}
   351  	}
   352  	return nil
   353  }
   354  
   355  // DeleteDir delete a folder and his content
   356  func (d *driver) DeleteDir(sctx *ftp.Context, path string) (err error) {
   357  	defer log.Trace(path, "")("err = %v", &err)
   358  	VFS, err := d.getVFS(sctx)
   359  	if err != nil {
   360  		return err
   361  	}
   362  	node, err := VFS.Stat(path)
   363  	if err != nil {
   364  		return err
   365  	}
   366  	if !node.IsDir() {
   367  		return errors.New("not a directory")
   368  	}
   369  	err = node.Remove()
   370  	if err != nil {
   371  		return err
   372  	}
   373  	return nil
   374  }
   375  
   376  // DeleteFile delete a file
   377  func (d *driver) DeleteFile(sctx *ftp.Context, path string) (err error) {
   378  	defer log.Trace(path, "")("err = %v", &err)
   379  	VFS, err := d.getVFS(sctx)
   380  	if err != nil {
   381  		return err
   382  	}
   383  	node, err := VFS.Stat(path)
   384  	if err != nil {
   385  		return err
   386  	}
   387  	if !node.IsFile() {
   388  		return errors.New("not a file")
   389  	}
   390  	err = node.Remove()
   391  	if err != nil {
   392  		return err
   393  	}
   394  	return nil
   395  }
   396  
   397  // Rename rename a file or folder
   398  func (d *driver) Rename(sctx *ftp.Context, oldName, newName string) (err error) {
   399  	defer log.Trace(oldName, "newName=%q", newName)("err = %v", &err)
   400  	VFS, err := d.getVFS(sctx)
   401  	if err != nil {
   402  		return err
   403  	}
   404  	return VFS.Rename(oldName, newName)
   405  }
   406  
   407  // MakeDir create a folder
   408  func (d *driver) MakeDir(sctx *ftp.Context, path string) (err error) {
   409  	defer log.Trace(path, "")("err = %v", &err)
   410  	VFS, err := d.getVFS(sctx)
   411  	if err != nil {
   412  		return err
   413  	}
   414  	dir, leaf, err := VFS.StatParent(path)
   415  	if err != nil {
   416  		return err
   417  	}
   418  	_, err = dir.Mkdir(leaf)
   419  	return err
   420  }
   421  
   422  // GetFile download a file
   423  func (d *driver) GetFile(sctx *ftp.Context, path string, offset int64) (size int64, fr io.ReadCloser, err error) {
   424  	defer log.Trace(path, "offset=%v", offset)("err = %v", &err)
   425  	VFS, err := d.getVFS(sctx)
   426  	if err != nil {
   427  		return 0, nil, err
   428  	}
   429  	node, err := VFS.Stat(path)
   430  	if err == vfs.ENOENT {
   431  		fs.Infof(path, "File not found")
   432  		return 0, nil, errors.New("file not found")
   433  	} else if err != nil {
   434  		return 0, nil, err
   435  	}
   436  	if !node.IsFile() {
   437  		return 0, nil, errors.New("not a file")
   438  	}
   439  
   440  	handle, err := node.Open(os.O_RDONLY)
   441  	if err != nil {
   442  		return 0, nil, err
   443  	}
   444  	_, err = handle.Seek(offset, io.SeekStart)
   445  	if err != nil {
   446  		return 0, nil, err
   447  	}
   448  
   449  	// Account the transfer
   450  	tr := accounting.GlobalStats().NewTransferRemoteSize(path, node.Size(), d.f, nil)
   451  	defer tr.Done(d.ctx, nil)
   452  
   453  	return node.Size(), handle, nil
   454  }
   455  
   456  // PutFile upload a file
   457  func (d *driver) PutFile(sctx *ftp.Context, path string, data io.Reader, offset int64) (n int64, err error) {
   458  	defer log.Trace(path, "offset=%d", offset)("err = %v", &err)
   459  
   460  	var isExist bool
   461  	VFS, err := d.getVFS(sctx)
   462  	if err != nil {
   463  		return 0, err
   464  	}
   465  	fi, err := VFS.Stat(path)
   466  	if err == nil {
   467  		isExist = true
   468  		if fi.IsDir() {
   469  			return 0, errors.New("can't create file - directory exists")
   470  		}
   471  	} else {
   472  		if os.IsNotExist(err) {
   473  			isExist = false
   474  		} else {
   475  			return 0, err
   476  		}
   477  	}
   478  
   479  	if offset > -1 && !isExist {
   480  		offset = -1
   481  	}
   482  
   483  	var f vfs.Handle
   484  
   485  	if offset == -1 {
   486  		if isExist {
   487  			err = VFS.Remove(path)
   488  			if err != nil {
   489  				return 0, err
   490  			}
   491  		}
   492  		f, err = VFS.Create(path)
   493  		if err != nil {
   494  			return 0, err
   495  		}
   496  		defer fs.CheckClose(f, &err)
   497  		n, err = io.Copy(f, data)
   498  		if err != nil {
   499  			return 0, err
   500  		}
   501  		return n, nil
   502  	}
   503  
   504  	f, err = VFS.OpenFile(path, os.O_APPEND|os.O_RDWR, 0660)
   505  	if err != nil {
   506  		return 0, err
   507  	}
   508  	defer fs.CheckClose(f, &err)
   509  
   510  	info, err := f.Stat()
   511  	if err != nil {
   512  		return 0, err
   513  	}
   514  	if offset > info.Size() {
   515  		return 0, fmt.Errorf("offset %d is beyond file size %d", offset, info.Size())
   516  	}
   517  
   518  	_, err = f.Seek(offset, io.SeekStart)
   519  	if err != nil {
   520  		return 0, err
   521  	}
   522  
   523  	bytes, err := io.Copy(f, data)
   524  	if err != nil {
   525  		return 0, err
   526  	}
   527  
   528  	return bytes, nil
   529  }
   530  
   531  // FileInfo struct to hold file info for ftp server
   532  type FileInfo struct {
   533  	os.FileInfo
   534  
   535  	mode  os.FileMode
   536  	owner uint32
   537  	group uint32
   538  }
   539  
   540  // Mode return mode of file.
   541  func (f *FileInfo) Mode() os.FileMode {
   542  	return f.mode
   543  }
   544  
   545  // Owner return owner of file. Try to find the username if possible
   546  func (f *FileInfo) Owner() string {
   547  	str := fmt.Sprint(f.owner)
   548  	u, err := user.LookupId(str)
   549  	if err != nil {
   550  		return str //User not found
   551  	}
   552  	return u.Username
   553  }
   554  
   555  // Group return group of file. Try to find the group name if possible
   556  func (f *FileInfo) Group() string {
   557  	str := fmt.Sprint(f.group)
   558  	g, err := user.LookupGroupId(str)
   559  	if err != nil {
   560  		return str //Group not found default to numerical value
   561  	}
   562  	return g.Name
   563  }
   564  
   565  // ModTime returns the time in UTC
   566  func (f *FileInfo) ModTime() time.Time {
   567  	return f.FileInfo.ModTime().UTC()
   568  }