tailscale/ipn/localapi/localapi_test.go
Tyler Smalley baa1fd976e
ipn/localapi: require local Windows admin to set serve path (#9969)
For a serve config with a path handler, ensure the caller is a local administrator on Windows.

updates #8489

Signed-off-by: Tyler Smalley <tyler@tailscale.com>
2023-10-26 14:40:44 -07:00

215 lines
5.4 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package localapi
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"strings"
"testing"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
)
func TestValidHost(t *testing.T) {
tests := []struct {
host string
valid bool
}{
{"", true},
{apitype.LocalAPIHost, true},
{"localhost:9109", false},
{"127.0.0.1:9110", false},
{"[::1]:9111", false},
{"100.100.100.100:41112", false},
{"10.0.0.1:41112", false},
{"37.16.9.210:41112", false},
}
for _, test := range tests {
t.Run(test.host, func(t *testing.T) {
h := &Handler{}
if got := h.validHost(test.host); got != test.valid {
t.Errorf("validHost(%q)=%v, want %v", test.host, got, test.valid)
}
})
}
}
func TestSetPushDeviceToken(t *testing.T) {
tstest.Replace(t, &validLocalHostForTesting, true)
h := &Handler{
PermitWrite: true,
b: &ipnlocal.LocalBackend{},
}
s := httptest.NewServer(h)
defer s.Close()
c := s.Client()
want := "my-test-device-token"
body, err := json.Marshal(apitype.SetPushDeviceTokenRequest{PushDeviceToken: want})
if err != nil {
t.Fatal(err)
}
req, err := http.NewRequest("POST", s.URL+"/localapi/v0/set-push-device-token", bytes.NewReader(body))
if err != nil {
t.Fatal(err)
}
res, err := c.Do(req)
if err != nil {
t.Fatal(err)
}
body, err = io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
if res.StatusCode != 200 {
t.Errorf("res.StatusCode=%d, want 200. body: %s", res.StatusCode, body)
}
if got := h.b.GetPushDeviceToken(); got != want {
t.Errorf("hostinfo.PushDeviceToken=%q, want %q", got, want)
}
}
type whoIsBackend struct {
whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
peerCaps map[netip.Addr]tailcfg.PeerCapMap
}
func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
return b.whoIs(ipp)
}
func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
return b.peerCaps[ip]
}
// Tests that the WhoIs handler accepts either IPs or IP:ports.
//
// From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
func TestWhoIsJustIP(t *testing.T) {
h := &Handler{
PermitRead: true,
}
for _, input := range []string{"100.101.102.103", "127.0.0.1:123"} {
rec := httptest.NewRecorder()
t.Run(input, func(t *testing.T) {
b := whoIsBackend{
whoIs: func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
if !strings.Contains(input, ":") {
want := netip.MustParseAddrPort("100.101.102.103:0")
if ipp != want {
t.Fatalf("backend called with %v; want %v", ipp, want)
}
}
return (&tailcfg.Node{
ID: 123,
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.101.102.103/32"),
},
}).View(),
tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
true
},
peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
"foo": {`"bar"`},
},
},
}
h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
var res apitype.WhoIsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
t.Fatal(err)
}
if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
t.Errorf("res.Node.ID=%v, want %v", got, want)
}
if got, want := res.UserProfile.DisplayName, "foo"; got != want {
t.Errorf("res.UserProfile.DisplayName=%q, want %q", got, want)
}
if got, want := len(res.CapMap), 1; got != want {
t.Errorf("capmap size=%v, want %v", got, want)
}
})
}
}
func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
tests := []struct {
name string
goos string
configIn *ipn.ServeConfig
h *Handler
want bool
}{
{
name: "linux",
goos: "linux",
configIn: &ipn.ServeConfig{},
h: &Handler{CallerIsLocalAdmin: false},
want: false,
},
{
name: "windows-not-path-handler",
goos: "windows",
configIn: &ipn.ServeConfig{
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
h: &Handler{CallerIsLocalAdmin: false},
want: false,
},
{
name: "windows-path-handler-admin",
goos: "windows",
configIn: &ipn.ServeConfig{
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Path: "/tmp"},
}},
},
},
h: &Handler{CallerIsLocalAdmin: true},
want: false,
},
{
name: "windows-path-handler-not-admin",
goos: "windows",
configIn: &ipn.ServeConfig{
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Path: "/tmp"},
}},
},
},
h: &Handler{CallerIsLocalAdmin: false},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := shouldDenyServeConfigForGOOSAndUserContext(tt.goos, tt.configIn, tt.h)
if got != tt.want {
t.Errorf("shouldDenyServeConfigForGOOSAndUserContext() got = %v, want %v", got, tt.want)
}
})
}
}