diff options
| author | Craig Hesling <craig@hesling.com> | 2025-09-02 02:27:34 -0700 |
|---|---|---|
| committer | Percy Wegmann <ox.to.a.cart@gmail.com> | 2025-09-03 14:28:22 -0500 |
| commit | 2b9d055101a0a2731af9ef5d2caf513bfb7da75e (patch) | |
| tree | 35a429a8ee50531f7893907e4264bc1164eb5b2c | |
| parent | 0f3598b46741cbd0c005dc7d95c6e24fc8cf1924 (diff) | |
| download | tailscale-2b9d055101a0a2731af9ef5d2caf513bfb7da75e.tar.xz tailscale-2b9d055101a0a2731af9ef5d2caf513bfb7da75e.zip | |
drive: fix StatCache mishandling of paths with spaces
Fix "file not found" errors when WebDAV clients access files/dirs inside
directories with spaces.
The issue occurred because StatCache was mixing URL-escaped and
unescaped paths, causing cache key mismatches.
Specifically, StatCache.set() parsed WebDAV responses containing
URL-escaped paths (ex. "Dir%20Space/file1.txt") and stored them
alongside unescaped cache keys (ex. "Dir Space/file1.txt").
This mismatch prevented StatCache.get() from correctly determining whether
a child file existed.
See https://github.com/tailscale/tailscale/issues/13632#issuecomment-3243522449
for the full explanation of the issue.
The decision to keep all paths references unescaped inside the StatCache
is consistent with net/http.Request.URL.Path and rewrite.go (sole consumer)
Update unit test to detect this directory space mishandling.
Fixes tailscale#13632
Signed-off-by: Craig Hesling <craig@hesling.com>
| -rw-r--r-- | drive/driveimpl/compositedav/stat_cache.go | 8 | ||||
| -rw-r--r-- | drive/driveimpl/compositedav/stat_cache_test.go | 8 |
2 files changed, 11 insertions, 5 deletions
diff --git a/drive/driveimpl/compositedav/stat_cache.go b/drive/driveimpl/compositedav/stat_cache.go index fc57ff064..36463fe7e 100644 --- a/drive/driveimpl/compositedav/stat_cache.go +++ b/drive/driveimpl/compositedav/stat_cache.go @@ -8,6 +8,7 @@ import ( "encoding/xml" "log" "net/http" + "net/url" "sync" "time" @@ -165,7 +166,12 @@ func (c *StatCache) set(name string, depth int, ce *cacheEntry) { children = make(map[string]*cacheEntry, len(ms.Responses)-1) for i := 0; i < len(ms.Responses); i++ { response := ms.Responses[i] - name := shared.Normalize(response.Href) + name, err := url.PathUnescape(response.Href) + if err != nil { + log.Printf("statcache.set child parse error: %s", err) + return + } + name = shared.Normalize(name) raw := marshalMultiStatus(response) entry := newCacheEntry(ce.Status, raw) if i == 0 { diff --git a/drive/driveimpl/compositedav/stat_cache_test.go b/drive/driveimpl/compositedav/stat_cache_test.go index fa63457a2..baa4fdda2 100644 --- a/drive/driveimpl/compositedav/stat_cache_test.go +++ b/drive/driveimpl/compositedav/stat_cache_test.go @@ -16,12 +16,12 @@ import ( "tailscale.com/tstest" ) -var parentPath = "/parent" +var parentPath = "/parent with spaces" -var childPath = "/parent/child.txt" +var childPath = "/parent with spaces/child.txt" var parentResponse = `<D:response> -<D:href>/parent/</D:href> +<D:href>/parent%20with%20spaces/</D:href> <D:propstat> <D:prop> <D:getlastmodified>Mon, 29 Apr 2024 19:52:23 GMT</D:getlastmodified> @@ -36,7 +36,7 @@ var parentResponse = `<D:response> var childResponse = ` <D:response> -<D:href>/parent/child.txt</D:href> +<D:href>/parent%20with%20spaces/child.txt</D:href> <D:propstat> <D:prop> <D:getlastmodified>Mon, 29 Apr 2024 19:52:23 GMT</D:getlastmodified> |
