github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynqmon/static.go (about) 1 package asynqmon 2 3 import ( 4 "embed" 5 "errors" 6 "html/template" 7 "io/fs" 8 "net/http" 9 "path/filepath" 10 "strings" 11 ) 12 13 // uiAssetsHandler is a http.Handler. 14 // The path to the static file directory and 15 // the path to the index file within that static directory are used to 16 // serve the SPA. 17 type uiAssetsHandler struct { 18 rootPath string 19 contents embed.FS 20 staticDirPath string 21 indexFileName string 22 prometheusAddr string 23 readOnly bool 24 } 25 26 // ServeHTTP inspects the URL path to locate a file within the static dir 27 // on the SPA handler. 28 // If path '/' is requested, it will serve the index file, otherwise it will 29 // serve the file specified by the URL path. 30 func (h *uiAssetsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 31 // Get the absolute path to prevent directory traversal. 32 path, err := filepath.Abs(r.URL.Path) 33 if err != nil { 34 http.Error(w, err.Error(), http.StatusBadRequest) 35 return 36 } 37 38 // Get the path relative to the root path. 39 if !strings.HasPrefix(path, h.rootPath) { 40 http.Error(w, "unexpected path prefix", http.StatusBadRequest) 41 return 42 } 43 path = strings.TrimPrefix(path, h.rootPath) 44 45 if code, err := h.serveFile(w, path); err != nil { 46 http.Error(w, err.Error(), code) 47 return 48 } 49 } 50 51 func (h *uiAssetsHandler) indexFilePath() string { 52 return filepath.Join(h.staticDirPath, h.indexFileName) 53 } 54 55 func (h *uiAssetsHandler) renderIndexFile(w http.ResponseWriter) error { 56 // Note: Replace the default delimiter ("{{") with a custom one 57 // since webpack escapes the '{' character when it compiles the index.html file. 58 // See the "homepage" field in package.json. 59 tmpl, err := template.New(h.indexFileName).Delims("/[[", "]]").ParseFS(h.contents, h.indexFilePath()) 60 if err != nil { 61 return err 62 } 63 data := struct { 64 RootPath string 65 PrometheusAddr string 66 ReadOnly bool 67 }{ 68 RootPath: h.rootPath, 69 PrometheusAddr: h.prometheusAddr, 70 ReadOnly: h.readOnly, 71 } 72 return tmpl.Execute(w, data) 73 } 74 75 // serveFile writes file requested at path and returns http status code and error if any. 76 // If requested path is root, it serves the index file. 77 // Otherwise, it looks for file requiested in the static content filesystem 78 // and serves if a file is found. 79 // If a requested file is not found in the filesystem, it serves the index file to 80 // make sure when user refreshes the page in SPA things still work. 81 func (h *uiAssetsHandler) serveFile(w http.ResponseWriter, path string) (code int, err error) { 82 if path == "/" || path == "" { 83 if err := h.renderIndexFile(w); err != nil { 84 return http.StatusInternalServerError, err 85 } 86 return http.StatusOK, nil 87 } 88 path = filepath.Join(h.staticDirPath, path) 89 bytes, err := h.contents.ReadFile(path) 90 if err != nil { 91 // If path is error (e.g. file not exist, path is a directory), serve index file. 92 var pathErr *fs.PathError 93 if errors.As(err, &pathErr) { 94 if err := h.renderIndexFile(w); err != nil { 95 return http.StatusInternalServerError, err 96 } 97 return http.StatusOK, nil 98 } 99 return http.StatusInternalServerError, err 100 } 101 // Setting the MIME type for .js files manually to application/javascript as 102 // http.DetectContentType is using https://mimesniff.spec.whatwg.org/ which 103 // will not recognize application/javascript for security reasons. 104 if strings.HasSuffix(path, ".js") { 105 w.Header().Add("Content-Type", "application/javascript; charset=utf-8") 106 } else { 107 w.Header().Add("Content-Type", http.DetectContentType(bytes)) 108 } 109 110 if _, err := w.Write(bytes); err != nil { 111 return http.StatusInternalServerError, err 112 } 113 return http.StatusOK, nil 114 }