From e83aa7575b2b18c6cbdb83e3703c33bdcafabc61 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Sat, 16 Aug 2025 22:12:01 +0100 Subject: [PATCH] appview,knotserver: use ETag based caching for blobs Change-Id: pwqwlvnsqtqrtpqxzmvypxqmvtykmsls this avoids stale raw content from being sent to clients. Signed-off-by: oppiliappan --- appview/repo/repo.go | 24 ++++++++++++++++++++++-- knotserver/routes.go | 19 ++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/appview/repo/repo.go b/appview/repo/repo.go index dbb94440..c223db59 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -612,15 +612,35 @@ func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { if !rp.config.Core.Dev { protocol = "https" } + blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) - resp, err := http.Get(blobURL) + + req, err := http.NewRequest("GET", blobURL, nil) + if err != nil { + log.Println("failed to create request", err) + return + } + + // forward the If-None-Match header + if clientETag := r.Header.Get("If-None-Match"); clientETag != "" { + req.Header.Set("If-None-Match", clientETag) + } + + client := &http.Client{} + resp, err := client.Do(req) if err != nil { - log.Println("failed to reach knotserver:", err) + log.Println("failed to reach knotserver", err) rp.pages.Error503(w) return } defer resp.Body.Close() + // forward 304 not modified + if resp.StatusCode == http.StatusNotModified { + w.WriteHeader(http.StatusNotModified) + return + } + if resp.StatusCode != http.StatusOK { log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode) w.WriteHeader(resp.StatusCode) diff --git a/knotserver/routes.go b/knotserver/routes.go index 19ef2c2e..be1cc5bd 100644 --- a/knotserver/routes.go +++ b/knotserver/routes.go @@ -286,22 +286,27 @@ func (h *Handle) BlobRaw(w http.ResponseWriter, r *http.Request) { mimeType = "image/svg+xml" } + contentHash := sha256.Sum256(contents) + eTag := fmt.Sprintf("\"%x\"", contentHash) + // allow image, video, and text/plain files to be served directly switch { - case strings.HasPrefix(mimeType, "image/"): - // allowed - case strings.HasPrefix(mimeType, "video/"): - // allowed + case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): + if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { + w.WriteHeader(http.StatusNotModified) + return + } + w.Header().Set("ETag", eTag) + case strings.HasPrefix(mimeType, "text/plain"): - // allowed + w.Header().Set("Cache-Control", "public, no-cache") + default: l.Error("attempted to serve disallowed file type", "mimetype", mimeType) writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) return } - w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours - w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents))) w.Header().Set("Content-Type", mimeType) w.Write(contents) } -- 2.43.0