client/web,cmd/tailscale: add prefix flag for web command

We already had a path on the web client server struct, but hadn't
plumbed it through to the CLI. Add that now and use it for Synology and
QNAP instead of hard-coding the path. (Adding flag for QNAP is
tailscale/tailscale-qpkg#112) This will allow supporting other
environments (like unraid) without additional changes to the client/web
package.

Also fix a small bug in unraid handling to only include the csrf token
on POST requests.

Updates tailscale/corp#13775

Signed-off-by: Will Norris <will@tailscale.com>
pull/9214/head
Will Norris 9 months ago committed by Will Norris
parent 34e3450734
commit 9a3bc9049c

@ -16,8 +16,6 @@ import (
"net/url" "net/url"
) )
const qnapPrefix = "/cgi-bin/qpkg/Tailscale/index.cgi/"
// authorizeQNAP authenticates the logged-in QNAP user and verifies // authorizeQNAP authenticates the logged-in QNAP user and verifies
// that they are authorized to use the web client. It returns true if the // that they are authorized to use the web client. It returns true if the
// request was handled and no further processing is required. // request was handled and no further processing is required.

@ -23,7 +23,7 @@ export function apiFetch(
const url = `api${endpoint}${search ? `?${search}` : ""}` const url = `api${endpoint}${search ? `?${search}` : ""}`
var contentType: string var contentType: string
if (unraidCsrfToken) { if (unraidCsrfToken && method === "POST") {
const params = new URLSearchParams() const params = new URLSearchParams()
params.append("csrf_token", unraidCsrfToken) params.append("csrf_token", unraidCsrfToken)
if (body) { if (body) {

@ -15,8 +15,6 @@ import (
"tailscale.com/util/groupmember" "tailscale.com/util/groupmember"
) )
const synologyPrefix = "/webman/3rdparty/Tailscale/index.cgi/"
// authorizeSynology authenticates the logged-in Synology user and verifies // authorizeSynology authenticates the logged-in Synology user and verifies
// that they are authorized to use the web client. It returns true if the // that they are authorized to use the web client. It returns true if the
// request was handled and no further processing is required. // request was handled and no further processing is required.

@ -58,7 +58,7 @@ type Server struct {
devProxy *httputil.ReverseProxy // only filled when devMode is on devProxy *httputil.ReverseProxy // only filled when devMode is on
cgiMode bool cgiMode bool
cgiPath string pathPrefix string
apiHandler http.Handler // csrf-protected api handler apiHandler http.Handler // csrf-protected api handler
} }
@ -69,8 +69,8 @@ type ServerOpts struct {
// CGIMode indicates if the server is running as a CGI script. // CGIMode indicates if the server is running as a CGI script.
CGIMode bool CGIMode bool
// If running in CGIMode, CGIPath is the URL path prefix to the CGI script. // PathPrefix is the URL prefix added to requests by CGI or reverse proxy.
CGIPath string PathPrefix string
// LocalClient is the tailscale.LocalClient to use for this web server. // LocalClient is the tailscale.LocalClient to use for this web server.
// If nil, a new one will be created. // If nil, a new one will be created.
@ -84,10 +84,10 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
opts.LocalClient = &tailscale.LocalClient{} opts.LocalClient = &tailscale.LocalClient{}
} }
s = &Server{ s = &Server{
devMode: opts.DevMode, devMode: opts.DevMode,
lc: opts.LocalClient, lc: opts.LocalClient,
cgiMode: opts.CGIMode, cgiMode: opts.CGIMode,
cgiPath: opts.CGIPath, pathPrefix: opts.PathPrefix,
} }
cleanup = func() {} cleanup = func() {}
if s.devMode { if s.devMode {
@ -116,20 +116,9 @@ func init() {
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
handler := s.serve handler := s.serve
// if running in cgi mode, strip the cgi path prefix // if path prefix is defined, strip it from requests.
if s.cgiMode { if s.pathPrefix != "" {
prefix := s.cgiPath handler = enforcePrefix(s.pathPrefix, handler)
if prefix == "" {
switch distro.Get() {
case distro.Synology:
prefix = synologyPrefix
case distro.QNAP:
prefix = qnapPrefix
}
}
if prefix != "" {
handler = enforcePrefix(prefix, handler)
}
} }
handler(w, r) handler(w, r)
@ -334,7 +323,6 @@ func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
} else { } else {
io.WriteString(w, "{}") io.WriteString(w, "{}")
} }
return
} }
func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData nodeUpdate) (authURL string, retErr error) { func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData nodeUpdate) (authURL string, retErr error) {
@ -487,6 +475,19 @@ func (s *Server) csrfKey() []byte {
// Unlike http.StripPrefix, it does not return a 404 if the prefix is not present. // Unlike http.StripPrefix, it does not return a 404 if the prefix is not present.
// Instead, it returns a redirect to the prefix path. // Instead, it returns a redirect to the prefix path.
func enforcePrefix(prefix string, h http.HandlerFunc) http.HandlerFunc { func enforcePrefix(prefix string, h http.HandlerFunc) http.HandlerFunc {
if prefix == "" {
return h
}
// ensure that prefix always has both a leading and trailing slash so
// that relative links for JS and CSS assets work correctly.
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
if !strings.HasSuffix(prefix, "/") {
prefix += "/"
}
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, prefix) { if !strings.HasPrefix(r.URL.Path, prefix) {
http.Redirect(w, r, prefix, http.StatusFound) http.Redirect(w, r, prefix, http.StatusFound)

@ -39,6 +39,7 @@ Tailscale, as opposed to a CLI or a native app.
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic") webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script") webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
webf.BoolVar(&webArgs.dev, "dev", false, "run web client in developer mode [this flag is in development, use is unsupported]") webf.BoolVar(&webArgs.dev, "dev", false, "run web client in developer mode [this flag is in development, use is unsupported]")
webf.StringVar(&webArgs.prefix, "prefix", "", "URL prefix added to requests (for cgi or reverse proxies)")
return webf return webf
})(), })(),
Exec: runWeb, Exec: runWeb,
@ -48,6 +49,7 @@ var webArgs struct {
listen string listen string
cgi bool cgi bool
dev bool dev bool
prefix string
} }
func tlsConfigFromEnvironment() *tls.Config { func tlsConfigFromEnvironment() *tls.Config {
@ -81,6 +83,7 @@ func runWeb(ctx context.Context, args []string) error {
webServer, cleanup := web.NewServer(ctx, web.ServerOpts{ webServer, cleanup := web.NewServer(ctx, web.ServerOpts{
DevMode: webArgs.dev, DevMode: webArgs.dev,
CGIMode: webArgs.cgi, CGIMode: webArgs.cgi,
PathPrefix: webArgs.prefix,
LocalClient: &localClient, LocalClient: &localClient,
}) })
defer cleanup() defer cleanup()

@ -1,2 +1,2 @@
#! /bin/sh #! /bin/sh
exec /var/packages/Tailscale/target/bin/tailscale web -cgi exec /var/packages/Tailscale/target/bin/tailscale web -cgi -prefix="/webman/3rdparty/Tailscale/index.cgi/"

Loading…
Cancel
Save