github.com/vmware/govmomi@v0.43.0/vim25/soap/json_client.go (about)

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