diff --git a/server/embed.go b/server/embed.go new file mode 100644 index 0000000..9777e25 --- /dev/null +++ b/server/embed.go @@ -0,0 +1,74 @@ +package server + +import ( + "embed" + "net/http" + "path" + "strings" + + "github.com/axllent/mailpit/config" +) + +var ( + //go:embed ui + distFS embed.FS +) + +// EmbedController is a simple controller to return a file from the embedded filesystem. +// +// This controller is replaces Go's default http.FileServer which, as of Go v1.23, removes +// the Content-Encoding header from error responses, breaking pages such as 404's while +// using gzip compression middleware. +func embedController(w http.ResponseWriter, r *http.Request) { + p := r.URL.Path + + if strings.HasSuffix(p, "/") { + p = p + "index.html" + } + + p = strings.TrimLeft(p, config.Webroot) // server webroot config + p = path.Join("ui", p) // add go:embed path to path prefix + + b, err := distFS.ReadFile(p) + if err != nil { + http.Error(w, "File not found", http.StatusNotFound) + return + } + + // ensure any HTML files have the correct nonce + if strings.HasSuffix(p, ".html") { + nonce := r.Header.Get("mp-nonce") + b = []byte(strings.ReplaceAll(string(b), "%%NONCE%%", nonce)) + } + + w.Header().Set("Content-Type", contentType(p)) + _, _ = w.Write(b) +} + +// ContentType supports only a few content types, limited to this application's needs. +func contentType(p string) string { + switch { + case strings.HasSuffix(p, ".html"): + return "text/html; charset=utf-8" + case strings.HasSuffix(p, ".css"): + return "text/css; charset=utf-8" + case strings.HasSuffix(p, ".js"): + return "application/javascript; charset=utf-8" + case strings.HasSuffix(p, ".json"): + return "application/json" + case strings.HasSuffix(p, ".svg"): + return "image/svg+xml" + case strings.HasSuffix(p, ".ico"): + return "image/x-icon" + case strings.HasSuffix(p, ".png"): + return "image/png" + case strings.HasSuffix(p, ".jpg"): + return "image/jpeg" + case strings.HasSuffix(p, ".gif"): + return "image/gif" + case strings.HasSuffix(p, ".woff2"): + return "font/woff2" + default: + return "text/plain" + } +} diff --git a/server/server.go b/server/server.go index 458ea53..889ba49 100644 --- a/server/server.go +++ b/server/server.go @@ -4,10 +4,8 @@ package server import ( "bytes" "compress/gzip" - "embed" "fmt" "io" - "io/fs" "net" "net/http" "os" @@ -31,9 +29,6 @@ import ( "github.com/lithammer/shortuuid/v4" ) -//go:embed ui -var embeddedFS embed.FS - var ( // AccessControlAllowOrigin CORS policy AccessControlAllowOrigin string @@ -48,12 +43,6 @@ func Listen() { isReady.Store(false) stats.Track() - serverRoot, err := fs.Sub(embeddedFS, "ui") - if err != nil { - logger.Log().Errorf("[http] %s", err.Error()) - os.Exit(1) - } - websockets.MessageHub = websockets.NewHub() go websockets.MessageHub.Run() @@ -70,12 +59,12 @@ func Listen() { r.HandleFunc(config.Webroot+"proxy", middleWareFunc(handlers.ProxyHandler)).Methods("GET") // virtual filesystem for /dist/ & some individual files - r.PathPrefix(config.Webroot + "dist/").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot))))) - r.PathPrefix(config.Webroot + "api/").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot))))) - r.Path(config.Webroot + "favicon.ico").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot))))) - r.Path(config.Webroot + "favicon.svg").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot))))) - r.Path(config.Webroot + "mailpit.svg").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot))))) - r.Path(config.Webroot + "notification.png").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot))))) + r.PathPrefix(config.Webroot + "dist/").Handler(middleWareFunc(embedController)) + r.PathPrefix(config.Webroot + "api/").Handler(middleWareFunc(embedController)) + r.Path(config.Webroot + "favicon.ico").Handler(middleWareFunc(embedController)) + r.Path(config.Webroot + "favicon.svg").Handler(middleWareFunc(embedController)) + r.Path(config.Webroot + "mailpit.svg").Handler(middleWareFunc(embedController)) + r.Path(config.Webroot + "notification.png").Handler(middleWareFunc(embedController)) // redirect to webroot if no trailing slash if config.Webroot != "/" { @@ -277,44 +266,6 @@ func middleWareFunc(fn http.HandlerFunc) http.HandlerFunc { } } -// MiddlewareHandler http middleware adds optional basic authentication -// and gzip compression -func middlewareHandler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Referrer-Policy", "no-referrer") - w.Header().Set("Content-Security-Policy", config.ContentSecurityPolicy) - - if AccessControlAllowOrigin != "" && strings.HasPrefix(r.RequestURI, config.Webroot+"api/") { - w.Header().Set("Access-Control-Allow-Origin", AccessControlAllowOrigin) - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "*") - } - - if auth.UICredentials != nil { - user, pass, ok := r.BasicAuth() - - if !ok { - basicAuthResponse(w) - return - } - - if !auth.UICredentials.Match(user, pass) { - basicAuthResponse(w) - return - } - } - - if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { - h.ServeHTTP(w, r) - return - } - w.Header().Set("Content-Encoding", "gzip") - gz := gzip.NewWriter(w) - defer gz.Close() - h.ServeHTTP(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r) - }) -} - // Redirect to webroot func addSlashToWebroot(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, config.Webroot, http.StatusFound) @@ -328,7 +279,7 @@ func apiWebsocket(w http.ResponseWriter, r *http.Request) { // Wrapper to artificially inject a basePath to the swagger.json if a webroot has been specified func swaggerBasePath(w http.ResponseWriter, _ *http.Request) { - f, err := embeddedFS.ReadFile("ui/api/v1/swagger.json") + f, err := distFS.ReadFile("ui/api/v1/swagger.json") if err != nil { panic(err) } @@ -363,7 +314,7 @@ func index(w http.ResponseWriter, r *http.Request) {