github.com/containers/podman/v4@v4.9.4/pkg/bindings/connection.go (about)

     1  package bindings
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/blang/semver/v4"
    17  	"github.com/containers/common/pkg/ssh"
    18  	"github.com/containers/podman/v4/version"
    19  	"github.com/sirupsen/logrus"
    20  	"golang.org/x/net/proxy"
    21  )
    22  
    23  type APIResponse struct {
    24  	*http.Response
    25  	Request *http.Request
    26  }
    27  
    28  type Connection struct {
    29  	URI    *url.URL
    30  	Client *http.Client
    31  }
    32  
    33  type valueKey string
    34  
    35  const (
    36  	clientKey  = valueKey("Client")
    37  	versionKey = valueKey("ServiceVersion")
    38  )
    39  
    40  type ConnectError struct {
    41  	Err error
    42  }
    43  
    44  func (c ConnectError) Error() string {
    45  	return "unable to connect to Podman socket: " + c.Err.Error()
    46  }
    47  
    48  func (c ConnectError) Unwrap() error {
    49  	return c.Err
    50  }
    51  
    52  func newConnectError(err error) error {
    53  	return ConnectError{Err: err}
    54  }
    55  
    56  // GetClient from context build by NewConnection()
    57  func GetClient(ctx context.Context) (*Connection, error) {
    58  	if c, ok := ctx.Value(clientKey).(*Connection); ok {
    59  		return c, nil
    60  	}
    61  	return nil, fmt.Errorf("%s not set in context", clientKey)
    62  }
    63  
    64  // ServiceVersion from context build by NewConnection()
    65  func ServiceVersion(ctx context.Context) *semver.Version {
    66  	if v, ok := ctx.Value(versionKey).(*semver.Version); ok {
    67  		return v
    68  	}
    69  	return new(semver.Version)
    70  }
    71  
    72  // JoinURL elements with '/'
    73  func JoinURL(elements ...string) string {
    74  	return "/" + strings.Join(elements, "/")
    75  }
    76  
    77  // NewConnection creates a new service connection without an identity
    78  func NewConnection(ctx context.Context, uri string) (context.Context, error) {
    79  	return NewConnectionWithIdentity(ctx, uri, "", false)
    80  }
    81  
    82  // NewConnectionWithIdentity takes a URI as a string and returns a context with the
    83  // Connection embedded as a value.  This context needs to be passed to each
    84  // endpoint to work correctly.
    85  //
    86  // A valid URI connection should be scheme://
    87  // For example tcp://localhost:<port>
    88  // or unix:///run/podman/podman.sock
    89  // or ssh://<user>@<host>[:port]/run/podman/podman.sock?secure=True
    90  func NewConnectionWithIdentity(ctx context.Context, uri string, identity string, machine bool) (context.Context, error) {
    91  	var (
    92  		err error
    93  	)
    94  	if v, found := os.LookupEnv("CONTAINER_HOST"); found && uri == "" {
    95  		uri = v
    96  	}
    97  
    98  	if v, found := os.LookupEnv("CONTAINER_SSHKEY"); found && len(identity) == 0 {
    99  		identity = v
   100  	}
   101  
   102  	_url, err := url.Parse(uri)
   103  	if err != nil {
   104  		return nil, fmt.Errorf("value of CONTAINER_HOST is not a valid url: %s: %w", uri, err)
   105  	}
   106  
   107  	// Now we set up the http Client to use the connection above
   108  	var connection Connection
   109  	switch _url.Scheme {
   110  	case "ssh":
   111  		port := 22
   112  		if _url.Port() != "" {
   113  			port, err = strconv.Atoi(_url.Port())
   114  			if err != nil {
   115  				return nil, err
   116  			}
   117  		}
   118  		conn, err := ssh.Dial(&ssh.ConnectionDialOptions{
   119  			Host:                        uri,
   120  			Identity:                    identity,
   121  			User:                        _url.User,
   122  			Port:                        port,
   123  			InsecureIsMachineConnection: machine,
   124  		}, "golang")
   125  		if err != nil {
   126  			return nil, newConnectError(err)
   127  		}
   128  		connection = Connection{URI: _url}
   129  		connection.Client = &http.Client{
   130  			Transport: &http.Transport{
   131  				DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
   132  					return ssh.DialNet(conn, "unix", _url)
   133  				},
   134  			}}
   135  	case "unix":
   136  		if !strings.HasPrefix(uri, "unix:///") {
   137  			// autofix unix://path_element vs unix:///path_element
   138  			_url.Path = JoinURL(_url.Host, _url.Path)
   139  			_url.Host = ""
   140  		}
   141  		connection = unixClient(_url)
   142  	case "tcp":
   143  		if !strings.HasPrefix(uri, "tcp://") {
   144  			return nil, errors.New("tcp URIs should begin with tcp://")
   145  		}
   146  		conn, err := tcpClient(_url)
   147  		if err != nil {
   148  			return nil, newConnectError(err)
   149  		}
   150  		connection = conn
   151  	default:
   152  		return nil, fmt.Errorf("unable to create connection. %q is not a supported schema", _url.Scheme)
   153  	}
   154  
   155  	ctx = context.WithValue(ctx, clientKey, &connection)
   156  	serviceVersion, err := pingNewConnection(ctx)
   157  	if err != nil {
   158  		return nil, newConnectError(err)
   159  	}
   160  	ctx = context.WithValue(ctx, versionKey, serviceVersion)
   161  	return ctx, nil
   162  }
   163  
   164  func tcpClient(_url *url.URL) (Connection, error) {
   165  	connection := Connection{
   166  		URI: _url,
   167  	}
   168  	dialContext := func(ctx context.Context, _, _ string) (net.Conn, error) {
   169  		return net.Dial("tcp", _url.Host)
   170  	}
   171  	// use proxy if env `CONTAINER_PROXY` set
   172  	if proxyURI, found := os.LookupEnv("CONTAINER_PROXY"); found {
   173  		proxyURL, err := url.Parse(proxyURI)
   174  		if err != nil {
   175  			return connection, fmt.Errorf("value of CONTAINER_PROXY is not a valid url: %s: %w", proxyURI, err)
   176  		}
   177  		proxyDialer, err := proxy.FromURL(proxyURL, proxy.Direct)
   178  		if err != nil {
   179  			return connection, fmt.Errorf("unable to dial to proxy %s, %w", proxyURI, err)
   180  		}
   181  		dialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
   182  			logrus.Debugf("use proxy %s, but proxy dialer does not support dial timeout", proxyURI)
   183  			return proxyDialer.Dial("tcp", _url.Host)
   184  		}
   185  		if f, ok := proxyDialer.(proxy.ContextDialer); ok {
   186  			dialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
   187  				// the default tcp dial timeout seems to be 75s, podman-remote will retry 3 times before exit.
   188  				// here we change proxy dial timeout to 3s
   189  				logrus.Debugf("use proxy %s with dial timeout 3s", proxyURI)
   190  				ctx, cancel := context.WithTimeout(ctx, time.Second*3)
   191  				defer cancel() // It's safe to cancel, `f.DialContext` only use ctx for returning the Conn, not the lifetime of the Conn.
   192  				return f.DialContext(ctx, "tcp", _url.Host)
   193  			}
   194  		}
   195  	}
   196  	connection.Client = &http.Client{
   197  		Transport: &http.Transport{
   198  			DialContext:        dialContext,
   199  			DisableCompression: true,
   200  		},
   201  	}
   202  	return connection, nil
   203  }
   204  
   205  // pingNewConnection pings to make sure the RESTFUL service is up
   206  // and running. it should only be used when initializing a connection
   207  func pingNewConnection(ctx context.Context) (*semver.Version, error) {
   208  	client, err := GetClient(ctx)
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	// the ping endpoint sits at / in this case
   213  	response, err := client.DoRequest(ctx, nil, http.MethodGet, "/_ping", nil, nil)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	defer response.Body.Close()
   218  
   219  	if response.StatusCode == http.StatusOK {
   220  		versionHdr := response.Header.Get("Libpod-API-Version")
   221  		if versionHdr == "" {
   222  			logrus.Warn("Service did not provide Libpod-API-Version Header")
   223  			return new(semver.Version), nil
   224  		}
   225  		versionSrv, err := semver.ParseTolerant(versionHdr)
   226  		if err != nil {
   227  			return nil, err
   228  		}
   229  
   230  		switch version.APIVersion[version.Libpod][version.MinimalAPI].Compare(versionSrv) {
   231  		case -1, 0:
   232  			// Server's job when Client version is equal or older
   233  			return &versionSrv, nil
   234  		case 1:
   235  			return nil, fmt.Errorf("server API version is too old. Client %q server %q",
   236  				version.APIVersion[version.Libpod][version.MinimalAPI].String(), versionSrv.String())
   237  		}
   238  	}
   239  	return nil, fmt.Errorf("ping response was %d", response.StatusCode)
   240  }
   241  
   242  func unixClient(_url *url.URL) Connection {
   243  	connection := Connection{URI: _url}
   244  	connection.Client = &http.Client{
   245  		Transport: &http.Transport{
   246  			DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
   247  				return (&net.Dialer{}).DialContext(ctx, "unix", _url.Path)
   248  			},
   249  			DisableCompression: true,
   250  		},
   251  	}
   252  	return connection
   253  }
   254  
   255  // DoRequest assembles the http request and returns the response.
   256  // The caller must close the response body.
   257  func (c *Connection) DoRequest(ctx context.Context, httpBody io.Reader, httpMethod, endpoint string, queryParams url.Values, headers http.Header, pathValues ...string) (*APIResponse, error) {
   258  	var (
   259  		err      error
   260  		response *http.Response
   261  	)
   262  
   263  	params := make([]interface{}, len(pathValues)+1)
   264  
   265  	if v := headers.Values("API-Version"); len(v) > 0 {
   266  		params[0] = v[0]
   267  	} else {
   268  		// Including the semver suffices breaks older services... so do not include them
   269  		v := version.APIVersion[version.Libpod][version.CurrentAPI]
   270  		params[0] = fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
   271  	}
   272  
   273  	for i, pv := range pathValues {
   274  		// url.URL lacks the semantics for escaping embedded path parameters... so we manually
   275  		//   escape each one and assume the caller included the correct formatting in "endpoint"
   276  		params[i+1] = url.PathEscape(pv)
   277  	}
   278  
   279  	uri := fmt.Sprintf("http://d/v%s/libpod"+endpoint, params...)
   280  	logrus.Debugf("DoRequest Method: %s URI: %v", httpMethod, uri)
   281  
   282  	req, err := http.NewRequestWithContext(ctx, httpMethod, uri, httpBody)
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	if len(queryParams) > 0 {
   287  		req.URL.RawQuery = queryParams.Encode()
   288  	}
   289  
   290  	for key, val := range headers {
   291  		if key == "API-Version" {
   292  			continue
   293  		}
   294  
   295  		for _, v := range val {
   296  			req.Header.Add(key, v)
   297  		}
   298  	}
   299  
   300  	// Give the Do three chances in the case of a comm/service hiccup
   301  	for i := 1; i <= 3; i++ {
   302  		response, err = c.Client.Do(req) //nolint:bodyclose // The caller has to close the body.
   303  		if err == nil {
   304  			break
   305  		}
   306  		time.Sleep(time.Duration(i*100) * time.Millisecond)
   307  	}
   308  	return &APIResponse{response, req}, err
   309  }
   310  
   311  // GetDialer returns raw Transport.DialContext from client
   312  func (c *Connection) GetDialer(ctx context.Context) (net.Conn, error) {
   313  	client := c.Client
   314  	transport := client.Transport.(*http.Transport)
   315  	if transport.DialContext != nil && transport.TLSClientConfig == nil {
   316  		return transport.DialContext(ctx, c.URI.Scheme, c.URI.String())
   317  	}
   318  
   319  	return nil, errors.New("unable to get dial context")
   320  }
   321  
   322  // IsInformational returns true if the response code is 1xx
   323  func (h *APIResponse) IsInformational() bool {
   324  	return h.Response.StatusCode/100 == 1
   325  }
   326  
   327  // IsSuccess returns true if the response code is 2xx
   328  func (h *APIResponse) IsSuccess() bool {
   329  	return h.Response.StatusCode/100 == 2
   330  }
   331  
   332  // IsRedirection returns true if the response code is 3xx
   333  func (h *APIResponse) IsRedirection() bool {
   334  	return h.Response.StatusCode/100 == 3
   335  }
   336  
   337  // IsClientError returns true if the response code is 4xx
   338  func (h *APIResponse) IsClientError() bool {
   339  	return h.Response.StatusCode/100 == 4
   340  }
   341  
   342  // IsConflictError returns true if the response code is 409
   343  func (h *APIResponse) IsConflictError() bool {
   344  	return h.Response.StatusCode == 409
   345  }
   346  
   347  // IsServerError returns true if the response code is 5xx
   348  func (h *APIResponse) IsServerError() bool {
   349  	return h.Response.StatusCode/100 == 5
   350  }