mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-30 05:25:35 +00:00
234 lines
5.9 KiB
Go
234 lines
5.9 KiB
Go
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||
|
|
||
|
// Package compositedav provides an http.Handler that composes multiple WebDAV
|
||
|
// services into a single WebDAV service that presents each of them as its own
|
||
|
// folder.
|
||
|
package compositedav
|
||
|
|
||
|
import (
|
||
|
"log"
|
||
|
"net/http"
|
||
|
"net/http/httputil"
|
||
|
"net/url"
|
||
|
"path"
|
||
|
"slices"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
|
||
|
"github.com/tailscale/xnet/webdav"
|
||
|
"tailscale.com/tailfs/tailfsimpl/dirfs"
|
||
|
"tailscale.com/tailfs/tailfsimpl/shared"
|
||
|
"tailscale.com/tstime"
|
||
|
"tailscale.com/types/logger"
|
||
|
)
|
||
|
|
||
|
// Child is a child folder of this compositedav.
|
||
|
type Child struct {
|
||
|
*dirfs.Child
|
||
|
|
||
|
// BaseURL is the base URL of the WebDAV service to which we'll proxy
|
||
|
// requests for this Child. We will append the filename from the original
|
||
|
// URL to this.
|
||
|
BaseURL string
|
||
|
|
||
|
// Transport (if specified) is the http transport to use when communicating
|
||
|
// with this Child's WebDAV service.
|
||
|
Transport http.RoundTripper
|
||
|
|
||
|
rp *httputil.ReverseProxy
|
||
|
initOnce sync.Once
|
||
|
}
|
||
|
|
||
|
// CloseIdleConnections forcibly closes any idle connections on this Child's
|
||
|
// reverse proxy.
|
||
|
func (c *Child) CloseIdleConnections() {
|
||
|
tr, ok := c.Transport.(*http.Transport)
|
||
|
if ok {
|
||
|
tr.CloseIdleConnections()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Child) init() {
|
||
|
c.initOnce.Do(func() {
|
||
|
c.rp = &httputil.ReverseProxy{
|
||
|
Transport: c.Transport,
|
||
|
Rewrite: func(r *httputil.ProxyRequest) {},
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Handler implements http.Handler by using a dirfs.FS for showing a virtual
|
||
|
// read-only folder that represents the Child WebDAV services as sub-folders
|
||
|
// and proxying all requests for resources on the children to those children
|
||
|
// via httputil.ReverseProxy instances.
|
||
|
type Handler struct {
|
||
|
// Logf specifies a logging function to use.
|
||
|
Logf logger.Logf
|
||
|
|
||
|
// Clock, if specified, determines the current time. If not specified, we
|
||
|
// default to time.Now().
|
||
|
Clock tstime.Clock
|
||
|
|
||
|
// StatCache is an optional cache for PROPFIND results.
|
||
|
StatCache *StatCache
|
||
|
|
||
|
// childrenMu guards the fields below. Note that we do read the contents of
|
||
|
// children after releasing the read lock, which we can do because we never
|
||
|
// modify children but only ever replace it in SetChildren.
|
||
|
childrenMu sync.RWMutex
|
||
|
children []*Child
|
||
|
staticRoot string
|
||
|
}
|
||
|
|
||
|
// ServeHTTP implements http.Handler.
|
||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||
|
if r.Method == "PROPFIND" {
|
||
|
h.handlePROPFIND(w, r)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if r.Method != "GET" {
|
||
|
// If the user is performing a modification (e.g. PUT, MKDIR, etc),
|
||
|
// we need to invalidate the StatCache to make sure we're not knowingly
|
||
|
// showing stale stats.
|
||
|
// TODO(oxtoacart): maybe be more selective about invalidating cache
|
||
|
h.StatCache.invalidate()
|
||
|
}
|
||
|
|
||
|
mpl := h.maxPathLength(r)
|
||
|
pathComponents := shared.CleanAndSplit(r.URL.Path)
|
||
|
|
||
|
if len(pathComponents) >= mpl {
|
||
|
h.delegate(pathComponents[mpl-1:], w, r)
|
||
|
return
|
||
|
}
|
||
|
h.handle(w, r)
|
||
|
}
|
||
|
|
||
|
// handle handles the request locally using our dirfs.FS.
|
||
|
func (h *Handler) handle(w http.ResponseWriter, r *http.Request) {
|
||
|
h.childrenMu.RLock()
|
||
|
clk, kids, root := h.Clock, h.children, h.staticRoot
|
||
|
h.childrenMu.RUnlock()
|
||
|
|
||
|
children := make([]*dirfs.Child, 0, len(kids))
|
||
|
for _, child := range kids {
|
||
|
children = append(children, child.Child)
|
||
|
}
|
||
|
wh := &webdav.Handler{
|
||
|
LockSystem: webdav.NewMemLS(),
|
||
|
FileSystem: &dirfs.FS{
|
||
|
Clock: clk,
|
||
|
Children: children,
|
||
|
StaticRoot: root,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
wh.ServeHTTP(w, r)
|
||
|
}
|
||
|
|
||
|
// delegate sends the request to the Child WebDAV server.
|
||
|
func (h *Handler) delegate(pathComponents []string, w http.ResponseWriter, r *http.Request) string {
|
||
|
childName := pathComponents[0]
|
||
|
child := h.GetChild(childName)
|
||
|
if child == nil {
|
||
|
w.WriteHeader(http.StatusNotFound)
|
||
|
return childName
|
||
|
}
|
||
|
u, err := url.Parse(child.BaseURL)
|
||
|
if err != nil {
|
||
|
h.logf("warning: parse base URL %s failed: %s", child.BaseURL, err)
|
||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
|
return childName
|
||
|
}
|
||
|
u.Path = path.Join(u.Path, shared.Join(pathComponents[1:]...))
|
||
|
r.URL = u
|
||
|
r.Host = u.Host
|
||
|
child.rp.ServeHTTP(w, r)
|
||
|
return childName
|
||
|
}
|
||
|
|
||
|
// SetChildren replaces the entire existing set of children with the given
|
||
|
// ones. If staticRoot is given, the children will appear with a subfolder
|
||
|
// bearing named <staticRoot>.
|
||
|
func (h *Handler) SetChildren(staticRoot string, children ...*Child) {
|
||
|
for _, child := range children {
|
||
|
child.init()
|
||
|
}
|
||
|
|
||
|
slices.SortFunc(children, func(a, b *Child) int {
|
||
|
return strings.Compare(a.Name, b.Name)
|
||
|
})
|
||
|
|
||
|
h.childrenMu.Lock()
|
||
|
oldChildren := children
|
||
|
h.children = children
|
||
|
h.staticRoot = staticRoot
|
||
|
h.childrenMu.Unlock()
|
||
|
|
||
|
for _, child := range oldChildren {
|
||
|
child.CloseIdleConnections()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// GetChild gets the Child identified by name, or nil if no matching child
|
||
|
// found.
|
||
|
func (h *Handler) GetChild(name string) *Child {
|
||
|
h.childrenMu.RLock()
|
||
|
defer h.childrenMu.RUnlock()
|
||
|
|
||
|
_, child := h.findChildLocked(name)
|
||
|
return child
|
||
|
}
|
||
|
|
||
|
// Close closes this Handler,including closing all idle connections on children
|
||
|
// and stopping the StatCache (if caching is enabled).
|
||
|
func (h *Handler) Close() {
|
||
|
h.childrenMu.RLock()
|
||
|
oldChildren := h.children
|
||
|
h.children = nil
|
||
|
h.childrenMu.RUnlock()
|
||
|
|
||
|
for _, child := range oldChildren {
|
||
|
child.CloseIdleConnections()
|
||
|
}
|
||
|
|
||
|
if h.StatCache != nil {
|
||
|
h.StatCache.stop()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (h *Handler) findChildLocked(name string) (int, *Child) {
|
||
|
var child *Child
|
||
|
i, found := slices.BinarySearchFunc(h.children, name, func(child *Child, name string) int {
|
||
|
return strings.Compare(child.Name, name)
|
||
|
})
|
||
|
if found {
|
||
|
return i, h.children[i]
|
||
|
}
|
||
|
return i, child
|
||
|
}
|
||
|
|
||
|
func (h *Handler) logf(format string, args ...any) {
|
||
|
if h.Logf != nil {
|
||
|
h.Logf(format, args...)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
log.Printf(format, args...)
|
||
|
}
|
||
|
|
||
|
// maxPathLength calculates the maximum length of a path that can be handled by
|
||
|
// this handler without delegating to a Child. It's always at least 1, and if
|
||
|
// staticRoot is configured, it's 2.
|
||
|
func (h *Handler) maxPathLength(r *http.Request) int {
|
||
|
h.childrenMu.RLock()
|
||
|
defer h.childrenMu.RUnlock()
|
||
|
|
||
|
if h.staticRoot != "" {
|
||
|
return 2
|
||
|
}
|
||
|
return 1
|
||
|
}
|