mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-15 01:06:13 +02:00
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:
parent
b50bfa231b
commit
bf1a9cd7dc
@ -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+
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user