diff --git a/client/web/assets.go b/client/web/assets.go index ccef6a0e1..0f92d93d9 100644 --- a/client/web/assets.go +++ b/client/web/assets.go @@ -4,6 +4,7 @@ package web import ( + "io" "io/fs" "log" "net/http" @@ -13,10 +14,13 @@ import ( "os/exec" "path/filepath" "strings" + "time" prebuilt "github.com/tailscale/web-client-prebuilt" ) +var start = time.Now() + func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) { if devMode { // When in dev mode, proxy asset requests to the Vite dev server. @@ -25,19 +29,46 @@ func assetsHandler(devMode bool) (_ http.Handler, cleanup func()) { } fsys := prebuilt.FS() - fileserver := http.FileServer(http.FS(fsys)) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, err := fs.Stat(fsys, strings.TrimPrefix(r.URL.Path, "/")) - if os.IsNotExist(err) { - // rewrite request to just fetch /index.html and let + path := strings.TrimPrefix(r.URL.Path, "/") + f, err := openPrecompressedFile(w, r, path, fsys) + if err != nil { + // Rewrite request to just fetch index.html and let // the frontend router handle it. r = r.Clone(r.Context()) - r.URL.Path = "/" + path = "index.html" + f, err = openPrecompressedFile(w, r, path, fsys) + } + if f == nil { + http.Error(w, err.Error(), http.StatusNotFound) + return } - fileserver.ServeHTTP(w, r) + defer f.Close() + + // fs.File does not claim to implement Seeker, but in practice it does. + fSeeker, ok := f.(io.ReadSeeker) + if !ok { + http.Error(w, "Not seekable", http.StatusInternalServerError) + return + } + + // Aggressively cache static assets, since we cache-bust our assets with + // hashed filenames. + w.Header().Set("Cache-Control", "public, max-age=31535996") + w.Header().Set("Vary", "Accept-Encoding") + + http.ServeContent(w, r, path, start, fSeeker) }), nil } +func openPrecompressedFile(w http.ResponseWriter, r *http.Request, path string, fs fs.FS) (fs.File, error) { + if f, err := fs.Open(path + ".gz"); err == nil { + w.Header().Set("Content-Encoding", "gzip") + return f, nil + } + return fs.Open(path) // fallback +} + // startDevServer starts the JS dev server that does on-demand rebuilding // and serving of web client JS and CSS resources. func startDevServer() (cleanup func()) { diff --git a/client/web/build/index.html b/client/web/build/index.html index c0d39ba94..0af7ea24c 100644 --- a/client/web/build/index.html +++ b/client/web/build/index.html @@ -6,10 +6,11 @@ - - + + + - +