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