github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/pkg/bindings/connection.go (about)

     1  package bindings
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/blang/semver"
    16  	"github.com/containers/podman/v2/pkg/terminal"
    17  	jsoniter "github.com/json-iterator/go"
    18  	"github.com/pkg/errors"
    19  	"github.com/sirupsen/logrus"
    20  	"golang.org/x/crypto/ssh"
    21  	"golang.org/x/crypto/ssh/agent"
    22  )
    23  
    24  var (
    25  	BasePath = &url.URL{
    26  		Scheme: "http",
    27  		Host:   "d",
    28  		Path:   "/v" + APIVersion.String() + "/libpod",
    29  	}
    30  )
    31  
    32  type APIResponse struct {
    33  	*http.Response
    34  	Request *http.Request
    35  }
    36  
    37  type Connection struct {
    38  	URI    *url.URL
    39  	Client *http.Client
    40  }
    41  
    42  type valueKey string
    43  
    44  const (
    45  	clientKey = valueKey("Client")
    46  )
    47  
    48  // GetClient from context build by NewConnection()
    49  func GetClient(ctx context.Context) (*Connection, error) {
    50  	c, ok := ctx.Value(clientKey).(*Connection)
    51  	if !ok {
    52  		return nil, errors.Errorf("ClientKey not set in context")
    53  	}
    54  	return c, nil
    55  }
    56  
    57  // JoinURL elements with '/'
    58  func JoinURL(elements ...string) string {
    59  	return "/" + strings.Join(elements, "/")
    60  }
    61  
    62  func NewConnection(ctx context.Context, uri string) (context.Context, error) {
    63  	return NewConnectionWithIdentity(ctx, uri, "")
    64  }
    65  
    66  // NewConnection takes a URI as a string and returns a context with the
    67  // Connection embedded as a value.  This context needs to be passed to each
    68  // endpoint to work correctly.
    69  //
    70  // A valid URI connection should be scheme://
    71  // For example tcp://localhost:<port>
    72  // or unix:///run/podman/podman.sock
    73  // or ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True
    74  func NewConnectionWithIdentity(ctx context.Context, uri string, identity string) (context.Context, error) {
    75  	var (
    76  		err    error
    77  		secure bool
    78  	)
    79  	if v, found := os.LookupEnv("CONTAINER_HOST"); found && uri == "" {
    80  		uri = v
    81  	}
    82  
    83  	if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found && len(identity) == 0 {
    84  		identity = v
    85  	}
    86  
    87  	passPhrase := ""
    88  	if v, found := os.LookupEnv("CONTAINER_PASSPHRASE"); found {
    89  		passPhrase = v
    90  	}
    91  
    92  	_url, err := url.Parse(uri)
    93  	if err != nil {
    94  		return nil, errors.Wrapf(err, "Value of CONTAINER_HOST is not a valid url: %s", uri)
    95  	}
    96  
    97  	// Now we setup the http Client to use the connection above
    98  	var connection Connection
    99  	switch _url.Scheme {
   100  	case "ssh":
   101  		secure, err = strconv.ParseBool(_url.Query().Get("secure"))
   102  		if err != nil {
   103  			secure = false
   104  		}
   105  		connection, err = sshClient(_url, secure, passPhrase, identity)
   106  	case "unix":
   107  		if !strings.HasPrefix(uri, "unix:///") {
   108  			// autofix unix://path_element vs unix:///path_element
   109  			_url.Path = JoinURL(_url.Host, _url.Path)
   110  			_url.Host = ""
   111  		}
   112  		connection = unixClient(_url)
   113  	case "tcp":
   114  		if !strings.HasPrefix(uri, "tcp://") {
   115  			return nil, errors.New("tcp URIs should begin with tcp://")
   116  		}
   117  		connection = tcpClient(_url)
   118  	default:
   119  		return nil, errors.Errorf("unable to create connection. %q is not a supported schema", _url.Scheme)
   120  	}
   121  	if err != nil {
   122  		return nil, errors.Wrapf(err, "failed to create %sClient", _url.Scheme)
   123  	}
   124  
   125  	ctx = context.WithValue(ctx, clientKey, &connection)
   126  	if err := pingNewConnection(ctx); err != nil {
   127  		return nil, err
   128  	}
   129  	return ctx, nil
   130  }
   131  
   132  func tcpClient(_url *url.URL) Connection {
   133  	connection := Connection{
   134  		URI: _url,
   135  	}
   136  	connection.Client = &http.Client{
   137  		Transport: &http.Transport{
   138  			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
   139  				return net.Dial("tcp", _url.Host)
   140  			},
   141  			DisableCompression: true,
   142  		},
   143  	}
   144  	return connection
   145  }
   146  
   147  // pingNewConnection pings to make sure the RESTFUL service is up
   148  // and running. it should only be used when initializing a connection
   149  func pingNewConnection(ctx context.Context) error {
   150  	client, err := GetClient(ctx)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	// the ping endpoint sits at / in this case
   155  	response, err := client.DoRequest(nil, http.MethodGet, "/_ping", nil, nil)
   156  	if err != nil {
   157  		return err
   158  	}
   159  
   160  	if response.StatusCode == http.StatusOK {
   161  		versionHdr := response.Header.Get("Libpod-API-Version")
   162  		if versionHdr == "" {
   163  			logrus.Info("Service did not provide Libpod-API-Version Header")
   164  			return nil
   165  		}
   166  		versionSrv, err := semver.ParseTolerant(versionHdr)
   167  		if err != nil {
   168  			return err
   169  		}
   170  
   171  		switch APIVersion.Compare(versionSrv) {
   172  		case -1, 0:
   173  			// Server's job when Client version is equal or older
   174  			return nil
   175  		case 1:
   176  			return errors.Errorf("server API version is too old. Client %q server %q", APIVersion.String(), versionSrv.String())
   177  		}
   178  	}
   179  	return errors.Errorf("ping response was %q", response.StatusCode)
   180  }
   181  
   182  func sshClient(_url *url.URL, secure bool, passPhrase string, identity string) (Connection, error) {
   183  	// if you modify the authmethods or their conditionals, you will also need to make similar
   184  	// changes in the client (currently cmd/podman/system/connection/add getUDS).
   185  	authMethods := []ssh.AuthMethod{}
   186  	if len(identity) > 0 {
   187  		auth, err := terminal.PublicKey(identity, []byte(passPhrase))
   188  		if err != nil {
   189  			return Connection{}, errors.Wrapf(err, "failed to parse identity %q", identity)
   190  		}
   191  		logrus.Debugf("public key signer enabled for identity %q", identity)
   192  		authMethods = append(authMethods, auth)
   193  	}
   194  
   195  	if sock, found := os.LookupEnv("SSH_AUTH_SOCK"); found {
   196  		logrus.Debugf("Found SSH_AUTH_SOCK %q, ssh-agent signer enabled", sock)
   197  
   198  		c, err := net.Dial("unix", sock)
   199  		if err != nil {
   200  			return Connection{}, err
   201  		}
   202  		a := agent.NewClient(c)
   203  		authMethods = append(authMethods, ssh.PublicKeysCallback(a.Signers))
   204  	}
   205  
   206  	if pw, found := _url.User.Password(); found {
   207  		authMethods = append(authMethods, ssh.Password(pw))
   208  	}
   209  	if len(authMethods) == 0 {
   210  		callback := func() (string, error) {
   211  			pass, err := terminal.ReadPassword("Login password:")
   212  			return string(pass), err
   213  		}
   214  		authMethods = append(authMethods, ssh.PasswordCallback(callback))
   215  	}
   216  
   217  	port := _url.Port()
   218  	if port == "" {
   219  		port = "22"
   220  	}
   221  
   222  	callback := ssh.InsecureIgnoreHostKey()
   223  	if secure {
   224  		host := _url.Hostname()
   225  		if port != "22" {
   226  			host = fmt.Sprintf("[%s]:%s", host, port)
   227  		}
   228  		key := terminal.HostKey(host)
   229  		if key != nil {
   230  			callback = ssh.FixedHostKey(key)
   231  		}
   232  	}
   233  
   234  	bastion, err := ssh.Dial("tcp",
   235  		net.JoinHostPort(_url.Hostname(), port),
   236  		&ssh.ClientConfig{
   237  			User:            _url.User.Username(),
   238  			Auth:            authMethods,
   239  			HostKeyCallback: callback,
   240  			HostKeyAlgorithms: []string{
   241  				ssh.KeyAlgoRSA,
   242  				ssh.KeyAlgoDSA,
   243  				ssh.KeyAlgoECDSA256,
   244  				ssh.KeyAlgoECDSA384,
   245  				ssh.KeyAlgoECDSA521,
   246  				ssh.KeyAlgoED25519,
   247  			},
   248  			Timeout: 5 * time.Second,
   249  		},
   250  	)
   251  	if err != nil {
   252  		return Connection{}, errors.Wrapf(err, "Connection to bastion host (%s) failed.", _url.String())
   253  	}
   254  
   255  	connection := Connection{URI: _url}
   256  	connection.Client = &http.Client{
   257  		Transport: &http.Transport{
   258  			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
   259  				return bastion.Dial("unix", _url.Path)
   260  			},
   261  		}}
   262  	return connection, nil
   263  }
   264  
   265  func unixClient(_url *url.URL) Connection {
   266  	connection := Connection{URI: _url}
   267  	connection.Client = &http.Client{
   268  		Transport: &http.Transport{
   269  			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
   270  				return (&net.Dialer{}).DialContext(ctx, "unix", _url.Path)
   271  			},
   272  			DisableCompression: true,
   273  		},
   274  	}
   275  	return connection
   276  }
   277  
   278  // DoRequest assembles the http request and returns the response
   279  func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string, queryParams url.Values, header map[string]string, pathValues ...string) (*APIResponse, error) {
   280  	var (
   281  		err      error
   282  		response *http.Response
   283  	)
   284  	safePathValues := make([]interface{}, len(pathValues))
   285  	// Make sure path values are http url safe
   286  	for i, pv := range pathValues {
   287  		safePathValues[i] = url.PathEscape(pv)
   288  	}
   289  	// Lets eventually use URL for this which might lead to safer
   290  	// usage
   291  	safeEndpoint := fmt.Sprintf(endpoint, safePathValues...)
   292  	e := BasePath.String() + safeEndpoint
   293  	req, err := http.NewRequest(httpMethod, e, httpBody)
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  	if len(queryParams) > 0 {
   298  		req.URL.RawQuery = queryParams.Encode()
   299  	}
   300  	for key, val := range header {
   301  		req.Header.Set(key, val)
   302  	}
   303  	req = req.WithContext(context.WithValue(context.Background(), clientKey, c))
   304  	// Give the Do three chances in the case of a comm/service hiccup
   305  	for i := 0; i < 3; i++ {
   306  		response, err = c.Client.Do(req) // nolint
   307  		if err == nil {
   308  			break
   309  		}
   310  		time.Sleep(time.Duration(i*100) * time.Millisecond)
   311  	}
   312  	return &APIResponse{response, req}, err
   313  }
   314  
   315  // FiltersToString converts our typical filter format of a
   316  // map[string][]string to a query/html safe string.
   317  func FiltersToString(filters map[string][]string) (string, error) {
   318  	lowerCaseKeys := make(map[string][]string)
   319  	for k, v := range filters {
   320  		lowerCaseKeys[strings.ToLower(k)] = v
   321  	}
   322  	return jsoniter.MarshalToString(lowerCaseKeys)
   323  }
   324  
   325  // IsInformation returns true if the response code is 1xx
   326  func (h *APIResponse) IsInformational() bool {
   327  	return h.Response.StatusCode/100 == 1
   328  }
   329  
   330  // IsSuccess returns true if the response code is 2xx
   331  func (h *APIResponse) IsSuccess() bool {
   332  	return h.Response.StatusCode/100 == 2
   333  }
   334  
   335  // IsRedirection returns true if the response code is 3xx
   336  func (h *APIResponse) IsRedirection() bool {
   337  	return h.Response.StatusCode/100 == 3
   338  }
   339  
   340  // IsClientError returns true if the response code is 4xx
   341  func (h *APIResponse) IsClientError() bool {
   342  	return h.Response.StatusCode/100 == 4
   343  }
   344  
   345  // IsServerError returns true if the response code is 5xx
   346  func (h *APIResponse) IsServerError() bool {
   347  	return h.Response.StatusCode/100 == 5
   348  }