github.com/vmware/govmomi@v0.51.0/toolbox/hgfs/server.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 hgfs
     6  
     7  import (
     8  	"errors"
     9  	"flag"
    10  	"fmt"
    11  	"io"
    12  	"log"
    13  	"math/rand"
    14  	"net/url"
    15  	"os"
    16  	"path/filepath"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  	"sync/atomic"
    21  )
    22  
    23  // See: https://github.com/vmware/open-vm-tools/blob/master/open-vm-tools/lib/hgfsServer/hgfsServer.c
    24  
    25  var (
    26  	// Trace enables hgfs packet tracing
    27  	Trace = false
    28  )
    29  
    30  // Server provides an HGFS protocol implementation to support guest tools VmxiHgfsSendPacketCommand
    31  type Server struct {
    32  	Capabilities []Capability
    33  
    34  	handlers map[int32]func(*Packet) (any, error)
    35  	schemes  map[string]FileHandler
    36  	sessions map[uint64]*session
    37  	mu       sync.Mutex
    38  	handle   uint32
    39  
    40  	chmod func(string, os.FileMode) error
    41  	chown func(string, int, int) error
    42  }
    43  
    44  // NewServer creates a new Server instance with the default handlers
    45  func NewServer() *Server {
    46  	if f := flag.Lookup("toolbox.trace"); f != nil {
    47  		Trace, _ = strconv.ParseBool(f.Value.String())
    48  	}
    49  
    50  	s := &Server{
    51  		sessions: make(map[uint64]*session),
    52  		schemes:  make(map[string]FileHandler),
    53  		chmod:    os.Chmod,
    54  		chown:    os.Chown,
    55  	}
    56  
    57  	s.handlers = map[int32]func(*Packet) (any, error){
    58  		OpCreateSessionV4:  s.CreateSessionV4,
    59  		OpDestroySessionV4: s.DestroySessionV4,
    60  		OpGetattrV2:        s.GetattrV2,
    61  		OpSetattrV2:        s.SetattrV2,
    62  		OpOpen:             s.Open,
    63  		OpClose:            s.Close,
    64  		OpOpenV3:           s.OpenV3,
    65  		OpReadV3:           s.ReadV3,
    66  		OpWriteV3:          s.WriteV3,
    67  	}
    68  
    69  	for op := range s.handlers {
    70  		s.Capabilities = append(s.Capabilities, Capability{Op: op, Flags: 0x1})
    71  	}
    72  
    73  	return s
    74  }
    75  
    76  // RegisterFileHandler enables dispatch to handler for the given scheme.
    77  func (s *Server) RegisterFileHandler(scheme string, handler FileHandler) {
    78  	if handler == nil {
    79  		delete(s.schemes, scheme)
    80  		return
    81  	}
    82  	s.schemes[scheme] = handler
    83  }
    84  
    85  // Dispatch unpacks the given request packet and dispatches to the appropriate handler
    86  func (s *Server) Dispatch(packet []byte) ([]byte, error) {
    87  	req := &Packet{}
    88  
    89  	err := req.UnmarshalBinary(packet)
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	if Trace {
    95  		fmt.Fprintf(os.Stderr, "[hgfs] request  %#v\n", req.Header)
    96  	}
    97  
    98  	var res any
    99  
   100  	handler, ok := s.handlers[req.Op]
   101  	if ok {
   102  		res, err = handler(req)
   103  	} else {
   104  		err = &Status{
   105  			Code: StatusOperationNotSupported,
   106  			Err:  fmt.Errorf("unsupported Op(%d)", req.Op),
   107  		}
   108  	}
   109  
   110  	return req.Reply(res, err)
   111  }
   112  
   113  // File interface abstracts standard i/o methods to support transfer
   114  // of regular files and archives of directories.
   115  type File interface {
   116  	io.Reader
   117  	io.Writer
   118  	io.Closer
   119  
   120  	Name() string
   121  }
   122  
   123  // FileHandler is the plugin interface for hgfs file transport.
   124  type FileHandler interface {
   125  	Stat(*url.URL) (os.FileInfo, error)
   126  	Open(*url.URL, int32) (File, error)
   127  }
   128  
   129  // urlParse attempts to convert the given name to a URL with scheme for use as FileHandler dispatch.
   130  func urlParse(name string) *url.URL {
   131  	var info os.FileInfo
   132  
   133  	u, err := url.Parse(name)
   134  	if err == nil && u.Scheme == "" {
   135  		info, err = os.Stat(u.Path)
   136  		if err == nil && info.IsDir() {
   137  			u.Scheme = ArchiveScheme // special case for IsDir()
   138  			return u
   139  		}
   140  	}
   141  
   142  	u, err = url.Parse(strings.TrimPrefix(name, "/")) // must appear to be an absolute path or hgfs errors
   143  	if err != nil {
   144  		u = &url.URL{Path: name}
   145  	}
   146  
   147  	if u.Scheme == "" {
   148  		ix := strings.Index(u.Path, "/")
   149  		if ix > 0 {
   150  			u.Scheme = u.Path[:ix]
   151  			u.Path = u.Path[ix:]
   152  		}
   153  	}
   154  
   155  	return u
   156  }
   157  
   158  // OpenFile selects the File implementation based on file type and mode.
   159  func (s *Server) OpenFile(name string, mode int32) (File, error) {
   160  	u := urlParse(name)
   161  
   162  	if h, ok := s.schemes[u.Scheme]; ok {
   163  		f, serr := h.Open(u, mode)
   164  		if serr != os.ErrNotExist {
   165  			return f, serr
   166  		}
   167  	}
   168  
   169  	switch mode {
   170  	case OpenModeReadOnly:
   171  		return os.Open(filepath.Clean(name))
   172  	case OpenModeWriteOnly:
   173  		flag := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
   174  		return os.OpenFile(name, flag, 0600)
   175  	default:
   176  		return nil, &Status{
   177  			Err:  fmt.Errorf("open mode(%d) not supported for file %q", mode, name),
   178  			Code: StatusAccessDenied,
   179  		}
   180  	}
   181  }
   182  
   183  // Stat wraps os.Stat such that we can report directory types as regular files to support archive streaming.
   184  // In the case of standard vmware-tools, attempts to transfer directories result
   185  // with a VIX_E_NOT_A_FILE (see InitiateFileTransfer{To,From}Guest).
   186  // Note that callers on the VMX side that reach this path are only concerned with:
   187  // - does the file exist?
   188  // - size:
   189  // + used for UI progress with desktop Drag-N-Drop operations, which toolbox does not support.
   190  // + sent to as Content-Length header in response to GET of FileTransferInformation.Url,
   191  // if the first ReadV3 size is > HGFS_LARGE_PACKET_MAX
   192  func (s *Server) Stat(name string) (os.FileInfo, error) {
   193  	u := urlParse(name)
   194  
   195  	if h, ok := s.schemes[u.Scheme]; ok {
   196  		sinfo, serr := h.Stat(u)
   197  		if serr != os.ErrNotExist {
   198  			return sinfo, serr
   199  		}
   200  	}
   201  
   202  	return os.Stat(name)
   203  }
   204  
   205  type session struct {
   206  	files map[uint32]File
   207  	mu    sync.Mutex
   208  }
   209  
   210  // TODO: we currently depend on the VMX to close files and remove sessions,
   211  // which it does provided it can communicate with the toolbox.  Let's look at
   212  // adding session expiration when implementing OpenModeWriteOnly support.
   213  func newSession() *session {
   214  	return &session{
   215  		files: make(map[uint32]File),
   216  	}
   217  }
   218  
   219  func (s *Server) getSession(p *Packet) (*session, error) {
   220  	s.mu.Lock()
   221  	session, ok := s.sessions[p.SessionID]
   222  	s.mu.Unlock()
   223  
   224  	if !ok {
   225  		return nil, &Status{
   226  			Code: StatusStaleSession,
   227  			Err:  errors.New("session not found"),
   228  		}
   229  	}
   230  
   231  	return session, nil
   232  }
   233  
   234  func (s *Server) removeSession(id uint64) bool {
   235  	s.mu.Lock()
   236  	session, ok := s.sessions[id]
   237  	delete(s.sessions, id)
   238  	s.mu.Unlock()
   239  
   240  	if !ok {
   241  		return false
   242  	}
   243  
   244  	session.mu.Lock()
   245  	defer session.mu.Unlock()
   246  
   247  	for _, f := range session.files {
   248  		log.Printf("[hgfs] session %X removed with open file: %s", id, f.Name())
   249  		_ = f.Close()
   250  	}
   251  
   252  	return true
   253  }
   254  
   255  // open-vm-tools' session max is 1024, there shouldn't be more than a handful at a given time in our use cases
   256  const maxSessions = 24
   257  
   258  // CreateSessionV4 handls OpCreateSessionV4 requests
   259  func (s *Server) CreateSessionV4(p *Packet) (any, error) {
   260  	const SessionMaxPacketSizeValid = 0x1
   261  
   262  	req := new(RequestCreateSessionV4)
   263  	err := UnmarshalBinary(p.Payload, req)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	res := &ReplyCreateSessionV4{
   269  		SessionID:       uint64(rand.Int63()),
   270  		NumCapabilities: uint32(len(s.Capabilities)),
   271  		MaxPacketSize:   LargePacketMax,
   272  		Flags:           SessionMaxPacketSizeValid,
   273  		Capabilities:    s.Capabilities,
   274  	}
   275  
   276  	s.mu.Lock()
   277  	defer s.mu.Unlock()
   278  	if len(s.sessions) > maxSessions {
   279  		return nil, &Status{Code: StatusTooManySessions}
   280  	}
   281  
   282  	s.sessions[res.SessionID] = newSession()
   283  
   284  	return res, nil
   285  }
   286  
   287  // DestroySessionV4 handls OpDestroySessionV4 requests
   288  func (s *Server) DestroySessionV4(p *Packet) (any, error) {
   289  	if s.removeSession(p.SessionID) {
   290  		return &ReplyDestroySessionV4{}, nil
   291  	}
   292  
   293  	return nil, &Status{Code: StatusStaleSession}
   294  }
   295  
   296  // Stat maps os.FileInfo to AttrV2
   297  func (a *AttrV2) Stat(info os.FileInfo) {
   298  	switch {
   299  	case info.IsDir():
   300  		a.Type = FileTypeDirectory
   301  	case info.Mode()&os.ModeSymlink == os.ModeSymlink:
   302  		a.Type = FileTypeSymlink
   303  	default:
   304  		a.Type = FileTypeRegular
   305  	}
   306  
   307  	a.Size = uint64(info.Size())
   308  
   309  	a.Mask = AttrValidType | AttrValidSize
   310  
   311  	a.sysStat(info)
   312  }
   313  
   314  // GetattrV2 handles OpGetattrV2 requests
   315  func (s *Server) GetattrV2(p *Packet) (any, error) {
   316  	res := &ReplyGetattrV2{}
   317  
   318  	req := new(RequestGetattrV2)
   319  	err := UnmarshalBinary(p.Payload, req)
   320  	if err != nil {
   321  		return nil, err
   322  	}
   323  
   324  	name := req.FileName.Path()
   325  	info, err := s.Stat(name)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  
   330  	res.Attr.Stat(info)
   331  
   332  	return res, nil
   333  }
   334  
   335  // SetattrV2 handles OpSetattrV2 requests
   336  func (s *Server) SetattrV2(p *Packet) (any, error) {
   337  	res := &ReplySetattrV2{}
   338  
   339  	req := new(RequestSetattrV2)
   340  	err := UnmarshalBinary(p.Payload, req)
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  
   345  	name := req.FileName.Path()
   346  
   347  	_, err = os.Stat(name)
   348  	if err != nil && os.IsNotExist(err) {
   349  		// assuming this is a virtual file
   350  		return res, nil
   351  	}
   352  
   353  	uid := -1
   354  	if req.Attr.Mask&AttrValidUserID == AttrValidUserID {
   355  		uid = int(req.Attr.UserID)
   356  	}
   357  
   358  	gid := -1
   359  	if req.Attr.Mask&AttrValidGroupID == AttrValidGroupID {
   360  		gid = int(req.Attr.GroupID)
   361  	}
   362  
   363  	err = s.chown(name, uid, gid)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  
   368  	var perm os.FileMode
   369  
   370  	if req.Attr.Mask&AttrValidOwnerPerms == AttrValidOwnerPerms {
   371  		perm |= os.FileMode(req.Attr.OwnerPerms) << 6
   372  	}
   373  
   374  	if req.Attr.Mask&AttrValidGroupPerms == AttrValidGroupPerms {
   375  		perm |= os.FileMode(req.Attr.GroupPerms) << 3
   376  	}
   377  
   378  	if req.Attr.Mask&AttrValidOtherPerms == AttrValidOtherPerms {
   379  		perm |= os.FileMode(req.Attr.OtherPerms)
   380  	}
   381  
   382  	if perm != 0 {
   383  		err = s.chmod(name, perm)
   384  		if err != nil {
   385  			return nil, err
   386  		}
   387  	}
   388  
   389  	return res, nil
   390  }
   391  
   392  func (s *Server) newHandle() uint32 {
   393  	return atomic.AddUint32(&s.handle, 1)
   394  }
   395  
   396  // Open handles OpOpen requests
   397  func (s *Server) Open(p *Packet) (any, error) {
   398  	req := new(RequestOpen)
   399  	err := UnmarshalBinary(p.Payload, req)
   400  	if err != nil {
   401  		return nil, err
   402  	}
   403  
   404  	session, err := s.getSession(p)
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  
   409  	name := req.FileName.Path()
   410  	mode := req.OpenMode
   411  
   412  	if mode != OpenModeReadOnly {
   413  		return nil, &Status{
   414  			Err:  fmt.Errorf("open mode(%d) not supported for file %q", mode, name),
   415  			Code: StatusAccessDenied,
   416  		}
   417  	}
   418  
   419  	file, err := s.OpenFile(name, mode)
   420  	if err != nil {
   421  		return nil, err
   422  	}
   423  
   424  	res := &ReplyOpen{
   425  		Handle: s.newHandle(),
   426  	}
   427  
   428  	session.mu.Lock()
   429  	session.files[res.Handle] = file
   430  	session.mu.Unlock()
   431  
   432  	return res, nil
   433  }
   434  
   435  // Close handles OpClose requests
   436  func (s *Server) Close(p *Packet) (any, error) {
   437  	req := new(RequestClose)
   438  	err := UnmarshalBinary(p.Payload, req)
   439  	if err != nil {
   440  		return nil, err
   441  	}
   442  
   443  	session, err := s.getSession(p)
   444  	if err != nil {
   445  		return nil, err
   446  	}
   447  
   448  	session.mu.Lock()
   449  	file, ok := session.files[req.Handle]
   450  	if ok {
   451  		delete(session.files, req.Handle)
   452  	}
   453  	session.mu.Unlock()
   454  
   455  	if ok {
   456  		err = file.Close()
   457  	} else {
   458  		return nil, &Status{Code: StatusInvalidHandle}
   459  	}
   460  
   461  	return &ReplyClose{}, err
   462  }
   463  
   464  // OpenV3 handles OpOpenV3 requests
   465  func (s *Server) OpenV3(p *Packet) (any, error) {
   466  	req := new(RequestOpenV3)
   467  	err := UnmarshalBinary(p.Payload, req)
   468  	if err != nil {
   469  		return nil, err
   470  	}
   471  
   472  	session, err := s.getSession(p)
   473  	if err != nil {
   474  		return nil, err
   475  	}
   476  
   477  	name := req.FileName.Path()
   478  
   479  	if req.DesiredLock != LockNone {
   480  		return nil, &Status{
   481  			Err:  fmt.Errorf("open lock type=%d not supported for file %q", req.DesiredLock, name),
   482  			Code: StatusOperationNotSupported,
   483  		}
   484  	}
   485  
   486  	file, err := s.OpenFile(name, req.OpenMode)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  
   491  	res := &ReplyOpenV3{
   492  		Handle: s.newHandle(),
   493  	}
   494  
   495  	session.mu.Lock()
   496  	session.files[res.Handle] = file
   497  	session.mu.Unlock()
   498  
   499  	return res, nil
   500  }
   501  
   502  // ReadV3 handles OpReadV3 requests
   503  func (s *Server) ReadV3(p *Packet) (any, error) {
   504  	req := new(RequestReadV3)
   505  	err := UnmarshalBinary(p.Payload, req)
   506  	if err != nil {
   507  		return nil, err
   508  	}
   509  
   510  	session, err := s.getSession(p)
   511  	if err != nil {
   512  		return nil, err
   513  	}
   514  
   515  	session.mu.Lock()
   516  	file, ok := session.files[req.Handle]
   517  	session.mu.Unlock()
   518  
   519  	if !ok {
   520  		return nil, &Status{Code: StatusInvalidHandle}
   521  	}
   522  
   523  	buf := make([]byte, req.RequiredSize)
   524  
   525  	// Use ReadFull as Read() of an archive io.Pipe may return much smaller chunks,
   526  	// such as when we've read a tar header.
   527  	n, err := io.ReadFull(file, buf)
   528  	if err != nil && n == 0 {
   529  		if err != io.EOF {
   530  			return nil, err
   531  		}
   532  	}
   533  
   534  	res := &ReplyReadV3{
   535  		ActualSize: uint32(n),
   536  		Payload:    buf[:n],
   537  	}
   538  
   539  	return res, nil
   540  }
   541  
   542  // WriteV3 handles OpWriteV3 requests
   543  func (s *Server) WriteV3(p *Packet) (any, error) {
   544  	req := new(RequestWriteV3)
   545  	err := UnmarshalBinary(p.Payload, req)
   546  	if err != nil {
   547  		return nil, err
   548  	}
   549  
   550  	session, err := s.getSession(p)
   551  	if err != nil {
   552  		return nil, err
   553  	}
   554  
   555  	session.mu.Lock()
   556  	file, ok := session.files[req.Handle]
   557  	session.mu.Unlock()
   558  
   559  	if !ok {
   560  		return nil, &Status{Code: StatusInvalidHandle}
   561  	}
   562  
   563  	n, err := file.Write(req.Payload)
   564  	if err != nil {
   565  		return nil, err
   566  	}
   567  
   568  	res := &ReplyWriteV3{
   569  		ActualSize: uint32(n),
   570  	}
   571  
   572  	return res, nil
   573  }