diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 18dd97d68..e5aa4e2b2 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -38,8 +38,6 @@ import ( "github.com/spf13/pflag" "github.com/restic/restic/internal/errors" - - "golang.org/x/term" ) // ErrNoRepository is used to report if opening a repository failed due @@ -311,52 +309,6 @@ func readPassword(in io.Reader) (password string, err error) { return sc.Text(), errors.WithStack(sc.Err()) } -// readPasswordTerminal reads the password from the given reader which must be a -// tty. Prompt is printed on the writer out before attempting to read the -// password. If the context is canceled, the function leaks the password reading -// goroutine. -func readPasswordTerminal(ctx context.Context, in *os.File, out *os.File, prompt string) (password string, err error) { - fd := int(out.Fd()) - state, err := term.GetState(fd) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err) - return "", err - } - - done := make(chan struct{}) - var buf []byte - - go func() { - defer close(done) - _, err = fmt.Fprint(out, prompt) - if err != nil { - return - } - buf, err = term.ReadPassword(int(in.Fd())) - if err != nil { - return - } - _, err = fmt.Fprintln(out) - }() - - select { - case <-ctx.Done(): - err := term.Restore(fd, state) - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err) - } - return "", ctx.Err() - case <-done: - // clean shutdown, nothing to do - } - - if err != nil { - return "", errors.Wrap(err, "ReadPassword") - } - - return string(buf), nil -} - // ReadPassword reads the password from a password file, the environment // variable RESTIC_PASSWORD or prompts the user. If the context is canceled, // the function leaks the password reading goroutine. @@ -378,7 +330,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string) (strin ) if terminal.StdinIsTerminal() { - password, err = readPasswordTerminal(ctx, os.Stdin, os.Stderr, prompt) + password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) } else { password, err = readPassword(os.Stdin) if terminal.StdoutIsTerminal() { diff --git a/internal/terminal/password.go b/internal/terminal/password.go new file mode 100644 index 000000000..6d1b6c912 --- /dev/null +++ b/internal/terminal/password.go @@ -0,0 +1,55 @@ +package terminal + +import ( + "context" + "fmt" + "os" + + "golang.org/x/term" +) + +// ReadPassword reads the password from the given reader which must be a +// tty. Prompt is printed on the writer out before attempting to read the +// password. If the context is canceled, the function leaks the password reading +// goroutine. +func ReadPassword(ctx context.Context, in *os.File, out *os.File, prompt string) (password string, err error) { + fd := int(out.Fd()) + state, err := term.GetState(fd) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "unable to get terminal state: %v\n", err) + return "", err + } + + done := make(chan struct{}) + var buf []byte + + go func() { + defer close(done) + _, err = fmt.Fprint(out, prompt) + if err != nil { + return + } + buf, err = term.ReadPassword(int(in.Fd())) + if err != nil { + return + } + _, err = fmt.Fprintln(out) + }() + + select { + case <-ctx.Done(): + err := term.Restore(fd, state) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "unable to restore terminal state: %v\n", err) + } + return "", ctx.Err() + case <-done: + // clean shutdown, nothing to do + } + + if err != nil { + return "", fmt.Errorf("ReadPassword: %w", err) + } + + return string(buf), nil +}