mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-30 21:12:48 +00:00

Over time all taildrop functionality will be contained in the taildrop package. This will include end to end unit tests. This is simply the first smallest piece to move over. There is no functionality change in this commit. Updates tailscale/corp#14772 Signed-off-by: Rhea Ghosh <rhea@tailscale.com> Co-authored-by: Joseph Tsai <joetsai@tailscale.com>
179 lines
4.8 KiB
Go
179 lines
4.8 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package taildrop
|
|
|
|
import (
|
|
"errors"
|
|
"hash/adler32"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"tailscale.com/tstime"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/multierr"
|
|
)
|
|
|
|
type Handler struct {
|
|
Logf logger.Logf
|
|
Clock tstime.Clock
|
|
|
|
RootDir string // empty means file receiving unavailable
|
|
|
|
// DirectFileMode is whether we're writing files directly to a
|
|
// download directory (as *.partial files), rather than making
|
|
// the frontend retrieve it over localapi HTTP and write it
|
|
// somewhere itself. This is used on the GUI macOS versions
|
|
// and on Synology.
|
|
// In DirectFileMode, the peerapi doesn't do the final rename
|
|
// from "foo.jpg.partial" to "foo.jpg" unless
|
|
// directFileDoFinalRename is set.
|
|
DirectFileMode bool
|
|
|
|
// DirectFileDoFinalRename is whether in directFileMode we
|
|
// additionally move the *.direct file to its final name after
|
|
// it's received.
|
|
DirectFileDoFinalRename bool
|
|
|
|
KnownEmpty atomic.Bool
|
|
}
|
|
|
|
var (
|
|
errNilHandler = errors.New("handler unavailable; not listening")
|
|
ErrNoTaildrop = errors.New("Taildrop disabled; no storage directory")
|
|
)
|
|
|
|
const (
|
|
// PartialSuffix is the suffix appended to files while they're
|
|
// still in the process of being transferred.
|
|
PartialSuffix = ".partial"
|
|
|
|
// deletedSuffix is the suffix for a deleted marker file
|
|
// that's placed next to a file (without the suffix) that we
|
|
// tried to delete, but Windows wouldn't let us. These are
|
|
// only written on Windows (and in tests), but they're not
|
|
// permitted to be uploaded directly on any platform, like
|
|
// partial files.
|
|
deletedSuffix = ".deleted"
|
|
)
|
|
|
|
// redacted is a fake path name we use in errors, to avoid
|
|
// accidentally logging actual filenames anywhere.
|
|
const redacted = "redacted"
|
|
|
|
func validFilenameRune(r rune) bool {
|
|
switch r {
|
|
case '/':
|
|
return false
|
|
case '\\', ':', '*', '"', '<', '>', '|':
|
|
// Invalid stuff on Windows, but we reject them everywhere
|
|
// for now.
|
|
// TODO(bradfitz): figure out a better plan. We initially just
|
|
// wrote things to disk URL path-escaped, but that's gross
|
|
// when debugging, and just moves the problem to callers.
|
|
// So now we put the UTF-8 filenames on disk directly as
|
|
// sent.
|
|
return false
|
|
}
|
|
return unicode.IsPrint(r)
|
|
}
|
|
|
|
func (s *Handler) DiskPath(baseName string) (fullPath string, ok bool) {
|
|
if !utf8.ValidString(baseName) {
|
|
return "", false
|
|
}
|
|
if strings.TrimSpace(baseName) != baseName {
|
|
return "", false
|
|
}
|
|
if len(baseName) > 255 {
|
|
return "", false
|
|
}
|
|
// TODO: validate unicode normalization form too? Varies by platform.
|
|
clean := path.Clean(baseName)
|
|
if clean != baseName ||
|
|
clean == "." || clean == ".." ||
|
|
strings.HasSuffix(clean, deletedSuffix) ||
|
|
strings.HasSuffix(clean, PartialSuffix) {
|
|
return "", false
|
|
}
|
|
for _, r := range baseName {
|
|
if !validFilenameRune(r) {
|
|
return "", false
|
|
}
|
|
}
|
|
if !filepath.IsLocal(baseName) {
|
|
return "", false
|
|
}
|
|
return filepath.Join(s.RootDir, baseName), true
|
|
}
|
|
|
|
type redactedErr struct {
|
|
msg string
|
|
inner error
|
|
}
|
|
|
|
func (re *redactedErr) Error() string {
|
|
return re.msg
|
|
}
|
|
|
|
func (re *redactedErr) Unwrap() error {
|
|
return re.inner
|
|
}
|
|
|
|
func redactString(s string) string {
|
|
hash := adler32.Checksum([]byte(s))
|
|
|
|
var buf [len(redacted) + len(".12345678")]byte
|
|
b := append(buf[:0], []byte(redacted)...)
|
|
b = append(b, '.')
|
|
b = strconv.AppendUint(b, uint64(hash), 16)
|
|
return string(b)
|
|
}
|
|
|
|
func RedactErr(root error) error {
|
|
// redactStrings is a list of sensitive strings that were redacted.
|
|
// It is not sufficient to just snub out sensitive fields in Go errors
|
|
// since some wrapper errors like fmt.Errorf pre-cache the error string,
|
|
// which would unfortunately remain unaffected.
|
|
var redactStrings []string
|
|
|
|
// Redact sensitive fields in known Go error types.
|
|
var unknownErrors int
|
|
multierr.Range(root, func(err error) bool {
|
|
switch err := err.(type) {
|
|
case *os.PathError:
|
|
redactStrings = append(redactStrings, err.Path)
|
|
err.Path = redactString(err.Path)
|
|
case *os.LinkError:
|
|
redactStrings = append(redactStrings, err.New, err.Old)
|
|
err.New = redactString(err.New)
|
|
err.Old = redactString(err.Old)
|
|
default:
|
|
unknownErrors++
|
|
}
|
|
return true
|
|
})
|
|
|
|
// If there are no redacted strings or no unknown error types,
|
|
// then we can return the possibly modified root error verbatim.
|
|
// Otherwise, we must replace redacted strings from any wrappers.
|
|
if len(redactStrings) == 0 || unknownErrors == 0 {
|
|
return root
|
|
}
|
|
|
|
// Stringify and replace any paths that we found above, then return
|
|
// the error wrapped in a type that uses the newly-redacted string
|
|
// while also allowing Unwrap()-ing to the inner error type(s).
|
|
s := root.Error()
|
|
for _, toRedact := range redactStrings {
|
|
s = strings.ReplaceAll(s, toRedact, redactString(toRedact))
|
|
}
|
|
return &redactedErr{msg: s, inner: root}
|
|
}
|