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 }