mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
50fb8b9123
Instead of modeling remote WebDAV servers as actual webdav.FS instances, we now just proxy traffic to them. This not only simplifies the code, but it also allows WebDAV locking to work correctly by making sure locks are handled by the servers that need to (i.e. the ones actually serving the files). Updates tailscale/corp#16827 Signed-off-by: Percy Wegmann <percy@tailscale.com>
353 lines
8.8 KiB
Go
353 lines
8.8 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package tailfsimpl
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/tailscale/xnet/webdav"
|
|
"tailscale.com/safesocket"
|
|
"tailscale.com/tailfs"
|
|
"tailscale.com/tailfs/tailfsimpl/compositedav"
|
|
"tailscale.com/tailfs/tailfsimpl/dirfs"
|
|
"tailscale.com/tailfs/tailfsimpl/shared"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote {
|
|
if logf == nil {
|
|
logf = log.Printf
|
|
}
|
|
fs := &FileSystemForRemote{
|
|
logf: logf,
|
|
lockSystem: webdav.NewMemLS(),
|
|
children: make(map[string]*compositedav.Child),
|
|
userServers: make(map[string]*userServer),
|
|
}
|
|
return fs
|
|
}
|
|
|
|
// FileSystemForRemote implements tailfs.FileSystemForRemote.
|
|
type FileSystemForRemote struct {
|
|
logf logger.Logf
|
|
lockSystem webdav.LockSystem
|
|
|
|
// mu guards the below values. Acquire a write lock before updating any of
|
|
// them, acquire a read lock before reading any of them.
|
|
mu sync.RWMutex
|
|
fileServerAddr string
|
|
shares map[string]*tailfs.Share
|
|
children map[string]*compositedav.Child
|
|
userServers map[string]*userServer
|
|
}
|
|
|
|
// SetFileServerAddr implements tailfs.FileSystemForRemote.
|
|
func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
|
|
s.mu.Lock()
|
|
s.fileServerAddr = addr
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
// SetShares implements tailfs.FileSystemForRemote.
|
|
func (s *FileSystemForRemote) SetShares(shares map[string]*tailfs.Share) {
|
|
userServers := make(map[string]*userServer)
|
|
if tailfs.AllowShareAs() {
|
|
// set up per-user server
|
|
for _, share := range shares {
|
|
p, found := userServers[share.As]
|
|
if !found {
|
|
p = &userServer{
|
|
logf: s.logf,
|
|
}
|
|
userServers[share.As] = p
|
|
}
|
|
p.shares = append(p.shares, share)
|
|
}
|
|
for _, p := range userServers {
|
|
go p.runLoop()
|
|
}
|
|
}
|
|
|
|
children := make(map[string]*compositedav.Child, len(shares))
|
|
for _, share := range shares {
|
|
children[share.Name] = s.buildChild(share)
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.shares = shares
|
|
oldUserServers := s.userServers
|
|
oldChildren := s.children
|
|
s.children = children
|
|
s.userServers = userServers
|
|
s.mu.Unlock()
|
|
|
|
s.stopUserServers(oldUserServers)
|
|
s.closeChildren(oldChildren)
|
|
}
|
|
|
|
func (s *FileSystemForRemote) buildChild(share *tailfs.Share) *compositedav.Child {
|
|
return &compositedav.Child{
|
|
Child: &dirfs.Child{
|
|
Name: share.Name,
|
|
},
|
|
BaseURL: fmt.Sprintf("http://%v/%v", hex.EncodeToString([]byte(share.Name)), url.PathEscape(share.Name)),
|
|
Transport: &http.Transport{
|
|
Dial: func(_, shareAddr string) (net.Conn, error) {
|
|
shareNameHex, _, err := net.SplitHostPort(shareAddr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse share address %v: %w", shareAddr, err)
|
|
}
|
|
|
|
// We had to encode the share name in hex to make sure it's a valid hostname
|
|
shareNameBytes, err := hex.DecodeString(shareNameHex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode share name from host %v: %v", shareNameHex, err)
|
|
}
|
|
shareName := string(shareNameBytes)
|
|
|
|
s.mu.RLock()
|
|
share, shareFound := s.shares[shareName]
|
|
userServers := s.userServers
|
|
fileServerAddr := s.fileServerAddr
|
|
s.mu.RUnlock()
|
|
|
|
if !shareFound {
|
|
return nil, fmt.Errorf("unknown share %v", shareName)
|
|
}
|
|
|
|
var addr string
|
|
if !tailfs.AllowShareAs() {
|
|
addr = fileServerAddr
|
|
} else {
|
|
userServer, found := userServers[share.As]
|
|
if found {
|
|
userServer.mu.RLock()
|
|
addr = userServer.addr
|
|
userServer.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
if addr == "" {
|
|
return nil, fmt.Errorf("unable to determine address for share %v", shareName)
|
|
}
|
|
|
|
_, err = netip.ParseAddrPort(addr)
|
|
if err == nil {
|
|
// this is a regular network address, dial normally
|
|
return net.Dial("tcp", addr)
|
|
}
|
|
// assume this is a safesocket address
|
|
return safesocket.Connect(addr)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ServeHTTPWithPerms implements tailfs.FileSystemForRemote.
|
|
func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions tailfs.Permissions, w http.ResponseWriter, r *http.Request) {
|
|
isWrite := writeMethods[r.Method]
|
|
if isWrite {
|
|
share := shared.CleanAndSplit(r.URL.Path)[0]
|
|
switch permissions.For(share) {
|
|
case tailfs.PermissionNone:
|
|
// If we have no permissions to this share, treat it as not found
|
|
// to avoid leaking any information about the share's existence.
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
case tailfs.PermissionReadOnly:
|
|
http.Error(w, "permission denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
}
|
|
|
|
s.mu.RLock()
|
|
childrenMap := s.children
|
|
s.mu.RUnlock()
|
|
|
|
children := make([]*compositedav.Child, 0, len(childrenMap))
|
|
// filter out shares to which the connecting principal has no access
|
|
for name, child := range childrenMap {
|
|
if permissions.For(name) == tailfs.PermissionNone {
|
|
continue
|
|
}
|
|
|
|
children = append(children, child)
|
|
}
|
|
|
|
h := compositedav.Handler{
|
|
Logf: s.logf,
|
|
}
|
|
h.SetChildren("", children...)
|
|
h.ServeHTTP(w, r)
|
|
}
|
|
|
|
func (s *FileSystemForRemote) stopUserServers(userServers map[string]*userServer) {
|
|
for _, server := range userServers {
|
|
if err := server.Close(); err != nil {
|
|
s.logf("error closing tailfs user server: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *FileSystemForRemote) closeChildren(children map[string]*compositedav.Child) {
|
|
for _, child := range children {
|
|
child.CloseIdleConnections()
|
|
}
|
|
}
|
|
|
|
// Close() implements tailfs.FileSystemForRemote.
|
|
func (s *FileSystemForRemote) Close() error {
|
|
s.mu.Lock()
|
|
userServers := s.userServers
|
|
children := s.children
|
|
s.userServers = make(map[string]*userServer)
|
|
s.children = make(map[string]*compositedav.Child)
|
|
s.mu.Unlock()
|
|
|
|
s.stopUserServers(userServers)
|
|
s.closeChildren(children)
|
|
return nil
|
|
}
|
|
|
|
// userServer runs tailscaled serve-tailfs to serve webdav content for the
|
|
// given Shares. All Shares are assumed to have the same Share.As, and the
|
|
// content is served as that Share.As user.
|
|
type userServer struct {
|
|
logf logger.Logf
|
|
shares []*tailfs.Share
|
|
|
|
// mu guards the below values. Acquire a write lock before updating any of
|
|
// them, acquire a read lock before reading any of them.
|
|
mu sync.RWMutex
|
|
cmd *exec.Cmd
|
|
addr string
|
|
closed bool
|
|
}
|
|
|
|
func (s *userServer) Close() error {
|
|
s.mu.Lock()
|
|
cmd := s.cmd
|
|
s.closed = true
|
|
s.mu.Unlock()
|
|
if cmd != nil && cmd.Process != nil {
|
|
return cmd.Process.Kill()
|
|
}
|
|
// not running, that's okay
|
|
return nil
|
|
}
|
|
|
|
func (s *userServer) runLoop() {
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
s.logf("can't find executable: %v", err)
|
|
return
|
|
}
|
|
maxSleepTime := 30 * time.Second
|
|
consecutiveFailures := float64(0)
|
|
var timeOfLastFailure time.Time
|
|
for {
|
|
s.mu.RLock()
|
|
closed := s.closed
|
|
s.mu.RUnlock()
|
|
if closed {
|
|
return
|
|
}
|
|
|
|
err := s.run(executable)
|
|
now := time.Now()
|
|
timeSinceLastFailure := now.Sub(timeOfLastFailure)
|
|
timeOfLastFailure = now
|
|
if timeSinceLastFailure < maxSleepTime {
|
|
consecutiveFailures++
|
|
} else {
|
|
consecutiveFailures = 1
|
|
}
|
|
sleepTime := time.Duration(math.Pow(2, consecutiveFailures)) * time.Millisecond
|
|
if sleepTime > maxSleepTime {
|
|
sleepTime = maxSleepTime
|
|
}
|
|
s.logf("user server % v stopped with error %v, will try again in %v", executable, err, sleepTime)
|
|
time.Sleep(sleepTime)
|
|
}
|
|
}
|
|
|
|
// Run runs the executable (tailscaled). This function only works on UNIX systems,
|
|
// but those are the only ones on which we use userServers anyway.
|
|
func (s *userServer) run(executable string) error {
|
|
// set up the command
|
|
args := []string{"serve-tailfs"}
|
|
for _, s := range s.shares {
|
|
args = append(args, s.Name, s.Path)
|
|
}
|
|
allArgs := []string{"-u", s.shares[0].As, executable}
|
|
allArgs = append(allArgs, args...)
|
|
cmd := exec.Command("sudo", allArgs...)
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("stdout pipe: %w", err)
|
|
}
|
|
defer stdout.Close()
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return fmt.Errorf("stderr pipe: %w", err)
|
|
}
|
|
defer stderr.Close()
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return fmt.Errorf("start: %w", err)
|
|
}
|
|
s.mu.Lock()
|
|
s.cmd = cmd
|
|
s.mu.Unlock()
|
|
|
|
// read address
|
|
stdoutScanner := bufio.NewScanner(stdout)
|
|
stdoutScanner.Scan()
|
|
if stdoutScanner.Err() != nil {
|
|
return fmt.Errorf("read addr: %w", stdoutScanner.Err())
|
|
}
|
|
addr := stdoutScanner.Text()
|
|
// send the rest of stdout and stderr to logger to avoid blocking
|
|
go func() {
|
|
for stdoutScanner.Scan() {
|
|
s.logf("tailscaled serve-tailfs stdout: %v", stdoutScanner.Text())
|
|
}
|
|
}()
|
|
stderrScanner := bufio.NewScanner(stderr)
|
|
go func() {
|
|
for stderrScanner.Scan() {
|
|
s.logf("tailscaled serve-tailfs stderr: %v", stderrScanner.Text())
|
|
}
|
|
}()
|
|
s.mu.Lock()
|
|
s.addr = strings.TrimSpace(addr)
|
|
s.mu.Unlock()
|
|
return cmd.Wait()
|
|
}
|
|
|
|
var writeMethods = map[string]bool{
|
|
"PUT": true,
|
|
"POST": true,
|
|
"COPY": true,
|
|
"LOCK": true,
|
|
"UNLOCK": true,
|
|
"MKCOL": true,
|
|
"MOVE": true,
|
|
"PROPPATCH": true,
|
|
}
|