mirror of
https://github.com/axllent/mailpit.git
synced 2025-08-13 20:04:49 +02:00
Chore: Replace http.FileServer with custom controller to correctly encode gzipped error responses for embed.FS
Go v1.23 removes the Content-Encoding header from error responses, breaking pages such as 404's while using gzip compression middleware.
This commit is contained in:
74
server/embed.go
Normal file
74
server/embed.go
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
@@ -4,10 +4,8 @@ package server
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"embed"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -31,9 +29,6 @@ import (
|
|||||||
"github.com/lithammer/shortuuid/v4"
|
"github.com/lithammer/shortuuid/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed ui
|
|
||||||
var embeddedFS embed.FS
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// AccessControlAllowOrigin CORS policy
|
// AccessControlAllowOrigin CORS policy
|
||||||
AccessControlAllowOrigin string
|
AccessControlAllowOrigin string
|
||||||
@@ -48,12 +43,6 @@ func Listen() {
|
|||||||
isReady.Store(false)
|
isReady.Store(false)
|
||||||
stats.Track()
|
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()
|
websockets.MessageHub = websockets.NewHub()
|
||||||
|
|
||||||
go websockets.MessageHub.Run()
|
go websockets.MessageHub.Run()
|
||||||
@@ -70,12 +59,12 @@ func Listen() {
|
|||||||
r.HandleFunc(config.Webroot+"proxy", middleWareFunc(handlers.ProxyHandler)).Methods("GET")
|
r.HandleFunc(config.Webroot+"proxy", middleWareFunc(handlers.ProxyHandler)).Methods("GET")
|
||||||
|
|
||||||
// virtual filesystem for /dist/ & some individual files
|
// 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 + "dist/").Handler(middleWareFunc(embedController))
|
||||||
r.PathPrefix(config.Webroot + "api/").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot)))))
|
r.PathPrefix(config.Webroot + "api/").Handler(middleWareFunc(embedController))
|
||||||
r.Path(config.Webroot + "favicon.ico").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot)))))
|
r.Path(config.Webroot + "favicon.ico").Handler(middleWareFunc(embedController))
|
||||||
r.Path(config.Webroot + "favicon.svg").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot)))))
|
r.Path(config.Webroot + "favicon.svg").Handler(middleWareFunc(embedController))
|
||||||
r.Path(config.Webroot + "mailpit.svg").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot)))))
|
r.Path(config.Webroot + "mailpit.svg").Handler(middleWareFunc(embedController))
|
||||||
r.Path(config.Webroot + "notification.png").Handler(middlewareHandler(http.StripPrefix(config.Webroot, http.FileServer(http.FS(serverRoot)))))
|
r.Path(config.Webroot + "notification.png").Handler(middleWareFunc(embedController))
|
||||||
|
|
||||||
// redirect to webroot if no trailing slash
|
// redirect to webroot if no trailing slash
|
||||||
if config.Webroot != "/" {
|
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
|
// Redirect to webroot
|
||||||
func addSlashToWebroot(w http.ResponseWriter, r *http.Request) {
|
func addSlashToWebroot(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, config.Webroot, http.StatusFound)
|
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
|
// Wrapper to artificially inject a basePath to the swagger.json if a webroot has been specified
|
||||||
func swaggerBasePath(w http.ResponseWriter, _ *http.Request) {
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -363,7 +314,7 @@ func index(w http.ResponseWriter, r *http.Request) {
|
|||||||
<body class="h-100">
|
<body class="h-100">
|
||||||
<div class="container-fluid h-100 d-flex flex-column" id="app" data-webroot="{{ .Webroot }}" data-version="{{ .Version }}">
|
<div class="container-fluid h-100 d-flex flex-column" id="app" data-webroot="{{ .Webroot }}" data-version="{{ .Version }}">
|
||||||
<noscript class="alert alert-warning position-absolute top-50 start-50 translate-middle">
|
<noscript class="alert alert-warning position-absolute top-50 start-50 translate-middle">
|
||||||
You need a browser with JavaScript support to use Mailpit
|
You need a browser with JavaScript enabled to use Mailpit
|
||||||
</noscript>
|
</noscript>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -394,6 +345,6 @@ func index(w http.ResponseWriter, r *http.Request) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/html")
|
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||||
_, _ = w.Write(buff.Bytes())
|
_, _ = w.Write(buff.Bytes())
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<meta name="referrer" content="no-referrer">
|
<meta name="referrer" content="no-referrer">
|
||||||
<meta name="robots" content="noindex, nofollow, noarchive">
|
<meta name="robots" content="noindex, nofollow, noarchive">
|
||||||
<link rel="icon" href="../../favicon.svg">
|
<link rel="icon" href="../../favicon.svg">
|
||||||
<script src="../../dist/docs.js"></script>
|
<script src="../../dist/docs.js" nonce="%%NONCE%%"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
Reference in New Issue
Block a user