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  }