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  }