Files
gpt-load/internal/webui/server.go
T
tbphp 425838f824 fix(web): 收
紧前端 CSP 样式权限
2026-07-23 09:59:33 +08:00

99 lines
2.6 KiB
Go

// Package webui serves the embedded management UI on explicit page routes.
package webui
import (
"embed"
"io/fs"
"mime"
"net/http"
"path"
"strings"
"github.com/gin-gonic/gin"
)
const (
distRoot = "dist"
indexCSP = "default-src 'self'; script-src 'self'; style-src 'self'; " +
"style-src-elem 'self'; style-src-attr 'unsafe-inline'; " +
"img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; " +
"base-uri 'self'; frame-ancestors 'none'; form-action 'self'"
fallbackIndex = `<!doctype html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>GPT-Load</title></head>
<body><main><h1>GPT-Load</h1><p>前端资源尚未构建,请运行 make build。</p></main></body>
</html>`
)
var pageRoutes = []string{
"/",
"/login",
"/import",
"/groups/:id",
"/access-keys",
"/monitor",
"/settings",
}
//go:embed all:dist
var embeddedFiles embed.FS
// Server serves immutable assets and the SPA index for known UI routes.
type Server struct {
files fs.FS
root string
index []byte
}
// NewServer creates an embedded UI server.
func NewServer() *Server {
return newServer(embeddedFiles, distRoot)
}
func newServer(files fs.FS, root string) *Server {
index, err := fs.ReadFile(files, path.Join(root, "index.html"))
if err != nil {
index = []byte(fallbackIndex)
}
return &Server{files: files, root: root, index: index}
}
// RegisterRoutes registers only the documented management UI paths.
func (s *Server) RegisterRoutes(engine *gin.Engine) {
for _, route := range pageRoutes {
engine.GET(route, s.serveIndex)
}
engine.GET("/assets/*filepath", s.serveAsset)
}
func (s *Server) serveIndex(c *gin.Context) {
c.Header("Cache-Control", "no-cache")
c.Header("Content-Security-Policy", indexCSP)
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "DENY")
c.Data(http.StatusOK, "text/html; charset=utf-8", s.index)
}
func (s *Server) serveAsset(c *gin.Context) {
assetPath := path.Clean(strings.TrimPrefix(c.Param("filepath"), "/"))
if assetPath == "." || assetPath == "" || strings.HasPrefix(assetPath, "../") {
c.Status(http.StatusNotFound)
return
}
content, err := fs.ReadFile(s.files, path.Join(s.root, "assets", assetPath))
if err != nil {
c.Status(http.StatusNotFound)
return
}
contentType := mime.TypeByExtension(path.Ext(assetPath))
if contentType == "" {
contentType = "application/octet-stream"
}
c.Header("Cache-Control", "public, max-age=31536000, immutable")
c.Header("X-Content-Type-Options", "nosniff")
c.Data(http.StatusOK, contentType, content)
}