github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/cmd/docker_init.go (about)

     1  package cmd
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"encoding/base64"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"strings"
    14  
    15  	dockerClient "github.com/docker/docker/client"
    16  	"golang.org/x/crypto/ssh"
    17  	"golang.org/x/term"
    18  
    19  	"github.com/buildpacks/pack/internal/sshdialer"
    20  	"github.com/buildpacks/pack/pkg/client"
    21  )
    22  
    23  func tryInitSSHDockerClient() (dockerClient.CommonAPIClient, error) {
    24  	dockerHost := os.Getenv("DOCKER_HOST")
    25  	_url, err := url.Parse(dockerHost)
    26  	isSSH := err == nil && _url.Scheme == "ssh"
    27  
    28  	if !isSSH {
    29  		return nil, nil
    30  	}
    31  
    32  	credentialsConfig := sshdialer.Config{
    33  		Identity:           os.Getenv("DOCKER_HOST_SSH_IDENTITY"),
    34  		PassPhrase:         os.Getenv("DOCKER_HOST_SSH_IDENTITY_PASSPHRASE"),
    35  		PasswordCallback:   newReadSecretCbk("please enter password:"),
    36  		PassPhraseCallback: newReadSecretCbk("please enter passphrase to private key:"),
    37  		HostKeyCallback:    newHostKeyCbk(),
    38  	}
    39  	dialContext, err := sshdialer.NewDialContext(_url, credentialsConfig)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	httpClient := &http.Client{
    45  		// No tls
    46  		// No proxy
    47  		Transport: &http.Transport{
    48  			DialContext: dialContext,
    49  		},
    50  	}
    51  
    52  	dockerClientOpts := []dockerClient.Opt{
    53  		dockerClient.WithVersion(client.DockerAPIVersion),
    54  		dockerClient.WithHTTPClient(httpClient),
    55  		dockerClient.WithHost("http://dummy"),
    56  		dockerClient.WithDialContext(dialContext),
    57  	}
    58  
    59  	return dockerClient.NewClientWithOpts(dockerClientOpts...)
    60  }
    61  
    62  // readSecret prompts for a secret and returns value input by user from stdin
    63  // Unlike terminal.ReadPassword(), $(echo $SECRET | podman...) is supported.
    64  // Additionally, all input after `<secret>/n` is queued to podman command.
    65  //
    66  // NOTE: this code is based on "github.com/containers/podman/v3/pkg/terminal"
    67  func readSecret(prompt string) (pw []byte, err error) {
    68  	fd := int(os.Stdin.Fd())
    69  	if term.IsTerminal(fd) {
    70  		fmt.Fprint(os.Stderr, prompt)
    71  		pw, err = term.ReadPassword(fd)
    72  		fmt.Fprintln(os.Stderr)
    73  		return
    74  	}
    75  
    76  	var b [1]byte
    77  	for {
    78  		n, err := os.Stdin.Read(b[:])
    79  		// terminal.readSecret discards any '\r', so we do the same
    80  		if n > 0 && b[0] != '\r' {
    81  			if b[0] == '\n' {
    82  				return pw, nil
    83  			}
    84  			pw = append(pw, b[0])
    85  			// limit size, so that a wrong input won't fill up the memory
    86  			if len(pw) > 1024 {
    87  				err = errors.New("password too long, 1024 byte limit")
    88  			}
    89  		}
    90  		if err != nil {
    91  			// terminal.readSecret accepts EOF-terminated passwords
    92  			// if non-empty, so we do the same
    93  			if err == io.EOF && len(pw) > 0 {
    94  				err = nil
    95  			}
    96  			return pw, err
    97  		}
    98  	}
    99  }
   100  
   101  func newReadSecretCbk(prompt string) sshdialer.SecretCallback {
   102  	var secretSet bool
   103  	var secret string
   104  	return func() (string, error) {
   105  		if secretSet {
   106  			return secret, nil
   107  		}
   108  
   109  		p, err := readSecret(prompt)
   110  		if err != nil {
   111  			return "", err
   112  		}
   113  		secretSet = true
   114  		secret = string(p)
   115  
   116  		return secret, err
   117  	}
   118  }
   119  
   120  func newHostKeyCbk() sshdialer.HostKeyCallback {
   121  	var trust []byte
   122  	return func(hostPort string, pubKey ssh.PublicKey) error {
   123  		if bytes.Equal(trust, pubKey.Marshal()) {
   124  			return nil
   125  		}
   126  		msg := `The authenticity of host %s cannot be established.
   127  %s key fingerprint is %s
   128  Are you sure you want to continue connecting (yes/no)? `
   129  		fmt.Fprintf(os.Stderr, msg, hostPort, pubKey.Type(), ssh.FingerprintSHA256(pubKey))
   130  		reader := bufio.NewReader(os.Stdin)
   131  		answer, err := reader.ReadString('\n')
   132  		if err != nil {
   133  			return err
   134  		}
   135  		answer = strings.TrimRight(answer, "\r\n")
   136  		answer = strings.ToLower(answer)
   137  
   138  		if answer == "yes" || answer == "y" {
   139  			trust = pubKey.Marshal()
   140  			fmt.Fprintf(os.Stderr, "To avoid this in future add following line into your ~/.ssh/known_hosts:\n%s %s %s\n",
   141  				hostPort, pubKey.Type(), base64.StdEncoding.EncodeToString(trust))
   142  			return nil
   143  		}
   144  
   145  		return errors.New("key rejected")
   146  	}
   147  }