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 }