From 51288221cee5b4b8d6d2160ed2c90de31b22dfc0 Mon Sep 17 00:00:00 2001 From: Denton Gentry Date: Sun, 29 Jan 2023 07:30:40 -0800 Subject: [PATCH] cmd/tailscale: use request Schema+Host for QNAP authLogin.cgi QNAP allows users to set the port number for the management WebUI, which includes authLogin.cgi. If they do, then connecting to localhost:8080 fails. https://github.com/tailscale/tailscale-qpkg/issues/74#issuecomment-1407486911 Fixes https://github.com/tailscale/tailscale/issues/7108 Signed-off-by: Denton Gentry --- cmd/tailscale/cli/web.go | 45 +++++++++++++++-------- cmd/tailscale/cli/web_test.go | 67 ++++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/cmd/tailscale/cli/web.go b/cmd/tailscale/cli/web.go index 37085ffc1..82105a6a3 100644 --- a/cmd/tailscale/cli/web.go +++ b/cmd/tailscale/cli/web.go @@ -228,33 +228,48 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) { return "", nil, fmt.Errorf("not authenticated by any mechanism") } -func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) { - query := url.Values{ - "qtoken": []string{token}, - "user": []string{user}, +// qnapAuthnURL returns the auth URL to use by inferring where the UI is +// running based on the request URL. This is necessary because QNAP has so +// many options, see https://github.com/tailscale/tailscale/issues/7108 +// and https://github.com/tailscale/tailscale/issues/6903 +func qnapAuthnURL(requestUrl string, query url.Values) string { + in, err := url.Parse(requestUrl) + scheme := "" + host := "" + if err != nil || in.Scheme == "" { + log.Printf("Cannot parse QNAP login URL %v", err) + + // try localhost and hope for the best + scheme = "http" + host = "localhost" + } else { + scheme = in.Scheme + host = in.Host } + u := url.URL{ - Scheme: "http", - Host: "127.0.0.1:8080", + Scheme: scheme, + Host: host, Path: "/cgi-bin/authLogin.cgi", RawQuery: query.Encode(), } - return qnapAuthnFinish(user, u.String()) + return u.String() +} + +func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) { + query := url.Values{ + "qtoken": []string{token}, + "user": []string{user}, + } + return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query)) } func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) { query := url.Values{ "sid": []string{sid}, } - u := url.URL{ - Scheme: "http", - Host: "127.0.0.1:8080", - Path: "/cgi-bin/authLogin.cgi", - RawQuery: query.Encode(), - } - - return qnapAuthnFinish(user, u.String()) + return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query)) } func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) { diff --git a/cmd/tailscale/cli/web_test.go b/cmd/tailscale/cli/web_test.go index 6c44c27d2..138580833 100644 --- a/cmd/tailscale/cli/web_test.go +++ b/cmd/tailscale/cli/web_test.go @@ -3,7 +3,10 @@ package cli -import "testing" +import ( + "net/url" + "testing" +) func TestUrlOfListenAddr(t *testing.T) { tests := []struct { @@ -34,9 +37,65 @@ func TestUrlOfListenAddr(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - url := urlOfListenAddr(tt.in) - if url != tt.want { - t.Errorf("expected url: %q, got: %q", tt.want, url) + u := urlOfListenAddr(tt.in) + if u != tt.want { + t.Errorf("expected url: %q, got: %q", tt.want, u) + } + }) + } +} + +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) } }) }