You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/cmd/tailscale/cli/web.go

165 lines
4.3 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"cmp"
"context"
"crypto/tls"
_ "embed"
"flag"
"fmt"
"log"
"net"
"net/http"
"net/http/cgi"
"net/netip"
"os"
"os/signal"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/client/web"
"tailscale.com/ipn"
)
var webCmd = &ffcli.Command{
Name: "web",
ShortUsage: "web [flags]",
ShortHelp: "Run a web server for controlling Tailscale",
LongHelp: strings.TrimSpace(`
"tailscale web" runs a webserver for controlling the Tailscale daemon.
It's primarily intended for use on Synology, QNAP, and other
NAS devices where a web interface is the natural place to control
Tailscale, as opposed to a CLI or a native app.
`),
FlagSet: (func() *flag.FlagSet {
webf := newFlagSet("web")
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.StringVar(&webArgs.prefix, "prefix", "", "URL prefix added to requests (for cgi or reverse proxies)")
return webf
})(),
Exec: runWeb,
}
var webArgs struct {
listen string
cgi bool
prefix string
}
func tlsConfigFromEnvironment() *tls.Config {
crt := os.Getenv("TLS_CRT_PEM")
key := os.Getenv("TLS_KEY_PEM")
if crt == "" || key == "" {
return nil
}
// We support passing in the complete certificate and key from environment
// variables because pfSense stores its cert+key in the PHP config. We populate
// TLS_CRT_PEM and TLS_KEY_PEM from PHP code before starting tailscale web.
// These are the PEM-encoded Certificate and Private Key.
cert, err := tls.X509KeyPair([]byte(crt), []byte(key))
if err != nil {
log.Printf("tlsConfigFromEnvironment: %v", err)
// Fallback to unencrypted HTTP.
return nil
}
return &tls.Config{Certificates: []tls.Certificate{cert}}
}
func runWeb(ctx context.Context, args []string) error {
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
if len(args) > 0 {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
var selfIP netip.Addr
st, err := localClient.StatusWithoutPeers(ctx)
if err == nil && st.Self != nil && len(st.Self.TailscaleIPs) > 0 {
selfIP = st.Self.TailscaleIPs[0]
}
var existingWebClient bool
if prefs, err := localClient.GetPrefs(ctx); err == nil {
existingWebClient = prefs.RunWebClient
}
if !existingWebClient {
// Also start full client in tailscaled.
log.Printf("starting tailscaled web client at %s:%d\n", selfIP.String(), web.ListenPort)
if err := setRunWebClient(ctx, true); err != nil {
return fmt.Errorf("starting web client in tailscaled: %w", err)
}
}
webServer, err := web.NewServer(web.ServerOpts{
Mode: web.LoginServerMode,
CGIMode: webArgs.cgi,
PathPrefix: webArgs.prefix,
LocalClient: &localClient,
})
if err != nil {
log.Printf("tailscale.web: %v", err)
return err
}
go func() {
select {
case <-ctx.Done():
// Shutdown the server.
webServer.Shutdown()
if !webArgs.cgi && !existingWebClient {
log.Println("stopping tailscaled web client")
// When not in cgi mode, shut down the tailscaled
// web client on cli termination.
if err := setRunWebClient(context.Background(), false); err != nil {
log.Printf("stopping tailscaled web client: %v", err)
}
}
}
os.Exit(0)
}()
if webArgs.cgi {
if err := cgi.Serve(webServer); err != nil {
log.Printf("tailscale.cgi: %v", err)
}
return nil
} else if tlsConfig := tlsConfigFromEnvironment(); tlsConfig != nil {
server := &http.Server{
Addr: webArgs.listen,
TLSConfig: tlsConfig,
Handler: webServer,
}
defer server.Shutdown(ctx)
log.Printf("web server running on: https://%s", server.Addr)
return server.ListenAndServeTLS("", "")
} else {
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
return http.ListenAndServe(webArgs.listen, webServer)
}
}
func setRunWebClient(ctx context.Context, val bool) error {
_, err := localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{RunWebClient: val},
RunWebClientSet: true,
})
return err
}
// urlOfListenAddr parses a given listen address into a formatted URL
func urlOfListenAddr(addr string) string {
host, port, _ := net.SplitHostPort(addr)
return fmt.Sprintf("http://%s", net.JoinHostPort(cmp.Or(host, "127.0.0.1"), port))
}