mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
da6eb076aa
Adds proxy to the localapi from /api/local/ web client endpoint. The localapi proxy is restricted to an allowlist of those actually used by the web client frontend. Updates tailscale/corp#13775 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
136 lines
3.5 KiB
Go
136 lines
3.5 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package web
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"tailscale.com/client/tailscale"
|
|
"tailscale.com/net/memnet"
|
|
)
|
|
|
|
func TestQnapAuthnURL(t *testing.T) {
|
|
query := url.Values{
|
|
"qtoken": []string{"token"},
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
want string
|
|
}{
|
|
{
|
|
name: "localhost http",
|
|
in: "http://localhost:8088/",
|
|
want: "http://localhost:8088/cgi-bin/authLogin.cgi?qtoken=token",
|
|
},
|
|
{
|
|
name: "localhost https",
|
|
in: "https://localhost:5000/",
|
|
want: "https://localhost:5000/cgi-bin/authLogin.cgi?qtoken=token",
|
|
},
|
|
{
|
|
name: "IP http",
|
|
in: "http://10.1.20.4:80/",
|
|
want: "http://10.1.20.4:80/cgi-bin/authLogin.cgi?qtoken=token",
|
|
},
|
|
{
|
|
name: "IP6 https",
|
|
in: "https://[ff7d:0:1:2::1]/",
|
|
want: "https://[ff7d:0:1:2::1]/cgi-bin/authLogin.cgi?qtoken=token",
|
|
},
|
|
{
|
|
name: "hostname https",
|
|
in: "https://qnap.example.com/",
|
|
want: "https://qnap.example.com/cgi-bin/authLogin.cgi?qtoken=token",
|
|
},
|
|
{
|
|
name: "invalid URL",
|
|
in: "This is not a URL, it is a really really really really really really really really really really really really long string to exercise the URL truncation code in the error path.",
|
|
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
|
|
},
|
|
{
|
|
name: "err != nil",
|
|
in: "http://192.168.0.%31/",
|
|
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
u := qnapAuthnURL(tt.in, query)
|
|
if u != tt.want {
|
|
t.Errorf("expected url: %q, got: %q", tt.want, u)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestServeAPI tests the web client api's handling of
|
|
// 1. invalid endpoint errors
|
|
// 2. localapi proxy allowlist
|
|
func TestServeAPI(t *testing.T) {
|
|
lal := memnet.Listen("local-tailscaled.sock:80")
|
|
defer lal.Close()
|
|
|
|
// Serve dummy localapi. Just returns "success".
|
|
go func() {
|
|
localapi := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Fprintf(w, "success")
|
|
})}
|
|
defer localapi.Close()
|
|
if err := localapi.Serve(lal); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}()
|
|
s := &Server{lc: &tailscale.LocalClient{Dial: lal.Dial}}
|
|
|
|
tests := []struct {
|
|
name string
|
|
reqPath string
|
|
wantResp string
|
|
wantStatus int
|
|
}{{
|
|
name: "invalid_endpoint",
|
|
reqPath: "/not-an-endpoint",
|
|
wantResp: "invalid endpoint",
|
|
wantStatus: http.StatusNotFound,
|
|
}, {
|
|
name: "not_in_localapi_allowlist",
|
|
reqPath: "/local/v0/not-allowlisted",
|
|
wantResp: "/v0/not-allowlisted not allowed from localapi proxy",
|
|
wantStatus: http.StatusForbidden,
|
|
}, {
|
|
name: "in_localapi_allowlist",
|
|
reqPath: "/local/v0/logout",
|
|
wantResp: "success", // Successfully allowed to hit localapi.
|
|
wantStatus: http.StatusOK,
|
|
}}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
r := httptest.NewRequest("POST", "/api"+tt.reqPath, nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
s.serveAPI(w, r)
|
|
res := w.Result()
|
|
defer res.Body.Close()
|
|
if gotStatus := res.StatusCode; tt.wantStatus != gotStatus {
|
|
t.Errorf("wrong status; want=%q, got=%q", tt.wantStatus, gotStatus)
|
|
}
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gotResp := strings.TrimSuffix(string(body), "\n") // trim trailing newline
|
|
if tt.wantResp != gotResp {
|
|
t.Errorf("wrong response; want=%q, got=%q", tt.wantResp, gotResp)
|
|
}
|
|
})
|
|
}
|
|
}
|