mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 04:55:31 +00:00
ipn/ipnlocal, etc: require file sharing capability to send/recv files
tailscale/corp#1582 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
2f422434aa
commit
b993d9802a
@ -66,6 +66,20 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
|
|||||||
return tsClient.Do(req)
|
return tsClient.Do(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type errorJSON struct {
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// bestError returns either err, or if body contains a valid JSON
|
||||||
|
// object of type errorJSON, its non-empty error body.
|
||||||
|
func bestError(err error, body []byte) error {
|
||||||
|
var j errorJSON
|
||||||
|
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
|
||||||
|
return errors.New(j.Error)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
|
func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
|
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -81,7 +95,8 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if res.StatusCode != wantStatus {
|
if res.StatusCode != wantStatus {
|
||||||
return nil, fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
|
err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
|
||||||
|
return nil, bestError(err, slurp)
|
||||||
}
|
}
|
||||||
return slurp, nil
|
return slurp, nil
|
||||||
}
|
}
|
||||||
|
@ -829,6 +829,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
|||||||
p.Endpoints = []string{"127.9.9.9:456"}
|
p.Endpoints = []string{"127.9.9.9:456"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if Debug.StripCaps {
|
||||||
|
resp.Node.Capabilities = nil
|
||||||
|
}
|
||||||
|
|
||||||
if pf := resp.PacketFilter; pf != nil {
|
if pf := resp.PacketFilter; pf != nil {
|
||||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||||
@ -1072,6 +1075,7 @@ type debug struct {
|
|||||||
OnlyDisco bool
|
OnlyDisco bool
|
||||||
Disco bool
|
Disco bool
|
||||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||||
|
StripCaps bool // strip all local node's control-provided capabilities
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDebug() debug {
|
func initDebug() debug {
|
||||||
@ -1080,6 +1084,7 @@ func initDebug() debug {
|
|||||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||||
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||||
|
StripCaps: envBool("TS_DEBUG_STRIP_CAPS"),
|
||||||
OnlyDisco: use == "only",
|
OnlyDisco: use == "only",
|
||||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||||
}
|
}
|
||||||
|
@ -2215,12 +2215,36 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
|
|||||||
return apiSrv.OpenFile(name)
|
return apiSrv.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.hasCapFileSharingLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) hasCapFileSharingLocked() bool {
|
||||||
|
nm := b.netMap
|
||||||
|
if nm == nil || nm.SelfNode == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, c := range nm.SelfNode.Capabilities {
|
||||||
|
if c == tailcfg.CapabilityFileSharing {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// FileTargets lists nodes that the current node can send files to.
|
// FileTargets lists nodes that the current node can send files to.
|
||||||
func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||||
var ret []*apitype.FileTarget
|
var ret []*apitype.FileTarget
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
if !b.hasCapFileSharingLocked() {
|
||||||
|
return nil, errors.New("file sharing not enabled by Tailscale admin")
|
||||||
|
}
|
||||||
nm := b.netMap
|
nm := b.netMap
|
||||||
if b.state != ipn.Running || nm == nil {
|
if b.state != ipn.Running || nm == nil {
|
||||||
return nil, errors.New("not connected")
|
return nil, errors.New("not connected")
|
||||||
|
@ -410,6 +410,10 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "not owner", http.StatusForbidden)
|
http.Error(w, "not owner", http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !h.ps.b.hasCapFileSharing() {
|
||||||
|
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
if r.Method != "PUT" {
|
if r.Method != "PUT" {
|
||||||
http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
|
http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
|
@ -88,7 +88,8 @@ type PeerStatus struct {
|
|||||||
KeepAlive bool
|
KeepAlive bool
|
||||||
ExitNode bool // true if this is the currently selected exit node.
|
ExitNode bool // true if this is the currently selected exit node.
|
||||||
|
|
||||||
PeerAPIURL []string
|
PeerAPIURL []string
|
||||||
|
Capabilities []string `json:",omitempty"`
|
||||||
|
|
||||||
// ShareeNode indicates this node exists in the netmap because
|
// ShareeNode indicates this node exists in the netmap because
|
||||||
// it's owned by a shared-to user and that node might connect
|
// it's owned by a shared-to user and that node might connect
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@ -299,6 +300,18 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
|||||||
io.Copy(w, rc)
|
io.Copy(w, rc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeErrorJSON(w http.ResponseWriter, err error) {
|
||||||
|
if err == nil {
|
||||||
|
err = errors.New("unexpected nil error")
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(500)
|
||||||
|
type E struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(E{err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.PermitRead {
|
if !h.PermitRead {
|
||||||
http.Error(w, "access denied", http.StatusForbidden)
|
http.Error(w, "access denied", http.StatusForbidden)
|
||||||
@ -310,7 +323,7 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
fts, err := h.b.FileTargets()
|
fts, err := h.b.FileTargets()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
writeErrorJSON(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
makeNonNil(&fts)
|
makeNonNil(&fts)
|
||||||
|
@ -177,7 +177,7 @@ type Node struct {
|
|||||||
// They're free-form strings, but should be in the form of URLs/URIs
|
// They're free-form strings, but should be in the form of URLs/URIs
|
||||||
// such as:
|
// such as:
|
||||||
// "https://tailscale.com/cap/is-admin"
|
// "https://tailscale.com/cap/is-admin"
|
||||||
// "https://tailscale.com/cap/recv-file"
|
// "https://tailscale.com/cap/file-sharing"
|
||||||
Capabilities []string `json:",omitempty"`
|
Capabilities []string `json:",omitempty"`
|
||||||
|
|
||||||
// The following three computed fields hold the various names that can
|
// The following three computed fields hold the various names that can
|
||||||
@ -1140,3 +1140,8 @@ type Oauth2Token struct {
|
|||||||
// mechanisms for that TokenSource will not be used.
|
// mechanisms for that TokenSource will not be used.
|
||||||
Expiry time.Time `json:"expiry,omitempty"`
|
Expiry time.Time `json:"expiry,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
|
||||||
|
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
|
||||||
|
)
|
||||||
|
@ -2984,6 +2984,11 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
|||||||
ss.HostName = c.netMap.Hostinfo.Hostname
|
ss.HostName = c.netMap.Hostinfo.Hostname
|
||||||
ss.DNSName = c.netMap.Name
|
ss.DNSName = c.netMap.Name
|
||||||
ss.UserID = c.netMap.User
|
ss.UserID = c.netMap.User
|
||||||
|
if c.netMap.SelfNode != nil {
|
||||||
|
if c := c.netMap.SelfNode.Capabilities; len(c) > 0 {
|
||||||
|
ss.Capabilities = append([]string(nil), c...)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ss.HostName, _ = os.Hostname()
|
ss.HostName, _ = os.Hostname()
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user