summaryrefslogtreecommitdiffhomepage
path: root/drive/remote.go
blob: 5f34d0023e6f7f4028380b7997af53e172c92578 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

package drive

//go:generate go run tailscale.com/cmd/viewer --type=Share --clonefunc

import (
	"bytes"
	"errors"
	"net/http"
	"strings"
)

var (
	// DisallowShareAs forcibly disables sharing as a specific user, only used
	// for testing.
	DisallowShareAs     = false
	ErrDriveNotEnabled  = errors.New("Taildrive not enabled")
	ErrInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
)

// AllowShareAs reports whether sharing files as a specific user is allowed.
func AllowShareAs() bool {
	return !DisallowShareAs && doAllowShareAs()
}

// Share configures a folder to be shared through drive.
type Share struct {
	// Name is how this share appears on remote nodes.
	Name string `json:"name,omitempty"`

	// Path is the path to the directory on this machine that's being shared.
	Path string `json:"path,omitempty"`

	// 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.
	// Can be left blank to use the default value of "whoever is running the
	// Tailscale GUI".
	As string `json:"who,omitempty"`

	// 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
	BookmarkData []byte `json:"bookmarkData,omitempty"`
}

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)
}

// FileSystemForRemote is the drive filesystem exposed to remote nodes. It
// provides a unified WebDAV interface to local directories that have been
// shared.
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.
	//
	// Note that this includes both the file server's secret token and its
	// address, delimited by a pipe |.
	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.
	SetShares(shares []*Share)

	// 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
}

// 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 !validShareName(name) {
		return "", ErrInvalidShareName
	}

	return name, nil
}

func validShareName(name string) bool {
	if name == "" {
		return false
	}
	for _, r := range name {
		if 'a' <= r && r <= 'z' || '0' <= r && r <= '9' {
			continue
		}
		switch r {
		case '_', ' ', '(', ')':
			continue
		}
		return false
	}
	return true
}