summaryrefslogtreecommitdiffhomepage
path: root/ipn/ipnlocal/peerapi.go
diff options
context:
space:
mode:
Diffstat (limited to 'ipn/ipnlocal/peerapi.go')
-rw-r--r--ipn/ipnlocal/peerapi.go636
1 files changed, 29 insertions, 607 deletions
diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go
index d9801d90d..4ce43f7c0 100644
--- a/ipn/ipnlocal/peerapi.go
+++ b/ipn/ipnlocal/peerapi.go
@@ -9,18 +9,13 @@ import (
"encoding/json"
"errors"
"fmt"
- "hash/adler32"
"hash/crc32"
"html"
"io"
- "io/fs"
"net"
"net/http"
"net/netip"
- "net/url"
"os"
- "path"
- "path/filepath"
"runtime"
"slices"
"sort"
@@ -29,18 +24,14 @@ import (
"sync"
"sync/atomic"
"time"
- "unicode"
- "unicode/utf8"
"github.com/kortschak/wol"
"golang.org/x/net/dns/dnsmessage"
"golang.org/x/net/http/httpguts"
- "tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
- "tailscale.com/logtail/backoff"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/interfaces"
"tailscale.com/net/netaddr"
@@ -49,8 +40,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/views"
"tailscale.com/util/clientmetric"
- "tailscale.com/util/multierr"
- "tailscale.com/version/distro"
"tailscale.com/wgengine/filter"
)
@@ -60,7 +49,7 @@ var initListenConfig func(*net.ListenConfig, netip.Addr, *interfaces.State, stri
// ("cleartext" HTTP/2) support to the peerAPI.
var addH2C func(*http.Server)
-type peerAPIServer struct {
+type PeerAPIServer struct {
b *LocalBackend
rootDir string // empty means file receiving unavailable
knownEmpty atomic.Bool
@@ -82,373 +71,7 @@ type peerAPIServer struct {
directFileDoFinalRename bool
}
-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"
-)
-
-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 *peerAPIServer) 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
-}
-
-// hasFilesWaiting reports whether any files are buffered in the
-// tailscaled daemon storage.
-func (s *peerAPIServer) hasFilesWaiting() bool {
- if s == nil || s.rootDir == "" || s.directFileMode {
- return false
- }
- if s.knownEmpty.Load() {
- // Optimization: this is usually empty, so avoid opening
- // the directory and checking. We can't cache the actual
- // has-files-or-not values as the macOS/iOS client might
- // in the future use+delete the files directly. So only
- // keep this negative cache.
- return false
- }
- f, err := os.Open(s.rootDir)
- if err != nil {
- return false
- }
- defer f.Close()
- for {
- des, err := f.ReadDir(10)
- for _, de := range des {
- name := de.Name()
- if strings.HasSuffix(name, partialSuffix) {
- continue
- }
- if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
- // After we're done looping over files, then try
- // to delete this file. Don't do it proactively,
- // as the OS may return "foo.jpg.deleted" before "foo.jpg"
- // and we don't want to delete the ".deleted" file before
- // enumerating to the "foo.jpg" file.
- defer tryDeleteAgain(filepath.Join(s.rootDir, name))
- continue
- }
- if de.Type().IsRegular() {
- _, err := os.Stat(filepath.Join(s.rootDir, name+deletedSuffix))
- if os.IsNotExist(err) {
- return true
- }
- if err == nil {
- tryDeleteAgain(filepath.Join(s.rootDir, name))
- continue
- }
- }
- }
- if err == io.EOF {
- s.knownEmpty.Store(true)
- }
- if err != nil {
- break
- }
- }
- return false
-}
-
-// WaitingFiles returns the list of files that have been sent by a
-// peer that are waiting in the buffered "pick up" directory owned by
-// the Tailscale daemon.
-//
-// As a side effect, it also does any lazy deletion of files as
-// required by Windows.
-func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
- if s == nil {
- return nil, errNilPeerAPIServer
- }
- if s.rootDir == "" {
- return nil, errNoTaildrop
- }
- if s.directFileMode {
- return nil, nil
- }
- f, err := os.Open(s.rootDir)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- var deleted map[string]bool // "foo.jpg" => true (if "foo.jpg.deleted" exists)
- for {
- des, err := f.ReadDir(10)
- for _, de := range des {
- name := de.Name()
- if strings.HasSuffix(name, partialSuffix) {
- continue
- }
- if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
- if deleted == nil {
- deleted = map[string]bool{}
- }
- deleted[name] = true
- continue
- }
- if de.Type().IsRegular() {
- fi, err := de.Info()
- if err != nil {
- continue
- }
- ret = append(ret, apitype.WaitingFile{
- Name: filepath.Base(name),
- Size: fi.Size(),
- })
- }
- }
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, err
- }
- }
- if len(deleted) > 0 {
- // Filter out any return values "foo.jpg" where a
- // "foo.jpg.deleted" marker file exists on disk.
- all := ret
- ret = ret[:0]
- for _, wf := range all {
- if !deleted[wf.Name] {
- ret = append(ret, wf)
- }
- }
- // And do some opportunistic deleting while we're here.
- // Maybe Windows is done virus scanning the file we tried
- // to delete a long time ago and will let us delete it now.
- for name := range deleted {
- tryDeleteAgain(filepath.Join(s.rootDir, name))
- }
- }
- sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
- return ret, nil
-}
-
-var (
- errNilPeerAPIServer = errors.New("peerapi unavailable; not listening")
- errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
-)
-
-// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
-// it failed earlier. This happens on Windows when various anti-virus
-// tools hook into filesystem operations and have the file open still
-// while we're trying to delete it. In that case we instead mark it as
-// deleted (writing a "foo.jpg.deleted" marker file), but then we
-// later try to clean them up.
-//
-// fullPath is the full path to the file without the deleted suffix.
-func tryDeleteAgain(fullPath string) {
- if err := os.Remove(fullPath); err == nil || os.IsNotExist(err) {
- os.Remove(fullPath + deletedSuffix)
- }
-}
-
-func (s *peerAPIServer) DeleteFile(baseName string) error {
- if s == nil {
- return errNilPeerAPIServer
- }
- if s.rootDir == "" {
- return errNoTaildrop
- }
- if s.directFileMode {
- return errors.New("deletes not allowed in direct mode")
- }
- path, ok := s.diskPath(baseName)
- if !ok {
- return errors.New("bad filename")
- }
- var bo *backoff.Backoff
- logf := s.b.logf
- t0 := s.b.clock.Now()
- for {
- err := os.Remove(path)
- if err != nil && !os.IsNotExist(err) {
- err = redactErr(err)
- // Put a retry loop around deletes on Windows. Windows
- // file descriptor closes are effectively asynchronous,
- // as a bunch of hooks run on/after close, and we can't
- // necessarily delete the file for a while after close,
- // as we need to wait for everybody to be done with
- // it. (on Windows, unlike Unix, a file can't be deleted
- // if it's open anywhere)
- // So try a few times but ultimately just leave a
- // "foo.jpg.deleted" marker file to note that it's
- // deleted and we clean it up later.
- if runtime.GOOS == "windows" {
- if bo == nil {
- bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
- }
- if s.b.clock.Since(t0) < 5*time.Second {
- bo.BackOff(context.Background(), err)
- continue
- }
- if err := touchFile(path + deletedSuffix); err != nil {
- logf("peerapi: failed to leave deleted marker: %v", err)
- }
- }
- logf("peerapi: failed to DeleteFile: %v", err)
- return err
- }
- return nil
- }
-}
-
-// redacted is a fake path name we use in errors, to avoid
-// accidentally logging actual filenames anywhere.
-const redacted = "redacted"
-
-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}
-}
-
-func touchFile(path string) error {
- f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
- if err != nil {
- return redactErr(err)
- }
- return f.Close()
-}
-
-func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
- if s == nil {
- return nil, 0, errNilPeerAPIServer
- }
- if s.rootDir == "" {
- return nil, 0, errNoTaildrop
- }
- if s.directFileMode {
- return nil, 0, errors.New("opens not allowed in direct mode")
- }
- path, ok := s.diskPath(baseName)
- if !ok {
- return nil, 0, errors.New("bad filename")
- }
- if fi, err := os.Stat(path + deletedSuffix); err == nil && fi.Mode().IsRegular() {
- tryDeleteAgain(path)
- return nil, 0, &fs.PathError{Op: "open", Path: redacted, Err: fs.ErrNotExist}
- }
- f, err := os.Open(path)
- if err != nil {
- return nil, 0, redactErr(err)
- }
- fi, err := f.Stat()
- if err != nil {
- f.Close()
- return nil, 0, redactErr(err)
- }
- return f, fi.Size(), nil
-}
-
-func (s *peerAPIServer) listen(ip netip.Addr, ifState *interfaces.State) (ln net.Listener, err error) {
+func (s *PeerAPIServer) listen(ip netip.Addr, ifState *interfaces.State) (ln net.Listener, err error) {
// Android for whatever reason often has problems creating the peerapi listener.
// But since we started intercepting it with netstack, it's not even important that
// we have a real kernel-level listener. So just create a dummy listener on Android
@@ -511,7 +134,7 @@ func (s *peerAPIServer) listen(ip netip.Addr, ifState *interfaces.State) (ln net
}
type peerAPIListener struct {
- ps *peerAPIServer
+ ps *PeerAPIServer
ip netip.Addr
lb *LocalBackend
@@ -578,7 +201,7 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
c.Close()
return
}
- h := &peerAPIHandler{
+ h := &PeerAPIHandler{
ps: pln.ps,
isSelf: nm.SelfNode.User() == peerNode.User(),
remoteAddr: src,
@@ -595,9 +218,9 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
go httpServer.Serve(netutil.NewOneConnListener(c, nil))
}
-// peerAPIHandler serves the PeerAPI for a source specific client.
-type peerAPIHandler struct {
- ps *peerAPIServer
+// PeerAPIHandler serves the PeerAPI for a source specific client.
+type PeerAPIHandler struct {
+ ps *PeerAPIServer
remoteAddr netip.AddrPort
isSelf bool // whether peerNode is owned by same user as this node
selfNode tailcfg.NodeView // this node; always non-nil
@@ -605,13 +228,13 @@ type peerAPIHandler struct {
peerUser tailcfg.UserProfile // profile of peerNode
}
-func (h *peerAPIHandler) logf(format string, a ...any) {
+func (h *PeerAPIHandler) logf(format string, a ...any) {
h.ps.b.logf("peerapi: "+format, a...)
}
// isAddressValid reports whether addr is a valid destination address for this
// node originating from the peer.
-func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
+func (h *PeerAPIHandler) isAddressValid(addr netip.Addr) bool {
if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil {
return *v == addr
}
@@ -622,7 +245,7 @@ func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
return views.SliceContains(h.selfNode.Addresses(), pfx)
}
-func (h *peerAPIHandler) validateHost(r *http.Request) error {
+func (h *PeerAPIHandler) validateHost(r *http.Request) error {
if r.Host == "peer" {
return nil
}
@@ -636,7 +259,7 @@ func (h *peerAPIHandler) validateHost(r *http.Request) error {
return nil
}
-func (h *peerAPIHandler) validatePeerAPIRequest(r *http.Request) error {
+func (h *PeerAPIHandler) validatePeerAPIRequest(r *http.Request) error {
if r.Referer() != "" {
return errors.New("unexpected Referer")
}
@@ -679,7 +302,7 @@ func peerAPIRequestShouldGetSecurityHeaders(r *http.Request) bool {
return false
}
-func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.validatePeerAPIRequest(r); err != nil {
metricInvalidRequests.Add(1)
h.logf("invalid request from %v: %v", h.remoteAddr, err)
@@ -693,6 +316,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
metricPutCalls.Add(1)
+ //TODO: fix this
h.handlePeerPut(w, r)
return
}
@@ -747,7 +371,7 @@ This is my Tailscale device. Your device is %v.
}
}
-func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Request) {
// http.Errors only useful if hitting endpoint manually
// otherwise rely on log lines when debugging ingress connections
// as connection is hijacked for bidi and is encrypted tls
@@ -808,7 +432,7 @@ func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Reque
h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConnOrReset, sendRST)
}
-func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
@@ -855,7 +479,7 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
fmt.Fprintln(w, "</table>")
}
-func (h *peerAPIHandler) handleServeDoctor(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeDoctor(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
@@ -873,7 +497,7 @@ func (h *peerAPIHandler) handleServeDoctor(w http.ResponseWriter, r *http.Reques
fmt.Fprintln(w, "</pre>")
}
-func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
@@ -971,76 +595,9 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
fmt.Fprintln(w, "</pre>")
}
-type incomingFile struct {
- name string // "foo.jpg"
- started time.Time
- size int64 // or -1 if unknown; never 0
- w io.Writer // underlying writer
- ph *peerAPIHandler
- partialPath string // non-empty in direct mode
-
- mu sync.Mutex
- copied int64
- done bool
- lastNotify time.Time
-}
-
-func (f *incomingFile) markAndNotifyDone() {
- f.mu.Lock()
- f.done = true
- f.mu.Unlock()
- b := f.ph.ps.b
- b.sendFileNotify()
-}
-
-func (f *incomingFile) Write(p []byte) (n int, err error) {
- n, err = f.w.Write(p)
-
- b := f.ph.ps.b
- var needNotify bool
- defer func() {
- if needNotify {
- b.sendFileNotify()
- }
- }()
- if n > 0 {
- f.mu.Lock()
- defer f.mu.Unlock()
- f.copied += int64(n)
- now := b.clock.Now()
- if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
- f.lastNotify = now
- needNotify = true
- }
- }
- return n, err
-}
-
-func (f *incomingFile) PartialFile() ipn.PartialFile {
- f.mu.Lock()
- defer f.mu.Unlock()
- return ipn.PartialFile{
- Name: f.name,
- Started: f.started,
- DeclaredSize: f.size,
- Received: f.copied,
- PartialPath: f.partialPath,
- Done: f.done,
- }
-}
-
-// canPutFile reports whether h can put a file ("Taildrop") to this node.
-func (h *peerAPIHandler) canPutFile() bool {
- if h.peerNode.UnsignedPeerAPIOnly() {
- // Unsigned peers can't send files.
- return false
- }
- return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityFileSharingSend)
-}
-
// canDebug reports whether h can debug this node (goroutines, metrics,
// magicsock internal state, etc).
-func (h *peerAPIHandler) canDebug() bool {
+func (h *PeerAPIHandler) canDebug() bool {
if !h.selfNode.HasCap(tailcfg.CapabilityDebug) {
// This node does not expose debug info.
return false
@@ -1053,7 +610,7 @@ func (h *peerAPIHandler) canDebug() bool {
}
// canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
-func (h *peerAPIHandler) canWakeOnLAN() bool {
+func (h *PeerAPIHandler) canWakeOnLAN() bool {
if h.peerNode.UnsignedPeerAPIOnly() {
return false
}
@@ -1063,150 +620,15 @@ func (h *peerAPIHandler) canWakeOnLAN() bool {
var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS")
// canIngress reports whether h can send ingress requests to this node.
-func (h *peerAPIHandler) canIngress() bool {
+func (h *PeerAPIHandler) canIngress() bool {
return h.peerHasCap(tailcfg.PeerCapabilityIngress) || (allowSelfIngress() && h.isSelf)
}
-func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
+func (h *PeerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
return h.ps.b.PeerCaps(h.remoteAddr.Addr()).HasCapability(wantCap)
}
-func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
- if !envknob.CanTaildrop() {
- http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
- return
- }
- if !h.canPutFile() {
- http.Error(w, "Taildrop access denied", http.StatusForbidden)
- return
- }
- if !h.ps.b.hasCapFileSharing() {
- http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
- return
- }
- if r.Method != "PUT" {
- http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
- return
- }
- if h.ps.rootDir == "" {
- http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
- return
- }
- if distro.Get() == distro.Unraid && !h.ps.directFileMode {
- http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
- return
- }
- rawPath := r.URL.EscapedPath()
- suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
- if !ok {
- http.Error(w, "misconfigured internals", 500)
- return
- }
- if suffix == "" {
- http.Error(w, "empty filename", 400)
- return
- }
- if strings.Contains(suffix, "/") {
- http.Error(w, "directories not supported", 400)
- return
- }
- baseName, err := url.PathUnescape(suffix)
- if err != nil {
- http.Error(w, "bad path encoding", 400)
- return
- }
- dstFile, ok := h.ps.diskPath(baseName)
- if !ok {
- http.Error(w, "bad filename", 400)
- return
- }
- t0 := h.ps.b.clock.Now()
- // TODO(bradfitz): prevent same filename being sent by two peers at once
-
- // prevent same filename being sent twice
- if _, err := os.Stat(dstFile); err == nil {
- http.Error(w, "file exists", http.StatusConflict)
- return
- }
-
- partialFile := dstFile + partialSuffix
- f, err := os.Create(partialFile)
- if err != nil {
- h.logf("put Create error: %v", redactErr(err))
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- var success bool
- defer func() {
- if !success {
- os.Remove(partialFile)
- }
- }()
- var finalSize int64
- var inFile *incomingFile
- if r.ContentLength != 0 {
- inFile = &incomingFile{
- name: baseName,
- started: h.ps.b.clock.Now(),
- size: r.ContentLength,
- w: f,
- ph: h,
- }
- if h.ps.directFileMode {
- inFile.partialPath = partialFile
- }
- h.ps.b.registerIncomingFile(inFile, true)
- defer h.ps.b.registerIncomingFile(inFile, false)
- n, err := io.Copy(inFile, r.Body)
- if err != nil {
- err = redactErr(err)
- f.Close()
- h.logf("put Copy error: %v", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- finalSize = n
- }
- if err := redactErr(f.Close()); err != nil {
- h.logf("put Close error: %v", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
- if inFile != nil { // non-zero length; TODO: notify even for zero length
- inFile.markAndNotifyDone()
- }
- } else {
- if err := os.Rename(partialFile, dstFile); err != nil {
- err = redactErr(err)
- h.logf("put final rename: %v", err)
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
-
- d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
- h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
-
- // TODO: set modtime
- // TODO: some real response
- success = true
- io.WriteString(w, "{}\n")
- h.ps.knownEmpty.Store(false)
- h.ps.b.sendFileNotify()
-}
-
-func approxSize(n int64) string {
- if n <= 1<<10 {
- return "<=1KB"
- }
- if n <= 1<<20 {
- return "<=1MB"
- }
- return fmt.Sprintf("~%dMB", n>>20)
-}
-
-func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
@@ -1222,7 +644,7 @@ func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Re
w.Write(buf)
}
-func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
@@ -1242,7 +664,7 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(data)
}
-func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
@@ -1250,7 +672,7 @@ func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Req
h.ps.b.magicConn().ServeHTTPDebug(w, r)
}
-func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
@@ -1259,7 +681,7 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
clientmetric.WritePrometheusExpositionFormat(w)
}
-func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
return
@@ -1272,7 +694,7 @@ func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Reques
dh.ServeHTTP(w, r)
}
-func (h *peerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request) {
if !h.canWakeOnLAN() {
http.Error(w, "no WoL access", http.StatusForbidden)
return
@@ -1327,7 +749,7 @@ func (h *peerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request)
json.NewEncoder(w).Encode(res)
}
-func (h *peerAPIHandler) replyToDNSQueries() bool {
+func (h *PeerAPIHandler) replyToDNSQueries() bool {
if h.isSelf {
// If the peer is owned by the same user, just allow it
// without further checks.
@@ -1371,7 +793,7 @@ func (h *peerAPIHandler) replyToDNSQueries() bool {
// handleDNSQuery implements a DoH server (RFC 8484) over the peerapi.
// It's not over HTTPS as the spec dictates, but rather HTTP-over-WireGuard.
-func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
+func (h *PeerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
if h.ps.resolver == nil {
http.Error(w, "DNS not wired up", http.StatusNotImplemented)
return