mirror of
https://github.com/tailscale/tailscale.git
synced 2025-03-27 19:43:01 +00:00
ipn/localapi: add API for getting file targets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
3e915ac783
commit
d717499ac4
@ -2023,6 +2023,97 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
|
||||
return apiSrv.OpenFile(name)
|
||||
}
|
||||
|
||||
// FileTarget is a node to which files can be sent, and the PeerAPI
|
||||
// URL base to do so via.
|
||||
type FileTarget struct {
|
||||
Node *tailcfg.Node
|
||||
|
||||
// PeerAPI is the http://ip:port URL base of the node's peer API,
|
||||
// without any path (not even a single slash).
|
||||
PeerAPIURL string
|
||||
}
|
||||
|
||||
// FileTargets lists nodes that the current node can send files to.
|
||||
func (b *LocalBackend) FileTargets() ([]*FileTarget, error) {
|
||||
var ret []*FileTarget
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
nm := b.netMap
|
||||
if b.state != ipn.Running || nm == nil {
|
||||
return nil, errors.New("not connected")
|
||||
}
|
||||
now := time.Now()
|
||||
for _, p := range nm.Peers {
|
||||
if p.User != nm.User || p.LastSeen == nil {
|
||||
continue
|
||||
}
|
||||
if t := *p.LastSeen; now.Sub(t) > 30*time.Minute {
|
||||
continue
|
||||
}
|
||||
peerAPI := peerAPIBase(b.netMap, p)
|
||||
if peerAPI == "" {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, &FileTarget{
|
||||
Node: p,
|
||||
PeerAPIURL: peerAPI,
|
||||
})
|
||||
}
|
||||
// TODO: sort a different way than the netmap already is?
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// peerAPIBase returns the "http://ip:port" URL base to reach peer's peerAPI.
|
||||
// It returns the empty string if the peer doesn't support the peerapi
|
||||
// or there's no matching address family based on the netmap's own addresses.
|
||||
func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
if nm == nil || peer == nil {
|
||||
return ""
|
||||
}
|
||||
var have4, have6 bool
|
||||
for _, a := range nm.Addresses {
|
||||
if !a.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case a.IP.Is4():
|
||||
have4 = true
|
||||
case a.IP.Is6():
|
||||
have6 = true
|
||||
}
|
||||
}
|
||||
var p4, p6 uint16
|
||||
for _, s := range peer.Hostinfo.Services {
|
||||
switch s.Proto {
|
||||
case "peerapi4":
|
||||
p4 = s.Port
|
||||
case "peerapi6":
|
||||
p6 = s.Port
|
||||
}
|
||||
}
|
||||
var ipp netaddr.IPPort
|
||||
switch {
|
||||
case have4 && p4 != 0:
|
||||
ipp = netaddr.IPPort{IP: nodeIP(peer, netaddr.IP.Is4), Port: p4}
|
||||
case have6 && p6 != 0:
|
||||
ipp = netaddr.IPPort{IP: nodeIP(peer, netaddr.IP.Is6), Port: p6}
|
||||
}
|
||||
if ipp.IP.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("http://%v", ipp)
|
||||
}
|
||||
|
||||
func nodeIP(n *tailcfg.Node, pred func(netaddr.IP) bool) netaddr.IP {
|
||||
for _, a := range n.Addresses {
|
||||
if a.IsSingleIP() && pred(a.IP) {
|
||||
return a.IP
|
||||
}
|
||||
}
|
||||
return netaddr.IP{}
|
||||
}
|
||||
|
||||
func isBSD(s string) bool {
|
||||
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
|
||||
}
|
||||
|
@ -291,3 +291,131 @@ func TestPeerRoutes(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeerAPIBase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
nm *netmap.NetworkMap
|
||||
peer *tailcfg.Node
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil_netmap",
|
||||
peer: new(tailcfg.Node),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "nil_peer",
|
||||
nm: new(netmap.NetworkMap),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "self_only_4_them_both",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
Hostinfo: tailcfg.Hostinfo{
|
||||
Services: []tailcfg.Service{
|
||||
{Proto: "peerapi4", Port: 444},
|
||||
{Proto: "peerapi6", Port: 666},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "http://100.64.1.2:444",
|
||||
},
|
||||
{
|
||||
name: "self_only_6_them_both",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("fe70::1/128"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
Hostinfo: tailcfg.Hostinfo{
|
||||
Services: []tailcfg.Service{
|
||||
{Proto: "peerapi4", Port: 444},
|
||||
{Proto: "peerapi6", Port: 666},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "http://[fe70::2]:666",
|
||||
},
|
||||
{
|
||||
name: "self_both_them_only_4",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::1/128"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
Hostinfo: tailcfg.Hostinfo{
|
||||
Services: []tailcfg.Service{
|
||||
{Proto: "peerapi4", Port: 444},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "http://100.64.1.2:444",
|
||||
},
|
||||
{
|
||||
name: "self_both_them_only_6",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::1/128"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
Hostinfo: tailcfg.Hostinfo{
|
||||
Services: []tailcfg.Service{
|
||||
{Proto: "peerapi6", Port: 666},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "http://[fe70::2]:666",
|
||||
},
|
||||
{
|
||||
name: "self_both_them_no_peerapi_service",
|
||||
nm: &netmap.NetworkMap{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::1/128"),
|
||||
},
|
||||
},
|
||||
peer: &tailcfg.Node{
|
||||
Addresses: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("100.64.1.2/32"),
|
||||
netaddr.MustParseIPPrefix("fe70::2/128"),
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := peerAPIBase(tt.nm, tt.peer)
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -83,6 +84,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveCheckIPForwarding(w, r)
|
||||
case "/localapi/v0/bugreport":
|
||||
h.serveBugReport(w, r)
|
||||
case "/localapi/v0/file-targets":
|
||||
h.serveFileTargets(w, r)
|
||||
case "/":
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
default:
|
||||
@ -231,6 +234,25 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
io.Copy(w, rc)
|
||||
}
|
||||
|
||||
func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "file access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET to list targets", 400)
|
||||
return
|
||||
}
|
||||
wfs, err := h.b.FileTargets()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
makeNonNil(&wfs)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(wfs)
|
||||
}
|
||||
|
||||
func defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
@ -241,3 +263,30 @@ func defBool(a string, def bool) bool {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// makeNonNil takes a pointer to a Go data structure
|
||||
// (currently only a slice or a map) and makes sure it's non-nil for
|
||||
// JSON serialization. (In particular, JavaScript clients usually want
|
||||
// the field to be defined after they decode the JSON.)
|
||||
func makeNonNil(ptr interface{}) {
|
||||
if ptr == nil {
|
||||
panic("nil interface")
|
||||
}
|
||||
rv := reflect.ValueOf(ptr)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
panic(fmt.Sprintf("kind %v, not Ptr", rv.Kind()))
|
||||
}
|
||||
if rv.Pointer() == 0 {
|
||||
panic("nil pointer")
|
||||
}
|
||||
rv = rv.Elem()
|
||||
if rv.Pointer() != 0 {
|
||||
return
|
||||
}
|
||||
switch rv.Type().Kind() {
|
||||
case reflect.Slice:
|
||||
rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
|
||||
case reflect.Map:
|
||||
rv.Set(reflect.MakeMap(rv.Type()))
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user