github.com/vmware/govmomi@v0.43.0/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 }