github.com/vmware/govmomi@v0.37.2/vapi/rest/client.go (about)

     1  /*
     2  Copyright (c) 2018 VMware, Inc. All Rights Reserved.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package rest
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"net/url"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/vmware/govmomi/vapi/internal"
    32  	"github.com/vmware/govmomi/vim25"
    33  	"github.com/vmware/govmomi/vim25/soap"
    34  	"github.com/vmware/govmomi/vim25/types"
    35  )
    36  
    37  // Client extends soap.Client to support JSON encoding, while inheriting security features, debug tracing and session persistence.
    38  type Client struct {
    39  	mu sync.Mutex
    40  
    41  	*soap.Client
    42  	sessionID string
    43  }
    44  
    45  // Session information
    46  type Session struct {
    47  	User         string    `json:"user"`
    48  	Created      time.Time `json:"created_time"`
    49  	LastAccessed time.Time `json:"last_accessed_time"`
    50  }
    51  
    52  // LocalizableMessage represents a localizable error
    53  type LocalizableMessage struct {
    54  	Args           []string `json:"args,omitempty"`
    55  	DefaultMessage string   `json:"default_message,omitempty"`
    56  	ID             string   `json:"id,omitempty"`
    57  }
    58  
    59  func (m *LocalizableMessage) Error() string {
    60  	return m.DefaultMessage
    61  }
    62  
    63  // NewClient creates a new Client instance.
    64  func NewClient(c *vim25.Client) *Client {
    65  	sc := c.Client.NewServiceClient(Path, "")
    66  
    67  	return &Client{Client: sc}
    68  }
    69  
    70  // SessionID is set by calling Login() or optionally with the given id param
    71  func (c *Client) SessionID(id ...string) string {
    72  	c.mu.Lock()
    73  	defer c.mu.Unlock()
    74  	if len(id) != 0 {
    75  		c.sessionID = id[0]
    76  	}
    77  	return c.sessionID
    78  }
    79  
    80  type marshaledClient struct {
    81  	SoapClient *soap.Client
    82  	SessionID  string
    83  }
    84  
    85  func (c *Client) MarshalJSON() ([]byte, error) {
    86  	m := marshaledClient{
    87  		SoapClient: c.Client,
    88  		SessionID:  c.sessionID,
    89  	}
    90  
    91  	return json.Marshal(m)
    92  }
    93  
    94  func (c *Client) UnmarshalJSON(b []byte) error {
    95  	var m marshaledClient
    96  
    97  	err := json.Unmarshal(b, &m)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	*c = Client{
   103  		Client:    m.SoapClient,
   104  		sessionID: m.SessionID,
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  // isAPI returns true if path starts with "/api"
   111  // This hack allows helpers to support both endpoints:
   112  // "/rest" - value wrapped responses and structured error responses
   113  // "/api" - raw responses and no structured error responses
   114  func isAPI(path string) bool {
   115  	return strings.HasPrefix(path, "/api")
   116  }
   117  
   118  // Resource helper for the given path.
   119  func (c *Client) Resource(path string) *Resource {
   120  	r := &Resource{u: c.URL()}
   121  	if !isAPI(path) {
   122  		path = Path + path
   123  	}
   124  	r.u.Path = path
   125  	return r
   126  }
   127  
   128  type Signer interface {
   129  	SignRequest(*http.Request) error
   130  }
   131  
   132  type signerContext struct{}
   133  
   134  func (c *Client) WithSigner(ctx context.Context, s Signer) context.Context {
   135  	return context.WithValue(ctx, signerContext{}, s)
   136  }
   137  
   138  type headersContext struct{}
   139  
   140  // WithHeader returns a new Context populated with the provided headers map.
   141  // Calls to a VAPI REST client with this context will populate the HTTP headers
   142  // map using the provided headers.
   143  func (c *Client) WithHeader(
   144  	ctx context.Context,
   145  	headers http.Header) context.Context {
   146  
   147  	return context.WithValue(ctx, headersContext{}, headers)
   148  }
   149  
   150  type statusError struct {
   151  	res *http.Response
   152  }
   153  
   154  func (e *statusError) Error() string {
   155  	return fmt.Sprintf("%s %s: %s", e.res.Request.Method, e.res.Request.URL, e.res.Status)
   156  }
   157  
   158  func IsStatusError(err error, code int) bool {
   159  	statusErr, ok := err.(*statusError)
   160  	if !ok || statusErr == nil || statusErr.res == nil {
   161  		return false
   162  	}
   163  	return statusErr.res.StatusCode == code
   164  }
   165  
   166  // RawResponse may be used with the Do method as the resBody argument in order
   167  // to capture the raw response data.
   168  type RawResponse struct {
   169  	bytes.Buffer
   170  }
   171  
   172  // Do sends the http.Request, decoding resBody if provided.
   173  func (c *Client) Do(ctx context.Context, req *http.Request, resBody interface{}) error {
   174  	switch req.Method {
   175  	case http.MethodPost, http.MethodPatch, http.MethodPut:
   176  		req.Header.Set("Content-Type", "application/json")
   177  	}
   178  
   179  	req.Header.Set("Accept", "application/json")
   180  
   181  	if id := c.SessionID(); id != "" {
   182  		req.Header.Set(internal.SessionCookieName, id)
   183  	}
   184  
   185  	if s, ok := ctx.Value(signerContext{}).(Signer); ok {
   186  		if err := s.SignRequest(req); err != nil {
   187  			return err
   188  		}
   189  	}
   190  
   191  	// OperationID (see soap.Client.soapRoundTrip)
   192  	if id, ok := ctx.Value(types.ID{}).(string); ok {
   193  		req.Header.Add("X-Request-ID", id)
   194  	}
   195  
   196  	if headers, ok := ctx.Value(headersContext{}).(http.Header); ok {
   197  		for k, v := range headers {
   198  			for _, v := range v {
   199  				req.Header.Add(k, v)
   200  			}
   201  		}
   202  	}
   203  
   204  	return c.Client.Do(ctx, req, func(res *http.Response) error {
   205  		switch res.StatusCode {
   206  		case http.StatusOK:
   207  		case http.StatusCreated:
   208  		case http.StatusAccepted:
   209  		case http.StatusNoContent:
   210  		case http.StatusBadRequest:
   211  			// TODO: structured error types
   212  			detail, err := io.ReadAll(res.Body)
   213  			if err != nil {
   214  				return err
   215  			}
   216  			return fmt.Errorf("%s: %s", res.Status, bytes.TrimSpace(detail))
   217  		default:
   218  			return &statusError{res}
   219  		}
   220  
   221  		if resBody == nil {
   222  			return nil
   223  		}
   224  
   225  		switch b := resBody.(type) {
   226  		case *RawResponse:
   227  			return res.Write(b)
   228  		case io.Writer:
   229  			_, err := io.Copy(b, res.Body)
   230  			return err
   231  		default:
   232  			d := json.NewDecoder(res.Body)
   233  			if isAPI(req.URL.Path) {
   234  				// Responses from the /api endpoint are not wrapped
   235  				return d.Decode(resBody)
   236  			}
   237  			// Responses from the /rest endpoint are wrapped in this structure
   238  			val := struct {
   239  				Value interface{} `json:"value,omitempty"`
   240  			}{
   241  				resBody,
   242  			}
   243  			return d.Decode(&val)
   244  		}
   245  	})
   246  }
   247  
   248  // authHeaders ensures the given map contains a REST auth header
   249  func (c *Client) authHeaders(h map[string]string) map[string]string {
   250  	if _, exists := h[internal.SessionCookieName]; exists {
   251  		return h
   252  	}
   253  	if h == nil {
   254  		h = make(map[string]string)
   255  	}
   256  
   257  	h[internal.SessionCookieName] = c.SessionID()
   258  
   259  	return h
   260  }
   261  
   262  // Download wraps soap.Client.Download, adding the REST authentication header
   263  func (c *Client) Download(ctx context.Context, u *url.URL, param *soap.Download) (io.ReadCloser, int64, error) {
   264  	p := *param
   265  	p.Headers = c.authHeaders(p.Headers)
   266  	return c.Client.Download(ctx, u, &p)
   267  }
   268  
   269  // DownloadFile wraps soap.Client.DownloadFile, adding the REST authentication header
   270  func (c *Client) DownloadFile(ctx context.Context, file string, u *url.URL, param *soap.Download) error {
   271  	p := *param
   272  	p.Headers = c.authHeaders(p.Headers)
   273  	return c.Client.DownloadFile(ctx, file, u, &p)
   274  }
   275  
   276  // Upload wraps soap.Client.Upload, adding the REST authentication header
   277  func (c *Client) Upload(ctx context.Context, f io.Reader, u *url.URL, param *soap.Upload) error {
   278  	p := *param
   279  	p.Headers = c.authHeaders(p.Headers)
   280  	return c.Client.Upload(ctx, f, u, &p)
   281  }
   282  
   283  // Login creates a new session via Basic Authentication with the given url.Userinfo.
   284  func (c *Client) Login(ctx context.Context, user *url.Userinfo) error {
   285  	req := c.Resource(internal.SessionPath).Request(http.MethodPost)
   286  
   287  	req.Header.Set(internal.UseHeaderAuthn, "true")
   288  
   289  	if user != nil {
   290  		if password, ok := user.Password(); ok {
   291  			req.SetBasicAuth(user.Username(), password)
   292  		}
   293  	}
   294  
   295  	var id string
   296  	err := c.Do(ctx, req, &id)
   297  	if err != nil {
   298  		return err
   299  	}
   300  
   301  	c.SessionID(id)
   302  
   303  	return nil
   304  }
   305  
   306  func (c *Client) LoginByToken(ctx context.Context) error {
   307  	return c.Login(ctx, nil)
   308  }
   309  
   310  // Session returns the user's current session.
   311  // Nil is returned if the session is not authenticated.
   312  func (c *Client) Session(ctx context.Context) (*Session, error) {
   313  	var s Session
   314  	req := c.Resource(internal.SessionPath).WithAction("get").Request(http.MethodPost)
   315  	err := c.Do(ctx, req, &s)
   316  	if err != nil {
   317  		if e, ok := err.(*statusError); ok {
   318  			if e.res.StatusCode == http.StatusUnauthorized {
   319  				return nil, nil
   320  			}
   321  		}
   322  		return nil, err
   323  	}
   324  	return &s, nil
   325  }
   326  
   327  // Logout deletes the current session.
   328  func (c *Client) Logout(ctx context.Context) error {
   329  	req := c.Resource(internal.SessionPath).Request(http.MethodDelete)
   330  	return c.Do(ctx, req, nil)
   331  }
   332  
   333  // Valid returns whether or not the client is valid and ready for use.
   334  // This should be called after unmarshalling the client.
   335  func (c *Client) Valid() bool {
   336  	if c == nil {
   337  		return false
   338  	}
   339  
   340  	if c.Client == nil {
   341  		return false
   342  	}
   343  
   344  	return true
   345  }
   346  
   347  // Path returns rest.Path (see cache.Client)
   348  func (c *Client) Path() string {
   349  	return Path
   350  }