From bf1a9cd7dc6f642cd32f261f5aaced2f3d8a9e94 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 10 Jan 2025 22:22:12 -0800 Subject: [PATCH] lanscaping: remove more taildrop/file sharing -rwxr-xr-x@ 1 bradfitz staff 12810322 Jan 10 22:21 /Users/bradfitz/bin/tailscaled.min -rwxr-xr-x@ 1 bradfitz staff 13566104 Jan 10 22:22 /Users/bradfitz/bin/tailscaled.minlinux Change-Id: Id21dc502e58a407ace166d6836311685fb47fbc6 Signed-off-by: Brad Fitzpatrick --- cmd/tailscaled/depaware-minlinux.txt | 8 +- ipn/ipnlocal/local.go | 188 ---------- ipn/ipnlocal/peerapi.go | 3 - ipn/localapi/localapi.go | 536 --------------------------- 4 files changed, 2 insertions(+), 733 deletions(-) diff --git a/cmd/tailscaled/depaware-minlinux.txt b/cmd/tailscaled/depaware-minlinux.txt index a83f18398..ed0164df0 100644 --- a/cmd/tailscaled/depaware-minlinux.txt +++ b/cmd/tailscaled/depaware-minlinux.txt @@ -112,7 +112,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/safesocket from tailscale.com/cmd/tailscaled+ tailscale.com/syncs from tailscale.com/cmd/tailscaled+ tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ - tailscale.com/taildrop from tailscale.com/ipn/ipnlocal+ tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tsd from tailscale.com/cmd/tailscaled+ tailscale.com/tstime from tailscale.com/control/controlclient+ @@ -149,7 +148,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal tailscale.com/util/groupmember from tailscale.com/ipn/ipnauth 💣 tailscale.com/util/hashx from tailscale.com/util/deephash - tailscale.com/util/httphdr from tailscale.com/ipn/localapi tailscale.com/util/httpm from tailscale.com/clientupdate/distsign+ tailscale.com/util/lineiter from tailscale.com/hostinfo+ L tailscale.com/util/linuxfw from tailscale.com/net/netns+ @@ -159,7 +157,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/util/nocasemaps from tailscale.com/types/ipproto tailscale.com/util/osdiag from tailscale.com/ipn/localapi tailscale.com/util/osuser from tailscale.com/ipn/ipnlocal+ - tailscale.com/util/progresstracking from tailscale.com/ipn/localapi tailscale.com/util/race from tailscale.com/net/dns/resolver tailscale.com/util/rands from tailscale.com/ipn/ipnlocal+ tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock @@ -270,7 +267,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de flag from net/http/httptest+ fmt from archive/tar+ hash from crypto+ - hash/adler32 from tailscale.com/taildrop hash/crc32 from compress/gzip+ hash/maphash from go4.org/mem html from net/http/pprof+ @@ -287,13 +283,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de math/rand from github.com/mdlayher/netlink+ math/rand/v2 from internal/concurrent+ mime from mime/multipart+ - mime/multipart from net/http+ + mime/multipart from net/http mime/quotedprintable from mime/multipart net from crypto/tls+ net/http from expvar+ net/http/httptest from tailscale.com/control/controlclient net/http/httptrace from golang.org/x/net/http2+ - net/http/httputil from tailscale.com/cmd/tailscaled+ + net/http/httputil from tailscale.com/cmd/tailscaled net/http/internal from net/http+ net/http/pprof from tailscale.com/cmd/tailscaled+ net/netip from github.com/gaissmai/bart+ diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 5054c01c9..dd70b346b 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -67,7 +67,6 @@ import ( "tailscale.com/portlist" "tailscale.com/syncs" "tailscale.com/tailcfg" - "tailscale.com/taildrop" "tailscale.com/tsd" "tailscale.com/tstime" "tailscale.com/types/dnstype" @@ -942,9 +941,6 @@ func (b *LocalBackend) Shutdown() { } b.mu.Unlock() - if b.peerAPIServer != nil { - b.peerAPIServer.taildrop.Shutdown() - } b.stopOfflineAutoUpdate() b.unregisterNetMon() @@ -2926,11 +2922,6 @@ func (b *LocalBackend) sendToLocked(n ipn.Notify, recipient notificationTarget) n.Version = version.Long() } - apiSrv := b.peerAPIServer - if mayDeref(apiSrv).taildrop.HasFilesWaiting() { - n.FilesWaiting = &empty.Message{} - } - for _, sess := range b.notifyWatchers { if recipient.match(sess.owner) { select { @@ -2955,10 +2946,6 @@ func (b *LocalBackend) sendFileNotify() { return } - // Make sure we always set n.IncomingFiles non-nil so it gets encoded - // in JSON to clients. They distinguish between empty and non-nil - // to know whether a Notify should be able about files. - n.IncomingFiles = apiSrv.taildrop.IncomingFiles() b.mu.Unlock() sort.Slice(n.IncomingFiles, func(i, j int) bool { @@ -4465,14 +4452,6 @@ func (b *LocalBackend) initPeerAPIListener() { ps := &peerAPIServer{ b: b, - taildrop: taildrop.ManagerOptions{ - Logf: b.logf, - Clock: tstime.DefaultClock{Clock: b.clock}, - State: b.store, - Dir: fileRoot, - DirectFileMode: b.directFileRoot != "", - SendFileNotify: b.sendFileNotify, - }.New(), } if dm, ok := b.sys.DNSManager.GetOK(); ok { ps.resolver = dm.Resolver() @@ -5571,177 +5550,10 @@ func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeK return mk, nk } -func (b *LocalBackend) removeFileWaiter(handle set.Handle) { - b.mu.Lock() - defer b.mu.Unlock() - delete(b.fileWaiters, handle) -} - -func (b *LocalBackend) addFileWaiter(wakeWaiter context.CancelFunc) set.Handle { - b.mu.Lock() - defer b.mu.Unlock() - return b.fileWaiters.Add(wakeWaiter) -} - -func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) { - b.mu.Lock() - apiSrv := b.peerAPIServer - b.mu.Unlock() - return mayDeref(apiSrv).taildrop.WaitingFiles() -} - -// AwaitWaitingFiles is like WaitingFiles but blocks while ctx is not done, -// waiting for any files to be available. -// -// On return, exactly one of the results will be non-empty or non-nil, -// respectively. -func (b *LocalBackend) AwaitWaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) { - if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 { - return ff, err - } - - for { - gotFile, gotFileCancel := context.WithCancel(context.Background()) - defer gotFileCancel() - - handle := b.addFileWaiter(gotFileCancel) - defer b.removeFileWaiter(handle) - - // Now that we've registered ourselves, check again, in case - // of race. Otherwise there's a small window where we could - // miss a file arrival and wait forever. - if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 { - return ff, err - } - - select { - case <-gotFile.Done(): - if ff, err := b.WaitingFiles(); err != nil || len(ff) > 0 { - return ff, err - } - case <-ctx.Done(): - return nil, ctx.Err() - } - } -} - -func (b *LocalBackend) DeleteFile(name string) error { - b.mu.Lock() - apiSrv := b.peerAPIServer - b.mu.Unlock() - return mayDeref(apiSrv).taildrop.DeleteFile(name) -} - -func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err error) { - b.mu.Lock() - apiSrv := b.peerAPIServer - b.mu.Unlock() - return mayDeref(apiSrv).taildrop.OpenFile(name) -} - -// hasCapFileSharing reports whether the current node has the file -// sharing capability enabled. -func (b *LocalBackend) hasCapFileSharing() bool { - b.mu.Lock() - defer b.mu.Unlock() - return b.capFileSharing -} - -// FileTargets lists nodes that the current node can send files to. -func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) { - var ret []*apitype.FileTarget - - b.mu.Lock() - defer b.mu.Unlock() - nm := b.netMap - if b.state != ipn.Running || nm == nil { - return nil, errors.New("not connected to the tailnet") - } - if !b.capFileSharing { - return nil, errors.New("file sharing not enabled by Tailscale admin") - } - for _, p := range b.peers { - if !b.peerIsTaildropTargetLocked(p) { - continue - } - if p.Hostinfo().OS() == "tvOS" { - continue - } - peerAPI := peerAPIBase(b.netMap, p) - if peerAPI == "" { - continue - } - ret = append(ret, &apitype.FileTarget{ - Node: p.AsStruct(), - PeerAPIURL: peerAPI, - }) - } - slices.SortFunc(ret, func(a, b *apitype.FileTarget) int { - return cmp.Compare(a.Node.Name, b.Node.Name) - }) - return ret, nil -} - -// peerIsTaildropTargetLocked reports whether p is a valid Taildrop file -// recipient from this node according to its ownership and the capabilities in -// the netmap. -// -// b.mu must be locked. -func (b *LocalBackend) peerIsTaildropTargetLocked(p tailcfg.NodeView) bool { - if b.netMap == nil || !p.Valid() { - return false - } - if b.netMap.User() == p.User() { - return true - } - if p.Addresses().Len() > 0 && - b.peerHasCapLocked(p.Addresses().At(0).Addr(), tailcfg.PeerCapabilityFileSharingTarget) { - // Explicitly noted in the netmap ACL caps as a target. - return true - } - return false -} - func (b *LocalBackend) peerHasCapLocked(addr netip.Addr, wantCap tailcfg.PeerCapability) bool { return b.peerCapsLocked(addr).HasCapability(wantCap) } -// SetDNS adds a DNS record for the given domain name & TXT record -// value. -// -// It's meant for use with dns-01 ACME (LetsEncrypt) challenges. -// -// This is the low-level interface. Other layers will provide more -// friendly options to get HTTPS certs. -func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error { - req := &tailcfg.SetDNSRequest{ - Version: 1, // TODO(bradfitz,maisem): use tailcfg.CurrentCapabilityVersion when using the Noise transport - Type: "TXT", - Name: name, - Value: value, - } - - b.mu.Lock() - cc := b.ccAuto - if prefs := b.pm.CurrentPrefs(); prefs.Valid() && prefs.Persist().Valid() { - req.NodeKey = prefs.Persist().PrivateNodeKey().Public() - } - b.mu.Unlock() - if cc == nil { - return errors.New("not connected") - } - if req.NodeKey.IsZero() { - return errors.New("no nodekey") - } - if name == "" { - return errors.New("missing 'name'") - } - if value == "" { - return errors.New("missing 'value'") - } - return cc.SetDNS(ctx, req) -} - func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) { svcs := peer.Hostinfo().Services() for _, s := range svcs.All() { diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index b57ebc7ba..3710f31b2 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -33,7 +33,6 @@ import ( "tailscale.com/net/netmon" "tailscale.com/net/netutil" "tailscale.com/tailcfg" - "tailscale.com/taildrop" "tailscale.com/types/views" "tailscale.com/util/clientmetric" "tailscale.com/util/httpm" @@ -58,8 +57,6 @@ type peerDNSQueryHandler interface { type peerAPIServer struct { b *LocalBackend resolver peerDNSQueryHandler - - taildrop *taildrop.Manager } func (s *peerAPIServer) listen(ip netip.Addr, ifState *netmon.State) (ln net.Listener, err error) { diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 1a9128160..030132b18 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -12,16 +12,10 @@ import ( "errors" "fmt" "io" - "maps" - "mime" - "mime/multipart" "net" "net/http" - "net/http/httputil" "net/netip" "net/url" - "os" - "path" "runtime" "slices" "strconv" @@ -32,7 +26,6 @@ import ( "golang.org/x/net/dns/dnsmessage" "tailscale.com/client/tailscale/apitype" "tailscale.com/clientupdate" - "tailscale.com/drive" "tailscale.com/envknob" "tailscale.com/hostinfo" "tailscale.com/ipn" @@ -41,7 +34,6 @@ import ( "tailscale.com/ipn/ipnstate" "tailscale.com/net/netutil" "tailscale.com/tailcfg" - "tailscale.com/taildrop" "tailscale.com/tstime" "tailscale.com/types/dnstype" "tailscale.com/types/key" @@ -49,11 +41,9 @@ import ( "tailscale.com/types/logid" "tailscale.com/types/ptr" "tailscale.com/util/clientmetric" - "tailscale.com/util/httphdr" "tailscale.com/util/httpm" "tailscale.com/util/mak" "tailscale.com/util/osdiag" - "tailscale.com/util/progresstracking" "tailscale.com/util/rands" "tailscale.com/util/syspolicy/rsop" "tailscale.com/util/syspolicy/setting" @@ -68,8 +58,6 @@ type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request) // then it's a prefix match. var handler = map[string]localAPIHandler{ // The prefix match handlers end with a slash: - "file-put/": (*Handler).serveFilePut, - "files/": (*Handler).serveFiles, "policy/": (*Handler).servePolicy, "profiles/": (*Handler).serveProfiles, @@ -93,9 +81,6 @@ var handler = map[string]localAPIHandler{ "disconnect-control": (*Handler).disconnectControl, "dns-osconfig": (*Handler).serveDNSOSConfig, "dns-query": (*Handler).serveDNSQuery, - "drive/fileserver-address": (*Handler).serveDriveServerAddr, - "drive/shares": (*Handler).serveShares, - "file-targets": (*Handler).serveFileTargets, "goroutines": (*Handler).serveGoroutines, "handle-push-message": (*Handler).serveHandlePushMessage, "id-token": (*Handler).serveIDToken, @@ -108,7 +93,6 @@ var handler = map[string]localAPIHandler{ "query-feature": (*Handler).serveQueryFeature, "reload-config": (*Handler).reloadConfig, "reset-auth": (*Handler).serveResetAuth, - "set-dns": (*Handler).serveSetDNS, "set-expiry-sooner": (*Handler).serveSetExpirySooner, "set-gui-visible": (*Handler).serveSetGUIVisible, "set-push-device-token": (*Handler).serveSetPushDeviceToken, @@ -1152,66 +1136,6 @@ func (h *Handler) serveCheckPrefs(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(res) } -func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) { - if !h.PermitWrite { - http.Error(w, "file access denied", http.StatusForbidden) - return - } - suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/files/") - if !ok { - http.Error(w, "misconfigured", http.StatusInternalServerError) - return - } - if suffix == "" { - if r.Method != "GET" { - http.Error(w, "want GET to list files", http.StatusBadRequest) - return - } - ctx := r.Context() - if s := r.FormValue("waitsec"); s != "" && s != "0" { - d, err := strconv.Atoi(s) - if err != nil { - http.Error(w, "invalid waitsec", http.StatusBadRequest) - return - } - deadline := time.Now().Add(time.Duration(d) * time.Second) - var cancel context.CancelFunc - ctx, cancel = context.WithDeadline(ctx, deadline) - defer cancel() - } - wfs, err := h.b.AwaitWaitingFiles(ctx) - if err != nil && ctx.Err() == nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(wfs) - return - } - name, err := url.PathUnescape(suffix) - if err != nil { - http.Error(w, "bad filename", http.StatusBadRequest) - return - } - if r.Method == "DELETE" { - if err := h.b.DeleteFile(name); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusNoContent) - return - } - rc, size, err := h.b.OpenFile(name) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - defer rc.Close() - w.Header().Set("Content-Length", fmt.Sprint(size)) - w.Header().Set("Content-Type", "application/octet-stream") - io.Copy(w, rc) -} - func writeErrorJSON(w http.ResponseWriter, err error) { if err == nil { err = errors.New("unexpected nil error") @@ -1224,348 +1148,6 @@ func writeErrorJSON(w http.ResponseWriter, err error) { json.NewEncoder(w).Encode(E{err.Error()}) } -func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) { - if !h.PermitRead { - http.Error(w, "access denied", http.StatusForbidden) - return - } - if r.Method != "GET" { - http.Error(w, "want GET to list targets", http.StatusBadRequest) - return - } - fts, err := h.b.FileTargets() - if err != nil { - writeErrorJSON(w, err) - return - } - mak.NonNilSliceForJSON(&fts) - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(fts) -} - -// serveFilePut sends a file to another node. -// -// It's sometimes possible for clients to do this themselves, without -// tailscaled, except in the case of tailscaled running in -// userspace-networking ("netstack") mode, in which case tailscaled -// needs to a do a netstack dial out. -// -// Instead, the CLI also goes through tailscaled so it doesn't need to be -// aware of the network mode in use. -// -// macOS/iOS have always used this localapi method to simplify the GUI -// clients. -// -// The Windows client currently (2021-11-30) uses the peerapi (/v0/put/) -// directly, as the Windows GUI always runs in tun mode anyway. -// -// In addition to single file PUTs, this endpoint accepts multipart file -// POSTS encoded as multipart/form-data.The first part should be an -// application/json file that contains a manifest consisting of a JSON array of -// OutgoingFiles which wecan use for tracking progress even before reading the -// file parts. -// -// URL format: -// -// - PUT /localapi/v0/file-put/:stableID/:escaped-filename -// - POST /localapi/v0/file-put/:stableID -func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) { - metricFilePutCalls.Add(1) - - if !h.PermitWrite { - http.Error(w, "file access denied", http.StatusForbidden) - return - } - - if r.Method != "PUT" && r.Method != "POST" { - http.Error(w, "want PUT to put file", http.StatusBadRequest) - return - } - - fts, err := h.b.FileTargets() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - upath, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/file-put/") - if !ok { - http.Error(w, "misconfigured", http.StatusInternalServerError) - return - } - var peerIDStr, filenameEscaped string - if r.Method == "PUT" { - ok := false - peerIDStr, filenameEscaped, ok = strings.Cut(upath, "/") - if !ok { - http.Error(w, "bogus URL", http.StatusBadRequest) - return - } - } else { - peerIDStr = upath - } - peerID := tailcfg.StableNodeID(peerIDStr) - - var ft *apitype.FileTarget - for _, x := range fts { - if x.Node.StableID == peerID { - ft = x - break - } - } - if ft == nil { - http.Error(w, "node not found", http.StatusNotFound) - return - } - dstURL, err := url.Parse(ft.PeerAPIURL) - if err != nil { - http.Error(w, "bogus peer URL", http.StatusInternalServerError) - return - } - - // Periodically report progress of outgoing files. - outgoingFiles := make(map[string]*ipn.OutgoingFile) - t := time.NewTicker(1 * time.Second) - progressUpdates := make(chan ipn.OutgoingFile) - defer close(progressUpdates) - - go func() { - defer t.Stop() - defer h.b.UpdateOutgoingFiles(outgoingFiles) - for { - select { - case u, ok := <-progressUpdates: - if !ok { - return - } - outgoingFiles[u.ID] = &u - case <-t.C: - h.b.UpdateOutgoingFiles(outgoingFiles) - } - } - }() - - switch r.Method { - case "PUT": - file := ipn.OutgoingFile{ - ID: rands.HexString(30), - PeerID: peerID, - Name: filenameEscaped, - DeclaredSize: r.ContentLength, - } - h.singleFilePut(r.Context(), progressUpdates, w, r.Body, dstURL, file) - case "POST": - h.multiFilePost(progressUpdates, w, r, peerID, dstURL) - default: - http.Error(w, "want PUT to put file", http.StatusBadRequest) - return - } -} - -func (h *Handler) multiFilePost(progressUpdates chan (ipn.OutgoingFile), w http.ResponseWriter, r *http.Request, peerID tailcfg.StableNodeID, dstURL *url.URL) { - _, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - if err != nil { - http.Error(w, fmt.Sprintf("invalid Content-Type for multipart POST: %s", err), http.StatusBadRequest) - return - } - - ww := &multiFilePostResponseWriter{} - defer func() { - if err := ww.Flush(w); err != nil { - h.logf("error: multiFilePostResponseWriter.Flush(): %s", err) - } - }() - - outgoingFilesByName := make(map[string]ipn.OutgoingFile) - first := true - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - part, err := mr.NextPart() - if err == io.EOF { - // No more parts. - return - } else if err != nil { - http.Error(ww, fmt.Sprintf("failed to decode multipart/form-data: %s", err), http.StatusBadRequest) - return - } - - if first { - first = false - if part.Header.Get("Content-Type") != "application/json" { - http.Error(ww, "first MIME part must be a JSON map of filename -> size", http.StatusBadRequest) - return - } - - var manifest []ipn.OutgoingFile - err := json.NewDecoder(part).Decode(&manifest) - if err != nil { - http.Error(ww, fmt.Sprintf("invalid manifest: %s", err), http.StatusBadRequest) - return - } - - for _, file := range manifest { - outgoingFilesByName[file.Name] = file - progressUpdates <- file - } - - continue - } - - if !h.singleFilePut(r.Context(), progressUpdates, ww, part, dstURL, outgoingFilesByName[part.FileName()]) { - return - } - - if ww.statusCode >= 400 { - // put failed, stop immediately - h.logf("error: singleFilePut: failed with status %d", ww.statusCode) - return - } - } -} - -// multiFilePostResponseWriter is a buffering http.ResponseWriter that can be -// reused across multiple singleFilePut calls and then flushed to the client -// when all files have been PUT. -type multiFilePostResponseWriter struct { - header http.Header - statusCode int - body *bytes.Buffer -} - -func (ww *multiFilePostResponseWriter) Header() http.Header { - if ww.header == nil { - ww.header = make(http.Header) - } - return ww.header -} - -func (ww *multiFilePostResponseWriter) WriteHeader(statusCode int) { - ww.statusCode = statusCode -} - -func (ww *multiFilePostResponseWriter) Write(p []byte) (int, error) { - if ww.body == nil { - ww.body = bytes.NewBuffer(nil) - } - return ww.body.Write(p) -} - -func (ww *multiFilePostResponseWriter) Flush(w http.ResponseWriter) error { - if ww.header != nil { - maps.Copy(w.Header(), ww.header) - } - if ww.statusCode > 0 { - w.WriteHeader(ww.statusCode) - } - if ww.body != nil { - _, err := io.Copy(w, ww.body) - return err - } - return nil -} - -func (h *Handler) singleFilePut( - ctx context.Context, - progressUpdates chan (ipn.OutgoingFile), - w http.ResponseWriter, - body io.Reader, - dstURL *url.URL, - outgoingFile ipn.OutgoingFile, -) bool { - outgoingFile.Started = time.Now() - body = progresstracking.NewReader(body, 1*time.Second, func(n int, err error) { - outgoingFile.Sent = int64(n) - progressUpdates <- outgoingFile - }) - - fail := func() { - outgoingFile.Finished = true - outgoingFile.Succeeded = false - progressUpdates <- outgoingFile - } - - // Before we PUT a file we check to see if there are any existing partial file and if so, - // we resume the upload from where we left off by sending the remaining file instead of - // the full file. - var offset int64 - var resumeDuration time.Duration - remainingBody := io.Reader(body) - client := &http.Client{ - Transport: h.b.Dialer().PeerAPITransport(), - Timeout: 10 * time.Second, - } - req, err := http.NewRequestWithContext(ctx, "GET", dstURL.String()+"/v0/put/"+outgoingFile.Name, nil) - if err != nil { - http.Error(w, "bogus peer URL", http.StatusInternalServerError) - fail() - return false - } - switch resp, err := client.Do(req); { - case err != nil: - h.logf("could not fetch remote hashes: %v", err) - case resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotFound: - // noop; implies older peerapi without resume support - case resp.StatusCode != http.StatusOK: - h.logf("fetch remote hashes status code: %d", resp.StatusCode) - default: - resumeStart := time.Now() - dec := json.NewDecoder(resp.Body) - offset, remainingBody, err = taildrop.ResumeReader(body, func() (out taildrop.BlockChecksum, err error) { - err = dec.Decode(&out) - return out, err - }) - if err != nil { - h.logf("reader could not be fully resumed: %v", err) - } - resumeDuration = time.Since(resumeStart).Round(time.Millisecond) - } - - outReq, err := http.NewRequestWithContext(ctx, "PUT", "http://peer/v0/put/"+outgoingFile.Name, remainingBody) - if err != nil { - http.Error(w, "bogus outreq", http.StatusInternalServerError) - fail() - return false - } - outReq.ContentLength = outgoingFile.DeclaredSize - if offset > 0 { - h.logf("resuming put at offset %d after %v", offset, resumeDuration) - rangeHdr, _ := httphdr.FormatRange([]httphdr.Range{{Start: offset, Length: 0}}) - outReq.Header.Set("Range", rangeHdr) - if outReq.ContentLength >= 0 { - outReq.ContentLength -= offset - } - } - - rp := httputil.NewSingleHostReverseProxy(dstURL) - rp.Transport = h.b.Dialer().PeerAPITransport() - rp.ServeHTTP(w, outReq) - - outgoingFile.Finished = true - outgoingFile.Succeeded = true - progressUpdates <- outgoingFile - - return true -} - -func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) { - if !h.PermitWrite { - http.Error(w, "access denied", http.StatusForbidden) - return - } - if r.Method != "POST" { - http.Error(w, "want POST", http.StatusBadRequest) - return - } - ctx := r.Context() - err := h.b.SetDNS(ctx, r.FormValue("name"), r.FormValue("value")) - if err != nil { - writeErrorJSON(w, err) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(struct{}{}) -} - func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { http.Error(w, "want GET", http.StatusBadRequest) @@ -2187,124 +1769,6 @@ func (h *Handler) serveDNSQuery(w http.ResponseWriter, r *http.Request) { }) } -// serveDriveServerAddr handles updates of the Taildrive file server address. -func (h *Handler) serveDriveServerAddr(w http.ResponseWriter, r *http.Request) { - if r.Method != "PUT" { - http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed) - return - } - - b, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - - h.b.DriveSetServerAddr(string(b)) - w.WriteHeader(http.StatusCreated) -} - -// serveShares handles the management of Taildrive shares. -// -// PUT - adds or updates an existing share -// DELETE - removes a share -// GET - gets a list of all shares, sorted by name -// POST - renames an existing share -func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) { - if !h.b.DriveSharingEnabled() { - http.Error(w, `taildrive sharing not enabled, please add the attribute "drive:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusForbidden) - return - } - switch r.Method { - case "PUT": - var share drive.Share - err := json.NewDecoder(r.Body).Decode(&share) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - share.Path = path.Clean(share.Path) - fi, err := os.Stat(share.Path) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if !fi.IsDir() { - http.Error(w, "not a directory", http.StatusBadRequest) - return - } - if drive.AllowShareAs() { - // share as the connected user - username, err := h.Actor.Username() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - share.As = username - } - err = h.b.DriveSetShare(&share) - if err != nil { - if errors.Is(err, drive.ErrInvalidShareName) { - http.Error(w, "invalid share name", http.StatusBadRequest) - return - } - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusCreated) - case "DELETE": - b, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - err = h.b.DriveRemoveShare(string(b)) - if err != nil { - if os.IsNotExist(err) { - http.Error(w, "share not found", http.StatusNotFound) - return - } - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusNoContent) - case "POST": - var names [2]string - err := json.NewDecoder(r.Body).Decode(&names) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - err = h.b.DriveRenameShare(names[0], names[1]) - if err != nil { - if os.IsNotExist(err) { - http.Error(w, "share not found", http.StatusNotFound) - return - } - if os.IsExist(err) { - http.Error(w, "share name already used", http.StatusBadRequest) - return - } - if errors.Is(err, drive.ErrInvalidShareName) { - http.Error(w, "invalid share name", http.StatusBadRequest) - return - } - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - w.WriteHeader(http.StatusNoContent) - case "GET": - shares := h.b.DriveGetShares() - err := json.NewEncoder(w).Encode(shares) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - default: - http.Error(w, "unsupported method", http.StatusMethodNotAllowed) - } -} - var ( metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")