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