diff options
Diffstat (limited to 'cmd/mirum-server/static.go')
| -rw-r--r-- | cmd/mirum-server/static.go | 235 | +0 −235 |
1 files changed, 0 insertions, 235 deletions
diff --git a/cmd/mirum-server/static.go b/cmd/mirum-server/static.go deleted file mode 100644 --- a/cmd/mirum-server/static.go +++ /dev/null @@ -1,235 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Nikolay Govorov -// SPDX-License-Identifier: AGPL-3.0-or-later - -package main - -import ( - "crypto/sha512" - "embed" - "encoding/base64" - "encoding/json" - "fmt" - "html/template" - "io/fs" - "log/slog" - "net/http" - "path" - "strings" -) - -//go:embed web/*.html -var templateFS embed.FS - -var shellTmpl = template.Must(template.ParseFS(templateFS, "web/shell.html")) - -type assetRef struct { - Href string - Integrity string // "sha384-BASE64" or empty -} - -type pageAssets struct { - CSS []assetRef // <link rel="stylesheet"> - Scripts []assetRef // <script type="module"> - Preloads []assetRef // <link rel="modulepreload"> - Preamble string // inline <script type="module"> body, dev only - - // Full Content-Security-Policy header for this entry, with hash-based script-src. - // Empty in dev mode; renderPage falls back to the global csp in that case. - CSP string -} - -func buildCSP(scripts, preloads, css []assetRef) string { - var sb strings.Builder - sb.WriteString(cspBase) - - writeHashes := func(refs []assetRef) { - for _, r := range refs { - if r.Integrity == "" { - continue - } - sb.WriteString(" '") - sb.WriteString(r.Integrity) - sb.WriteString("'") - } - } - - sb.WriteString("; script-src") - writeHashes(scripts) - writeHashes(preloads) - - // style-src hash-sources for external stylesheets are inconsistently - // supported (Firefox blocks them even when the hash matches). Allow - // same-origin loading and rely on the integrity="" attribute rendered - // into the <link> tag for tamper detection. - sb.WriteString("; style-src 'self'") - writeHashes(css) - - return sb.String() -} - -type assetResolver struct { - assets map[string]pageAssets // keyed by entry name (e.g. "dashboard") -} - -func newAssetResolver() *assetResolver { - if viteDevURL != "" { - return &assetResolver{} - } - - if len(manifestJSON) == 0 { - slog.Warn("vite manifest not found, frontend assets unavailable") - return &assetResolver{assets: map[string]pageAssets{}} - } - - type viteManifestEntry struct { - Name string `json:"name"` - File string `json:"file"` - Src string `json:"src"` - CSS []string `json:"css"` - Imports []string `json:"imports"` - IsEntry bool `json:"isEntry"` - } - - var manifest map[string]viteManifestEntry - if err := json.Unmarshal(manifestJSON, &manifest); err != nil { - slog.Error("failed to parse vite manifest", "err", err) - return &assetResolver{assets: map[string]pageAssets{}} - } - - refOf := func(file string) assetRef { - integrity := "" - - data, err := assetsFS.ReadFile("static/" + file) - if err != nil { - slog.Error("asset not found for integrity hash", "file", file, "err", err) - } else { - sum := sha512.Sum384(data) - integrity = "sha384-" + base64.StdEncoding.EncodeToString(sum[:]) - } - - return assetRef{Href: "/" + file, Integrity: integrity} - } - - assets := make(map[string]pageAssets) - for key, entry := range manifest { - if !entry.IsEntry { - continue - } - - // Entry key is e.g. "entries/dashboard.tsx", derive "dashboard". - name := strings.TrimSuffix(path.Base(key), path.Ext(key)) - - seen := make(map[string]bool) - var scripts, preloads, css []assetRef - - var walk func(keys []string) - walk = func(keys []string) { - for _, k := range keys { - if seen[k] { - continue - } - seen[k] = true - chunk, ok := manifest[k] - if !ok { - continue - } - if chunk.IsEntry { - scripts = append(scripts, refOf(chunk.File)) - } else { - preloads = append(preloads, refOf(chunk.File)) - } - for _, c := range chunk.CSS { - if seen[c] { - continue - } - seen[c] = true - css = append(css, refOf(c)) - } - walk(chunk.Imports) - } - } - walk([]string{key}) - - assets[name] = pageAssets{ - Scripts: scripts, - Preloads: preloads, - CSS: css, - CSP: buildCSP(scripts, preloads, css), - } - } - - return &assetResolver{assets: assets} -} - -// resolve returns page assets for the given entry name. Integrity is omitted -// in dev mode because Vite's HMR mutates file contents between requests. -func (ar *assetResolver) resolve(name string) pageAssets { - if viteDevURL != "" { - return pageAssets{ - Scripts: []assetRef{ - {Href: viteDevURL + "/@vite/client"}, - {Href: viteDevURL + "/entries/" + name + ".tsx"}, - }, - // @vitejs/plugin-react requires this preamble to install the - // React Refresh hooks before any component module runs. Vite - // auto-injects it when it serves HTML, so we reproduce it here. - Preamble: `import RefreshRuntime from "` + viteDevURL + `/@react-refresh" -RefreshRuntime.injectIntoGlobalHook(window) -window.$RefreshReg$ = () => {} -window.$RefreshSig$ = () => (type) => type -window.__vite_plugin_react_preamble_installed__ = true`, - } - } - return ar.assets[name] -} - -func (ar *assetResolver) renderPage(w http.ResponseWriter, entry string, status int, data any) { - dataJSON, err := json.Marshal(data) - if err != nil { - http.Error(w, "internal error", http.StatusInternalServerError) - return - } - - assets := ar.resolve(entry) - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.Header().Set("Cache-Control", "no-cache") - if assets.CSP != "" { - w.Header().Set("Content-Security-Policy", assets.CSP) - } - if status != 0 && status != http.StatusOK { - w.WriteHeader(status) - } - - if err := shellTmpl.Execute(w, struct { - DataJSON template.JS - Scripts []assetRef - Preloads []assetRef - CSS []assetRef - Preamble template.JS - }{ - DataJSON: template.JS(dataJSON), - Scripts: assets.Scripts, - Preloads: assets.Preloads, - CSS: assets.CSS, - Preamble: template.JS(assets.Preamble), - }); err != nil { - http.Error(w, "internal error", http.StatusInternalServerError) - } -} - -func assetsHandler() http.Handler { - sub, err := fs.Sub(assetsFS, "static") - if err != nil { - panic(fmt.Sprintf("static fs: %v", err)) - } - - fileServer := http.FileServer(http.FS(sub)) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.HasSuffix(r.URL.Path, "/") { - http.NotFound(w, r) - return - } - fileServer.ServeHTTP(w, r) - }) -} |
