Skip to content

Commit

Permalink
drive: rewrite LOCK paths
Browse files Browse the repository at this point in the history
Fixes #12097

Signed-off-by: Percy Wegmann <percy@tailscale.com>
  • Loading branch information
oxtoacart committed May 16, 2024
1 parent 7ef2f72 commit 659cbbb
Show file tree
Hide file tree
Showing 6 changed files with 380 additions and 87 deletions.
16 changes: 11 additions & 5 deletions drive/driveimpl/compositedav/compositedav.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,15 @@ var cacheInvalidatingMethods = map[string]bool{

// ServeHTTP implements http.Handler.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method == "PROPFIND" {
h.handlePROPFIND(w, r)
pathComponents := shared.CleanAndSplit(r.URL.Path)
mpl := h.maxPathLength(r)

switch r.Method {
case "PROPFIND":
h.handlePROPFIND(w, r, pathComponents, mpl)
return
case "LOCK":
h.handleLOCK(w, r, pathComponents, mpl)
return
}

Expand All @@ -107,9 +114,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.StatCache.invalidate()
}

mpl := h.maxPathLength(r)
pathComponents := shared.CleanAndSplit(r.URL.Path)

if len(pathComponents) >= mpl {
h.delegate(mpl, pathComponents[mpl-1:], w, r)
return
Expand Down Expand Up @@ -141,6 +145,8 @@ func (h *Handler) handle(w http.ResponseWriter, r *http.Request) {

// delegate sends the request to the Child WebDAV server.
func (h *Handler) delegate(mpl int, pathComponents []string, w http.ResponseWriter, r *http.Request) {
rewriteIfHeader(r, pathComponents, mpl)

dest := r.Header.Get("Destination")
if dest != "" {
// Rewrite destination header
Expand Down
77 changes: 0 additions & 77 deletions drive/driveimpl/compositedav/propfind.go

This file was deleted.

122 changes: 122 additions & 0 deletions drive/driveimpl/compositedav/rewriting.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package compositedav

import (
"bytes"
"fmt"
"math"
"net/http"
"regexp"
"strings"

"tailscale.com/drive/driveimpl/shared"
)

var (
responseHrefRegex = regexp.MustCompile(`(?s)(<D:(response|lockroot)>)<D:href>/?([^<]*)/?</D:href>`)
ifHrefRegex = regexp.MustCompile(`^<(https?://[^/]+)?([^>]+)>`)
)

func (h *Handler) handlePROPFIND(w http.ResponseWriter, r *http.Request, pathComponents []string, mpl int) {
if shouldDelegateToChild(r, pathComponents, mpl) {
// Delegate to a Child.
depth := getDepth(r)

status, result := h.StatCache.getOr(r.URL.Path, depth, func() (int, []byte) {
return h.delegateRewriting(w, r, pathComponents, mpl)
})

respondRewritten(w, status, result)
return
}

h.handle(w, r)
}

func (h *Handler) handleLOCK(w http.ResponseWriter, r *http.Request, pathComponents []string, mpl int) {
if shouldDelegateToChild(r, pathComponents, mpl) {
// Delegate to a Child.
status, result := h.delegateRewriting(w, r, pathComponents, mpl)
respondRewritten(w, status, result)
return
}

http.Error(w, "locking of top level directories is not allowed", http.StatusMethodNotAllowed)
}

// shouldDelegateToChild decides whether a request should be delegated to a
// child filesystem, as opposed to being handled by this filesystem. It checks
// the depth of the requested path, and if it's deeper than the portion of the
// tree that's handled by the parent, returns true.
func shouldDelegateToChild(r *http.Request, pathComponents []string, mpl int) bool {
return !shared.IsRoot(r.URL.Path) && len(pathComponents)+getDepth(r) > mpl
}

func (h *Handler) delegateRewriting(w http.ResponseWriter, r *http.Request, pathComponents []string, mpl int) (int, []byte) {
// Use a buffering ResponseWriter so that we can manipulate the result.
// The only thing we use from the original ResponseWriter is Header().
bw := &bufferingResponseWriter{ResponseWriter: w}

h.delegate(mpl, pathComponents[mpl-1:], bw, r)

// Fixup paths to add the requested path as a prefix, escaped for inclusion in XML.
pp := shared.EscapeForXML(shared.Join(pathComponents[0:mpl]...))
b := responseHrefRegex.ReplaceAll(bw.buf.Bytes(), []byte(fmt.Sprintf("$1<D:href>%s/$3</D:href>", pp)))
return bw.status, b
}

func respondRewritten(w http.ResponseWriter, status int, result []byte) {
w.Header().Del("Content-Length")
w.WriteHeader(status)
if result != nil {
w.Write(result)
}
}

func getDepth(r *http.Request) int {
switch r.Header.Get("Depth") {
case "0":
return 0
case "1":
return 1
case "infinity":
return math.MaxInt16 // a really large number, but not infinity (avoids wrapping when we do arithmetic with this)
}
return 0
}

type bufferingResponseWriter struct {
http.ResponseWriter
status int
buf bytes.Buffer
}

func (bw *bufferingResponseWriter) WriteHeader(statusCode int) {
bw.status = statusCode
}

func (bw *bufferingResponseWriter) Write(p []byte) (int, error) {
return bw.buf.Write(p)
}

// rewriteIfHeader rewrites URLs in the If header by removing the host and the
// portion of the path that corresponds to this composite filesystem. This way,
// when we delegate requests to child filesystems, the If header will reference
// a path that makes sense on those filesystems.
//
// See http://www.webdav.org/specs/rfc4918.html#HEADER_If
func rewriteIfHeader(r *http.Request, pathComponents []string, mpl int) {
ih := r.Header.Get("If")
if ih == "" {
return
}
matches := ifHrefRegex.FindStringSubmatch(ih)
if len(matches) == 3 {
pp := shared.JoinEscaped(pathComponents[0:mpl]...)
p := strings.Replace(shared.JoinEscaped(pathComponents...), pp, "", 1)
nih := ifHrefRegex.ReplaceAllString(ih, fmt.Sprintf("<%s>", p))
r.Header.Set("If", nih)
}
}

0 comments on commit 659cbbb

Please sign in to comment.