From e243593e069196082047f187dc8f6124bec82255 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 23 Oct 2024 02:08:17 +0300 Subject: [PATCH] server: add Cache-Control and ETag headers --- cmd/gomuks/media.go | 11 ++++++++--- cmd/gomuks/server.go | 23 ++++++++++++++++++++++- pkg/hicli/database/media.go | 10 ++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/cmd/gomuks/media.go b/cmd/gomuks/media.go index 409b27a..ff5d481 100644 --- a/cmd/gomuks/media.go +++ b/cmd/gomuks/media.go @@ -57,7 +57,7 @@ var ErrBadGateway = mautrix.RespError{ StatusCode: http.StatusBadGateway, } -func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWriter, entry *database.Media, force bool) bool { +func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWriter, r *http.Request, entry *database.Media, force bool) bool { if !entry.UseCache() { if force { mautrix.MNotFound.WithMessage("Media not found in cache").Write(w) @@ -69,6 +69,9 @@ func (gmx *Gomuks) downloadMediaFromCache(ctx context.Context, w http.ResponseWr w.Header().Set("Mau-Cached-Error", "true") entry.Error.Write(w) return true + } else if r.Header.Get("If-None-Match") == entry.ETag() { + w.WriteHeader(http.StatusNotModified) + return true } log := zerolog.Ctx(ctx) cacheFile, err := os.Open(gmx.cacheEntryToPath(entry.Hash[:])) @@ -102,6 +105,8 @@ func cacheEntryToHeaders(w http.ResponseWriter, entry *database.Media) { w.Header().Set("Content-Length", strconv.FormatInt(entry.Size, 10)) w.Header().Set("Content-Disposition", mime.FormatMediaType(entry.ContentDisposition(), map[string]string{"filename": entry.FileName})) w.Header().Set("Content-Security-Policy", "sandbox; default-src 'none'; script-src 'none';") + w.Header().Set("Cache-Control", "max-age=2592000, immutable") + w.Header().Set("ETag", entry.ETag()) } type noErrorWriter struct { @@ -193,7 +198,7 @@ func (gmx *Gomuks) DownloadMedia(w http.ResponseWriter, r *http.Request) { return } - if gmx.downloadMediaFromCache(ctx, w, cacheEntry, false) { + if gmx.downloadMediaFromCache(ctx, w, r, cacheEntry, false) { return } @@ -319,7 +324,7 @@ func (gmx *Gomuks) DownloadMedia(w http.ResponseWriter, r *http.Request) { return } if w != nil { - gmx.downloadMediaFromCache(ctx, w, cacheEntry, true) + gmx.downloadMediaFromCache(ctx, w, r, cacheEntry, true) } } diff --git a/cmd/gomuks/server.go b/cmd/gomuks/server.go index 7f57ab2..bd41636 100644 --- a/cmd/gomuks/server.go +++ b/cmd/gomuks/server.go @@ -22,6 +22,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "fmt" "io/fs" "net/http" _ "net/http/pprof" @@ -64,7 +65,7 @@ func (gmx *Gomuks) StartServer() { if frontend, err := fs.Sub(web.Frontend, "dist"); err != nil { gmx.Log.Warn().Msg("Frontend not found") } else { - router.Handle("/", http.FileServerFS(frontend)) + router.Handle("/", FrontendCacheMiddleware(http.FileServerFS(frontend))) } gmx.Server = &http.Server{ Addr: gmx.Config.Web.ListenAddress, @@ -79,6 +80,26 @@ func (gmx *Gomuks) StartServer() { gmx.Log.Info().Str("address", gmx.Config.Web.ListenAddress).Msg("Server started") } +func FrontendCacheMiddleware(next http.Handler) http.Handler { + var frontendCacheETag string + if Commit != "unknown" && !ParsedBuildTime.IsZero() { + frontendCacheETag = fmt.Sprintf(`"%s-%s"`, Commit, ParsedBuildTime.Format(time.RFC3339)) + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("If-None-Match") == frontendCacheETag { + w.WriteHeader(http.StatusNotModified) + return + } + if strings.HasPrefix(r.URL.Path, "/assets/") { + w.Header().Set("Cache-Control", "max-age=604800, immutable") + } + if frontendCacheETag != "" { + w.Header().Set("ETag", frontendCacheETag) + } + next.ServeHTTP(w, r) + }) +} + var ( ErrInvalidHeader = mautrix.RespError{ErrCode: "FI.MAU.GOMUKS.INVALID_HEADER", StatusCode: http.StatusForbidden} ErrMissingCookie = mautrix.RespError{ErrCode: "FI.MAU.GOMUKS.MISSING_COOKIE", Err: "Missing gomuks_auth cookie", StatusCode: http.StatusUnauthorized} diff --git a/pkg/hicli/database/media.go b/pkg/hicli/database/media.go index eb2ec5d..d060ca7 100644 --- a/pkg/hicli/database/media.go +++ b/pkg/hicli/database/media.go @@ -9,6 +9,7 @@ package database import ( "context" "database/sql" + "fmt" "net/http" "slices" "time" @@ -93,6 +94,8 @@ func (me *MediaError) Write(w http.ResponseWriter) { } me.Matrix.ExtraData["fi.mau.hicli.error_ts"] = me.ReceivedAt.UnixMilli() me.Matrix.ExtraData["fi.mau.hicli.next_retry_ts"] = me.ReceivedAt.Add(me.backoff()).UnixMilli() + w.Header().Set("Mau-Errored-At", me.ReceivedAt.Format(http.TimeFormat)) + w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d", max(int(time.Until(me.ReceivedAt.Add(me.backoff())).Seconds()), 0))) me.Matrix.WithStatus(me.StatusCode).Write(w) } @@ -106,6 +109,13 @@ type Media struct { Error *MediaError } +func (m *Media) ETag() string { + if m.Hash == nil { + return "" + } + return fmt.Sprintf(`"%x"`, m.Hash) +} + func (m *Media) UseCache() bool { return m != nil && (m.Hash != nil || m.Error.UseCache()) }