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 <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2025-01-10 22:22:12 -08:00
parent b50bfa231b
commit bf1a9cd7dc
4 changed files with 2 additions and 733 deletions

View File

@ -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+

View File

@ -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() {

View File

@ -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) {

View File

@ -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")