drive: rewrite LOCK paths
Fixes #12097 Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:
committed by
Percy Wegmann
parent
87f00d76c4
commit
59848fe14b
@@ -14,6 +14,8 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -30,14 +32,29 @@ import (
|
||||
const (
|
||||
domain = `test$%domain.com`
|
||||
|
||||
remote1 = `rem ote$%1`
|
||||
remote2 = `_rem ote$%2`
|
||||
share11 = `sha re$%11`
|
||||
share12 = `_sha re$%12`
|
||||
file111 = `fi le$%111.txt`
|
||||
remote1 = `rem ote$%<>1`
|
||||
remote2 = `_rem ote$%<>2`
|
||||
share11 = `sha re$%<>11`
|
||||
share12 = `_sha re$%<>12`
|
||||
file112 = `file112.txt`
|
||||
)
|
||||
|
||||
var (
|
||||
file111 = `fi le$%<>111.txt`
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "windows" {
|
||||
// file with less than and greater than doesn't work on Windows
|
||||
file111 = `fi le$%111.txt`
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
lockRootRegex = regexp.MustCompile(`<D:lockroot><D:href>/?([^<]*)/?</D:href>`)
|
||||
lockTokenRegex = regexp.MustCompile(`<D:locktoken><D:href>([0-9]+)/?</D:href>`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// set AllowShareAs() to false so that we don't try to use sub-processes
|
||||
// for access files on disk.
|
||||
@@ -145,6 +162,206 @@ func TestSecretTokenAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLOCK(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, drive.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{DisableKeepAlives: true},
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("http://%s/%s/%s/%s/%s",
|
||||
s.local.l.Addr(),
|
||||
url.PathEscape(domain),
|
||||
url.PathEscape(remote1),
|
||||
url.PathEscape(share11),
|
||||
url.PathEscape(file111))
|
||||
|
||||
// First acquire a lock with a short timeout
|
||||
req, err := http.NewRequest("LOCK", u, strings.NewReader(lockBody))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Depth", "infinity")
|
||||
req.Header.Set("Timeout", "Second-1")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected LOCK to succeed, but got status %d", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
submatches := lockRootRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find lockroot")
|
||||
}
|
||||
want := shared.EscapeForXML(pathTo(remote1, share11, file111))
|
||||
got := submatches[1]
|
||||
if got != want {
|
||||
t.Fatalf("want lockroot %q, got %q", want, got)
|
||||
}
|
||||
|
||||
submatches = lockTokenRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find locktoken")
|
||||
}
|
||||
lockToken := submatches[1]
|
||||
ifHeader := fmt.Sprintf("<%s> (<%s>)", u, lockToken)
|
||||
|
||||
// Then refresh the lock with a longer timeout
|
||||
req, err = http.NewRequest("LOCK", u, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Depth", "infinity")
|
||||
req.Header.Set("Timeout", "Second-600")
|
||||
req.Header.Set("If", ifHeader)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected LOCK refresh to succeed, but got status %d", resp.StatusCode)
|
||||
}
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
submatches = lockRootRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find lockroot after refresh")
|
||||
}
|
||||
want = shared.EscapeForXML(pathTo(remote1, share11, file111))
|
||||
got = submatches[1]
|
||||
if got != want {
|
||||
t.Fatalf("want lockroot after refresh %q, got %q", want, got)
|
||||
}
|
||||
|
||||
submatches = lockTokenRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find locktoken after refresh")
|
||||
}
|
||||
if submatches[1] != lockToken {
|
||||
t.Fatalf("on refresh, lock token changed from %q to %q", lockToken, submatches[1])
|
||||
}
|
||||
|
||||
// Then wait past the original timeout, then try to delete without the lock
|
||||
// (should fail)
|
||||
time.Sleep(1 * time.Second)
|
||||
req, err = http.NewRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 423 {
|
||||
t.Fatalf("deleting without lock token should fail with 423, but got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Then delete with the lock (should succeed)
|
||||
req, err = http.NewRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
req.Header.Set("If", ifHeader)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("deleting with lock token should have succeeded with 204, but got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUNLOCK(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, drive.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{DisableKeepAlives: true},
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("http://%s/%s/%s/%s/%s",
|
||||
s.local.l.Addr(),
|
||||
url.PathEscape(domain),
|
||||
url.PathEscape(remote1),
|
||||
url.PathEscape(share11),
|
||||
url.PathEscape(file111))
|
||||
|
||||
// Acquire a lock
|
||||
req, err := http.NewRequest("LOCK", u, strings.NewReader(lockBody))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Depth", "infinity")
|
||||
req.Header.Set("Timeout", "Second-600")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected LOCK to succeed, but got status %d", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
submatches := lockTokenRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find locktoken")
|
||||
}
|
||||
lockToken := submatches[1]
|
||||
|
||||
// Release the lock
|
||||
req, err = http.NewRequest("UNLOCK", u, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Lock-Token", fmt.Sprintf("<%s>", lockToken))
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("expected UNLOCK to succeed with a 204, but got status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Then delete without the lock (should succeed)
|
||||
req, err = http.NewRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("deleting without lock should have succeeded with 204, but got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
type local struct {
|
||||
l net.Listener
|
||||
fs *FileSystemForLocal
|
||||
@@ -486,3 +703,9 @@ func (a *noopAuthenticator) Clone() gowebdav.Authenticator {
|
||||
func (a *noopAuthenticator) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const lockBody = `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:lockinfo xmlns:D='DAV:'>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockinfo>`
|
||||
|
||||
Reference in New Issue
Block a user