client.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. /*
  2. Copyright (c) 2018 VMware, Inc. All Rights Reserved.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package rest
  14. import (
  15. "bytes"
  16. "context"
  17. "encoding/json"
  18. "fmt"
  19. "io"
  20. "net/http"
  21. "net/url"
  22. "strings"
  23. "sync"
  24. "time"
  25. "github.com/vmware/govmomi/vapi/internal"
  26. "github.com/vmware/govmomi/vim25"
  27. "github.com/vmware/govmomi/vim25/soap"
  28. "github.com/vmware/govmomi/vim25/types"
  29. )
  30. // Client extends soap.Client to support JSON encoding, while inheriting security features, debug tracing and session persistence.
  31. type Client struct {
  32. mu sync.Mutex
  33. *soap.Client
  34. sessionID string
  35. }
  36. // Session information
  37. type Session struct {
  38. User string `json:"user"`
  39. Created time.Time `json:"created_time"`
  40. LastAccessed time.Time `json:"last_accessed_time"`
  41. }
  42. // LocalizableMessage represents a localizable error
  43. type LocalizableMessage struct {
  44. Args []string `json:"args,omitempty"`
  45. DefaultMessage string `json:"default_message,omitempty"`
  46. ID string `json:"id,omitempty"`
  47. }
  48. func (m *LocalizableMessage) Error() string {
  49. return m.DefaultMessage
  50. }
  51. // NewClient creates a new Client instance.
  52. func NewClient(c *vim25.Client) *Client {
  53. sc := c.Client.NewServiceClient(Path, "")
  54. return &Client{Client: sc}
  55. }
  56. // SessionID is set by calling Login() or optionally with the given id param
  57. func (c *Client) SessionID(id ...string) string {
  58. c.mu.Lock()
  59. defer c.mu.Unlock()
  60. if len(id) != 0 {
  61. c.sessionID = id[0]
  62. }
  63. return c.sessionID
  64. }
  65. type marshaledClient struct {
  66. SoapClient *soap.Client
  67. SessionID string
  68. }
  69. func (c *Client) MarshalJSON() ([]byte, error) {
  70. m := marshaledClient{
  71. SoapClient: c.Client,
  72. SessionID: c.sessionID,
  73. }
  74. return json.Marshal(m)
  75. }
  76. func (c *Client) UnmarshalJSON(b []byte) error {
  77. var m marshaledClient
  78. err := json.Unmarshal(b, &m)
  79. if err != nil {
  80. return err
  81. }
  82. *c = Client{
  83. Client: m.SoapClient,
  84. sessionID: m.SessionID,
  85. }
  86. return nil
  87. }
  88. // isAPI returns true if path starts with "/api"
  89. // This hack allows helpers to support both endpoints:
  90. // "/rest" - value wrapped responses and structured error responses
  91. // "/api" - raw responses and no structured error responses
  92. func isAPI(path string) bool {
  93. return strings.HasPrefix(path, "/api")
  94. }
  95. // Resource helper for the given path.
  96. func (c *Client) Resource(path string) *Resource {
  97. r := &Resource{u: c.URL()}
  98. if !isAPI(path) {
  99. path = Path + path
  100. }
  101. r.u.Path = path
  102. return r
  103. }
  104. type Signer interface {
  105. SignRequest(*http.Request) error
  106. }
  107. type signerContext struct{}
  108. func (c *Client) WithSigner(ctx context.Context, s Signer) context.Context {
  109. return context.WithValue(ctx, signerContext{}, s)
  110. }
  111. type headersContext struct{}
  112. // WithHeader returns a new Context populated with the provided headers map.
  113. // Calls to a VAPI REST client with this context will populate the HTTP headers
  114. // map using the provided headers.
  115. func (c *Client) WithHeader(
  116. ctx context.Context,
  117. headers http.Header) context.Context {
  118. return context.WithValue(ctx, headersContext{}, headers)
  119. }
  120. type statusError struct {
  121. res *http.Response
  122. }
  123. func (e *statusError) Error() string {
  124. return fmt.Sprintf("%s %s: %s", e.res.Request.Method, e.res.Request.URL, e.res.Status)
  125. }
  126. func IsStatusError(err error, code int) bool {
  127. statusErr, ok := err.(*statusError)
  128. if !ok || statusErr == nil || statusErr.res == nil {
  129. return false
  130. }
  131. return statusErr.res.StatusCode == code
  132. }
  133. // RawResponse may be used with the Do method as the resBody argument in order
  134. // to capture the raw response data.
  135. type RawResponse struct {
  136. bytes.Buffer
  137. }
  138. // Do sends the http.Request, decoding resBody if provided.
  139. func (c *Client) Do(ctx context.Context, req *http.Request, resBody interface{}) error {
  140. switch req.Method {
  141. case http.MethodPost, http.MethodPatch, http.MethodPut:
  142. req.Header.Set("Content-Type", "application/json")
  143. }
  144. req.Header.Set("Accept", "application/json")
  145. if id := c.SessionID(); id != "" {
  146. req.Header.Set(internal.SessionCookieName, id)
  147. }
  148. if s, ok := ctx.Value(signerContext{}).(Signer); ok {
  149. if err := s.SignRequest(req); err != nil {
  150. return err
  151. }
  152. }
  153. // OperationID (see soap.Client.soapRoundTrip)
  154. if id, ok := ctx.Value(types.ID{}).(string); ok {
  155. req.Header.Add("X-Request-ID", id)
  156. }
  157. if headers, ok := ctx.Value(headersContext{}).(http.Header); ok {
  158. for k, v := range headers {
  159. for _, v := range v {
  160. req.Header.Add(k, v)
  161. }
  162. }
  163. }
  164. return c.Client.Do(ctx, req, func(res *http.Response) error {
  165. switch res.StatusCode {
  166. case http.StatusOK:
  167. case http.StatusCreated:
  168. case http.StatusAccepted:
  169. case http.StatusNoContent:
  170. case http.StatusBadRequest:
  171. // TODO: structured error types
  172. detail, err := io.ReadAll(res.Body)
  173. if err != nil {
  174. return err
  175. }
  176. return fmt.Errorf("%s: %s", res.Status, bytes.TrimSpace(detail))
  177. default:
  178. return &statusError{res}
  179. }
  180. if resBody == nil {
  181. return nil
  182. }
  183. switch b := resBody.(type) {
  184. case *RawResponse:
  185. return res.Write(b)
  186. case io.Writer:
  187. _, err := io.Copy(b, res.Body)
  188. return err
  189. default:
  190. d := json.NewDecoder(res.Body)
  191. if isAPI(req.URL.Path) {
  192. // Responses from the /api endpoint are not wrapped
  193. return d.Decode(resBody)
  194. }
  195. // Responses from the /rest endpoint are wrapped in this structure
  196. val := struct {
  197. Value interface{} `json:"value,omitempty"`
  198. }{
  199. resBody,
  200. }
  201. return d.Decode(&val)
  202. }
  203. })
  204. }
  205. // authHeaders ensures the given map contains a REST auth header
  206. func (c *Client) authHeaders(h map[string]string) map[string]string {
  207. if _, exists := h[internal.SessionCookieName]; exists {
  208. return h
  209. }
  210. if h == nil {
  211. h = make(map[string]string)
  212. }
  213. h[internal.SessionCookieName] = c.SessionID()
  214. return h
  215. }
  216. // Download wraps soap.Client.Download, adding the REST authentication header
  217. func (c *Client) Download(ctx context.Context, u *url.URL, param *soap.Download) (io.ReadCloser, int64, error) {
  218. p := *param
  219. p.Headers = c.authHeaders(p.Headers)
  220. return c.Client.Download(ctx, u, &p)
  221. }
  222. // DownloadFile wraps soap.Client.DownloadFile, adding the REST authentication header
  223. func (c *Client) DownloadFile(ctx context.Context, file string, u *url.URL, param *soap.Download) error {
  224. p := *param
  225. p.Headers = c.authHeaders(p.Headers)
  226. return c.Client.DownloadFile(ctx, file, u, &p)
  227. }
  228. // Upload wraps soap.Client.Upload, adding the REST authentication header
  229. func (c *Client) Upload(ctx context.Context, f io.Reader, u *url.URL, param *soap.Upload) error {
  230. p := *param
  231. p.Headers = c.authHeaders(p.Headers)
  232. return c.Client.Upload(ctx, f, u, &p)
  233. }
  234. // Login creates a new session via Basic Authentication with the given url.Userinfo.
  235. func (c *Client) Login(ctx context.Context, user *url.Userinfo) error {
  236. req := c.Resource(internal.SessionPath).Request(http.MethodPost)
  237. req.Header.Set(internal.UseHeaderAuthn, "true")
  238. if user != nil {
  239. if password, ok := user.Password(); ok {
  240. req.SetBasicAuth(user.Username(), password)
  241. }
  242. }
  243. var id string
  244. err := c.Do(ctx, req, &id)
  245. if err != nil {
  246. return err
  247. }
  248. c.SessionID(id)
  249. return nil
  250. }
  251. func (c *Client) LoginByToken(ctx context.Context) error {
  252. return c.Login(ctx, nil)
  253. }
  254. // Session returns the user's current session.
  255. // Nil is returned if the session is not authenticated.
  256. func (c *Client) Session(ctx context.Context) (*Session, error) {
  257. var s Session
  258. req := c.Resource(internal.SessionPath).WithAction("get").Request(http.MethodPost)
  259. err := c.Do(ctx, req, &s)
  260. if err != nil {
  261. if e, ok := err.(*statusError); ok {
  262. if e.res.StatusCode == http.StatusUnauthorized {
  263. return nil, nil
  264. }
  265. }
  266. return nil, err
  267. }
  268. return &s, nil
  269. }
  270. // Logout deletes the current session.
  271. func (c *Client) Logout(ctx context.Context) error {
  272. req := c.Resource(internal.SessionPath).Request(http.MethodDelete)
  273. return c.Do(ctx, req, nil)
  274. }
  275. // Valid returns whether or not the client is valid and ready for use.
  276. // This should be called after unmarshalling the client.
  277. func (c *Client) Valid() bool {
  278. if c == nil {
  279. return false
  280. }
  281. if c.Client == nil {
  282. return false
  283. }
  284. return true
  285. }
  286. // Path returns rest.Path (see cache.Client)
  287. func (c *Client) Path() string {
  288. return Path
  289. }