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 }