serve ftp: implement --auth-proxy
This commit is contained in:
parent
b94eef16c1
commit
72782bdda6
2 changed files with 131 additions and 36 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue