serve ftp: implement --auth-proxy

This commit is contained in:
Nick Craig-Wood 2019-08-01 15:02:31 +01:00
parent b94eef16c1
commit 72782bdda6
2 changed files with 131 additions and 36 deletions

View file

@ -5,17 +5,21 @@
package ftp package ftp
import ( import (
"errors" "bytes"
"fmt" "fmt"
"io" "io"
"net" "net"
"os" "os"
"os/user" "os/user"
"runtime"
"strconv" "strconv"
"sync" "sync"
ftp "github.com/goftp/server" ftp "github.com/goftp/server"
"github.com/pkg/errors"
"github.com/rclone/rclone/cmd" "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"
"github.com/rclone/rclone/fs/accounting" "github.com/rclone/rclone/fs/accounting"
"github.com/rclone/rclone/fs/config/flags" "github.com/rclone/rclone/fs/config/flags"
@ -61,6 +65,7 @@ func AddFlags(flagSet *pflag.FlagSet) {
func init() { func init() {
vfsflags.AddFlags(Command.Flags()) vfsflags.AddFlags(Command.Flags())
proxyflags.AddFlags(Command.Flags())
AddFlags(Command.Flags()) AddFlags(Command.Flags())
} }
@ -88,10 +93,15 @@ then using Authentication is advised - see the next section for info.
By default this will serve files without needing a login. By default this will serve files without needing a login.
You can set a single username and password with the --user and --pass flags. You can set a single username and password with the --user and --pass flags.
` + vfs.Help, ` + vfs.Help + proxy.Help,
Run: func(command *cobra.Command, args []string) { Run: func(command *cobra.Command, args []string) {
var f fs.Fs
if proxyflags.Opt.AuthProxy == "" {
cmd.CheckArgs(1, 1, command, args) cmd.CheckArgs(1, 1, command, args)
f := cmd.NewFsSrc(args) f = cmd.NewFsSrc(args)
} else {
cmd.CheckArgs(0, 0, command, args)
}
cmd.Run(false, false, command, func() error { cmd.Run(false, false, command, func() error {
s, err := newServer(f, &Opt) s, err := newServer(f, &Opt)
if err != nil { if err != nil {
@ -107,6 +117,10 @@ type server struct {
f fs.Fs f fs.Fs
srv *ftp.Server srv *ftp.Server
opt Options opt Options
vfs *vfs.VFS
proxy *proxy.Proxy
pendingMu sync.Mutex
pending map[string]*Driver // pending Driver~s that haven't got their VFS
} }
// Make a new FTP to serve the remote // Make a new FTP to serve the remote
@ -123,18 +137,23 @@ func newServer(f fs.Fs, opt *Options) (*server, error) {
s := &server{ s := &server{
f: f, f: f,
opt: *opt, opt: *opt,
pending: make(map[string]*Driver),
} }
if proxyflags.Opt.AuthProxy != "" {
s.proxy = proxy.New(&proxyflags.Opt)
} else {
s.vfs = vfs.New(f, &vfsflags.Opt)
}
ftpopt := &ftp.ServerOpts{ ftpopt := &ftp.ServerOpts{
Name: "Rclone FTP Server", Name: "Rclone FTP Server",
WelcomeMessage: "Welcome on Rclone FTP Server", WelcomeMessage: "Welcome to Rclone " + fs.Version + " FTP Server",
Factory: &DriverFactory{ Factory: s, // implemented by NewDriver method
vfs: vfs.New(f, &vfsflags.Opt),
},
Hostname: host, Hostname: host,
Port: portNum, Port: portNum,
PublicIp: opt.PublicIP, PublicIp: opt.PublicIP,
PassivePorts: opt.PassivePorts, PassivePorts: opt.PassivePorts,
Auth: &Auth{s}, Auth: s, // implemented by CheckPasswd method
Logger: &Logger{}, Logger: &Logger{},
//TODO implement a maximum of https://godoc.org/github.com/goftp/server#ServerOpts //TODO implement a maximum of https://godoc.org/github.com/goftp/server#ServerOpts
} }
@ -181,38 +200,106 @@ func (l *Logger) PrintResponse(sessionID string, code int, message string) {
fs.Infof(sessionID, "< %d %s", code, message) fs.Infof(sessionID, "< %d %s", code, message)
} }
//Auth struct to handle ftp auth (temporary simple for POC) // findID finds the connection ID of the calling program. It does
type Auth struct { // this in an incredibly hacky way by looking in the stack trace.
s *server //
// callerName should be the name of the function that we are looking
// for with a trailing '('
//
// What is really needed is a change of calling protocol so
// CheckPassword is called with the connection.
func findID(callerName []byte) (string, error) {
// Dump the stack in this format
// github.com/rclone/rclone/vendor/github.com/goftp/server.(*Conn).Serve(0xc0000b2680)
// /home/ncw/go/src/github.com/rclone/rclone/vendor/github.com/goftp/server/conn.go:116 +0x11d
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
buf = buf[:n]
// look for callerName first
i := bytes.Index(buf, callerName)
if i < 0 {
return "", errors.Errorf("findID: caller name not found in:\n%s", buf)
} }
buf = buf[i+len(callerName):]
// find next ')'
i = bytes.IndexByte(buf, ')')
if i < 0 {
return "", errors.Errorf("findID: end of args not found in:\n%s", buf)
}
buf = buf[:i]
// trim off first argument
// find next ','
i = bytes.IndexByte(buf, ',')
if i >= 0 {
buf = buf[:i]
}
return string(buf), nil
}
var connServeFunction = []byte("(*Conn).Serve(")
// CheckPasswd handle auth based on configuration // CheckPasswd handle auth based on configuration
func (a *Auth) CheckPasswd(user, pass string) (bool, error) { func (s *server) CheckPasswd(user, pass string) (ok bool, err error) {
return a.s.opt.BasicUser == user && (a.s.opt.BasicPass == "" || a.s.opt.BasicPass == pass), nil var VFS *vfs.VFS
if s.proxy != nil {
VFS, _, err = s.proxy.Call(user, pass)
if err != nil {
fs.Infof(nil, "proxy login failed: %v", err)
return false, nil
}
id, err := findID(connServeFunction)
if err != nil {
fs.Infof(nil, "proxy login failed: failed to read ID from stack: %v", err)
return false, nil
}
s.pendingMu.Lock()
d := s.pending[id]
delete(s.pending, id)
s.pendingMu.Unlock()
if d == nil {
return false, errors.Errorf("proxy login failed: failed to find pending Driver under ID %q", id)
}
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
} }
//DriverFactory factory of ftp driver for each session // NewDriver starts a new session for each client connection
type DriverFactory struct { func (s *server) NewDriver() (ftp.Driver, error) {
vfs *vfs.VFS
}
//NewDriver start a new session
func (f *DriverFactory) NewDriver() (ftp.Driver, error) {
log.Trace("", "Init driver")("") log.Trace("", "Init driver")("")
return &Driver{ d := &Driver{
vfs: f.vfs, s: s,
}, nil vfs: s.vfs, // this can be nil if proxy set
}
return d, nil
} }
//Driver implementation of ftp server //Driver implementation of ftp server
type Driver struct { type Driver struct {
s *server
vfs *vfs.VFS vfs *vfs.VFS
lock sync.Mutex lock sync.Mutex
} }
//Init a connection //Init a connection
func (d *Driver) Init(*ftp.Conn) { func (d *Driver) Init(c *ftp.Conn) {
defer log.Trace("", "Init session")("") defer log.Trace("", "Init session")("")
if d.s.proxy != nil {
id := fmt.Sprintf("%p", c)
d.s.pendingMu.Lock()
d.s.pending[id] = d
d.s.pendingMu.Unlock()
}
} }
//Stat get information on file or folder //Stat get information on file or folder

View file

@ -18,6 +18,7 @@ import (
_ "github.com/rclone/rclone/backend/local" _ "github.com/rclone/rclone/backend/local"
"github.com/rclone/rclone/fstest" "github.com/rclone/rclone/fstest"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
const ( const (
@ -87,3 +88,10 @@ func TestFTP(t *testing.T) {
} }
assert.NoError(t, err, "Running ftp integration tests") assert.NoError(t, err, "Running ftp integration tests")
} }
func TestFindID(t *testing.T) {
id, err := findID([]byte("TestFindID("))
require.NoError(t, err)
// id should be the argument to this function
assert.Equal(t, fmt.Sprintf("%p", t), id)
}