From bbddadbd042a5470df82d8643e609b41cb165866 Mon Sep 17 00:00:00 2001 From: Stephen Harris Date: Thu, 8 Oct 2020 08:27:39 -0400 Subject: [PATCH] sftp: remember entered password in AskPass mode As reported in https://github.com/rclone/rclone/issues/4660#issuecomment-705502792 After switching to a password callback function, if the ssh connection aborts and needs to be reconnected then the user is-reprompted for their password. Instead we now remember the password they entered and just give that back. We do lose the ability for them to correct mistakes, but that's the situation from before switching to callbacks. We keep the benefits of not asking for passwords until the SSH connection succeeds (right known_hosts entry, for example). This required a small refactor of how `f := &Fs{}` was built, so we can store the saved password in the Fs object --- backend/sftp/sftp.go | 50 ++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/backend/sftp/sftp.go b/backend/sftp/sftp.go index d5848b01d..a50f1cc3a 100644 --- a/backend/sftp/sftp.go +++ b/backend/sftp/sftp.go @@ -235,6 +235,7 @@ type Fs struct { poolMu sync.Mutex pool []*conn pacer *fs.Pacer // pacer for operations + savedpswd string } // Object is a remote SFTP file that has been stat'd (so it exists, but is not necessarily open for reading) @@ -413,6 +414,10 @@ func (f *Fs) putSftpConnection(pc **conn, err error) { // NewFs creates a new Fs object from the name and root. It connects to // the host specified in the config file. func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { + // This will hold the Fs object. We need to create it here + // so we can refer to it in the SSH callback, but it's populated + // in NewFsWithConnection + f := &Fs{} ctx := context.Background() // Parse config into Options struct opt := new(Options) @@ -566,33 +571,42 @@ func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) { // Config for password if none was defined and we're allowed to // We don't ask now; we ask if the ssh connection succeeds if opt.Pass == "" && opt.AskPassword { - sshConfig.Auth = append(sshConfig.Auth, ssh.PasswordCallback(getPass)) + sshConfig.Auth = append(sshConfig.Auth, ssh.PasswordCallback(f.getPass)) } - return NewFsWithConnection(ctx, name, root, m, opt, sshConfig) + return NewFsWithConnection(ctx, f, name, root, m, opt, sshConfig) } // If we're in password mode and ssh connection succeeds then this -// callback is called -func getPass() (string, error) { - _, _ = fmt.Fprint(os.Stderr, "Enter SFTP password: ") - return config.ReadPassword(), nil +// callback is called. First time around we ask the user, and then +// save it so on reconnection we give back the previous string. +// This removes the ability to let the user correct a mistaken entry, +// but means that reconnects are transparent. +// We'll re-use config.Pass for this, 'cos we know it's not been +// specified. +func (f *Fs) getPass() (string, error) { + for f.savedpswd == "" { + _, _ = fmt.Fprint(os.Stderr, "Enter SFTP password: ") + f.savedpswd = config.ReadPassword() + } + return f.savedpswd, nil } // NewFsWithConnection creates a new Fs object from the name and root and an ssh.ClientConfig. It connects to // the host specified in the ssh.ClientConfig -func NewFsWithConnection(ctx context.Context, name string, root string, m configmap.Mapper, opt *Options, sshConfig *ssh.ClientConfig) (fs.Fs, error) { - f := &Fs{ - name: name, - root: root, - absRoot: root, - opt: *opt, - m: m, - config: sshConfig, - url: "sftp://" + opt.User + "@" + opt.Host + ":" + opt.Port + "/" + root, - mkdirLock: newStringLock(), - pacer: fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), - } +func NewFsWithConnection(ctx context.Context, f *Fs, name string, root string, m configmap.Mapper, opt *Options, sshConfig *ssh.ClientConfig) (fs.Fs, error) { + // Populate the Filesystem Object + f.name = name + f.root = root + f.absRoot = root + f.opt = *opt + f.m = m + f.config = sshConfig + f.url = "sftp://" + opt.User + "@" + opt.Host + ":" + opt.Port + "/" + root + f.mkdirLock = newStringLock() + f.pacer = fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))) + f.savedpswd = "" + f.features = (&fs.Features{ CanHaveEmptyDirectories: true, SlowHash: true,