tailfs: replace webdavfs with reverse proxies
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>
This commit is contained in:
committed by
Percy Wegmann
parent
e1bd7488d0
commit
50fb8b9123
@@ -4,9 +4,7 @@
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
@@ -19,10 +17,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"github.com/studio-b12/gowebdav"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/webdavfs"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -46,24 +43,28 @@ func init() {
|
||||
// going over the Tailscale network stack.
|
||||
func TestDirectoryListing(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
defer s.stop()
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.checkDirList("root directory should contain the one and only domain once a remote has been set", "/", domain)
|
||||
s.checkDirList("domain should contain its only remote", shared.Join(domain), remote1)
|
||||
s.checkDirList("remote with no shares should be empty", shared.Join(domain, remote1))
|
||||
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.checkDirList("remote with one share should contain that share", shared.Join(domain, remote1), share11)
|
||||
s.addShare(remote1, share12, tailfs.PermissionReadOnly)
|
||||
s.checkDirList("remote with two shares should contain both in lexicographical order", shared.Join(domain, remote1), share12, share11)
|
||||
s.checkDirListIncremental("remote with two shares should contain both in lexicographical order even when reading directory incrementally", shared.Join(domain, remote1), share12, share11)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
s.checkDirList("remote share should contain file", shared.Join(domain, remote1, share11), file111)
|
||||
|
||||
s.addRemote(remote2)
|
||||
s.checkDirList("domain with two remotes should contain both in lexicographical order", shared.Join(domain), remote2, remote1)
|
||||
|
||||
s.freezeRemote(remote1)
|
||||
s.checkDirList("domain with two remotes should contain both in lexicographical order even if one is unreachable", shared.Join(domain), remote2, remote1)
|
||||
s.checkDirList("directory listing for offline remote should return empty list", shared.Join(domain, remote1))
|
||||
_, err := s.client.ReadDir(shared.Join(domain, remote1))
|
||||
if err == nil {
|
||||
t.Error("directory listing for offline remote should fail")
|
||||
}
|
||||
s.unfreezeRemote(remote1)
|
||||
|
||||
s.checkDirList("attempt at lateral traversal should simply list shares", shared.Join(domain, remote1, share11, ".."), share12, share11)
|
||||
@@ -71,7 +72,6 @@ func TestDirectoryListing(t *testing.T) {
|
||||
|
||||
func TestFileManipulation(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
defer s.stop()
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
@@ -86,178 +86,6 @@ func TestFileManipulation(t *testing.T) {
|
||||
s.writeFile("writing file to non-existent share should fail", remote1, "non-existent", file111, "hello world", false)
|
||||
}
|
||||
|
||||
func TestFileOps(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
s := newSystem(t)
|
||||
defer s.stop()
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
fi, err := s.fs.Stat(context.Background(), pathTo(remote1, share11, file111))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to Stat: %s", err)
|
||||
}
|
||||
bt, ok := fi.(webdav.BirthTimer)
|
||||
if !ok {
|
||||
t.Fatal("FileInfo should be a BirthTimer")
|
||||
}
|
||||
birthTime, err := bt.BirthTime(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to BirthTime: %s", err)
|
||||
}
|
||||
if birthTime.IsZero() {
|
||||
t.Fatal("BirthTime() should return a non-zero time")
|
||||
}
|
||||
|
||||
_, err = s.fs.OpenFile(ctx, pathTo(remote1, share11, "nonexistent.txt"), os.O_RDONLY, 0)
|
||||
if err == nil {
|
||||
t.Fatal("opening non-existent file for read should fail")
|
||||
}
|
||||
|
||||
dir, err := s.fs.OpenFile(ctx, shared.Join(domain, remote1), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open directory for read: %s", err)
|
||||
}
|
||||
defer dir.Close()
|
||||
|
||||
_, err = dir.Seek(0, io.SeekStart)
|
||||
if err == nil {
|
||||
t.Fatal("seeking in directory should fail")
|
||||
}
|
||||
|
||||
_, err = dir.Read(make([]byte, 8))
|
||||
if err == nil {
|
||||
t.Fatal("reading bytes from directory should fail")
|
||||
}
|
||||
_, err = dir.Write(make([]byte, 8))
|
||||
if err == nil {
|
||||
t.Fatal("writing bytes to directory should fail")
|
||||
}
|
||||
|
||||
readOnlyFile, err := s.fs.OpenFile(ctx, pathTo(remote1, share11, file111), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open file for read: %s", err)
|
||||
}
|
||||
defer readOnlyFile.Close()
|
||||
|
||||
n, err := readOnlyFile.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to seek 0 from start of read-only file: %s", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Fatal("seeking 0 from start of read-only file should return 0")
|
||||
}
|
||||
|
||||
n, err = readOnlyFile.Seek(1, io.SeekStart)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to seek 1 from start of read-only file: %s", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatal("seeking 1 from start of read-only file should return 1")
|
||||
}
|
||||
|
||||
n, err = readOnlyFile.Seek(0, io.SeekEnd)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to seek 0 from end of read-only file: %s", err)
|
||||
}
|
||||
if n != fi.Size() {
|
||||
t.Fatal("seeking 0 from end of read-only file should return file size")
|
||||
}
|
||||
|
||||
_, err = readOnlyFile.Seek(1, io.SeekEnd)
|
||||
if err == nil {
|
||||
t.Fatal("seeking 1 from end of read-only file should fail")
|
||||
}
|
||||
|
||||
_, err = readOnlyFile.Seek(0, io.SeekCurrent)
|
||||
if err == nil {
|
||||
t.Fatal("seeking from current of read-only file should fail")
|
||||
}
|
||||
|
||||
_, err = readOnlyFile.Write(make([]byte, 8))
|
||||
if err == nil {
|
||||
t.Fatal("writing bytes to read-only file should fail")
|
||||
}
|
||||
|
||||
writeOnlyFile, err := s.fs.OpenFile(ctx, pathTo(remote1, share11, file111), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to OpenFile for write: %s", err)
|
||||
}
|
||||
defer writeOnlyFile.Close()
|
||||
|
||||
_, err = writeOnlyFile.Seek(0, io.SeekStart)
|
||||
if err == nil {
|
||||
t.Fatal("seeking in write only file should fail")
|
||||
}
|
||||
|
||||
_, err = writeOnlyFile.Read(make([]byte, 8))
|
||||
if err == nil {
|
||||
t.Fatal("reading bytes from a write only file should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileRewind(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
s := newSystem(t)
|
||||
defer s.stop()
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
|
||||
// Create a file slightly longer than our max rewind buffer of 512
|
||||
fileLength := webdavfs.MaxRewindBuffer + 1
|
||||
data := make([]byte, fileLength)
|
||||
for i := 0; i < fileLength; i++ {
|
||||
data[i] = byte(i % 256)
|
||||
}
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, string(data), true)
|
||||
|
||||
// Try reading and rewinding in every size up to the maximum buffer length
|
||||
for i := 0; i < webdavfs.MaxRewindBuffer; i++ {
|
||||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
|
||||
f, err := s.fs.OpenFile(ctx, pathTo(remote1, share11, file111), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("failed top OpenFile for read: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
b := make([]byte, fileLength)
|
||||
|
||||
n, err := io.ReadFull(f, b[:i])
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read first %d bytes from file: %s", i, err)
|
||||
}
|
||||
if n != i {
|
||||
log.Fatalf("Reading first %d bytes should report correct count, but reported %d", i, n)
|
||||
}
|
||||
|
||||
_, err = f.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to seek back %d bytes: %s", i, err)
|
||||
}
|
||||
|
||||
n, err = io.ReadFull(f, b)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read full file: %s", err)
|
||||
}
|
||||
if n != fileLength {
|
||||
t.Fatalf("reading full file reported incorrect count, got %d, want %d", n, fileLength)
|
||||
}
|
||||
if string(b) != string(data) {
|
||||
t.Fatalf("read wrong data, got %q, want %q", b, data)
|
||||
}
|
||||
|
||||
_, err = f.Seek(0, io.SeekStart)
|
||||
if err == nil {
|
||||
t.Fatal("Attempting to seek to beginning of file after having read past rewind buffer should fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type local struct {
|
||||
l net.Listener
|
||||
fs *FileSystemForLocal
|
||||
@@ -289,7 +117,7 @@ func (r *remote) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
type system struct {
|
||||
t *testing.T
|
||||
local *local
|
||||
fs webdav.FileSystem
|
||||
client *gowebdav.Client
|
||||
remotes map[string]*remote
|
||||
}
|
||||
|
||||
@@ -314,15 +142,16 @@ func newSystem(t *testing.T) *system {
|
||||
}
|
||||
}()
|
||||
|
||||
return &system{
|
||||
t: t,
|
||||
local: &local{l: l, fs: fs},
|
||||
fs: webdavfs.New(webdavfs.Options{
|
||||
URL: fmt.Sprintf("http://%s", l.Addr()),
|
||||
Transport: &http.Transport{DisableKeepAlives: true},
|
||||
}),
|
||||
client := gowebdav.NewClient(fmt.Sprintf("http://%s", l.Addr()), "", "")
|
||||
client.SetTransport(&http.Transport{DisableKeepAlives: true})
|
||||
s := &system{
|
||||
t: t,
|
||||
local: &local{l: l, fs: fs},
|
||||
client: client,
|
||||
remotes: make(map[string]*remote),
|
||||
}
|
||||
t.Cleanup(s.stop)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *system) addRemote(name string) {
|
||||
@@ -357,7 +186,13 @@ func (s *system) addRemote(name string) {
|
||||
URL: fmt.Sprintf("http://%s", r.l.Addr()),
|
||||
})
|
||||
}
|
||||
s.local.fs.SetRemotes(domain, remotes, &http.Transport{})
|
||||
s.local.fs.SetRemotes(
|
||||
domain,
|
||||
remotes,
|
||||
&http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
ResponseHeaderTimeout: 5 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *system) addShare(remoteName, shareName string, permission tailfs.Permission) {
|
||||
@@ -399,26 +234,11 @@ func (s *system) unfreezeRemote(remoteName string) {
|
||||
|
||||
func (s *system) writeFile(label, remoteName, shareName, name, contents string, expectSuccess bool) {
|
||||
path := pathTo(remoteName, shareName, name)
|
||||
file, err := s.fs.OpenFile(context.Background(), path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
err := s.client.Write(path, []byte(contents), 0644)
|
||||
if expectSuccess && err != nil {
|
||||
s.t.Fatalf("%v: expected success writing file %q, but got error %v", label, path, err)
|
||||
}
|
||||
defer func() {
|
||||
if !expectSuccess && err == nil {
|
||||
s.t.Fatalf("%v: expected error writing file %q", label, path)
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
err = file.Close()
|
||||
if expectSuccess && err != nil {
|
||||
s.t.Fatalf("error closing %v: %v", path, err)
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = file.Write([]byte(contents))
|
||||
if expectSuccess && err != nil {
|
||||
s.t.Fatalf("%v: writing file %q: %v", label, path, err)
|
||||
} else if !expectSuccess && err == nil {
|
||||
s.t.Fatalf("%v: expected error writing file %q", label, path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,12 +257,7 @@ func (s *system) checkFileContents(remoteName, shareName, name string) {
|
||||
}
|
||||
|
||||
func (s *system) checkDirList(label string, path string, want ...string) {
|
||||
file, err := s.fs.OpenFile(context.Background(), path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
s.t.Fatalf("failed to OpenFile: %s", err)
|
||||
}
|
||||
|
||||
got, err := file.Readdir(0)
|
||||
got, err := s.client.ReadDir(path)
|
||||
if err != nil {
|
||||
s.t.Fatalf("failed to Readdir: %s", err)
|
||||
}
|
||||
@@ -460,35 +275,6 @@ func (s *system) checkDirList(label string, path string, want ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *system) checkDirListIncremental(label string, path string, want ...string) {
|
||||
file, err := s.fs.OpenFile(context.Background(), path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
s.t.Fatal(err)
|
||||
}
|
||||
|
||||
var gotNames []string
|
||||
for {
|
||||
got, err := file.Readdir(1)
|
||||
for _, fi := range got {
|
||||
gotNames = append(gotNames, fi.Name())
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
s.t.Fatalf("failed to Readdir: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(want) == 0 && len(gotNames) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(want, gotNames); diff != "" {
|
||||
s.t.Errorf("%v: (-got, +want):\n%s", label, diff)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *system) stat(remoteName, shareName, name string) os.FileInfo {
|
||||
filename := filepath.Join(s.remotes[remoteName].shares[shareName], name)
|
||||
fi, err := os.Stat(filename)
|
||||
@@ -501,7 +287,7 @@ func (s *system) stat(remoteName, shareName, name string) os.FileInfo {
|
||||
|
||||
func (s *system) statViaWebDAV(remoteName, shareName, name string) os.FileInfo {
|
||||
path := pathTo(remoteName, shareName, name)
|
||||
fi, err := s.fs.Stat(context.Background(), path)
|
||||
fi, err := s.client.Stat(path)
|
||||
if err != nil {
|
||||
s.t.Fatalf("failed to Stat: %s", err)
|
||||
}
|
||||
@@ -521,17 +307,10 @@ func (s *system) read(remoteName, shareName, name string) string {
|
||||
|
||||
func (s *system) readViaWebDAV(remoteName, shareName, name string) string {
|
||||
path := pathTo(remoteName, shareName, name)
|
||||
file, err := s.fs.OpenFile(context.Background(), path, os.O_RDONLY, 0)
|
||||
b, err := s.client.Read(path)
|
||||
if err != nil {
|
||||
s.t.Fatalf("failed to OpenFile: %s", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
s.t.Fatalf("failed to ReadAll: %s", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user