// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package tailfs import ( "net" "net/http" "sync" "github.com/tailscale/xnet/webdav" "tailscale.com/tailfs/shared" ) // FileServer is a standalone WebDAV server that dynamically serves up shares. // It's typically used in a separate process from the actual Tailfs server to // serve up files as an unprivileged user. type FileServer struct { l net.Listener shareHandlers map[string]http.Handler sharesMu sync.RWMutex } // NewFileServer constructs a FileServer. // // The server attempts to listen at a random address on 127.0.0.1. // The listen address is available via the Addr() method. // // The server has to be told about shares before it can serve them. This is // accomplished either by calling SetShares(), or locking the shares with // LockShares(), clearing them with ClearSharesLocked(), adding them // individually with AddShareLocked(), and finally unlocking them with // UnlockShares(). // // The server doesn't actually process requests until the Serve() method is // called. func NewFileServer() (*FileServer, error) { // path := filepath.Join(os.TempDir(), fmt.Sprintf("%v.socket", uuid.New().String())) // l, err := safesocket.Listen(path) // if err != nil { // TODO(oxtoacart): actually get safesocket working in more environments (MacOS Sandboxed, Windows, ???) l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return nil, err } // } return &FileServer{ l: l, shareHandlers: make(map[string]http.Handler), }, nil } // Addr returns the address at which this FileServer is listening. func (s *FileServer) Addr() string { return s.l.Addr().String() } // Serve() starts serving files and blocks until it encounters a fatal error. func (s *FileServer) Serve() error { return http.Serve(s.l, s) } // LockShares locks the map of shares in preparation for manipulating it. func (s *FileServer) LockShares() { s.sharesMu.Lock() } // UnlockShares unlocks the map of shares. func (s *FileServer) UnlockShares() { s.sharesMu.Unlock() } // ClearSharesLocked clears the map of shares, assuming that LockShares() has // been called first. func (s *FileServer) ClearSharesLocked() { s.shareHandlers = make(map[string]http.Handler) } // AddShareLocked adds a share to the map of shares, assuming that LockShares() // has been called first. func (s *FileServer) AddShareLocked(share, path string) { s.shareHandlers[share] = &webdav.Handler{ FileSystem: &birthTimingFS{webdav.Dir(path)}, LockSystem: webdav.NewMemLS(), } } // SetShares sets the full map of shares to the new value, mapping name->path. func (s *FileServer) SetShares(shares map[string]string) { s.LockShares() defer s.UnlockShares() s.ClearSharesLocked() for name, path := range shares { s.AddShareLocked(name, path) } } // ServeHTTP implements the http.Handler interface. func (s *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { parts := shared.CleanAndSplit(r.URL.Path) r.URL.Path = shared.Join(parts[1:]...) share := parts[0] s.sharesMu.RLock() h, found := s.shareHandlers[share] s.sharesMu.RUnlock() if !found { w.WriteHeader(http.StatusNotFound) return } h.ServeHTTP(w, r) } func (s *FileServer) Close() error { return s.l.Close() }