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 }