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 }