From 91a353f2b03842266c4507e14c977a95e44cf396 Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 6 Feb 2023 14:58:28 +0100 Subject: [PATCH] Add restore handler for password input This commit restores the terminal state in case the program is interrupted while being in password read mode. This ensures the terminal remains usable, also if the password input is being cancelled. Signed-off-by: phoenix --- pkg/auth/auth.go | 4 ++-- pkg/util/util.go | 5 +++++ pkg/util/util_supported.go | 44 ++++++++++++++++++++++++++++++++++++++ pkg/util/util_windows.go | 15 +++++++++++++ 4 files changed, 66 insertions(+), 2 deletions(-) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 57a6e4cc8..bbb32624e 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -10,13 +10,13 @@ import ( "path/filepath" "strings" + "github.com/containers/common/pkg/util" "github.com/containers/image/v5/docker" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/pkg/docker/config" "github.com/containers/image/v5/pkg/sysregistriesv2" "github.com/containers/image/v5/types" "github.com/sirupsen/logrus" - terminal "golang.org/x/term" ) // ErrNewCredentialsInvalid means that the new user-provided credentials are @@ -269,7 +269,7 @@ func getUserAndPass(opts *LoginOptions, password, userFromAuthFile string) (user } if password == "" { fmt.Fprint(opts.Stdout, "Password: ") - pass, err := terminal.ReadPassword(int(os.Stdin.Fd())) + pass, err := util.ReadPassword(int(os.Stdin.Fd())) if err != nil { return "", "", fmt.Errorf("reading password: %w", err) } diff --git a/pkg/util/util.go b/pkg/util/util.go index dd79f91fb..516422f09 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -2,6 +2,7 @@ package util import ( "bytes" + "errors" "fmt" "os" "os/exec" @@ -19,6 +20,10 @@ const ( UnknownPackage = "Unknown" ) +var ( + ErrInterrupt = errors.New("Interrupt") +) + // Note: This function is copied from containers/podman libpod/util.go // Please see https://github.com/containers/common/pull/1460 func queryPackageVersion(cmdArg ...string) string { diff --git a/pkg/util/util_supported.go b/pkg/util/util_supported.go index 0cd53af53..aa659c17e 100644 --- a/pkg/util/util_supported.go +++ b/pkg/util/util_supported.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "os" + "os/signal" "path/filepath" "sync" "syscall" @@ -14,6 +15,7 @@ import ( "github.com/containers/storage/pkg/homedir" "github.com/containers/storage/pkg/unshare" "github.com/sirupsen/logrus" + terminal "golang.org/x/term" ) var ( @@ -89,3 +91,45 @@ func GetRuntimeDir() (string, error) { } return rootlessRuntimeDir, nil } + +// ReadPassword reads a password from the terminal without echo. +func ReadPassword(fd int) ([]byte, error) { + // Store and restore the terminal status on interruptions to + // avoid that the terminal remains in the password state + // This is necessary as for https://github.com/golang/go/issues/31180 + + oldState, err := terminal.GetState(fd) + if err != nil { + return make([]byte, 0), err + } + + type Buffer struct { + Buffer []byte + Error error + } + errorChannel := make(chan Buffer, 1) + + // SIGINT and SIGTERM restore the terminal, otherwise the no-echo mode would remain intact + interruptChannel := make(chan os.Signal, 1) + signal.Notify(interruptChannel, syscall.SIGINT, syscall.SIGTERM) + defer func() { + signal.Stop(interruptChannel) + close(interruptChannel) + }() + go func() { + for range interruptChannel { + if oldState != nil { + _ = terminal.Restore(fd, oldState) + } + errorChannel <- Buffer{Buffer: make([]byte, 0), Error: ErrInterrupt} + } + }() + + go func() { + buf, err := terminal.ReadPassword(fd) + errorChannel <- Buffer{Buffer: buf, Error: err} + }() + + buf := <-errorChannel + return buf.Buffer, buf.Error +} diff --git a/pkg/util/util_windows.go b/pkg/util/util_windows.go index 1525bdc34..cc431e8ad 100644 --- a/pkg/util/util_windows.go +++ b/pkg/util/util_windows.go @@ -5,9 +5,24 @@ package util import ( "errors" + + terminal "golang.org/x/term" ) // getRuntimeDir returns the runtime directory func GetRuntimeDir() (string, error) { return "", errors.New("this function is not implemented for windows") } + +// ReadPassword reads a password from the terminal. +func ReadPassword(fd int) ([]byte, error) { + oldState, err := terminal.GetState(fd) + if err != nil { + return make([]byte, 0), err + } + buf, err := terminal.ReadPassword(fd) + if oldState != nil { + _ = terminal.Restore(fd, oldState) + } + return buf, err +}