ipn/ipnlocal: push down a user-specific root dir to peerapi handler

And add a put handler.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-03-29 10:42:33 -07:00
parent 662fbd4a09
commit 35596ae5ce
2 changed files with 111 additions and 12 deletions

View File

@ -11,6 +11,7 @@
"fmt" "fmt"
"net" "net"
"os" "os"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -27,6 +28,7 @@
"tailscale.com/net/dns" "tailscale.com/net/dns"
"tailscale.com/net/interfaces" "tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr" "tailscale.com/net/tsaddr"
"tailscale.com/paths"
"tailscale.com/portlist" "tailscale.com/portlist"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/empty" "tailscale.com/types/empty"
@ -1448,7 +1450,22 @@ func (b *LocalBackend) initPeerAPIListener() {
} }
b.peerAPIListeners = nil b.peerAPIListeners = nil
if len(b.netMap.Addresses) == 0 || b.netMap.SelfNode == nil { selfNode := b.netMap.SelfNode
if len(b.netMap.Addresses) == 0 || selfNode == nil {
return
}
stateFile := paths.DefaultTailscaledStateFile()
if stateFile == "" {
b.logf("peerapi disabled; no state directory")
return
}
baseDir := fmt.Sprintf("%s-uid-%d",
strings.ReplaceAll(b.activeLogin, "@", "-"),
selfNode.User)
dir := filepath.Join(filepath.Dir(stateFile), "files", baseDir)
if err := os.MkdirAll(dir, 0700); err != nil {
b.logf("peerapi disabled; error making directory: %v", err)
return return
} }
@ -1458,16 +1475,23 @@ func (b *LocalBackend) initPeerAPIListener() {
tunName, _ = tunDev.Name() tunName, _ = tunDev.Name()
} }
ps := &peerAPIServer{
b: b,
rootDir: dir,
tunName: tunName,
selfNode: selfNode,
}
for _, a := range b.netMap.Addresses { for _, a := range b.netMap.Addresses {
ln, err := peerAPIListen(a.IP, b.prevIfState, tunName) ln, err := ps.listen(a.IP, b.prevIfState)
if err != nil { if err != nil {
b.logf("[unexpected] peerAPI listen(%q) error: %v", a.IP, err) b.logf("[unexpected] peerAPI listen(%q) error: %v", a.IP, err)
continue continue
} }
pln := &peerAPIListener{ pln := &peerAPIListener{
ln: ln, ps: ps,
lb: b, ln: ln,
selfNode: b.netMap.SelfNode, lb: b,
} }
pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.Port())) pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.Port()))

View File

@ -13,8 +13,13 @@
"io" "io"
"net" "net"
"net/http" "net/http"
"net/url"
"os"
"path"
"path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"inet.af/netaddr" "inet.af/netaddr"
"tailscale.com/net/interfaces" "tailscale.com/net/interfaces"
@ -23,7 +28,14 @@
var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
func peerAPIListen(ip netaddr.IP, ifState *interfaces.State, tunIfName string) (ln net.Listener, err error) { type peerAPIServer struct {
b *LocalBackend
rootDir string
tunName string
selfNode *tailcfg.Node
}
func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) {
ipStr := ip.String() ipStr := ip.String()
var lc net.ListenConfig var lc net.ListenConfig
@ -31,7 +43,7 @@ func peerAPIListen(ip netaddr.IP, ifState *interfaces.State, tunIfName string) (
// On iOS/macOS, this sets the lc.Control hook to // On iOS/macOS, this sets the lc.Control hook to
// setsockopt the interface index to bind to, to get // setsockopt the interface index to bind to, to get
// out of the network sandbox. // out of the network sandbox.
if err := initListenConfig(&lc, ip, ifState, tunIfName); err != nil { if err := initListenConfig(&lc, ip, ifState, s.tunName); err != nil {
return nil, err return nil, err
} }
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" { if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
@ -67,10 +79,10 @@ func peerAPIListen(ip netaddr.IP, ifState *interfaces.State, tunIfName string) (
} }
type peerAPIListener struct { type peerAPIListener struct {
ln net.Listener ps *peerAPIServer
lb *LocalBackend ln net.Listener
urlStr string lb *LocalBackend
selfNode *tailcfg.Node urlStr string
} }
func (pln *peerAPIListener) Port() int { func (pln *peerAPIListener) Port() int {
@ -112,7 +124,8 @@ func (pln *peerAPIListener) serve() {
continue continue
} }
h := &peerAPIHandler{ h := &peerAPIHandler{
isSelf: pln.selfNode.User == peerNode.User, ps: pln.ps,
isSelf: pln.ps.selfNode.User == peerNode.User,
remoteAddr: ipp, remoteAddr: ipp,
peerNode: peerNode, peerNode: peerNode,
peerUser: peerUser, peerUser: peerUser,
@ -145,6 +158,7 @@ func (l *oneConnListener) Close() error { return nil }
// peerAPIHandler serves the Peer API for a source specific client. // peerAPIHandler serves the Peer API for a source specific client.
type peerAPIHandler struct { type peerAPIHandler struct {
ps *peerAPIServer
remoteAddr netaddr.IPPort remoteAddr netaddr.IPPort
isSelf bool // whether peerNode is owned by same user as this node isSelf bool // whether peerNode is owned by same user as this node
peerNode *tailcfg.Node // peerNode is who's making the request peerNode *tailcfg.Node // peerNode is who's making the request
@ -152,7 +166,15 @@ type peerAPIHandler struct {
lb *LocalBackend lb *LocalBackend
} }
func (h *peerAPIHandler) logf(format string, a ...interface{}) {
h.ps.b.logf("peerapi: "+format, a...)
}
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
h.put(w, r)
return
}
who := h.peerUser.DisplayName who := h.peerUser.DisplayName
fmt.Fprintf(w, `<html> fmt.Fprintf(w, `<html>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@ -165,3 +187,56 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "<p>You are the owner of this node.\n") fmt.Fprintf(w, "<p>You are the owner of this node.\n")
} }
} }
func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
if r.Method != "PUT" {
http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
return
}
if h.ps.rootDir == "" {
http.Error(w, "no rootdir", http.StatusInternalServerError)
return
}
name := path.Base(r.URL.Path)
if name == "." || name == "/" {
http.Error(w, "bad filename", http.StatusForbidden)
return
}
fileBase := strings.ReplaceAll(url.PathEscape(name), ":", "%3a")
dstFile := filepath.Join(h.ps.rootDir, fileBase)
f, err := os.Create(dstFile)
if err != nil {
h.logf("put Create error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var success bool
defer func() {
if !success {
os.Remove(dstFile)
}
}()
n, err := io.Copy(f, r.Body)
if err != nil {
f.Close()
h.logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := f.Close(); err != nil {
h.logf("put Close error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
h.logf("put(%q): %d bytes from %v/%v", name, n, h.remoteAddr.IP, h.peerNode.ComputedName)
// TODO: set modtime
// TODO: some real response
success = true
io.WriteString(w, "{}\n")
}