From 0d1550898efa30e49d1a5916e9a2270fa92e8237 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 19 Apr 2021 20:21:48 -0700 Subject: [PATCH] ipn/ipnlocal: add some peerapi tests Updates tailscale/corp#1594 Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/peerapi.go | 6 +- ipn/ipnlocal/peerapi_test.go | 153 +++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 ipn/ipnlocal/peerapi_test.go diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index b1a43b3c2..2064b5f66 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -290,7 +290,6 @@ func (pln *peerAPIListener) serve() { remoteAddr: ipp, peerNode: peerNode, peerUser: peerUser, - lb: pln.lb, } httpServer := &http.Server{ Handler: h, @@ -324,7 +323,6 @@ type peerAPIHandler struct { isSelf bool // whether peerNode is owned by same user as this node peerNode *tailcfg.Node // peerNode is who's making the request peerUser tailcfg.UserProfile // profile of peerNode - lb *LocalBackend } func (h *peerAPIHandler) logf(format string, a ...interface{}) { @@ -333,7 +331,7 @@ func (h *peerAPIHandler) logf(format string, a ...interface{}) { func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/v0/put/") { - h.put(w, r) + h.handlePeerPut(w, r) return } who := h.peerUser.DisplayName @@ -405,7 +403,7 @@ func (f *incomingFile) PartialFile() ipn.PartialFile { } } -func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) { +func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) { if !h.isSelf { http.Error(w, "not owner", http.StatusForbidden) return diff --git a/ipn/ipnlocal/peerapi_test.go b/ipn/ipnlocal/peerapi_test.go new file mode 100644 index 000000000..1979792f3 --- /dev/null +++ b/ipn/ipnlocal/peerapi_test.go @@ -0,0 +1,153 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package ipnlocal + +import ( + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "tailscale.com/tailcfg" + "tailscale.com/types/netmap" +) + +type peerAPITestEnv struct { + ph *peerAPIHandler + rr *httptest.ResponseRecorder + logBuf bytes.Buffer +} + +func (e *peerAPITestEnv) logf(format string, a ...interface{}) { + fmt.Fprintf(&e.logBuf, format, a...) +} + +type check func(*testing.T, *peerAPITestEnv) + +func checks(vv ...check) []check { return vv } + +func httpStatus(wantStatus int) check { + return func(t *testing.T, e *peerAPITestEnv) { + if res := e.rr.Result(); res.StatusCode != wantStatus { + t.Errorf("HTTP response code = %v; want %v", res.Status, wantStatus) + } + } +} + +func bodyContains(sub string) check { + return func(t *testing.T, e *peerAPITestEnv) { + if body := e.rr.Body.String(); !strings.Contains(body, sub) { + t.Errorf("HTTP response body does not contain %q; got: %s", sub, body) + } + } +} + +func fileHasSize(name string, size int64) check { + return func(t *testing.T, e *peerAPITestEnv) { + root := e.ph.ps.rootDir + if root == "" { + t.Errorf("no rootdir; can't check whether %q has size %v", name, size) + return + } + path := filepath.Join(root, name) + if fi, err := os.Stat(path); err != nil { + t.Errorf("fileHasSize(%q, %v): %v", name, size, err) + } else if fi.Size() != size { + t.Errorf("file %q has size %v; want %v", name, fi.Size(), size) + } + } +} + +func TestHandlePeerPut(t *testing.T) { + tests := []struct { + name string + isSelf bool // the peer sending the request is owned by us + capSharing bool // self node has file sharing capabilty + omitRoot bool // don't configure + req *http.Request + checks []check + }{ + { + name: "reject_non_owner_put", + isSelf: false, + capSharing: true, + req: httptest.NewRequest("PUT", "/v0/put/foo", nil), + checks: checks( + httpStatus(http.StatusForbidden), + bodyContains("not owner"), + ), + }, + { + name: "owner_without_cap", + isSelf: true, + capSharing: false, + req: httptest.NewRequest("PUT", "/v0/put/foo", nil), + checks: checks( + httpStatus(http.StatusForbidden), + bodyContains("file sharing not enabled by Tailscale admin"), + ), + }, + { + name: "owner_with_cap_no_rootdir", + omitRoot: true, + isSelf: true, + capSharing: true, + req: httptest.NewRequest("PUT", "/v0/put/foo", nil), + checks: checks( + httpStatus(http.StatusInternalServerError), + bodyContains("no rootdir"), + ), + }, + { + name: "owner_with_cap", + isSelf: true, + capSharing: true, + req: httptest.NewRequest("PUT", "/v0/put/foo", nil), + checks: checks( + httpStatus(200), + bodyContains("{}"), + fileHasSize("foo", 0), + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var caps []string + if tt.capSharing { + caps = append(caps, tailcfg.CapabilityFileSharing) + } + var e peerAPITestEnv + lb := &LocalBackend{ + netMap: &netmap.NetworkMap{ + SelfNode: &tailcfg.Node{ + Capabilities: caps, + }, + }, + logf: e.logf, + } + e.ph = &peerAPIHandler{ + isSelf: tt.isSelf, + peerNode: &tailcfg.Node{ + ComputedName: "some-peer-name", + }, + ps: &peerAPIServer{ + b: lb, + }, + } + if !tt.omitRoot { + e.ph.ps.rootDir = t.TempDir() + } + e.rr = httptest.NewRecorder() + e.ph.ServeHTTP(e.rr, tt.req) + for _, f := range tt.checks { + f(t, &e) + } + }) + } +}