mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
client/web: add ServerMode to web.Server
Adds a new Mode to the web server, indicating the specific scenario the constructed server is intended to be run in. Also starts filling this from the cli/web and ipn/ipnlocal callers. From cli/web this gets filled conditionally based on whether the preview web client node cap is set. If not set, the existing "legacy" client is served. If set, both a login/lobby and full management client are started (in "login" and "manage" modes respectively). Updates tailscale/corp#14335 Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
This commit is contained in:
parent
7145016414
commit
191e2ce719
@ -37,13 +37,13 @@
|
||||
|
||||
// Server is the backend server for a Tailscale web client.
|
||||
type Server struct {
|
||||
mode ServerMode
|
||||
|
||||
logf logger.Logf
|
||||
lc *tailscale.LocalClient
|
||||
timeNow func() time.Time
|
||||
|
||||
devMode bool
|
||||
tsDebugMode string
|
||||
|
||||
devMode bool
|
||||
cgiMode bool
|
||||
pathPrefix string
|
||||
|
||||
@ -65,6 +65,31 @@ type Server struct {
|
||||
browserSessions sync.Map
|
||||
}
|
||||
|
||||
// ServerMode specifies the mode of a running web.Server.
|
||||
type ServerMode string
|
||||
|
||||
const (
|
||||
// LoginServerMode serves a readonly login client for logging a
|
||||
// node into a tailnet, and viewing a readonly interface of the
|
||||
// node's current Tailscale settings.
|
||||
//
|
||||
// In this mode, API calls are authenticated via platform auth.
|
||||
LoginServerMode ServerMode = "login"
|
||||
|
||||
// ManageServerMode serves a management client for editing tailscale
|
||||
// settings of a node.
|
||||
//
|
||||
// This mode restricts the app to only being assessible over Tailscale,
|
||||
// and API calls are authenticated via browser sessions associated with
|
||||
// the source's Tailscale identity. If the source browser does not have
|
||||
// a valid session, a readonly version of the app is displayed.
|
||||
ManageServerMode ServerMode = "manage"
|
||||
|
||||
// LegacyServerMode serves the legacy web client, visible to users
|
||||
// prior to release of tailscale/corp#14335.
|
||||
LegacyServerMode ServerMode = "legacy"
|
||||
)
|
||||
|
||||
var (
|
||||
exitNodeRouteV4 = netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 = netip.MustParsePrefix("::/0")
|
||||
@ -72,6 +97,12 @@ type Server struct {
|
||||
|
||||
// ServerOpts contains options for constructing a new Server.
|
||||
type ServerOpts struct {
|
||||
// Mode specifies the mode of web client being constructed.
|
||||
Mode ServerMode
|
||||
|
||||
// DevMode indicates that the server should be started with frontend
|
||||
// assets served by a Vite dev server, allowing for local development
|
||||
// on the web client frontend.
|
||||
DevMode bool
|
||||
|
||||
// CGIMode indicates if the server is running as a CGI script.
|
||||
@ -88,6 +119,8 @@ type ServerOpts struct {
|
||||
// time.Now is used as default.
|
||||
TimeNow func() time.Time
|
||||
|
||||
// Logf optionally provides a logger function.
|
||||
// log.Printf is used as default.
|
||||
Logf logger.Logf
|
||||
}
|
||||
|
||||
@ -96,10 +129,19 @@ type ServerOpts struct {
|
||||
// ctx is only required to live the duration of the NewServer call,
|
||||
// and not the lifespan of the web server.
|
||||
func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
switch opts.Mode {
|
||||
case LoginServerMode, ManageServerMode, LegacyServerMode:
|
||||
// valid types
|
||||
case "":
|
||||
return nil, fmt.Errorf("must specify a Mode")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid Mode provided")
|
||||
}
|
||||
if opts.LocalClient == nil {
|
||||
opts.LocalClient = &tailscale.LocalClient{}
|
||||
}
|
||||
s = &Server{
|
||||
mode: opts.Mode,
|
||||
logf: opts.Logf,
|
||||
devMode: opts.DevMode,
|
||||
lc: opts.LocalClient,
|
||||
@ -113,7 +155,6 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
if s.logf == nil {
|
||||
s.logf = log.Printf
|
||||
}
|
||||
s.tsDebugMode = s.debugMode()
|
||||
s.assetsHandler, s.assetsCleanup = assetsHandler(opts.DevMode)
|
||||
|
||||
var metric string // clientmetric to report on startup
|
||||
@ -124,9 +165,7 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
// The client is secured by limiting the interface it listens on,
|
||||
// or by authenticating requests before they reach the web client.
|
||||
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
|
||||
if s.tsDebugMode == "login" {
|
||||
// For the login client, we don't serve the full web client API,
|
||||
// only the login endpoints.
|
||||
if s.mode == LoginServerMode {
|
||||
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
|
||||
metric = "web_login_client_initialization"
|
||||
} else {
|
||||
@ -146,25 +185,12 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown() {
|
||||
s.logf("web.Server: shutting down")
|
||||
if s.assetsCleanup != nil {
|
||||
s.assetsCleanup()
|
||||
}
|
||||
}
|
||||
|
||||
// debugMode returns the debug mode the web client is being run in.
|
||||
// The empty string is returned in the case that this instance is
|
||||
// not running in any debug mode.
|
||||
func (s *Server) debugMode() string {
|
||||
if !s.devMode {
|
||||
return "" // debug modes only available in dev
|
||||
}
|
||||
switch mode := os.Getenv("TS_DEBUG_WEB_CLIENT_MODE"); mode {
|
||||
case "login", "full": // valid debug modes
|
||||
return mode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ServeHTTP processes all requests for the Tailscale web client.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
handler := s.serve
|
||||
@ -203,7 +229,7 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
|
||||
// authorizeRequest manages writing out any relevant authorization
|
||||
// errors to the ResponseWriter itself.
|
||||
func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||
if s.tsDebugMode == "full" { // client using tailscale auth
|
||||
if s.mode == ManageServerMode { // client using tailscale auth
|
||||
_, err := s.lc.WhoIs(r.Context(), r.RemoteAddr)
|
||||
switch {
|
||||
case err != nil:
|
||||
@ -256,11 +282,9 @@ func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
|
||||
case httpm.GET:
|
||||
// TODO(soniaappasamy): we may want a minimal node data response here
|
||||
s.serveGetNodeData(w, r)
|
||||
case httpm.POST:
|
||||
// TODO(soniaappasamy): implement
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
http.Error(w, "invalid endpoint", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
@ -404,6 +428,12 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
||||
profile := st.User[st.Self.UserID]
|
||||
deviceName := strings.Split(st.Self.DNSName, ".")[0]
|
||||
versionShort := strings.Split(st.Version, "-")[0]
|
||||
var debugMode string
|
||||
if s.mode == ManageServerMode {
|
||||
debugMode = "full"
|
||||
} else if s.mode == LoginServerMode {
|
||||
debugMode = "login"
|
||||
}
|
||||
data := &nodeData{
|
||||
Profile: profile,
|
||||
Status: st.BackendState,
|
||||
@ -415,7 +445,7 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
|
||||
IsUnraid: distro.Get() == distro.Unraid,
|
||||
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
|
||||
IPNVersion: versionShort,
|
||||
DebugMode: s.tsDebugMode,
|
||||
DebugMode: debugMode, // TODO(sonia,will): just pass back s.mode directly?
|
||||
}
|
||||
for _, r := range prefs.AdvertiseRoutes {
|
||||
if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
|
||||
|
@ -337,9 +337,9 @@ func() *ipnstate.PeerStatus { return self },
|
||||
go localapi.Serve(lal)
|
||||
|
||||
s := &Server{
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
tsDebugMode: "full",
|
||||
timeNow: time.Now,
|
||||
mode: ManageServerMode,
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
timeNow: time.Now,
|
||||
}
|
||||
validCookie := "ts-cookie"
|
||||
s.browserSessions.Store(validCookie, &browserSession{
|
||||
@ -428,9 +428,9 @@ func() *ipnstate.PeerStatus { return self },
|
||||
sixtyDaysAgo := timeNow.Add(-sessionCookieExpiry * 2)
|
||||
|
||||
s := &Server{
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
tsDebugMode: "full",
|
||||
timeNow: func() time.Time { return timeNow },
|
||||
mode: ManageServerMode,
|
||||
lc: &tailscale.LocalClient{Dial: lal.Dial},
|
||||
timeNow: func() time.Time { return timeNow },
|
||||
}
|
||||
|
||||
successCookie := "ts-cookie-success"
|
||||
|
@ -14,10 +14,13 @@
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/web"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/cmpx"
|
||||
)
|
||||
|
||||
@ -76,11 +79,31 @@ func tlsConfigFromEnvironment() *tls.Config {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
st, err := localClient.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
hasPreviewCap := st.Self.HasCap(tailcfg.CapabilityPreviewWebClient)
|
||||
|
||||
cliServerMode := web.LegacyServerMode
|
||||
if hasPreviewCap {
|
||||
cliServerMode = web.LoginServerMode
|
||||
// Also start full client in tailscaled.
|
||||
log.Printf("starting tailscaled web client at %s:5252\n", st.Self.TailscaleIPs[0])
|
||||
if err := setRunWebClient(ctx, true); err != nil {
|
||||
return fmt.Errorf("starting web client in tailscaled: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
webServer, err := web.NewServer(web.ServerOpts{
|
||||
Mode: cliServerMode,
|
||||
DevMode: webArgs.dev,
|
||||
CGIMode: webArgs.cgi,
|
||||
PathPrefix: webArgs.prefix,
|
||||
@ -90,24 +113,35 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
log.Printf("tailscale.web: %v", err)
|
||||
return err
|
||||
}
|
||||
defer webServer.Shutdown()
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// Shutdown the server.
|
||||
webServer.Shutdown()
|
||||
if hasPreviewCap && !webArgs.cgi {
|
||||
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 err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
tlsConfig := tlsConfigFromEnvironment()
|
||||
if tlsConfig != 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 {
|
||||
@ -116,6 +150,14 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -52,6 +52,7 @@ func (b *LocalBackend) WebClientInit() (err error) {
|
||||
|
||||
b.logf("WebClientInit: initializing web ui")
|
||||
if b.webClient.server, err = web.NewServer(web.ServerOpts{
|
||||
Mode: web.ManageServerMode,
|
||||
// TODO(sonia): allow passing back dev mode flag
|
||||
LocalClient: b.webClient.lc,
|
||||
Logf: b.logf,
|
||||
|
@ -31,6 +31,7 @@ func main() {
|
||||
|
||||
// Serve the Tailscale web client.
|
||||
ws, err := web.NewServer(web.ServerOpts{
|
||||
Mode: web.LegacyServerMode,
|
||||
DevMode: *devMode,
|
||||
LocalClient: lc,
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user