// 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". localapi := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "success") })} defer localapi.Close() go localapi.Serve(lal) 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) } }) } }