github.com/vmware/govmomi@v0.51.0/vim25/soap/client.go (about)

     1  // © Broadcom. All Rights Reserved.
     2  // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package soap
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"context"
    11  	"crypto/sha1"
    12  	"crypto/sha256"
    13  	"crypto/tls"
    14  	"crypto/x509"
    15  	"encoding/json"
    16  	"errors"
    17  	"fmt"
    18  	"io"
    19  	"log"
    20  	"net"
    21  	"net/http"
    22  	"net/http/cookiejar"
    23  	"net/url"
    24  	"os"
    25  	"path"
    26  	"path/filepath"
    27  	"reflect"
    28  	"regexp"
    29  	"runtime"
    30  	"strings"
    31  	"sync"
    32  
    33  	"github.com/vmware/govmomi/internal/version"
    34  	"github.com/vmware/govmomi/vim25/progress"
    35  	"github.com/vmware/govmomi/vim25/types"
    36  	"github.com/vmware/govmomi/vim25/xml"
    37  )
    38  
    39  type HasFault interface {
    40  	Fault() *Fault
    41  }
    42  
    43  type RoundTripper interface {
    44  	RoundTrip(ctx context.Context, req, res HasFault) error
    45  }
    46  
    47  const (
    48  	SessionCookieName = "vmware_soap_session"
    49  )
    50  
    51  // defaultUserAgent is the default user agent string, e.g.
    52  // "govc govmomi/0.28.0 (go1.18.3;linux;amd64)"
    53  var defaultUserAgent = fmt.Sprintf(
    54  	"%s %s/%s (%s)",
    55  	execName(),
    56  	version.ClientName,
    57  	version.ClientVersion,
    58  	strings.Join([]string{runtime.Version(), runtime.GOOS, runtime.GOARCH}, ";"),
    59  )
    60  
    61  type Client struct {
    62  	http.Client
    63  
    64  	u *url.URL
    65  	k bool // Named after curl's -k flag
    66  	d *debugContainer
    67  	t *http.Transport
    68  
    69  	hostsMu sync.Mutex
    70  	hosts   map[string]string
    71  
    72  	Namespace string     `json:"namespace"` // Vim namespace
    73  	Version   string     `json:"version"`   // Vim version
    74  	Types     types.Func `json:"types"`
    75  	UserAgent string     `json:"userAgent"`
    76  
    77  	// Cookie returns a value for the SOAP Header.Cookie.
    78  	// This SOAP request header is used for authentication by
    79  	// API endpoints such as pbm, vslm and sms.
    80  	// When nil, no SOAP Header.Cookie is set.
    81  	Cookie          func() *HeaderElement
    82  	insecureCookies bool
    83  
    84  	useJSON bool
    85  }
    86  
    87  var schemeMatch = regexp.MustCompile(`^\w+://`)
    88  
    89  type errInvalidCACertificate struct {
    90  	File string
    91  }
    92  
    93  func (e errInvalidCACertificate) Error() string {
    94  	return fmt.Sprintf(
    95  		"invalid certificate '%s', cannot be used as a trusted CA certificate",
    96  		e.File,
    97  	)
    98  }
    99  
   100  // ParseURL is wrapper around url.Parse, where Scheme defaults to "https" and Path defaults to "/sdk"
   101  func ParseURL(s string) (*url.URL, error) {
   102  	var err error
   103  	var u *url.URL
   104  
   105  	if s != "" {
   106  		// Default the scheme to https
   107  		if !schemeMatch.MatchString(s) {
   108  			s = "https://" + s
   109  		}
   110  
   111  		s := strings.TrimSuffix(s, "/")
   112  		u, err = url.Parse(s)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  
   117  		// Default the path to /sdk
   118  		if u.Path == "" {
   119  			u.Path = "/sdk"
   120  		}
   121  
   122  		if u.User == nil {
   123  			u.User = url.UserPassword("", "")
   124  		}
   125  	}
   126  
   127  	return u, nil
   128  }
   129  
   130  // Go's ForceAttemptHTTP2 default is true, we disable by default.
   131  // This undocumented env var can be used to enable.
   132  var http2 = os.Getenv("GOVMOMI_HTTP2") == "true"
   133  
   134  func NewClient(u *url.URL, insecure bool) *Client {
   135  	var t *http.Transport
   136  
   137  	if d, ok := http.DefaultTransport.(*http.Transport); ok {
   138  		// Inherit the same defaults explicitly set in http.DefaultTransport,
   139  		// unless otherwise noted.
   140  		t = &http.Transport{
   141  			Proxy:                 d.Proxy,
   142  			DialContext:           d.DialContext,
   143  			ForceAttemptHTTP2:     http2, // false by default in govmomi
   144  			MaxIdleConns:          d.MaxIdleConns,
   145  			IdleConnTimeout:       d.IdleConnTimeout,
   146  			TLSHandshakeTimeout:   d.TLSHandshakeTimeout,
   147  			ExpectContinueTimeout: d.ExpectContinueTimeout,
   148  		}
   149  	} else {
   150  		t = new(http.Transport)
   151  	}
   152  
   153  	t.TLSClientConfig = &tls.Config{
   154  		InsecureSkipVerify: insecure,
   155  	}
   156  
   157  	c := newClientWithTransport(u, insecure, t)
   158  
   159  	// Always set DialTLS and DialTLSContext, even if InsecureSkipVerify=true,
   160  	// because of how certificate verification has been delegated to the host's
   161  	// PKI framework in Go 1.18. Please see the following links for more info:
   162  	//
   163  	//   * https://tip.golang.org/doc/go1.18 (search for "Certificate.Verify")
   164  	//   * https://github.com/square/certigo/issues/264
   165  	t.DialTLSContext = c.dialTLSContext
   166  
   167  	return c
   168  }
   169  
   170  func newClientWithTransport(u *url.URL, insecure bool, t *http.Transport) *Client {
   171  	c := Client{
   172  		u: u,
   173  		k: insecure,
   174  		d: newDebug(),
   175  		t: t,
   176  
   177  		Types: types.TypeFunc(),
   178  	}
   179  
   180  	c.hosts = make(map[string]string)
   181  
   182  	c.Client.Transport = c.t
   183  	c.Client.Jar, _ = cookiejar.New(nil)
   184  
   185  	// Remove user information from a copy of the URL
   186  	c.u = c.URL()
   187  	c.u.User = nil
   188  
   189  	if c.u.Scheme == "http" {
   190  		c.insecureCookies = os.Getenv("GOVMOMI_INSECURE_COOKIES") == "true"
   191  	}
   192  
   193  	return &c
   194  }
   195  
   196  func (c *Client) DefaultTransport() *http.Transport {
   197  	return c.t
   198  }
   199  
   200  // NewServiceClient creates a NewClient with the given URL.Path and namespace.
   201  func (c *Client) NewServiceClient(path string, namespace string) *Client {
   202  	return c.newServiceClientWithTransport(path, namespace, c.t)
   203  }
   204  
   205  func sessionCookie(jar http.CookieJar, u *url.URL) *HeaderElement {
   206  	for _, cookie := range jar.Cookies(u) {
   207  		if cookie.Name == SessionCookieName {
   208  			return &HeaderElement{Value: cookie.Value}
   209  		}
   210  	}
   211  	return nil
   212  }
   213  
   214  // SessionCookie returns a SessionCookie with value of the vmware_soap_session http.Cookie.
   215  func (c *Client) SessionCookie() *HeaderElement {
   216  	u := c.URL()
   217  
   218  	if cookie := sessionCookie(c.Jar, u); cookie != nil {
   219  		return cookie
   220  	}
   221  
   222  	// Default "/sdk" Path would match above,
   223  	// but saw a case of Path == "sdk", where above returns nil.
   224  	// The jar entry Path is normally "/", so fallback to that.
   225  	u.Path = "/"
   226  
   227  	return sessionCookie(c.Jar, u)
   228  }
   229  
   230  func (c *Client) newServiceClientWithTransport(path string, namespace string, t *http.Transport) *Client {
   231  	vc := c.URL()
   232  	u, err := url.Parse(path)
   233  	if err != nil {
   234  		log.Panicf("url.Parse(%q): %s", path, err)
   235  	}
   236  	if u.Host == "" {
   237  		u.Scheme = vc.Scheme
   238  		u.Host = vc.Host
   239  	}
   240  
   241  	client := newClientWithTransport(u, c.k, t)
   242  	client.Namespace = "urn:" + namespace
   243  
   244  	// Copy the trusted thumbprints
   245  	c.hostsMu.Lock()
   246  	for k, v := range c.hosts {
   247  		client.hosts[k] = v
   248  	}
   249  	c.hostsMu.Unlock()
   250  
   251  	// Copy the cookies
   252  	client.Client.Jar.SetCookies(u, c.Client.Jar.Cookies(u))
   253  
   254  	// Copy any query params (e.g. GOVMOMI_TUNNEL_PROXY_PORT used in testing)
   255  	client.u.RawQuery = vc.RawQuery
   256  
   257  	client.UserAgent = c.UserAgent
   258  
   259  	vimTypes := c.Types
   260  	client.Types = func(name string) (reflect.Type, bool) {
   261  		kind, ok := vimTypes(name)
   262  		if ok {
   263  			return kind, ok
   264  		}
   265  		// vim25/xml typeToString() does not have an option to include namespace prefix.
   266  		// Workaround this by re-trying the lookup with the namespace prefix.
   267  		return vimTypes(namespace + ":" + name)
   268  	}
   269  
   270  	return client
   271  }
   272  
   273  // UseJSON changes the protocol between SOAP and JSON. Starting with vCenter
   274  // 8.0.1 JSON over HTTP can be used. Note this method has no locking and clients
   275  // should be careful to not interfere with concurrent use of the client
   276  // instance.
   277  func (c *Client) UseJSON(useJSON bool) {
   278  	c.useJSON = useJSON
   279  }
   280  
   281  // SetRootCAs defines the set of PEM-encoded file locations of root certificate
   282  // authorities the client uses when verifying server certificates instead of the
   283  // TLS defaults which uses the host's root CA set. Multiple PEM file locations
   284  // can be specified using the OS-specific PathListSeparator.
   285  //
   286  // See: http.Client.Transport.TLSClientConfig.RootCAs and
   287  // https://pkg.go.dev/os#PathListSeparator
   288  func (c *Client) SetRootCAs(pemPaths string) error {
   289  	pool := x509.NewCertPool()
   290  
   291  	for _, name := range filepath.SplitList(pemPaths) {
   292  		pem, err := os.ReadFile(filepath.Clean(name))
   293  		if err != nil {
   294  			return err
   295  		}
   296  
   297  		if ok := pool.AppendCertsFromPEM(pem); !ok {
   298  			return errInvalidCACertificate{
   299  				File: name,
   300  			}
   301  		}
   302  	}
   303  
   304  	c.t.TLSClientConfig.RootCAs = pool
   305  
   306  	return nil
   307  }
   308  
   309  // Add default https port if missing
   310  func hostAddr(addr string) string {
   311  	_, port := splitHostPort(addr)
   312  	if port == "" {
   313  		return addr + ":443"
   314  	}
   315  	return addr
   316  }
   317  
   318  // SetThumbprint sets the known certificate thumbprint for the given host.
   319  // A custom DialTLS function is used to support thumbprint based verification.
   320  // We first try tls.Dial with the default tls.Config, only falling back to thumbprint verification
   321  // if it fails with an x509.UnknownAuthorityError or x509.HostnameError
   322  //
   323  // See: http.Client.Transport.DialTLS
   324  func (c *Client) SetThumbprint(host string, thumbprint string) {
   325  	host = hostAddr(host)
   326  
   327  	c.hostsMu.Lock()
   328  	if thumbprint == "" {
   329  		delete(c.hosts, host)
   330  	} else {
   331  		c.hosts[host] = thumbprint
   332  	}
   333  	c.hostsMu.Unlock()
   334  }
   335  
   336  // Thumbprint returns the certificate thumbprint for the given host if known to this client.
   337  func (c *Client) Thumbprint(host string) string {
   338  	host = hostAddr(host)
   339  	c.hostsMu.Lock()
   340  	defer c.hostsMu.Unlock()
   341  	return c.hosts[host]
   342  }
   343  
   344  // KnownThumbprint checks whether the provided thumbprint is known to this client.
   345  func (c *Client) KnownThumbprint(tp string) bool {
   346  	c.hostsMu.Lock()
   347  	defer c.hostsMu.Unlock()
   348  
   349  	for _, v := range c.hosts {
   350  		if v == tp {
   351  			return true
   352  		}
   353  	}
   354  
   355  	return false
   356  }
   357  
   358  // LoadThumbprints from file with the give name.
   359  // If name is empty or name does not exist this function will return nil.
   360  func (c *Client) LoadThumbprints(file string) error {
   361  	if file == "" {
   362  		return nil
   363  	}
   364  
   365  	for _, name := range filepath.SplitList(file) {
   366  		err := c.loadThumbprints(name)
   367  		if err != nil {
   368  			return err
   369  		}
   370  	}
   371  
   372  	return nil
   373  }
   374  
   375  func (c *Client) loadThumbprints(name string) error {
   376  	f, err := os.Open(filepath.Clean(name))
   377  	if err != nil {
   378  		if os.IsNotExist(err) {
   379  			return nil
   380  		}
   381  		return err
   382  	}
   383  
   384  	scanner := bufio.NewScanner(f)
   385  
   386  	for scanner.Scan() {
   387  		e := strings.SplitN(scanner.Text(), " ", 2)
   388  		if len(e) != 2 {
   389  			continue
   390  		}
   391  
   392  		c.SetThumbprint(e[0], e[1])
   393  	}
   394  
   395  	_ = f.Close()
   396  
   397  	return scanner.Err()
   398  }
   399  
   400  var fips140 = strings.Contains(os.Getenv("GODEBUG"), "fips140=only")
   401  
   402  // ThumbprintSHA1 returns the thumbprint of the given cert in the same format used by the SDK and Client.SetThumbprint.
   403  //
   404  // See: SSLVerifyFault.Thumbprint, SessionManagerGenericServiceTicket.Thumbprint, HostConnectSpec.SslThumbprint
   405  // When GODEBUG contains "fips140=only", this function returns an empty string.
   406  func ThumbprintSHA1(cert *x509.Certificate) string {
   407  	if fips140 {
   408  		return ""
   409  	}
   410  	sum := sha1.Sum(cert.Raw)
   411  	hex := make([]string, len(sum))
   412  	for i, b := range sum {
   413  		hex[i] = fmt.Sprintf("%02X", b)
   414  	}
   415  	return strings.Join(hex, ":")
   416  }
   417  
   418  // ThumbprintSHA256 returns the sha256 thumbprint of the given cert.
   419  func ThumbprintSHA256(cert *x509.Certificate) string {
   420  	sum := sha256.Sum256(cert.Raw)
   421  	hex := make([]string, len(sum))
   422  	for i, b := range sum {
   423  		hex[i] = fmt.Sprintf("%02X", b)
   424  	}
   425  	return strings.Join(hex, ":")
   426  }
   427  
   428  func thumbprintMatches(thumbprint string, cert *x509.Certificate) bool {
   429  	return thumbprint == ThumbprintSHA256(cert) || thumbprint == ThumbprintSHA1(cert)
   430  }
   431  
   432  func (c *Client) dialTLSContext(
   433  	ctx context.Context,
   434  	network, addr string) (net.Conn, error) {
   435  
   436  	// Would be nice if there was a tls.Config.Verify func,
   437  	// see tls.clientHandshakeState.doFullHandshake
   438  
   439  	conn, err := tls.Dial(network, addr, c.t.TLSClientConfig)
   440  
   441  	if err == nil {
   442  		return conn, nil
   443  	}
   444  
   445  	// Allow a thumbprint verification attempt if the error indicates
   446  	// the failure was due to lack of trust.
   447  	if !IsCertificateUntrusted(err) {
   448  		return nil, err
   449  	}
   450  
   451  	thumbprint := c.Thumbprint(addr)
   452  	if thumbprint == "" {
   453  		return nil, err
   454  	}
   455  
   456  	config := &tls.Config{InsecureSkipVerify: true}
   457  	conn, err = tls.Dial(network, addr, config)
   458  	if err != nil {
   459  		return nil, err
   460  	}
   461  
   462  	cert := conn.ConnectionState().PeerCertificates[0]
   463  	if thumbprintMatches(thumbprint, cert) {
   464  		return conn, nil
   465  	}
   466  
   467  	_ = conn.Close()
   468  
   469  	return nil, fmt.Errorf("host %q thumbprint does not match %q", addr, thumbprint)
   470  }
   471  
   472  // splitHostPort is similar to net.SplitHostPort,
   473  // but rather than return error if there isn't a ':port',
   474  // return an empty string for the port.
   475  func splitHostPort(host string) (string, string) {
   476  	ix := strings.LastIndex(host, ":")
   477  
   478  	if ix <= strings.LastIndex(host, "]") {
   479  		return host, ""
   480  	}
   481  
   482  	name := host[:ix]
   483  	port := host[ix+1:]
   484  
   485  	return name, port
   486  }
   487  
   488  const sdkTunnel = "sdkTunnel:8089"
   489  
   490  // Certificate returns the current TLS certificate.
   491  func (c *Client) Certificate() *tls.Certificate {
   492  	certs := c.t.TLSClientConfig.Certificates
   493  	if len(certs) == 0 {
   494  		return nil
   495  	}
   496  	return &certs[0]
   497  }
   498  
   499  // SetCertificate st a certificate for TLS use.
   500  func (c *Client) SetCertificate(cert tls.Certificate) {
   501  	t := c.Client.Transport.(*http.Transport)
   502  
   503  	// Extension or HoK certificate
   504  	t.TLSClientConfig.Certificates = []tls.Certificate{cert}
   505  }
   506  
   507  // UseServiceVersion sets Client.Version to the current version of the service endpoint via /sdk/vimServiceVersions.xml
   508  func (c *Client) UseServiceVersion(kind ...string) error {
   509  	ns := "vim"
   510  	if len(kind) != 0 {
   511  		ns = kind[0]
   512  	}
   513  
   514  	u := c.URL()
   515  	u.Path = path.Join("/sdk", ns+"ServiceVersions.xml")
   516  
   517  	res, err := c.Get(u.String())
   518  	if err != nil {
   519  		return err
   520  	}
   521  
   522  	if res.StatusCode != http.StatusOK {
   523  		return fmt.Errorf("http.Get(%s): %s", u.Path, res.Status)
   524  	}
   525  
   526  	v := struct {
   527  		Namespace *string `xml:"namespace>name"`
   528  		Version   *string `xml:"namespace>version"`
   529  	}{
   530  		&c.Namespace,
   531  		&c.Version,
   532  	}
   533  
   534  	err = xml.NewDecoder(res.Body).Decode(&v)
   535  	_ = res.Body.Close()
   536  	if err != nil {
   537  		return fmt.Errorf("xml.Decode(%s): %s", u.Path, err)
   538  	}
   539  
   540  	return nil
   541  }
   542  
   543  // Tunnel returns a Client configured to proxy requests through vCenter's http port 80,
   544  // to the SDK tunnel virtual host.  Use of the SDK tunnel is required by LoginExtensionByCertificate()
   545  // and optional for other methods.
   546  func (c *Client) Tunnel() *Client {
   547  	tunnel := c.newServiceClientWithTransport(c.u.Path, c.Namespace, c.DefaultTransport().Clone())
   548  
   549  	t := tunnel.Client.Transport.(*http.Transport)
   550  	// Proxy to vCenter host on port 80
   551  	host := tunnel.u.Hostname()
   552  	// Should be no reason to change the default port other than testing
   553  	key := "GOVMOMI_TUNNEL_PROXY_PORT"
   554  
   555  	port := tunnel.URL().Query().Get(key)
   556  	if port == "" {
   557  		port = os.Getenv(key)
   558  	}
   559  
   560  	if port != "" {
   561  		host += ":" + port
   562  	}
   563  
   564  	t.Proxy = http.ProxyURL(&url.URL{
   565  		Scheme: "http",
   566  		Host:   host,
   567  	})
   568  
   569  	// Rewrite url Host to use the sdk tunnel, required for a certificate request.
   570  	tunnel.u.Host = sdkTunnel
   571  	return tunnel
   572  }
   573  
   574  // URL returns the URL to which the client is configured
   575  func (c *Client) URL() *url.URL {
   576  	urlCopy := *c.u
   577  	return &urlCopy
   578  }
   579  
   580  type marshaledClient struct {
   581  	Cookies  []*http.Cookie `json:"cookies"`
   582  	URL      *url.URL       `json:"url"`
   583  	Insecure bool           `json:"insecure"`
   584  	Version  string         `json:"version"`
   585  	UseJSON  bool           `json:"useJSON"`
   586  }
   587  
   588  // MarshalJSON writes the Client configuration to JSON.
   589  func (c *Client) MarshalJSON() ([]byte, error) {
   590  	m := marshaledClient{
   591  		Cookies:  c.Jar.Cookies(c.u),
   592  		URL:      c.u,
   593  		Insecure: c.k,
   594  		Version:  c.Version,
   595  		UseJSON:  c.useJSON,
   596  	}
   597  
   598  	return json.Marshal(m)
   599  }
   600  
   601  // UnmarshalJSON rads Client configuration from JSON.
   602  func (c *Client) UnmarshalJSON(b []byte) error {
   603  	var m marshaledClient
   604  
   605  	err := json.Unmarshal(b, &m)
   606  	if err != nil {
   607  		return err
   608  	}
   609  
   610  	*c = *NewClient(m.URL, m.Insecure)
   611  	c.Version = m.Version
   612  	c.Jar.SetCookies(m.URL, m.Cookies)
   613  	c.useJSON = m.UseJSON
   614  
   615  	return nil
   616  }
   617  
   618  func (c *Client) setInsecureCookies(res *http.Response) {
   619  	cookies := res.Cookies()
   620  	if len(cookies) != 0 {
   621  		for _, cookie := range cookies {
   622  			cookie.Secure = false
   623  		}
   624  		c.Jar.SetCookies(c.u, cookies)
   625  	}
   626  }
   627  
   628  // Do is equivalent to http.Client.Do and takes care of API specifics including
   629  // logging, user-agent header, handling cookies, measuring responsiveness of the
   630  // API
   631  func (c *Client) Do(ctx context.Context, req *http.Request, f func(*http.Response) error) error {
   632  	if ctx == nil {
   633  		ctx = context.Background()
   634  	}
   635  	// Create debugging context for this round trip
   636  	d := c.d.newRoundTrip()
   637  	if d.enabled() {
   638  		defer d.done()
   639  	}
   640  
   641  	// use default
   642  	if c.UserAgent == "" {
   643  		c.UserAgent = defaultUserAgent
   644  	}
   645  
   646  	req.Header.Set(`User-Agent`, c.UserAgent)
   647  
   648  	ext := ""
   649  	if d.enabled() {
   650  		ext = d.debugRequest(req)
   651  	}
   652  
   653  	res, err := c.Client.Do(req.WithContext(ctx))
   654  	if err != nil {
   655  		return err
   656  	}
   657  
   658  	if d.enabled() {
   659  		d.debugResponse(res, ext)
   660  	}
   661  
   662  	if c.insecureCookies {
   663  		c.setInsecureCookies(res)
   664  	}
   665  
   666  	defer res.Body.Close()
   667  
   668  	return f(res)
   669  }
   670  
   671  // Signer can be implemented by soap.Header.Security to sign requests.
   672  // If the soap.Header.Security field is set to an implementation of Signer via WithHeader(),
   673  // then Client.RoundTrip will call Sign() to marshal the SOAP request.
   674  type Signer interface {
   675  	Sign(Envelope) ([]byte, error)
   676  }
   677  
   678  type headerContext struct{}
   679  
   680  // WithHeader can be used to modify the outgoing request soap.Header fields.
   681  func (c *Client) WithHeader(ctx context.Context, header Header) context.Context {
   682  	return context.WithValue(ctx, headerContext{}, header)
   683  }
   684  
   685  type statusError struct {
   686  	res *http.Response
   687  }
   688  
   689  // Temporary returns true for HTTP response codes that can be retried
   690  // See vim25.IsTemporaryNetworkError
   691  func (e *statusError) Temporary() bool {
   692  	switch e.res.StatusCode {
   693  	case http.StatusBadGateway:
   694  		return true
   695  	}
   696  	return false
   697  }
   698  
   699  func (e *statusError) Error() string {
   700  	return e.res.Status
   701  }
   702  
   703  func newStatusError(res *http.Response) error {
   704  	return &url.Error{
   705  		Op:  res.Request.Method,
   706  		URL: res.Request.URL.Path,
   707  		Err: &statusError{res},
   708  	}
   709  }
   710  
   711  // RoundTrip executes an API request to VMOMI server.
   712  func (c *Client) RoundTrip(ctx context.Context, reqBody, resBody HasFault) error {
   713  	if !c.useJSON {
   714  		return c.soapRoundTrip(ctx, reqBody, resBody)
   715  	}
   716  	return c.jsonRoundTrip(ctx, reqBody, resBody)
   717  }
   718  
   719  func (c *Client) soapRoundTrip(ctx context.Context, reqBody, resBody HasFault) error {
   720  	var err error
   721  	var b []byte
   722  
   723  	reqEnv := Envelope{Body: reqBody}
   724  	resEnv := Envelope{Body: resBody}
   725  
   726  	h, ok := ctx.Value(headerContext{}).(Header)
   727  	if !ok {
   728  		h = Header{}
   729  	}
   730  
   731  	// We added support for OperationID before soap.Header was exported.
   732  	if id, ok := ctx.Value(types.ID{}).(string); ok {
   733  		h.ID = id
   734  	}
   735  
   736  	if c.Cookie != nil {
   737  		h.Cookie = c.Cookie()
   738  	}
   739  	if h.Cookie != nil || h.ID != "" || h.Security != nil {
   740  		reqEnv.Header = &h // XML marshal header only if a field is set
   741  	}
   742  
   743  	if signer, ok := h.Security.(Signer); ok {
   744  		b, err = signer.Sign(reqEnv)
   745  		if err != nil {
   746  			return err
   747  		}
   748  	} else {
   749  		b, err = xml.Marshal(reqEnv)
   750  		if err != nil {
   751  			panic(err)
   752  		}
   753  	}
   754  
   755  	rawReqBody := io.MultiReader(strings.NewReader(xml.Header), bytes.NewReader(b))
   756  	req, err := http.NewRequest("POST", c.u.String(), rawReqBody)
   757  	if err != nil {
   758  		panic(err)
   759  	}
   760  
   761  	req.Header.Set(`Content-Type`, `text/xml; charset="utf-8"`)
   762  
   763  	action := h.Action
   764  	if action == "" {
   765  		action = fmt.Sprintf("%s/%s", c.Namespace, c.Version)
   766  	}
   767  	req.Header.Set(`SOAPAction`, action)
   768  
   769  	return c.Do(ctx, req, func(res *http.Response) error {
   770  		switch res.StatusCode {
   771  		case http.StatusOK:
   772  			// OK
   773  		case http.StatusInternalServerError:
   774  			// Error, but typically includes a body explaining the error
   775  		default:
   776  			return newStatusError(res)
   777  		}
   778  
   779  		dec := xml.NewDecoder(res.Body)
   780  		dec.TypeFunc = c.Types
   781  		err = dec.Decode(&resEnv)
   782  		if err != nil {
   783  			return err
   784  		}
   785  
   786  		if f := resBody.Fault(); f != nil {
   787  			return WrapSoapFault(f)
   788  		}
   789  
   790  		return err
   791  	})
   792  }
   793  
   794  func (c *Client) CloseIdleConnections() {
   795  	c.t.CloseIdleConnections()
   796  }
   797  
   798  // ParseURL wraps url.Parse to rewrite the URL.Host field
   799  // In the case of VM guest uploads or NFC lease URLs, a Host
   800  // field with a value of "*" is rewritten to the Client's URL.Host.
   801  func (c *Client) ParseURL(urlStr string) (*url.URL, error) {
   802  	u, err := url.Parse(urlStr)
   803  	if err != nil {
   804  		return nil, err
   805  	}
   806  
   807  	host, _ := splitHostPort(u.Host)
   808  	if host == "*" {
   809  		// Also use Client's port, to support port forwarding
   810  		u.Host = c.URL().Host
   811  	}
   812  
   813  	return u, nil
   814  }
   815  
   816  type Upload struct {
   817  	Type          string
   818  	Method        string
   819  	ContentLength int64
   820  	Headers       map[string]string
   821  	Ticket        *http.Cookie
   822  	Progress      progress.Sinker
   823  	Close         bool
   824  }
   825  
   826  var DefaultUpload = Upload{
   827  	Type:   "application/octet-stream",
   828  	Method: "PUT",
   829  }
   830  
   831  // Upload PUTs the local file to the given URL
   832  func (c *Client) Upload(ctx context.Context, f io.Reader, u *url.URL, param *Upload) error {
   833  	var err error
   834  
   835  	if param.Progress != nil {
   836  		pr := progress.NewReader(ctx, param.Progress, f, param.ContentLength)
   837  		f = pr
   838  
   839  		// Mark progress reader as done when returning from this function.
   840  		defer func() {
   841  			pr.Done(err)
   842  		}()
   843  	}
   844  
   845  	req, err := http.NewRequest(param.Method, u.String(), f)
   846  	if err != nil {
   847  		return err
   848  	}
   849  
   850  	req = req.WithContext(ctx)
   851  	req.Close = param.Close
   852  	req.ContentLength = param.ContentLength
   853  	req.Header.Set("Content-Type", param.Type)
   854  
   855  	for k, v := range param.Headers {
   856  		req.Header.Add(k, v)
   857  	}
   858  
   859  	if param.Ticket != nil {
   860  		req.AddCookie(param.Ticket)
   861  	}
   862  
   863  	res, err := c.Client.Do(req)
   864  	if err != nil {
   865  		return err
   866  	}
   867  
   868  	defer res.Body.Close()
   869  
   870  	switch res.StatusCode {
   871  	case http.StatusOK:
   872  	case http.StatusCreated:
   873  	default:
   874  		err = errors.New(res.Status)
   875  	}
   876  
   877  	return err
   878  }
   879  
   880  // UploadFile PUTs the local file to the given URL
   881  func (c *Client) UploadFile(ctx context.Context, file string, u *url.URL, param *Upload) error {
   882  	if param == nil {
   883  		p := DefaultUpload // Copy since we set ContentLength
   884  		param = &p
   885  	}
   886  
   887  	s, err := os.Stat(file)
   888  	if err != nil {
   889  		return err
   890  	}
   891  
   892  	f, err := os.Open(filepath.Clean(file))
   893  	if err != nil {
   894  		return err
   895  	}
   896  	defer f.Close()
   897  
   898  	param.ContentLength = s.Size()
   899  
   900  	return c.Upload(ctx, f, u, param)
   901  }
   902  
   903  type Download struct {
   904  	Method   string
   905  	Headers  map[string]string
   906  	Ticket   *http.Cookie
   907  	Progress progress.Sinker
   908  	Writer   io.Writer
   909  	Close    bool
   910  }
   911  
   912  var DefaultDownload = Download{
   913  	Method: "GET",
   914  }
   915  
   916  // DownloadRequest wraps http.Client.Do, returning the http.Response without checking its StatusCode
   917  func (c *Client) DownloadRequest(ctx context.Context, u *url.URL, param *Download) (*http.Response, error) {
   918  	req, err := http.NewRequest(param.Method, u.String(), nil)
   919  	if err != nil {
   920  		return nil, err
   921  	}
   922  
   923  	req = req.WithContext(ctx)
   924  	req.Close = param.Close
   925  
   926  	for k, v := range param.Headers {
   927  		req.Header.Add(k, v)
   928  	}
   929  
   930  	if param.Ticket != nil {
   931  		req.AddCookie(param.Ticket)
   932  	}
   933  
   934  	return c.Client.Do(req)
   935  }
   936  
   937  // Download GETs the remote file from the given URL
   938  func (c *Client) Download(ctx context.Context, u *url.URL, param *Download) (io.ReadCloser, int64, error) {
   939  	res, err := c.DownloadRequest(ctx, u, param)
   940  	if err != nil {
   941  		return nil, 0, err
   942  	}
   943  
   944  	switch res.StatusCode {
   945  	case http.StatusOK:
   946  	default:
   947  		err = fmt.Errorf("download(%s): %s", u, res.Status)
   948  	}
   949  
   950  	if err != nil {
   951  		return nil, 0, err
   952  	}
   953  
   954  	r := res.Body
   955  
   956  	return r, res.ContentLength, nil
   957  }
   958  
   959  func (c *Client) WriteFile(ctx context.Context, file string, src io.Reader, size int64, s progress.Sinker, w io.Writer) error {
   960  	var err error
   961  
   962  	r := src
   963  
   964  	fh, err := os.Create(file)
   965  	if err != nil {
   966  		return err
   967  	}
   968  
   969  	if s != nil {
   970  		pr := progress.NewReader(ctx, s, src, size)
   971  		r = pr
   972  
   973  		// Mark progress reader as done when returning from this function.
   974  		defer func() {
   975  			pr.Done(err)
   976  		}()
   977  	}
   978  
   979  	if w == nil {
   980  		w = fh
   981  	} else {
   982  		w = io.MultiWriter(w, fh)
   983  	}
   984  
   985  	_, err = io.Copy(w, r)
   986  
   987  	cerr := fh.Close()
   988  
   989  	if err == nil {
   990  		err = cerr
   991  	}
   992  
   993  	return err
   994  }
   995  
   996  // DownloadFile GETs the given URL to a local file
   997  func (c *Client) DownloadFile(ctx context.Context, file string, u *url.URL, param *Download) error {
   998  	var err error
   999  	if param == nil {
  1000  		param = &DefaultDownload
  1001  	}
  1002  
  1003  	rc, contentLength, err := c.Download(ctx, u, param)
  1004  	if err != nil {
  1005  		return err
  1006  	}
  1007  
  1008  	return c.WriteFile(ctx, file, rc, contentLength, param.Progress, param.Writer)
  1009  }
  1010  
  1011  // execName gets the name of the executable for the current process
  1012  func execName() string {
  1013  	name, err := os.Executable()
  1014  	if err != nil {
  1015  		return "N/A"
  1016  	}
  1017  	return strings.TrimSuffix(filepath.Base(name), ".exe")
  1018  }