github.com/vmware/govmomi@v0.37.2/session/cache/session.go (about) 1 /* 2 Copyright (c) 2020 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 cache 18 19 import ( 20 "context" 21 "crypto/sha1" 22 "encoding/json" 23 "fmt" 24 "net/url" 25 "os" 26 "os/user" 27 "path/filepath" 28 29 "github.com/vmware/govmomi/session" 30 "github.com/vmware/govmomi/vapi/rest" 31 "github.com/vmware/govmomi/vim25" 32 "github.com/vmware/govmomi/vim25/soap" 33 "github.com/vmware/govmomi/vim25/types" 34 ) 35 36 // Client interface to support client session caching 37 type Client interface { 38 json.Marshaler 39 json.Unmarshaler 40 41 Valid() bool 42 Path() string 43 } 44 45 // Session provides methods to cache authenticated vim25.Client and rest.Client sessions. 46 // Use of session cache avoids the expense of creating and deleting vSphere sessions. 47 // It also helps avoid the problem of "leaking sessions", as Session.Login will only 48 // create a new authenticated session if the cached session does not exist or is invalid. 49 // By default, username/password authentication is used to create new sessions. 50 // The Session.Login{SOAP,REST} fields can be set to use other methods, 51 // such as SAML token authentication (see govc session.login for example). 52 // 53 // When Reauth is set to true, Login skips loading file cache and performs username/password 54 // authentication, which is helpful in the case that the password in URL is different than 55 // previously cached session. Comparing to `Passthrough`, the file cache will be updated after 56 // authentication is done. 57 type Session struct { 58 URL *url.URL // URL of a vCenter or ESXi instance 59 DirSOAP string // DirSOAP cache directory. Defaults to "$HOME/.govmomi/sessions" 60 DirREST string // DirREST cache directory. Defaults to "$HOME/.govmomi/rest_sessions" 61 Insecure bool // Insecure param for soap.NewClient (tls.Config.InsecureSkipVerify) 62 Passthrough bool // Passthrough disables caching when set to true 63 Reauth bool // Reauth skips loading of cached sessions when set to true 64 65 LoginSOAP func(context.Context, *vim25.Client) error // LoginSOAP defaults to session.Manager.Login() 66 LoginREST func(context.Context, *rest.Client) error // LoginREST defaults to rest.Client.Login() 67 } 68 69 var ( 70 home = os.Getenv("GOVMOMI_HOME") 71 ) 72 73 func init() { 74 if home == "" { 75 dir, err := os.UserHomeDir() 76 if err != nil { 77 dir = os.Getenv("HOME") 78 } 79 home = filepath.Join(dir, ".govmomi") 80 } 81 } 82 83 // Endpoint returns a copy of the Session.URL with Password, Query and Fragment removed. 84 func (s *Session) Endpoint() *url.URL { 85 if s.URL == nil { 86 return nil 87 } 88 p := &url.URL{ 89 Scheme: s.URL.Scheme, 90 Host: s.URL.Host, 91 Path: s.URL.Path, 92 } 93 if u := s.URL.User; u != nil { 94 p.User = url.User(u.Username()) // Remove password 95 } 96 return p 97 } 98 99 // key is a digest of the URL scheme + username + host + Client.Path() 100 func (s *Session) key(path string) string { 101 p := s.Endpoint() 102 p.Path = path 103 104 // Key session file off of full URI and insecure setting. 105 // Hash key to get a predictable, canonical format. 106 key := fmt.Sprintf("%s#insecure=%t", p.String(), s.Insecure) 107 return fmt.Sprintf("%040x", sha1.Sum([]byte(key))) 108 } 109 110 func (s *Session) file(p string) string { 111 dir := "" 112 113 switch p { 114 case rest.Path: 115 dir = s.DirREST 116 if dir == "" { 117 dir = filepath.Join(home, "rest_sessions") 118 } 119 default: 120 dir = s.DirSOAP 121 if dir == "" { 122 dir = filepath.Join(home, "sessions") 123 } 124 } 125 126 return filepath.Join(dir, s.key(p)) 127 } 128 129 // Save a Client in the file cache. 130 // Session will not be saved if Session.Passthrough is true. 131 func (s *Session) Save(c Client) error { 132 if s.Passthrough { 133 return nil 134 } 135 136 p := s.file(c.Path()) 137 138 err := os.MkdirAll(filepath.Dir(p), 0700) 139 if err != nil { 140 return err 141 } 142 143 f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY, 0600) 144 if err != nil { 145 return err 146 } 147 148 err = json.NewEncoder(f).Encode(c) 149 if err != nil { 150 _ = f.Close() 151 return err 152 } 153 154 return f.Close() 155 } 156 157 func (s *Session) get(c Client) (bool, error) { 158 f, err := os.Open(s.file(c.Path())) 159 if err != nil { 160 if os.IsNotExist(err) { 161 return false, nil 162 } 163 164 return false, err 165 } 166 167 dec := json.NewDecoder(f) 168 err = dec.Decode(c) 169 if err != nil { 170 _ = f.Close() 171 return false, err 172 } 173 174 return c.Valid(), f.Close() 175 } 176 177 func localTicket(ctx context.Context, m *session.Manager) (*url.Userinfo, error) { 178 name := os.Getenv("USER") 179 u, err := user.Current() 180 if err == nil { 181 name = u.Username 182 } 183 184 ticket, err := m.AcquireLocalTicket(ctx, name) 185 if err != nil { 186 return nil, err 187 } 188 189 password, err := os.ReadFile(ticket.PasswordFilePath) 190 if err != nil { 191 return nil, err 192 } 193 194 return url.UserPassword(ticket.UserName, string(password)), nil 195 } 196 197 func (s *Session) loginSOAP(ctx context.Context, c *vim25.Client) error { 198 m := session.NewManager(c) 199 u := s.URL.User 200 name := u.Username() 201 202 if name == "" && !c.IsVC() { 203 // If no username is provided, try to acquire a local ticket. 204 // When invoked remotely, ESX returns an InvalidRequestFault. 205 // So, rather than return an error here, fallthrough to Login() with the original User to 206 // to avoid what would be a confusing error message. 207 luser, lerr := localTicket(ctx, m) 208 if lerr == nil { 209 // We are running directly on an ESX or Workstation host and can use the ticket with Login() 210 u = luser 211 name = u.Username() 212 } 213 } 214 if name == "" { 215 // ServiceContent does not require authentication 216 return nil 217 } 218 219 return m.Login(ctx, u) 220 } 221 222 func (s *Session) loginREST(ctx context.Context, c *rest.Client) error { 223 return c.Login(ctx, s.URL.User) 224 } 225 226 func soapSessionValid(ctx context.Context, client *vim25.Client) (bool, error) { 227 m := session.NewManager(client) 228 u, err := m.UserSession(ctx) 229 if err != nil { 230 if soap.IsSoapFault(err) { 231 fault := soap.ToSoapFault(err).VimFault() 232 // If the PropertyCollector is not found, the saved session for this URL is not valid 233 if _, ok := fault.(types.ManagedObjectNotFound); ok { 234 return false, nil 235 } 236 } 237 238 return false, err 239 } 240 241 return u != nil, nil 242 } 243 244 func restSessionValid(ctx context.Context, client *rest.Client) (bool, error) { 245 s, err := client.Session(ctx) 246 if err != nil { 247 return false, err 248 } 249 return s != nil, nil 250 } 251 252 // Load a Client from the file cache. 253 // Returns false if no cache exists or is invalid. 254 // An error is returned if the file cannot be opened or is not json encoded. 255 // After loading the Client from the file: 256 // Returns true if the session is still valid, false otherwise indicating the client requires authentication. 257 // An error is returned if the session ID cannot be validated. 258 // Returns false if Session.Passthrough is true. 259 func (s *Session) Load(ctx context.Context, c Client, config func(*soap.Client) error) (bool, error) { 260 if s.Passthrough || s.Reauth { 261 return false, nil 262 } 263 264 ok, err := s.get(c) 265 if err != nil { 266 return false, err 267 268 } 269 if !ok { 270 return false, nil 271 } 272 273 switch client := c.(type) { 274 case *vim25.Client: 275 if config != nil { 276 if err := config(client.Client); err != nil { 277 return false, err 278 } 279 } 280 return soapSessionValid(ctx, client) 281 case *rest.Client: 282 if config != nil { 283 if err := config(client.Client); err != nil { 284 return false, err 285 } 286 } 287 return restSessionValid(ctx, client) 288 default: 289 panic(fmt.Sprintf("unsupported client type=%T", client)) 290 } 291 } 292 293 // Login returns a cached session via Load() if valid. 294 // Otherwise, creates a new authenticated session and saves to the cache. 295 // The config func can be used to apply soap.Client configuration, such as TLS settings. 296 // When Session.Passthrough is true, Login will always create a new session. 297 func (s *Session) Login(ctx context.Context, c Client, config func(*soap.Client) error) error { 298 ok, err := s.Load(ctx, c, config) 299 if err != nil { 300 return err 301 } 302 if ok { 303 return nil 304 } 305 306 sc := soap.NewClient(s.URL, s.Insecure) 307 308 if config != nil { 309 err = config(sc) 310 if err != nil { 311 return err 312 } 313 } 314 315 switch client := c.(type) { 316 case *vim25.Client: 317 vc, err := vim25.NewClient(ctx, sc) 318 if err != nil { 319 return err 320 } 321 322 login := s.loginSOAP 323 if s.LoginSOAP != nil { 324 login = s.LoginSOAP 325 } 326 if err = login(ctx, vc); err != nil { 327 return err 328 } 329 330 *client = *vc 331 c = client 332 case *rest.Client: 333 client.Client = sc.NewServiceClient(rest.Path, "") 334 335 login := s.loginREST 336 if s.LoginREST != nil { 337 login = s.LoginREST 338 } 339 if err = login(ctx, client); err != nil { 340 return err 341 } 342 343 c = client 344 default: 345 panic(fmt.Sprintf("unsupported client type=%T", client)) 346 } 347 348 return s.Save(c) 349 } 350 351 // Login calls the Logout method for the given Client if Session.Passthrough is true. 352 // Otherwise returns nil. 353 func (s *Session) Logout(ctx context.Context, c Client) error { 354 if s.Passthrough { 355 switch client := c.(type) { 356 case *vim25.Client: 357 return session.NewManager(client).Logout(ctx) 358 case *rest.Client: 359 return client.Logout(ctx) 360 default: 361 panic(fmt.Sprintf("unsupported client type=%T", client)) 362 } 363 } 364 return nil 365 }