github.com/divyam234/rclone@v1.64.1/cmd/serve/ftp/ftp.go (about)

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