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 }