2023-08-10 18:58:59 +01:00
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
package web
|
|
|
|
|
|
|
|
import (
|
2023-12-07 19:02:08 +00:00
|
|
|
"io"
|
2023-11-14 23:00:46 +00:00
|
|
|
"io/fs"
|
2023-08-10 18:58:59 +01:00
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httputil"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
2023-12-07 19:02:08 +00:00
|
|
|
"time"
|
2023-09-08 20:30:07 +01:00
|
|
|
|
2023-09-19 17:46:06 +01:00
|
|
|
prebuilt "github.com/tailscale/web-client-prebuilt"
|
2023-08-10 18:58:59 +01:00
|
|
|
)
|
|
|
|
|
2023-12-07 19:02:08 +00:00
|
|
|
var start = time.Now()
|
|
|
|
|
2023-09-08 20:30:07 +01:00
|
|
|
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
|
|
|
|
}
|
2023-11-14 23:00:46 +00:00
|
|
|
|
|
|
|
fsys := prebuilt.FS()
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2023-12-07 19:02:08 +00:00
|
|
|
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
|
2023-11-14 23:00:46 +00:00
|
|
|
// the frontend router handle it.
|
|
|
|
r = r.Clone(r.Context())
|
2023-12-07 19:02:08 +00:00
|
|
|
path = "index.html"
|
|
|
|
f, err = openPrecompressedFile(w, r, path, fsys)
|
|
|
|
}
|
|
|
|
if f == nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusNotFound)
|
|
|
|
return
|
2023-11-14 23:00:46 +00:00
|
|
|
}
|
2023-12-07 19:02:08 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-12-12 20:37:24 +00:00
|
|
|
if strings.HasPrefix(path, "assets/") {
|
|
|
|
// 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")
|
|
|
|
}
|
2023-12-07 19:02:08 +00:00
|
|
|
|
|
|
|
http.ServeContent(w, r, path, start, fSeeker)
|
2023-11-14 23:00:46 +00:00
|
|
|
}), nil
|
2023-09-08 20:30:07 +01:00
|
|
|
}
|
|
|
|
|
2023-12-07 19:02:08 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-08-10 18:58:59 +01:00
|
|
|
// startDevServer starts the JS dev server that does on-demand rebuilding
|
|
|
|
// and serving of web client JS and CSS resources.
|
2023-09-08 20:30:07 +01:00
|
|
|
func startDevServer() (cleanup func()) {
|
2023-08-10 18:58:59 +01:00
|
|
|
root := gitRootDir()
|
|
|
|
webClientPath := filepath.Join(root, "client", "web")
|
|
|
|
|
|
|
|
yarn := filepath.Join(root, "tool", "yarn")
|
|
|
|
node := filepath.Join(root, "tool", "node")
|
|
|
|
vite := filepath.Join(webClientPath, "node_modules", ".bin", "vite")
|
|
|
|
|
2023-11-04 00:19:20 +00:00
|
|
|
log.Printf("installing JavaScript deps using %s...", yarn)
|
2023-08-10 18:58:59 +01:00
|
|
|
out, err := exec.Command(yarn, "--non-interactive", "-s", "--cwd", webClientPath, "install").CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("error running tailscale web's yarn install: %v, %s", err, out)
|
|
|
|
}
|
|
|
|
log.Printf("starting JavaScript dev server...")
|
|
|
|
cmd := exec.Command(node, vite)
|
|
|
|
cmd.Dir = webClientPath
|
|
|
|
cmd.Stdout = os.Stdout
|
|
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
|
|
log.Fatalf("Starting JS dev server: %v", err)
|
|
|
|
}
|
|
|
|
log.Printf("JavaScript dev server running as pid %d", cmd.Process.Pid)
|
|
|
|
return func() {
|
|
|
|
cmd.Process.Signal(os.Interrupt)
|
|
|
|
err := cmd.Wait()
|
|
|
|
log.Printf("JavaScript dev server exited: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-08 20:30:07 +01:00
|
|
|
// devServerProxy returns a reverse proxy to the vite dev server.
|
|
|
|
func devServerProxy() *httputil.ReverseProxy {
|
2023-08-10 18:58:59 +01:00
|
|
|
// We use Vite to develop on the web client.
|
|
|
|
// Vite starts up its own local server for development,
|
|
|
|
// which we proxy requests to from Server.ServeHTTP.
|
|
|
|
// Here we set up the proxy to Vite's server.
|
|
|
|
handleErr := func(w http.ResponseWriter, r *http.Request, err error) {
|
|
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
|
|
w.WriteHeader(http.StatusBadGateway)
|
|
|
|
w.Write([]byte("The web client development server isn't running. " +
|
|
|
|
"Run `./tool/yarn --cwd client/web start` from " +
|
|
|
|
"the repo root to start the development server."))
|
|
|
|
w.Write([]byte("\n\nError: " + err.Error()))
|
|
|
|
}
|
|
|
|
viteTarget, _ := url.Parse("http://127.0.0.1:4000")
|
2023-09-08 20:30:07 +01:00
|
|
|
devProxy := httputil.NewSingleHostReverseProxy(viteTarget)
|
|
|
|
devProxy.ErrorHandler = handleErr
|
|
|
|
return devProxy
|
2023-08-10 18:58:59 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func gitRootDir() string {
|
|
|
|
top, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("failed to find git top level (not in corp git?): %v", err)
|
|
|
|
}
|
|
|
|
return strings.TrimSpace(string(top))
|
|
|
|
}
|