ConfigureAuth used the readInput() utility to read the username and password. However, this utility did not return errors it encountered, but instead did an os.Exit(1). A result of this was that the terminal was not restored if an error happened. When reading the password, the terminal is configured to disable echo (i.e. characters are not printed), and failing to restore the previous state means that the terminal is now "non-functional". This patch: - changes readInput() to return errors it encounters - uses a defer() to restore terminal state Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
199 lines
6.5 KiB
Go
199 lines
6.5 KiB
Go
package command
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
|
|
configtypes "github.com/docker/cli/cli/config/types"
|
|
"github.com/docker/cli/cli/streams"
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/docker/docker/api/types"
|
|
registrytypes "github.com/docker/docker/api/types/registry"
|
|
"github.com/docker/docker/registry"
|
|
"github.com/moby/term"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload
|
|
func EncodeAuthToBase64(authConfig registrytypes.AuthConfig) (string, error) {
|
|
buf, err := json.Marshal(authConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.URLEncoding.EncodeToString(buf), nil
|
|
}
|
|
|
|
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
|
|
// for the given command.
|
|
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
|
|
return func() (string, error) {
|
|
fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName)
|
|
indexServer := registry.GetAuthConfigKey(index)
|
|
isDefaultRegistry := indexServer == registry.IndexServer
|
|
authConfig, err := GetDefaultAuthConfig(cli, true, indexServer, isDefaultRegistry)
|
|
if err != nil {
|
|
fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
|
|
}
|
|
err = ConfigureAuth(cli, "", "", &authConfig, isDefaultRegistry)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return EncodeAuthToBase64(authConfig)
|
|
}
|
|
}
|
|
|
|
// ResolveAuthConfig returns auth-config for the given registry from the
|
|
// credential-store. It returns an empty AuthConfig if no credentials were
|
|
// found.
|
|
//
|
|
// It is similar to [registry.ResolveAuthConfig], but uses the credentials-
|
|
// store, instead of looking up credentials from a map.
|
|
func ResolveAuthConfig(_ context.Context, cli Cli, index *registrytypes.IndexInfo) registrytypes.AuthConfig {
|
|
configKey := index.Name
|
|
if index.Official {
|
|
configKey = registry.IndexServer
|
|
}
|
|
|
|
a, _ := cli.ConfigFile().GetAuthConfig(configKey)
|
|
return registrytypes.AuthConfig(a)
|
|
}
|
|
|
|
// GetDefaultAuthConfig gets the default auth config given a serverAddress
|
|
// If credentials for given serverAddress exists in the credential store, the configuration will be populated with values in it
|
|
func GetDefaultAuthConfig(cli Cli, checkCredStore bool, serverAddress string, isDefaultRegistry bool) (registrytypes.AuthConfig, error) {
|
|
if !isDefaultRegistry {
|
|
serverAddress = registry.ConvertToHostname(serverAddress)
|
|
}
|
|
authconfig := configtypes.AuthConfig{}
|
|
var err error
|
|
if checkCredStore {
|
|
authconfig, err = cli.ConfigFile().GetAuthConfig(serverAddress)
|
|
if err != nil {
|
|
return registrytypes.AuthConfig{
|
|
ServerAddress: serverAddress,
|
|
}, err
|
|
}
|
|
}
|
|
authconfig.ServerAddress = serverAddress
|
|
authconfig.IdentityToken = ""
|
|
res := registrytypes.AuthConfig(authconfig)
|
|
return res, nil
|
|
}
|
|
|
|
// ConfigureAuth handles prompting of user's username and password if needed
|
|
func ConfigureAuth(cli Cli, flUser, flPassword string, authconfig *registrytypes.AuthConfig, isDefaultRegistry bool) error {
|
|
// On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210
|
|
if runtime.GOOS == "windows" {
|
|
cli.SetIn(streams.NewIn(os.Stdin))
|
|
}
|
|
|
|
// Some links documenting this:
|
|
// - https://code.google.com/archive/p/mintty/issues/56
|
|
// - https://github.com/docker/docker/issues/15272
|
|
// - https://mintty.github.io/ (compatibility)
|
|
// Linux will hit this if you attempt `cat | docker login`, and Windows
|
|
// will hit this if you attempt docker login from mintty where stdin
|
|
// is a pipe, not a character based console.
|
|
if flPassword == "" && !cli.In().IsTerminal() {
|
|
return errors.Errorf("Error: Cannot perform an interactive login from a non TTY device")
|
|
}
|
|
|
|
authconfig.Username = strings.TrimSpace(authconfig.Username)
|
|
|
|
if flUser = strings.TrimSpace(flUser); flUser == "" {
|
|
if isDefaultRegistry {
|
|
// if this is a default registry (docker hub), then display the following message.
|
|
fmt.Fprintln(cli.Out(), "Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.")
|
|
}
|
|
promptWithDefault(cli.Out(), "Username", authconfig.Username)
|
|
var err error
|
|
flUser, err = readInput(cli.In())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
flUser = strings.TrimSpace(flUser)
|
|
if flUser == "" {
|
|
flUser = authconfig.Username
|
|
}
|
|
}
|
|
if flUser == "" {
|
|
return errors.Errorf("Error: Non-null Username Required")
|
|
}
|
|
if flPassword == "" {
|
|
oldState, err := term.SaveState(cli.In().FD())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(cli.Out(), "Password: ")
|
|
_ = term.DisableEcho(cli.In().FD(), oldState)
|
|
defer func() {
|
|
_ = term.RestoreTerminal(cli.In().FD(), oldState)
|
|
}()
|
|
flPassword, err = readInput(cli.In())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprint(cli.Out(), "\n")
|
|
if flPassword == "" {
|
|
return errors.Errorf("Error: Password Required")
|
|
}
|
|
}
|
|
|
|
authconfig.Username = flUser
|
|
authconfig.Password = flPassword
|
|
|
|
return nil
|
|
}
|
|
|
|
// readInput reads, and returns user input from in. It tries to return a
|
|
// single line, not including the end-of-line bytes.
|
|
func readInput(in io.Reader) (string, error) {
|
|
line, _, err := bufio.NewReader(in).ReadLine()
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "error while reading input")
|
|
}
|
|
return string(line), nil
|
|
}
|
|
|
|
func promptWithDefault(out io.Writer, prompt string, configDefault string) {
|
|
if configDefault == "" {
|
|
fmt.Fprintf(out, "%s: ", prompt)
|
|
} else {
|
|
fmt.Fprintf(out, "%s (%s): ", prompt, configDefault)
|
|
}
|
|
}
|
|
|
|
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image
|
|
func RetrieveAuthTokenFromImage(ctx context.Context, cli Cli, image string) (string, error) {
|
|
// Retrieve encoded auth token from the image reference
|
|
authConfig, err := resolveAuthConfigFromImage(ctx, cli, image)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
encodedAuth, err := EncodeAuthToBase64(authConfig)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return encodedAuth, nil
|
|
}
|
|
|
|
// resolveAuthConfigFromImage retrieves that AuthConfig using the image string
|
|
func resolveAuthConfigFromImage(ctx context.Context, cli Cli, image string) (registrytypes.AuthConfig, error) {
|
|
registryRef, err := reference.ParseNormalizedNamed(image)
|
|
if err != nil {
|
|
return registrytypes.AuthConfig{}, err
|
|
}
|
|
repoInfo, err := registry.ParseRepositoryInfo(registryRef)
|
|
if err != nil {
|
|
return registrytypes.AuthConfig{}, err
|
|
}
|
|
return ResolveAuthConfig(ctx, cli, repoInfo.Index), nil
|
|
}
|