github.com/vmware/govmomi@v0.51.0/vim25/soap/json_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  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"mime"
    14  	"net/http"
    15  	"reflect"
    16  	"strings"
    17  
    18  	"github.com/vmware/govmomi/vim25/xml"
    19  
    20  	"github.com/vmware/govmomi/vim25/types"
    21  )
    22  
    23  const (
    24  	sessionHeader = "vmware-api-session-id"
    25  )
    26  
    27  var (
    28  	// errInvalidResponse is used during unmarshaling when the response content
    29  	// does not match expectations e.g. unexpected HTTP status code or MIME
    30  	// type.
    31  	errInvalidResponse error = errors.New("Invalid response")
    32  	// errInputError is used as root error when the request is malformed.
    33  	errInputError error = errors.New("Invalid input error")
    34  )
    35  
    36  // Handles round trip using json HTTP
    37  func (c *Client) jsonRoundTrip(ctx context.Context, req, res HasFault) error {
    38  	this, method, params, err := unpackSOAPRequest(req)
    39  	if err != nil {
    40  		return fmt.Errorf("Cannot unpack the request. %w", err)
    41  	}
    42  
    43  	return c.invoke(ctx, this, method, params, res)
    44  }
    45  
    46  // Invoke calls a managed object method
    47  func (c *Client) invoke(ctx context.Context, this types.ManagedObjectReference, method string, params any, res HasFault) error {
    48  	buffer := bytes.Buffer{}
    49  	if params != nil {
    50  		marshaller := types.NewJSONEncoder(&buffer)
    51  		err := marshaller.Encode(params)
    52  		if err != nil {
    53  			return fmt.Errorf("Encoding request to JSON failed. %w", err)
    54  		}
    55  	}
    56  	uri := c.getPathForName(this, method)
    57  	req, err := http.NewRequest(http.MethodPost, uri, &buffer)
    58  	if err != nil {
    59  		return err
    60  	}
    61  
    62  	if c.Cookie != nil {
    63  		if cookie := c.Cookie(); cookie != nil {
    64  			req.Header.Add(sessionHeader, cookie.Value)
    65  		}
    66  	}
    67  
    68  	result, err := getSOAPResultPtr(res)
    69  	if err != nil {
    70  		return fmt.Errorf("Cannot get pointer to the result structure. %w", err)
    71  	}
    72  
    73  	return c.Do(ctx, req, c.responseUnmarshaler(&result))
    74  }
    75  
    76  // responseUnmarshaler create unmarshaler function for VMOMI JSON request. The
    77  // unmarshaler checks for errors and tries to load the response body in the
    78  // result structure. It is assumed that result is pointer to a data structure or
    79  // interface{}.
    80  func (c *Client) responseUnmarshaler(result any) func(resp *http.Response) error {
    81  	return func(resp *http.Response) error {
    82  		if resp.StatusCode == http.StatusNoContent ||
    83  			(!isError(resp.StatusCode) && resp.ContentLength == 0) {
    84  			return nil
    85  		}
    86  
    87  		if e := checkJSONContentType(resp); e != nil {
    88  			return e
    89  		}
    90  
    91  		if resp.StatusCode == 500 {
    92  			bodyBytes, e := io.ReadAll(resp.Body)
    93  			if e != nil {
    94  				return e
    95  			}
    96  			var serverErr any
    97  			dec := types.NewJSONDecoder(bytes.NewReader(bodyBytes))
    98  			e = dec.Decode(&serverErr)
    99  			if e != nil {
   100  				return e
   101  			}
   102  			var faultStringStruct struct {
   103  				FaultString string `json:"faultstring,omitempty"`
   104  			}
   105  			dec = types.NewJSONDecoder(bytes.NewReader(bodyBytes))
   106  			e = dec.Decode(&faultStringStruct)
   107  			if e != nil {
   108  				return e
   109  			}
   110  
   111  			f := &Fault{
   112  				XMLName: xml.Name{
   113  					Space: c.Namespace,
   114  					Local: reflect.TypeOf(serverErr).Name() + "Fault",
   115  				},
   116  				String: faultStringStruct.FaultString,
   117  				Code:   "ServerFaultCode",
   118  			}
   119  			f.Detail.Fault = serverErr
   120  			return WrapSoapFault(f)
   121  		}
   122  
   123  		if isError(resp.StatusCode) {
   124  			return fmt.Errorf("Unexpected HTTP error code: %v. %w", resp.StatusCode, errInvalidResponse)
   125  		}
   126  
   127  		dec := types.NewJSONDecoder(resp.Body)
   128  		e := dec.Decode(result)
   129  		if e != nil {
   130  			return e
   131  		}
   132  
   133  		c.checkForSessionHeader(resp)
   134  
   135  		return nil
   136  	}
   137  }
   138  
   139  func isError(statusCode int) bool {
   140  	return statusCode < http.StatusOK || statusCode >= http.StatusMultipleChoices
   141  }
   142  
   143  // checkForSessionHeader checks if we have new session id.
   144  // This is a hack that intercepts the session id header and then repeats it.
   145  // It is very similar to cookie store but only for the special vCenter
   146  // session header.
   147  func (c *Client) checkForSessionHeader(resp *http.Response) {
   148  	sessionKey := resp.Header.Get(sessionHeader)
   149  	if sessionKey != "" {
   150  		c.Cookie = func() *HeaderElement {
   151  			return &HeaderElement{Value: sessionKey}
   152  		}
   153  	}
   154  }
   155  
   156  // Checks if the payload of an HTTP response has the JSON MIME type.
   157  func checkJSONContentType(resp *http.Response) error {
   158  	contentType := resp.Header.Get("content-type")
   159  	mediaType, _, err := mime.ParseMediaType(contentType)
   160  	if err != nil {
   161  		return fmt.Errorf("error parsing content-type: %v, error %w", contentType, err)
   162  	}
   163  	if mediaType != "application/json" {
   164  		return fmt.Errorf("content-type is not application/json: %v. %w", contentType, errInvalidResponse)
   165  	}
   166  	return nil
   167  }
   168  
   169  func (c *Client) getPathForName(this types.ManagedObjectReference, name string) string {
   170  	const urnPrefix = "urn:"
   171  	ns := c.Namespace
   172  	if strings.HasPrefix(ns, urnPrefix) {
   173  		ns = ns[len(urnPrefix):]
   174  	}
   175  	return fmt.Sprintf("%v/%v/%v/%v/%v/%v", c.u, ns, c.Version, this.Type, this.Value, name)
   176  }
   177  
   178  // unpackSOAPRequest converts SOAP request into this value, method nam and
   179  // parameters using reflection. The input is a one of the *Body structures
   180  // defined in methods.go. It is expected to have "Req" field that is a non-null
   181  // pointer to a struct. The struct simple type name is the method name. The
   182  // struct "This" member is the this MoRef value.
   183  func unpackSOAPRequest(req HasFault) (this types.ManagedObjectReference, method string, params any, err error) {
   184  	reqBodyPtr := reflect.ValueOf(req)
   185  	if reqBodyPtr.Kind() != reflect.Ptr {
   186  		err = fmt.Errorf("Expected pointer to request body as input. %w", errInputError)
   187  		return
   188  	}
   189  	reqBody := reqBodyPtr.Elem()
   190  	if reqBody.Kind() != reflect.Struct {
   191  		err = fmt.Errorf("Expected Request body to be structure. %w", errInputError)
   192  		return
   193  	}
   194  	methodRequestPtr := reqBody.FieldByName("Req")
   195  	if methodRequestPtr.Kind() != reflect.Ptr {
   196  		err = fmt.Errorf("Expected method request body field to be pointer to struct. %w", errInputError)
   197  		return
   198  	}
   199  	methodRequest := methodRequestPtr.Elem()
   200  	if methodRequest.Kind() != reflect.Struct {
   201  		err = fmt.Errorf("Expected method request body to be structure. %w", errInputError)
   202  		return
   203  	}
   204  	thisValue := methodRequest.FieldByName("This")
   205  	if thisValue.Kind() != reflect.Struct {
   206  		err = fmt.Errorf("Expected This field in the method request body to be structure. %w", errInputError)
   207  		return
   208  	}
   209  	var ok bool
   210  	if this, ok = thisValue.Interface().(types.ManagedObjectReference); !ok {
   211  		err = fmt.Errorf("Expected This field to be MoRef. %w", errInputError)
   212  		return
   213  	}
   214  	method = methodRequest.Type().Name()
   215  	params = methodRequestPtr.Interface()
   216  
   217  	return
   218  
   219  }
   220  
   221  // getSOAPResultPtr extract a pointer to the result data structure using go
   222  // reflection from a SOAP data structure used for marshalling.
   223  func getSOAPResultPtr(result HasFault) (res any, err error) {
   224  	resBodyPtr := reflect.ValueOf(result)
   225  	if resBodyPtr.Kind() != reflect.Ptr {
   226  		err = fmt.Errorf("Expected pointer to result body as input. %w", errInputError)
   227  		return
   228  	}
   229  	resBody := resBodyPtr.Elem()
   230  	if resBody.Kind() != reflect.Struct {
   231  		err = fmt.Errorf("Expected result body to be structure. %w", errInputError)
   232  		return
   233  	}
   234  	methodResponsePtr := resBody.FieldByName("Res")
   235  	if methodResponsePtr.Kind() != reflect.Ptr {
   236  		err = fmt.Errorf("Expected method response body field to be pointer to struct. %w", errInputError)
   237  		return
   238  	}
   239  	if methodResponsePtr.IsNil() {
   240  		methodResponsePtr.Set(reflect.New(methodResponsePtr.Type().Elem()))
   241  	}
   242  	methodResponse := methodResponsePtr.Elem()
   243  	if methodResponse.Kind() != reflect.Struct {
   244  		err = fmt.Errorf("Expected method response body to be structure. %w", errInputError)
   245  		return
   246  	}
   247  	returnval := methodResponse.FieldByName("Returnval")
   248  	if !returnval.IsValid() {
   249  		// void method and we return nil, nil
   250  		return
   251  	}
   252  	res = returnval.Addr().Interface()
   253  	return
   254  }