diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9af04e6..545b951 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,7 +48,12 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.23.x" + go-version: "1.23.1" + + - name: patch net/http/fs.go + run: | + patch -p0 --forward `go env GOROOT`/src/net/http/fs.go < ./script/patch/fs.patch || true + patch -p0 --forward `go env GOMODCACHE`/github.com/gin-gonic/gin@v1.10.0/context.go < ./script/patch/gin-context.patch || true - name: generate swagger docs run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68e3733..268a0d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,12 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: "1.23.x" + go-version: "1.23.1" + + - name: patch net/http/fs.go + run: | + patch -p0 --forward `go env GOROOT`/src/net/http/fs.go < ./script/patch/fs.patch || true + patch -p0 --forward `go env GOMODCACHE`/github.com/gin-gonic/gin@v1.10.0/context.go < ./script/patch/gin-context.patch || true - name: generate swagger docs run: | diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 1e9c054..de27146 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -21,6 +21,7 @@ import ( "github.com/nezhahq/nezha/cmd/dashboard/controller/waf" docs "github.com/nezhahq/nezha/cmd/dashboard/docs" "github.com/nezhahq/nezha/model" + "github.com/nezhahq/nezha/pkg/utils" "github.com/nezhahq/nezha/service/singleton" ) @@ -281,9 +282,9 @@ func getUid(c *gin.Context) uint64 { } func fallbackToFrontend(frontendDist fs.FS) func(*gin.Context) { - checkLocalFileOrFs := func(c *gin.Context, fs fs.FS, path string) bool { + checkLocalFileOrFs := func(c *gin.Context, fs fs.FS, path string, customStatusCode int) bool { if _, err := os.Stat(path); err == nil { - c.File(path) + c.FileWithCustomStatusCode(path, customStatusCode) return true } f, err := fs.Open(path) @@ -298,7 +299,7 @@ func fallbackToFrontend(frontendDist fs.FS) func(*gin.Context) { if fileStat.IsDir() { return false } - http.ServeContent(c.Writer, c.Request, path, fileStat.ModTime(), f.(io.ReadSeeker)) + http.ServeContentCustomStatusCode(c.Writer, c.Request, path, fileStat.ModTime(), f.(io.ReadSeeker), customStatusCode) return true } return func(c *gin.Context) { @@ -309,29 +310,21 @@ func fallbackToFrontend(frontendDist fs.FS) func(*gin.Context) { if strings.HasPrefix(c.Request.URL.Path, "/dashboard") { stripPath := strings.TrimPrefix(c.Request.URL.Path, "/dashboard") localFilePath := path.Join(singleton.Conf.AdminTemplate, stripPath) - if stripPath == "/" { - c.Status(http.StatusOK) - } - if checkLocalFileOrFs(c, frontendDist, localFilePath) { + statusCode := utils.IfOr(stripPath == "/", http.StatusOK, http.StatusNotFound) + if checkLocalFileOrFs(c, frontendDist, localFilePath, http.StatusOK) { return - } else { - c.Status(http.StatusNotFound) } - if !checkLocalFileOrFs(c, frontendDist, singleton.Conf.AdminTemplate+"/index.html") { + if !checkLocalFileOrFs(c, frontendDist, singleton.Conf.AdminTemplate+"/index.html", statusCode) { c.JSON(http.StatusNotFound, newErrorResponse(errors.New("404 Not Found"))) } return } localFilePath := path.Join(singleton.Conf.UserTemplate, c.Request.URL.Path) - if c.Request.URL.Path == "/" { - c.Status(http.StatusOK) - } - if checkLocalFileOrFs(c, frontendDist, localFilePath) { + if checkLocalFileOrFs(c, frontendDist, localFilePath, http.StatusOK) { return - } else { - c.Status(http.StatusNotFound) } - if !checkLocalFileOrFs(c, frontendDist, singleton.Conf.UserTemplate+"/index.html") { + statusCode := utils.IfOr(c.Request.URL.Path == "/", http.StatusOK, http.StatusNotFound) + if !checkLocalFileOrFs(c, frontendDist, singleton.Conf.UserTemplate+"/index.html", statusCode) { c.JSON(http.StatusNotFound, newErrorResponse(errors.New("404 Not Found"))) } } diff --git a/script/patch/fs.patch b/script/patch/fs.patch new file mode 100644 index 0000000..f379c55 --- /dev/null +++ b/script/patch/fs.patch @@ -0,0 +1,102 @@ +@@ -249,9 +249,24 @@ + } + return size, nil + } +- serveContent(w, req, name, modtime, sizeFunc, content) ++ serveContent(w, req, name, modtime, sizeFunc, content, StatusOK) + } + ++func ServeContentCustomStatusCode(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker, code int) { ++ sizeFunc := func() (int64, error) { ++ size, err := content.Seek(0, io.SeekEnd) ++ if err != nil { ++ return 0, errSeeker ++ } ++ _, err = content.Seek(0, io.SeekStart) ++ if err != nil { ++ return 0, errSeeker ++ } ++ return size, nil ++ } ++ serveContent(w, req, name, modtime, sizeFunc, content, code) ++} ++ + // errSeeker is returned by ServeContent's sizeFunc when the content + // doesn't seek properly. The underlying Seeker's error text isn't + // included in the sizeFunc reply so it's not sent over HTTP to end +@@ -266,15 +281,13 @@ + // if modtime.IsZero(), modtime is unknown. + // content must be seeked to the beginning of the file. + // The sizeFunc is called at most once. Its error, if any, is sent in the HTTP response. +-func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker) { ++func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time, sizeFunc func() (int64, error), content io.ReadSeeker, code int) { + setLastModified(w, modtime) + done, rangeReq := checkPreconditions(w, r, modtime) + if done { + return + } + +- code := StatusOK +- + // If Content-Type isn't set, use the file's extension to find it, but + // if the Content-Type is unset explicitly, do not sniff the type. + ctypes, haveType := w.Header()["Content-Type"] +@@ -671,7 +684,7 @@ + } + + // name is '/'-separated, not filepath.Separator. +-func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool) { ++func serveFile(w ResponseWriter, r *Request, fs FileSystem, name string, redirect bool, statusCode int) { + const indexPage = "/index.html" + + // redirect .../index.html to .../ +@@ -753,7 +766,7 @@ + + // serveContent will check modification time + sizeFunc := func() (int64, error) { return d.Size(), nil } +- serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f) ++ serveContent(w, r, d.Name(), d.ModTime(), sizeFunc, f, statusCode) + } + + // toHTTPError returns a non-specific HTTP error message and status code +@@ -814,7 +827,21 @@ + return + } + dir, file := filepath.Split(name) +- serveFile(w, r, Dir(dir), file, false) ++ serveFile(w, r, Dir(dir), file, false, StatusOK) ++} ++ ++func ServeFileWithCustomStatusCode(w ResponseWriter, r *Request, name string, statusCode int) { ++ if containsDotDot(r.URL.Path) { ++ // Too many programs use r.URL.Path to construct the argument to ++ // serveFile. Reject the request under the assumption that happened ++ // here and ".." may not be wanted. ++ // Note that name might not contain "..", for example if code (still ++ // incorrectly) used filepath.Join(myDir, r.URL.Path). ++ serveError(w, "invalid URL path", StatusBadRequest) ++ return ++ } ++ dir, file := filepath.Split(name) ++ serveFile(w, r, Dir(dir), file, false, statusCode) + } + + // ServeFileFS replies to the request with the contents +@@ -847,7 +874,7 @@ + serveError(w, "invalid URL path", StatusBadRequest) + return + } +- serveFile(w, r, FS(fsys), name, false) ++ serveFile(w, r, FS(fsys), name, false, StatusOK) + } + + func containsDotDot(v string) bool { +@@ -983,7 +1010,7 @@ + upath = "/" + upath + r.URL.Path = upath + } +- serveFile(w, r, f.root, path.Clean(upath), true) ++ serveFile(w, r, f.root, path.Clean(upath), true, StatusOK) + } + + // httpRange specifies the byte range to be sent to the client. diff --git a/script/patch/gin-context.patch b/script/patch/gin-context.patch new file mode 100644 index 0000000..866c414 --- /dev/null +++ b/script/patch/gin-context.patch @@ -0,0 +1,11 @@ +@@ -1073,6 +1073,10 @@ + // File writes the specified file into the body stream in an efficient way. + func (c *Context) File(filepath string) { + http.ServeFile(c.Writer, c.Request, filepath) ++} ++ ++func (c *Context) FileWithCustomStatusCode(filepath string, statusCode int) { ++ http.ServeFileWithCustomStatusCode(c.Writer, c.Request, filepath, statusCode) + } + + // FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way.