1
0

dist.go 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. package controller
  2. import (
  3. "bytes"
  4. "embed"
  5. htmlpkg "html"
  6. "net/http"
  7. "strings"
  8. "time"
  9. "github.com/gin-gonic/gin"
  10. "github.com/mhsanaei/3x-ui/v3/config"
  11. "github.com/mhsanaei/3x-ui/v3/logger"
  12. "github.com/mhsanaei/3x-ui/v3/web/session"
  13. )
  14. var distFS embed.FS
  15. func SetDistFS(fs embed.FS) {
  16. distFS = fs
  17. }
  18. var distPageBuildTime = time.Now()
  19. // ServeOpenAPISpec returns the generated OpenAPI 3.0 description of the
  20. // panel API. Postman / Insomnia / openapi-generator consume this URL
  21. // directly; the in-panel Swagger UI page also fetches it. The spec is
  22. // produced at frontend build time by scripts/build-openapi.mjs and
  23. // embedded into the binary via the dist FS.
  24. func ServeOpenAPISpec(c *gin.Context) {
  25. body, err := distFS.ReadFile("dist/openapi.json")
  26. if err != nil {
  27. c.JSON(http.StatusNotFound, gin.H{"success": false, "msg": "openapi.json not found"})
  28. return
  29. }
  30. c.Header("Cache-Control", "public, max-age=300")
  31. c.Data(http.StatusOK, "application/json; charset=utf-8", body)
  32. }
  33. func serveDistPage(c *gin.Context, name string) {
  34. body, err := distFS.ReadFile("dist/" + name)
  35. if err != nil {
  36. c.String(http.StatusInternalServerError, "missing embedded page: %s", name)
  37. return
  38. }
  39. basePath := c.GetString("base_path")
  40. if basePath == "" {
  41. basePath = "/"
  42. }
  43. if basePath != "/" {
  44. body = bytes.ReplaceAll(body, []byte(`src="/assets/`), []byte(`src="`+basePath+`assets/`))
  45. body = bytes.ReplaceAll(body, []byte(`href="/assets/`), []byte(`href="`+basePath+`assets/`))
  46. }
  47. jsEscape := strings.NewReplacer(
  48. `\`, `\\`,
  49. `"`, `\"`,
  50. "\n", `\n`,
  51. "\r", `\r`,
  52. "<", `<`,
  53. ">", `>`,
  54. "&", `&`,
  55. )
  56. escapedBase := jsEscape.Replace(basePath)
  57. csrfToken, err := session.EnsureCSRFToken(c)
  58. if err != nil {
  59. logger.Warning("Unable to mint CSRF token for", name+":", err)
  60. csrfToken = ""
  61. }
  62. csrfMeta := []byte(`<meta name="csrf-token" content="` + htmlpkg.EscapeString(csrfToken) + `">`)
  63. basePathMeta := []byte(`<meta name="base-path" content="` + htmlpkg.EscapeString(basePath) + `">`)
  64. nonceAttr := ""
  65. if nonce := c.GetString("csp_nonce"); nonce != "" {
  66. nonceAttr = ` nonce="` + htmlpkg.EscapeString(nonce) + `"`
  67. }
  68. script := `<script` + nonceAttr + `>window.X_UI_BASE_PATH="` + escapedBase + `"`
  69. if name != "login.html" {
  70. escapedVer := jsEscape.Replace(config.GetVersion())
  71. script += `;window.X_UI_CUR_VER="` + escapedVer + `"`
  72. }
  73. script += `;</script>`
  74. inject := []byte(script)
  75. inject = append(inject, csrfMeta...)
  76. inject = append(inject, basePathMeta...)
  77. inject = append(inject, []byte(`</head>`)...)
  78. out := bytes.Replace(body, []byte("</head>"), inject, 1)
  79. c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
  80. c.Header("Pragma", "no-cache")
  81. c.Header("Expires", "0")
  82. c.Header("Last-Modified", distPageBuildTime.UTC().Format(http.TimeFormat))
  83. c.Data(http.StatusOK, "text/html; charset=utf-8", out)
  84. }