2024-02-02 18:45:32 +00:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
2024-04-02 20:32:30 +00:00
|
|
|
package drive
|
2024-02-02 18:45:32 +00:00
|
|
|
|
2024-03-07 16:56:11 +00:00
|
|
|
//go:generate go run tailscale.com/cmd/viewer --type=Share --clonefunc
|
|
|
|
|
2024-02-02 18:45:32 +00:00
|
|
|
import (
|
2024-03-07 16:56:11 +00:00
|
|
|
"bytes"
|
2024-04-05 18:43:13 +00:00
|
|
|
"errors"
|
2024-02-02 18:45:32 +00:00
|
|
|
"net/http"
|
2024-04-05 18:43:13 +00:00
|
|
|
"regexp"
|
2024-03-07 16:56:11 +00:00
|
|
|
"strings"
|
2024-02-02 18:45:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2024-02-09 17:26:43 +00:00
|
|
|
// DisallowShareAs forcibly disables sharing as a specific user, only used
|
|
|
|
// for testing.
|
2024-04-05 18:43:13 +00:00
|
|
|
DisallowShareAs = false
|
|
|
|
ErrDriveNotEnabled = errors.New("Taildrive not enabled")
|
|
|
|
ErrInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
shareNameRegex = regexp.MustCompile(`^[a-z0-9_\(\) ]+$`)
|
2024-02-02 18:45:32 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// AllowShareAs reports whether sharing files as a specific user is allowed.
|
|
|
|
func AllowShareAs() bool {
|
2024-02-09 17:26:43 +00:00
|
|
|
return !DisallowShareAs && doAllowShareAs()
|
2024-02-02 18:45:32 +00:00
|
|
|
}
|
|
|
|
|
2024-04-03 17:09:58 +00:00
|
|
|
// Share configures a folder to be shared through drive.
|
2024-02-02 18:45:32 +00:00
|
|
|
type Share struct {
|
|
|
|
// Name is how this share appears on remote nodes.
|
2024-02-28 03:22:45 +00:00
|
|
|
Name string `json:"name,omitempty"`
|
2024-02-09 17:26:43 +00:00
|
|
|
|
2024-02-02 18:45:32 +00:00
|
|
|
// Path is the path to the directory on this machine that's being shared.
|
2024-02-28 03:22:45 +00:00
|
|
|
Path string `json:"path,omitempty"`
|
2024-02-09 17:26:43 +00:00
|
|
|
|
2024-02-02 18:45:32 +00:00
|
|
|
// As is the UNIX or Windows username of the local account used for this
|
|
|
|
// share. File read/write permissions are enforced based on this username.
|
2024-02-09 17:26:43 +00:00
|
|
|
// Can be left blank to use the default value of "whoever is running the
|
|
|
|
// Tailscale GUI".
|
2024-02-28 03:22:45 +00:00
|
|
|
As string `json:"who,omitempty"`
|
2024-02-28 03:21:16 +00:00
|
|
|
|
|
|
|
// BookmarkData contains security-scoped bookmark data for the Sandboxed
|
|
|
|
// Mac application. The Sandboxed Mac application gains permission to
|
|
|
|
// access the Share's folder as a result of a user selecting it in a file
|
|
|
|
// picker. In order to retain access to it across restarts, it needs to
|
|
|
|
// hold on to a security-scoped bookmark. That bookmark is stored here. See
|
|
|
|
// https://developer.apple.com/documentation/security/app_sandbox/accessing_files_from_the_macos_app_sandbox#4144043
|
2024-02-28 03:22:45 +00:00
|
|
|
BookmarkData []byte `json:"bookmarkData,omitempty"`
|
2024-02-02 18:45:32 +00:00
|
|
|
}
|
|
|
|
|
2024-03-07 16:56:11 +00:00
|
|
|
func ShareViewsEqual(a, b ShareView) bool {
|
|
|
|
if !a.Valid() && !b.Valid() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if !a.Valid() || !b.Valid() {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return a.Name() == b.Name() && a.Path() == b.Path() && a.As() == b.As() && a.BookmarkData().Equal(b.ж.BookmarkData)
|
|
|
|
}
|
|
|
|
|
|
|
|
func SharesEqual(a, b *Share) bool {
|
|
|
|
if a == nil && b == nil {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if a == nil || b == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return a.Name == b.Name && a.Path == b.Path && a.As == b.As && bytes.Equal(a.BookmarkData, b.BookmarkData)
|
|
|
|
}
|
|
|
|
|
|
|
|
func CompareShares(a, b *Share) int {
|
|
|
|
if a == nil && b == nil {
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
if a == nil {
|
|
|
|
return -1
|
|
|
|
}
|
|
|
|
if b == nil {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
return strings.Compare(a.Name, b.Name)
|
|
|
|
}
|
|
|
|
|
2024-04-03 17:09:58 +00:00
|
|
|
// FileSystemForRemote is the drive filesystem exposed to remote nodes. It
|
2024-02-02 18:45:32 +00:00
|
|
|
// provides a unified WebDAV interface to local directories that have been
|
|
|
|
// shared.
|
2024-02-09 17:26:43 +00:00
|
|
|
type FileSystemForRemote interface {
|
|
|
|
// SetFileServerAddr sets the address of the file server to which we
|
|
|
|
// should proxy. This is used on platforms like Windows and MacOS
|
|
|
|
// sandboxed where we can't spawn user-specific sub-processes and instead
|
|
|
|
// rely on the UI application that's already running as an unprivileged
|
|
|
|
// user to access the filesystem for us.
|
2024-05-01 15:45:57 +00:00
|
|
|
//
|
|
|
|
// Note that this includes both the file server's secret token and its
|
|
|
|
// address, delimited by a pipe |.
|
2024-02-09 17:26:43 +00:00
|
|
|
SetFileServerAddr(addr string)
|
|
|
|
|
|
|
|
// SetShares sets the complete set of shares exposed by this node. If
|
|
|
|
// AllowShareAs() reports true, we will use one subprocess per user to
|
|
|
|
// access the filesystem (see userServer). Otherwise, we will use the file
|
|
|
|
// server configured via SetFileServerAddr.
|
2024-03-07 16:56:11 +00:00
|
|
|
SetShares(shares []*Share)
|
2024-02-09 17:26:43 +00:00
|
|
|
|
|
|
|
// ServeHTTPWithPerms behaves like the similar method from http.Handler but
|
|
|
|
// also accepts a Permissions map that captures the permissions of the
|
|
|
|
// connecting node.
|
|
|
|
ServeHTTPWithPerms(permissions Permissions, w http.ResponseWriter, r *http.Request)
|
|
|
|
|
|
|
|
// Close() stops serving the WebDAV content
|
|
|
|
Close() error
|
2024-02-02 18:45:32 +00:00
|
|
|
}
|
2024-04-05 18:43:13 +00:00
|
|
|
|
|
|
|
// NormalizeShareName normalizes the given share name and returns an error if
|
|
|
|
// it contains any disallowed characters.
|
|
|
|
func NormalizeShareName(name string) (string, error) {
|
|
|
|
// Force all share names to lowercase to avoid potential incompatibilities
|
|
|
|
// with clients that don't support case-sensitive filenames.
|
|
|
|
name = strings.ToLower(name)
|
|
|
|
|
|
|
|
// Trim whitespace
|
|
|
|
name = strings.TrimSpace(name)
|
|
|
|
|
|
|
|
if !shareNameRegex.MatchString(name) {
|
|
|
|
return "", ErrInvalidShareName
|
|
|
|
}
|
|
|
|
|
|
|
|
return name, nil
|
|
|
|
}
|