From d75fbe4852e4f8eabcbc37ecf895ac8a82a455d0 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Wed, 31 Jul 2019 22:20:46 +0100 Subject: [PATCH] serve sftp: implement auth proxy --- cmd/serve/sftp/connection.go | 7 ++-- cmd/serve/sftp/handler.go | 4 +-- cmd/serve/sftp/server.go | 67 +++++++++++++++++++++++++++++------- cmd/serve/sftp/sftp.go | 15 ++++++-- 4 files changed, 71 insertions(+), 22 deletions(-) diff --git a/cmd/serve/sftp/connection.go b/cmd/serve/sftp/connection.go index 806787d36..51408a8b5 100644 --- a/cmd/serve/sftp/connection.go +++ b/cmd/serve/sftp/connection.go @@ -47,7 +47,6 @@ func shellUnEscape(str string) string { // Info about the current connection type conn struct { vfs *vfs.VFS - f fs.Fs handlers sftp.Handlers what string } @@ -65,7 +64,7 @@ func (c *conn) execCommand(ctx context.Context, out io.Writer, command string) ( fs.Debugf(c.what, "exec command: binary = %q, args = %q", binary, args) switch binary { case "df": - about := c.f.Features().About + about := c.vfs.Fs().Features().About if about == nil { return errors.New("df not supported") } @@ -121,7 +120,7 @@ func (c *conn) execCommand(ctx context.Context, out io.Writer, command string) ( // special cases for rclone command detection switch args { case "'abc' | md5sum": - if c.f.Hashes().Contains(hash.MD5) { + if c.vfs.Fs().Hashes().Contains(hash.MD5) { _, err = fmt.Fprintf(out, "0bee89b07a248e27c83fc3d5951213c1 -\n") if err != nil { return errors.Wrap(err, "send output failed") @@ -130,7 +129,7 @@ func (c *conn) execCommand(ctx context.Context, out io.Writer, command string) ( return errors.New("md5 hash not supported") } case "'abc' | sha1sum": - if c.f.Hashes().Contains(hash.SHA1) { + if c.vfs.Fs().Hashes().Contains(hash.SHA1) { _, err = fmt.Fprintf(out, "03cfd743661f07975fa2f1220c5194cbaff48451 -\n") if err != nil { return errors.Wrap(err, "send output failed") diff --git a/cmd/serve/sftp/handler.go b/cmd/serve/sftp/handler.go index bd4c6e00b..5e25d28c5 100644 --- a/cmd/serve/sftp/handler.go +++ b/cmd/serve/sftp/handler.go @@ -19,14 +19,14 @@ type vfsHandler struct { } // vfsHandler returns a Handlers object with the test handlers. -func newVFSHandler(vfs *vfs.VFS) (sftp.Handlers, error) { +func newVFSHandler(vfs *vfs.VFS) sftp.Handlers { v := vfsHandler{VFS: vfs} return sftp.Handlers{ FileGet: v, FilePut: v, FileCmd: v, FileList: v, - }, nil + } } func (v vfsHandler) Fileread(r *sftp.Request) (io.ReaderAt, error) { diff --git a/cmd/serve/sftp/server.go b/cmd/serve/sftp/server.go index 0d6778016..6bf48d42b 100644 --- a/cmd/serve/sftp/server.go +++ b/cmd/serve/sftp/server.go @@ -18,7 +18,8 @@ import ( "strings" "github.com/pkg/errors" - "github.com/pkg/sftp" + "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/config" "github.com/rclone/rclone/lib/env" @@ -33,21 +34,47 @@ type server struct { opt Options vfs *vfs.VFS config *ssh.ServerConfig - handlers sftp.Handlers listener net.Listener waitChan chan struct{} // for waiting on the listener to close + proxy *proxy.Proxy } func newServer(f fs.Fs, opt *Options) *server { s := &server{ f: f, - vfs: vfs.New(f, &vfsflags.Opt), opt: *opt, waitChan: make(chan struct{}), } + if proxyflags.Opt.AuthProxy != "" { + s.proxy = proxy.New(&proxyflags.Opt) + } else { + s.vfs = vfs.New(f, &vfsflags.Opt) + } return s } +// getVFS gets the vfs from s or the proxy +func (s *server) getVFS(what string, sshConn *ssh.ServerConn) (VFS *vfs.VFS) { + if s.proxy == nil { + return s.vfs + } + if sshConn.Permissions == nil && sshConn.Permissions.Extensions == nil { + fs.Infof(what, "SSH Permissions Extensions not found") + return nil + } + key := sshConn.Permissions.Extensions["_vfsKey"] + if key == "" { + fs.Infof(what, "VFS key not found") + return nil + } + VFS = s.proxy.Get(key) + if VFS == nil { + fs.Infof(what, "failed to read VFS from cache") + return nil + } + return VFS +} + func (s *server) acceptConnections() { for { nConn, err := s.listener.Accept() @@ -73,11 +100,15 @@ func (s *server) acceptConnections() { go ssh.DiscardRequests(reqs) c := &conn{ - vfs: s.vfs, - f: s.f, - handlers: s.handlers, - what: what, + what: what, + vfs: s.getVFS(what, sshConn), } + if c.vfs == nil { + fs.Infof(what, "Closing unauthenticated connection (couldn't find VFS)") + _ = nConn.Close() + continue + } + c.handlers = newVFSHandler(c.vfs) // Accept all channels go c.handleChannels(chans) @@ -109,7 +140,19 @@ func (s *server) serve() (err error) { ServerVersion: "SSH-2.0-" + fs.Config.UserAgent, PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { fs.Debugf(describeConn(c), "Password login attempt for %s", c.User()) - if s.opt.User != "" && s.opt.Pass != "" { + if s.proxy != nil { + // query the proxy for the config + _, vfsKey, err := s.proxy.Call(c.User(), string(pass)) + if err != nil { + return nil, err + } + // just return the Key so we can get it back from the cache + return &ssh.Permissions{ + Extensions: map[string]string{ + "_vfsKey": vfsKey, + }, + }, nil + } else if s.opt.User != "" && s.opt.Pass != "" { userOK := subtle.ConstantTimeCompare([]byte(c.User()), []byte(s.opt.User)) passOK := subtle.ConstantTimeCompare(pass, []byte(s.opt.Pass)) if (userOK & passOK) == 1 { @@ -120,6 +163,9 @@ func (s *server) serve() (err error) { }, PublicKeyCallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { fs.Debugf(describeConn(c), "Public key login attempt for %s", c.User()) + if s.proxy != nil { + return nil, errors.New("public key login not allowed when using auth proxy") + } if _, ok := authorizedKeysMap[string(pubKey.Marshal())]; ok { return &ssh.Permissions{ // Record the public key used for authentication. @@ -178,11 +224,6 @@ func (s *server) serve() (err error) { } fs.Logf(nil, "SFTP server listening on %v\n", s.listener.Addr()) - s.handlers, err = newVFSHandler(s.vfs) - if err != nil { - return errors.Wrap(err, "serve sftp: failed to create fs") - } - go s.acceptConnections() return nil diff --git a/cmd/serve/sftp/sftp.go b/cmd/serve/sftp/sftp.go index bbd4a4f59..73accdad5 100644 --- a/cmd/serve/sftp/sftp.go +++ b/cmd/serve/sftp/sftp.go @@ -6,6 +6,9 @@ package sftp import ( "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/config/flags" "github.com/rclone/rclone/fs/rc" "github.com/rclone/rclone/vfs" @@ -46,6 +49,7 @@ func AddFlags(flagSet *pflag.FlagSet, Opt *Options) { func init() { vfsflags.AddFlags(Command.Flags()) + proxyflags.AddFlags(Command.Flags()) AddFlags(Command.Flags(), &Opt) } @@ -84,10 +88,15 @@ reachable externally then supply "--addr :2022" for example. Note that the default of "--vfs-cache-mode off" is fine for the rclone sftp backend, but it may not be with other SFTP clients. -` + vfs.Help, +` + vfs.Help + proxy.Help, Run: func(command *cobra.Command, args []string) { - cmd.CheckArgs(1, 1, command, args) - f := cmd.NewFsSrc(args) + 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, true, command, func() error { s := newServer(f, &Opt) err := s.Serve()