Scert/backend/internal/httpapi/frontend.go

104 lines
2.5 KiB
Go

package httpapi
import (
"net/http"
"os"
pathpkg "path"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
// frontend 为浏览器提供前端静态文件,并为 Vue 的 history 路由回退到 index.html。
func (s *Server) frontend(c *gin.Context) {
if c.Request.Method != http.MethodGet && c.Request.Method != http.MethodHead {
c.Status(http.StatusNotFound)
return
}
s.configMu.RLock()
enabled := s.config.Web.Enabled
s.configMu.RUnlock()
if !enabled || strings.TrimSpace(s.frontendDir) == "" {
c.Status(http.StatusNotFound)
return
}
requestPath := pathpkg.Clean("/" + strings.TrimPrefix(c.Request.URL.Path, "/"))
if requestPath == "." || requestPath == "/" {
requestPath = "/"
}
if isCoreRequestPath(requestPath) {
c.Status(http.StatusNotFound)
return
}
if isHiddenFrontendPath(requestPath) {
c.Status(http.StatusNotFound)
return
}
staticDir, err := filepath.Abs(s.frontendDir)
if err != nil {
c.Status(http.StatusNotFound)
return
}
relativePath := strings.TrimPrefix(requestPath, "/")
candidate := filepath.Join(staticDir, filepath.FromSlash(relativePath))
if !isWithinDirectory(staticDir, candidate) {
c.Status(http.StatusNotFound)
return
}
if info, statErr := os.Stat(candidate); statErr == nil && info.Mode().IsRegular() {
serveFrontendFile(c, candidate)
return
}
if requestPath != "/" && filepath.Ext(requestPath) != "" {
c.Status(http.StatusNotFound)
return
}
indexPath := filepath.Join(staticDir, "index.html")
if info, statErr := os.Stat(indexPath); statErr != nil || !info.Mode().IsRegular() {
c.Status(http.StatusNotFound)
return
}
serveFrontendFile(c, indexPath)
}
func serveFrontendFile(c *gin.Context, path string) {
c.Header("Cache-Control", "no-cache")
if filepath.Base(path) != "index.html" {
c.Header("Cache-Control", "public, max-age=31536000, immutable")
}
http.ServeFile(c.Writer, c.Request, path)
}
func isCoreRequestPath(path string) bool {
return path == "/api" ||
strings.HasPrefix(path, "/api/") ||
path == "/healthz" ||
path == "/readyz" ||
path == "/reset" ||
strings.HasPrefix(path, "/reset/")
}
func isHiddenFrontendPath(path string) bool {
for _, segment := range strings.Split(strings.Trim(path, "/"), "/") {
if strings.HasPrefix(segment, ".") {
return true
}
}
return false
}
func isWithinDirectory(directory, candidate string) bool {
relative, err := filepath.Rel(directory, candidate)
if err != nil {
return false
}
return relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
}