client/web: clean up assets handling

A #cleanup that moves all frontend asset handling into assets.go
(formerly dev.go), and stores a single assetsHandler field back
to web.Server that manages when to serve the dev vite proxy versus
static files itself.

Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
pull/9313/head
Sonia Appasamy 1 year ago committed by Sonia Appasamy
parent 4a38d8d372
commit 1eadb2b608

@ -4,6 +4,8 @@
package web package web
import ( import (
"embed"
"io/fs"
"log" "log"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -12,11 +14,42 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"tailscale.com/util/must"
) )
// This contains all files needed to build the frontend assets.
// Because we assign this to the blank identifier, it does not actually embed the files.
// However, this does cause `go mod vendor` to include the files when vendoring the package.
// External packages that use the web client can `go mod vendor`, run `yarn build` to
// build the assets, then those asset bundles will be embedded.
//
//go:embed yarn.lock index.html *.js *.json src/*
var _ embed.FS
//go:embed build/*
var embeddedFS embed.FS
// staticfiles serves static files from the build directory.
var staticfiles http.Handler
func init() {
buildFiles := must.Get(fs.Sub(embeddedFS, "build"))
staticfiles = http.FileServer(http.FS(buildFiles))
}
func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) {
if devMode {
// When in dev mode, proxy asset requests to the Vite dev server.
cleanup := startDevServer()
return devServerProxy(), cleanup
}
return staticfiles, nil
}
// startDevServer starts the JS dev server that does on-demand rebuilding // startDevServer starts the JS dev server that does on-demand rebuilding
// and serving of web client JS and CSS resources. // and serving of web client JS and CSS resources.
func (s *Server) startDevServer() (cleanup func()) { func startDevServer() (cleanup func()) {
root := gitRootDir() root := gitRootDir()
webClientPath := filepath.Join(root, "client", "web") webClientPath := filepath.Join(root, "client", "web")
@ -45,10 +78,8 @@ func (s *Server) startDevServer() (cleanup func()) {
} }
} }
func (s *Server) addProxyToDevServer() { // devServerProxy returns a reverse proxy to the vite dev server.
if !s.devMode { func devServerProxy() *httputil.ReverseProxy {
return // only using Vite proxy in dev mode
}
// We use Vite to develop on the web client. // We use Vite to develop on the web client.
// Vite starts up its own local server for development, // Vite starts up its own local server for development,
// which we proxy requests to from Server.ServeHTTP. // which we proxy requests to from Server.ServeHTTP.
@ -62,8 +93,9 @@ func (s *Server) addProxyToDevServer() {
w.Write([]byte("\n\nError: " + err.Error())) w.Write([]byte("\n\nError: " + err.Error()))
} }
viteTarget, _ := url.Parse("http://127.0.0.1:4000") viteTarget, _ := url.Parse("http://127.0.0.1:4000")
s.devProxy = httputil.NewSingleHostReverseProxy(viteTarget) devProxy := httputil.NewSingleHostReverseProxy(viteTarget)
s.devProxy.ErrorHandler = handleErr devProxy.ErrorHandler = handleErr
return devProxy
} }
func gitRootDir() string { func gitRootDir() string {

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" /> <link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQflAx4QGA4EvmzDAAAA30lEQVRIx2NgGAWMCKa8JKM4A8Ovt88ekyLCDGOoyDBJMjExMbFy8zF8/EKsCAMDE8yAPyIwFps48SJIBpAL4AZwvoSx/r0lXgQpDN58EWL5x/7/H+vL20+JFxluQKVe5b3Ke5V+0kQQCamfoYKBg4GDwUKI8d0BYkWQkrLKewYBKPPDHUFiRaiZkBgmwhj/F5IgggyUJ6i8V3mv0kCayDAAeEsklXqGAgYGhgV3CnGrwVciYSYk0kokhgS44/JxqqFpiYSZbEgskd4dEBRk1GD4wdB5twKXmlHAwMDAAACdEZau06NQUwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wNy0xNVQxNTo1Mzo0MCswMDowMCVXsDIAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDctMTVUMTU6NTM6NDArMDA6MDBUCgiOAAAAAElFTkSuQmCC" />
<script type="module" crossorigin src="./assets/index-f8beba53.js"></script> <script type="module" crossorigin src="./assets/index-4d1f45ea.js"></script>
<link rel="stylesheet" href="./assets/index-8612dca6.css"> <link rel="stylesheet" href="./assets/index-8612dca6.css">
</head> </head>
<body> <body>

@ -7,14 +7,11 @@ package web
import ( import (
"context" "context"
"crypto/rand" "crypto/rand"
"embed"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/fs"
"log" "log"
"net/http" "net/http"
"net/http/httputil"
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
@ -31,35 +28,20 @@ import (
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/httpm" "tailscale.com/util/httpm"
"tailscale.com/util/must"
"tailscale.com/version/distro" "tailscale.com/version/distro"
) )
// This contains all files needed to build the frontend assets.
// Because we assign this to the blank identifier, it does not actually embed the files.
// However, this does cause `go mod vendor` to include the files when vendoring the package.
// External packages that use the web client can `go mod vendor`, run `yarn build` to
// build the assets, then those asset bundles will be embedded.
//
//go:embed yarn.lock index.html *.js *.json src/*
var _ embed.FS
//go:embed build/*
var embeddedFS embed.FS
// staticfiles serves static files from the build directory.
var staticfiles http.Handler
// Server is the backend server for a Tailscale web client. // Server is the backend server for a Tailscale web client.
type Server struct { type Server struct {
lc *tailscale.LocalClient lc *tailscale.LocalClient
devMode bool devMode bool
devProxy *httputil.ReverseProxy // only filled when devMode is on
cgiMode bool cgiMode bool
pathPrefix string pathPrefix string
apiHandler http.Handler // csrf-protected api handler
assetsHandler http.Handler // serves frontend assets
apiHandler http.Handler // serves api endpoints; csrf-protected
} }
// ServerOpts contains options for constructing a new Server. // ServerOpts contains options for constructing a new Server.
@ -89,11 +71,7 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
cgiMode: opts.CGIMode, cgiMode: opts.CGIMode,
pathPrefix: opts.PathPrefix, pathPrefix: opts.PathPrefix,
} }
cleanup = func() {} s.assetsHandler, cleanup = assetsHandler(opts.DevMode)
if s.devMode {
cleanup = s.startDevServer()
s.addProxyToDevServer()
}
// Create handler for "/api" requests with CSRF protection. // Create handler for "/api" requests with CSRF protection.
// We don't require secure cookies, since the web client is regularly used // We don't require secure cookies, since the web client is regularly used
@ -107,11 +85,6 @@ func NewServer(ctx context.Context, opts ServerOpts) (s *Server, cleanup func())
return s, cleanup return s, cleanup
} }
func init() {
buildFiles := must.Get(fs.Sub(embeddedFS, "build"))
staticfiles = http.FileServer(http.FS(buildFiles))
}
// ServeHTTP processes all requests for the Tailscale web client. // ServeHTTP processes all requests for the Tailscale web client.
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
@ -151,14 +124,11 @@ func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
// Pass API requests through to the API handler. // Pass API requests through to the API handler.
s.apiHandler.ServeHTTP(w, r) s.apiHandler.ServeHTTP(w, r)
return return
case s.devMode:
// When in dev mode, proxy non-api requests to the Vite dev server.
s.devProxy.ServeHTTP(w, r)
return
default: default:
// Otherwise, serve static files from the embedded filesystem. if !s.devMode {
s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1) s.lc.IncrementCounter(context.Background(), "web_client_page_load", 1)
staticfiles.ServeHTTP(w, r) }
s.assetsHandler.ServeHTTP(w, r)
return return
} }
} }

Loading…
Cancel
Save