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