104 lines
2.5 KiB
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))
|
|
}
|