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 }