mirror of
https://github.com/rclone/rclone.git
synced 2024-11-25 01:44:41 +01:00
530 lines
13 KiB
Go
530 lines
13 KiB
Go
//go:build !plan9
|
|
// +build !plan9
|
|
|
|
// Package ftp implements an FTP server for rclone
|
|
package ftp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/user"
|
|
"regexp"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/cmd"
|
|
"github.com/rclone/rclone/cmd/serve/proxy"
|
|
"github.com/rclone/rclone/cmd/serve/proxy/proxyflags"
|
|
"github.com/rclone/rclone/fs"
|
|
"github.com/rclone/rclone/fs/accounting"
|
|
"github.com/rclone/rclone/fs/config/flags"
|
|
"github.com/rclone/rclone/fs/log"
|
|
"github.com/rclone/rclone/fs/rc"
|
|
"github.com/rclone/rclone/vfs"
|
|
"github.com/rclone/rclone/vfs/vfsflags"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/pflag"
|
|
ftp "goftp.io/server/core"
|
|
)
|
|
|
|
// Options contains options for the http Server
|
|
type Options struct {
|
|
//TODO add more options
|
|
ListenAddr string // Port to listen on
|
|
PublicIP string // Passive ports range
|
|
PassivePorts string // Passive ports range
|
|
BasicUser string // single username for basic auth if not using Htpasswd
|
|
BasicPass string // password for BasicUser
|
|
TLSCert string // TLS PEM key (concatenation of certificate and CA certificate)
|
|
TLSKey string // TLS PEM Private key
|
|
}
|
|
|
|
// DefaultOpt is the default values used for Options
|
|
var DefaultOpt = Options{
|
|
ListenAddr: "localhost:2121",
|
|
PublicIP: "",
|
|
PassivePorts: "30000-32000",
|
|
BasicUser: "anonymous",
|
|
BasicPass: "",
|
|
}
|
|
|
|
// Opt is options set by command line flags
|
|
var Opt = DefaultOpt
|
|
|
|
// AddFlags adds flags for ftp
|
|
func AddFlags(flagSet *pflag.FlagSet) {
|
|
rc.AddOption("ftp", &Opt)
|
|
flags.StringVarP(flagSet, &Opt.ListenAddr, "addr", "", Opt.ListenAddr, "IPaddress:Port or :Port to bind server to")
|
|
flags.StringVarP(flagSet, &Opt.PublicIP, "public-ip", "", Opt.PublicIP, "Public IP address to advertise for passive connections")
|
|
flags.StringVarP(flagSet, &Opt.PassivePorts, "passive-port", "", Opt.PassivePorts, "Passive port range to use")
|
|
flags.StringVarP(flagSet, &Opt.BasicUser, "user", "", Opt.BasicUser, "User name for authentication")
|
|
flags.StringVarP(flagSet, &Opt.BasicPass, "pass", "", Opt.BasicPass, "Password for authentication (empty value allow every password)")
|
|
flags.StringVarP(flagSet, &Opt.TLSCert, "cert", "", Opt.TLSCert, "TLS PEM key (concatenation of certificate and CA certificate)")
|
|
flags.StringVarP(flagSet, &Opt.TLSKey, "key", "", Opt.TLSKey, "TLS PEM Private key")
|
|
}
|
|
|
|
func init() {
|
|
vfsflags.AddFlags(Command.Flags())
|
|
proxyflags.AddFlags(Command.Flags())
|
|
AddFlags(Command.Flags())
|
|
}
|
|
|
|
// Command definition for cobra
|
|
var Command = &cobra.Command{
|
|
Use: "ftp remote:path",
|
|
Short: `Serve remote:path over FTP.`,
|
|
Long: `
|
|
Run a basic FTP server to serve a remote over FTP protocol.
|
|
This can be viewed with a FTP client or you can make a remote of
|
|
type FTP to read and write it.
|
|
|
|
### Server options
|
|
|
|
Use --addr to specify which IP address and port the server should
|
|
listen on, e.g. --addr 1.2.3.4:8000 or --addr :8080 to listen to all
|
|
IPs. By default it only listens on localhost. You can use port
|
|
:0 to let the OS choose an available port.
|
|
|
|
If you set --addr to listen on a public or LAN accessible IP address
|
|
then using Authentication is advised - see the next section for info.
|
|
|
|
#### Authentication
|
|
|
|
By default this will serve files without needing a login.
|
|
|
|
You can set a single username and password with the --user and --pass flags.
|
|
` + vfs.Help + proxy.Help,
|
|
Annotations: map[string]string{
|
|
"versionIntroduced": "v1.44",
|
|
},
|
|
Run: func(command *cobra.Command, args []string) {
|
|
var f fs.Fs
|
|
if proxyflags.Opt.AuthProxy == "" {
|
|
cmd.CheckArgs(1, 1, command, args)
|
|
f = cmd.NewFsSrc(args)
|
|
} else {
|
|
cmd.CheckArgs(0, 0, command, args)
|
|
}
|
|
cmd.Run(false, false, command, func() error {
|
|
s, err := newServer(context.Background(), f, &Opt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.serve()
|
|
})
|
|
},
|
|
}
|
|
|
|
// server contains everything to run the server
|
|
type server struct {
|
|
f fs.Fs
|
|
srv *ftp.Server
|
|
ctx context.Context // for global config
|
|
opt Options
|
|
vfs *vfs.VFS
|
|
proxy *proxy.Proxy
|
|
useTLS bool
|
|
}
|
|
|
|
var passivePortsRe = regexp.MustCompile(`^\s*\d+\s*-\s*\d+\s*$`)
|
|
|
|
// Make a new FTP to serve the remote
|
|
func newServer(ctx context.Context, f fs.Fs, opt *Options) (*server, error) {
|
|
host, port, err := net.SplitHostPort(opt.ListenAddr)
|
|
if err != nil {
|
|
return nil, errors.New("failed to parse host:port")
|
|
}
|
|
portNum, err := strconv.Atoi(port)
|
|
if err != nil {
|
|
return nil, errors.New("failed to parse host:port")
|
|
}
|
|
|
|
s := &server{
|
|
f: f,
|
|
ctx: ctx,
|
|
opt: *opt,
|
|
}
|
|
if proxyflags.Opt.AuthProxy != "" {
|
|
s.proxy = proxy.New(ctx, &proxyflags.Opt)
|
|
} else {
|
|
s.vfs = vfs.New(f, &vfsflags.Opt)
|
|
}
|
|
s.useTLS = s.opt.TLSKey != ""
|
|
|
|
// Check PassivePorts format since the server library doesn't!
|
|
if !passivePortsRe.MatchString(opt.PassivePorts) {
|
|
return nil, fmt.Errorf("invalid format for passive ports %q", opt.PassivePorts)
|
|
}
|
|
|
|
ftpopt := &ftp.ServerOpts{
|
|
Name: "Rclone FTP Server",
|
|
WelcomeMessage: "Welcome to Rclone " + fs.Version + " FTP Server",
|
|
Factory: s, // implemented by NewDriver method
|
|
Hostname: host,
|
|
Port: portNum,
|
|
PublicIP: opt.PublicIP,
|
|
PassivePorts: opt.PassivePorts,
|
|
Auth: s, // implemented by CheckPasswd method
|
|
Logger: &Logger{},
|
|
TLS: s.useTLS,
|
|
CertFile: s.opt.TLSCert,
|
|
KeyFile: s.opt.TLSKey,
|
|
//TODO implement a maximum of https://godoc.org/goftp.io/server#ServerOpts
|
|
}
|
|
s.srv = ftp.NewServer(ftpopt)
|
|
return s, nil
|
|
}
|
|
|
|
// serve runs the ftp server
|
|
func (s *server) serve() error {
|
|
fs.Logf(s.f, "Serving FTP on %s", s.srv.Hostname+":"+strconv.Itoa(s.srv.Port))
|
|
return s.srv.ListenAndServe()
|
|
}
|
|
|
|
// close stops the ftp server
|
|
//
|
|
//lint:ignore U1000 unused when not building linux
|
|
func (s *server) close() error {
|
|
fs.Logf(s.f, "Stopping FTP on %s", s.srv.Hostname+":"+strconv.Itoa(s.srv.Port))
|
|
return s.srv.Shutdown()
|
|
}
|
|
|
|
// Logger ftp logger output formatted message
|
|
type Logger struct{}
|
|
|
|
// Print log simple text message
|
|
func (l *Logger) Print(sessionID string, message interface{}) {
|
|
fs.Infof(sessionID, "%s", message)
|
|
}
|
|
|
|
// Printf log formatted text message
|
|
func (l *Logger) Printf(sessionID string, format string, v ...interface{}) {
|
|
fs.Infof(sessionID, format, v...)
|
|
}
|
|
|
|
// PrintCommand log formatted command execution
|
|
func (l *Logger) PrintCommand(sessionID string, command string, params string) {
|
|
if command == "PASS" {
|
|
fs.Infof(sessionID, "> PASS ****")
|
|
} else {
|
|
fs.Infof(sessionID, "> %s %s", command, params)
|
|
}
|
|
}
|
|
|
|
// PrintResponse log responses
|
|
func (l *Logger) PrintResponse(sessionID string, code int, message string) {
|
|
fs.Infof(sessionID, "< %d %s", code, message)
|
|
}
|
|
|
|
// CheckPasswd handle auth based on configuration
|
|
//
|
|
// This is not used - the one in Driver should be called instead
|
|
func (s *server) CheckPasswd(user, pass string) (ok bool, err error) {
|
|
err = errors.New("internal error: server.CheckPasswd should never be called")
|
|
fs.Errorf(nil, "Error: %v", err)
|
|
return false, err
|
|
}
|
|
|
|
// NewDriver starts a new session for each client connection
|
|
func (s *server) NewDriver() (ftp.Driver, error) {
|
|
log.Trace("", "Init driver")("")
|
|
d := &Driver{
|
|
s: s,
|
|
vfs: s.vfs, // this can be nil if proxy set
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// Driver implementation of ftp server
|
|
type Driver struct {
|
|
s *server
|
|
vfs *vfs.VFS
|
|
lock sync.Mutex
|
|
}
|
|
|
|
// CheckPasswd handle auth based on configuration
|
|
func (d *Driver) CheckPasswd(user, pass string) (ok bool, err error) {
|
|
s := d.s
|
|
if s.proxy != nil {
|
|
var VFS *vfs.VFS
|
|
VFS, _, err = s.proxy.Call(user, pass, false)
|
|
if err != nil {
|
|
fs.Infof(nil, "proxy login failed: %v", err)
|
|
return false, nil
|
|
}
|
|
d.vfs = VFS
|
|
} else {
|
|
ok = s.opt.BasicUser == user && (s.opt.BasicPass == "" || s.opt.BasicPass == pass)
|
|
if !ok {
|
|
fs.Infof(nil, "login failed: bad credentials")
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Stat get information on file or folder
|
|
func (d *Driver) Stat(path string) (fi ftp.FileInfo, err error) {
|
|
defer log.Trace(path, "")("fi=%+v, err = %v", &fi, &err)
|
|
n, err := d.vfs.Stat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &FileInfo{n, n.Mode(), d.vfs.Opt.UID, d.vfs.Opt.GID}, err
|
|
}
|
|
|
|
// ChangeDir move current folder
|
|
func (d *Driver) ChangeDir(path string) (err error) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
defer log.Trace(path, "")("err = %v", &err)
|
|
n, err := d.vfs.Stat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !n.IsDir() {
|
|
return errors.New("not a directory")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListDir list content of a folder
|
|
func (d *Driver) ListDir(path string, callback func(ftp.FileInfo) error) (err error) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
defer log.Trace(path, "")("err = %v", &err)
|
|
node, err := d.vfs.Stat(path)
|
|
if err == vfs.ENOENT {
|
|
return errors.New("directory not found")
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
if !node.IsDir() {
|
|
return errors.New("not a directory")
|
|
}
|
|
|
|
dir := node.(*vfs.Dir)
|
|
dirEntries, err := dir.ReadDirAll()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Account the transfer
|
|
tr := accounting.GlobalStats().NewTransferRemoteSize(path, node.Size())
|
|
defer func() {
|
|
tr.Done(d.s.ctx, err)
|
|
}()
|
|
|
|
for _, file := range dirEntries {
|
|
err = callback(&FileInfo{file, file.Mode(), d.vfs.Opt.UID, d.vfs.Opt.GID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteDir delete a folder and his content
|
|
func (d *Driver) DeleteDir(path string) (err error) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
defer log.Trace(path, "")("err = %v", &err)
|
|
node, err := d.vfs.Stat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !node.IsDir() {
|
|
return errors.New("not a directory")
|
|
}
|
|
err = node.Remove()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteFile delete a file
|
|
func (d *Driver) DeleteFile(path string) (err error) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
defer log.Trace(path, "")("err = %v", &err)
|
|
node, err := d.vfs.Stat(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !node.IsFile() {
|
|
return errors.New("not a file")
|
|
}
|
|
err = node.Remove()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Rename rename a file or folder
|
|
func (d *Driver) Rename(oldName, newName string) (err error) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
defer log.Trace(oldName, "newName=%q", newName)("err = %v", &err)
|
|
return d.vfs.Rename(oldName, newName)
|
|
}
|
|
|
|
// MakeDir create a folder
|
|
func (d *Driver) MakeDir(path string) (err error) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
defer log.Trace(path, "")("err = %v", &err)
|
|
dir, leaf, err := d.vfs.StatParent(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = dir.Mkdir(leaf)
|
|
return err
|
|
}
|
|
|
|
// GetFile download a file
|
|
func (d *Driver) GetFile(path string, offset int64) (size int64, fr io.ReadCloser, err error) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
defer log.Trace(path, "offset=%v", offset)("err = %v", &err)
|
|
node, err := d.vfs.Stat(path)
|
|
if err == vfs.ENOENT {
|
|
fs.Infof(path, "File not found")
|
|
return 0, nil, errors.New("file not found")
|
|
} else if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
if !node.IsFile() {
|
|
return 0, nil, errors.New("not a file")
|
|
}
|
|
|
|
handle, err := node.Open(os.O_RDONLY)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
_, err = handle.Seek(offset, io.SeekStart)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
// Account the transfer
|
|
tr := accounting.GlobalStats().NewTransferRemoteSize(path, node.Size())
|
|
defer tr.Done(d.s.ctx, nil)
|
|
|
|
return node.Size(), handle, nil
|
|
}
|
|
|
|
// PutFile upload a file
|
|
func (d *Driver) PutFile(path string, data io.Reader, appendData bool) (n int64, err error) {
|
|
d.lock.Lock()
|
|
defer d.lock.Unlock()
|
|
defer log.Trace(path, "append=%v", appendData)("err = %v", &err)
|
|
var isExist bool
|
|
node, err := d.vfs.Stat(path)
|
|
if err == nil {
|
|
isExist = true
|
|
if node.IsDir() {
|
|
return 0, errors.New("a dir has the same name")
|
|
}
|
|
} else {
|
|
if os.IsNotExist(err) {
|
|
isExist = false
|
|
} else {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
if appendData && !isExist {
|
|
appendData = false
|
|
}
|
|
|
|
if !appendData {
|
|
if isExist {
|
|
err = node.Remove()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
f, err := d.vfs.OpenFile(path, os.O_RDWR|os.O_CREATE, 0660)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer closeIO(path, f)
|
|
bytes, err := io.Copy(f, data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return bytes, nil
|
|
}
|
|
|
|
of, err := d.vfs.OpenFile(path, os.O_APPEND|os.O_RDWR, 0660)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer closeIO(path, of)
|
|
|
|
_, err = of.Seek(0, io.SeekEnd)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
bytes, err := io.Copy(of, data)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return bytes, nil
|
|
}
|
|
|
|
// FileInfo struct to hold file info for ftp server
|
|
type FileInfo struct {
|
|
os.FileInfo
|
|
|
|
mode os.FileMode
|
|
owner uint32
|
|
group uint32
|
|
}
|
|
|
|
// Mode return mode of file.
|
|
func (f *FileInfo) Mode() os.FileMode {
|
|
return f.mode
|
|
}
|
|
|
|
// Owner return owner of file. Try to find the username if possible
|
|
func (f *FileInfo) Owner() string {
|
|
str := fmt.Sprint(f.owner)
|
|
u, err := user.LookupId(str)
|
|
if err != nil {
|
|
return str //User not found
|
|
}
|
|
return u.Username
|
|
}
|
|
|
|
// Group return group of file. Try to find the group name if possible
|
|
func (f *FileInfo) Group() string {
|
|
str := fmt.Sprint(f.group)
|
|
g, err := user.LookupGroupId(str)
|
|
if err != nil {
|
|
return str //Group not found default to numerical value
|
|
}
|
|
return g.Name
|
|
}
|
|
|
|
// ModTime returns the time in UTC
|
|
func (f *FileInfo) ModTime() time.Time {
|
|
return f.FileInfo.ModTime().UTC()
|
|
}
|
|
|
|
func closeIO(path string, c io.Closer) {
|
|
err := c.Close()
|
|
if err != nil {
|
|
log.Trace(path, "")("err = %v", &err)
|
|
}
|
|
}
|