forked from tangled.org/core
Monorepo for Tangled

appview,knotserver: use ETag based caching for blobs

this avoids stale raw content from being sent to clients.

Signed-off-by: oppiliappan <me@oppi.li>

authored by oppi.li and committed by Tangled 6be6a26b c4d3a083

Changed files
+34 -9
appview
repo
knotserver
+22 -2
appview/repo/repo.go
··· 734 734 if !rp.config.Core.Dev { 735 735 protocol = "https" 736 736 } 737 + 737 738 blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) 738 - resp, err := http.Get(blobURL) 739 + 740 + req, err := http.NewRequest("GET", blobURL, nil) 739 741 if err != nil { 740 - log.Println("failed to reach knotserver:", err) 742 + log.Println("failed to create request", err) 743 + return 744 + } 745 + 746 + // forward the If-None-Match header 747 + if clientETag := r.Header.Get("If-None-Match"); clientETag != "" { 748 + req.Header.Set("If-None-Match", clientETag) 749 + } 750 + 751 + client := &http.Client{} 752 + resp, err := client.Do(req) 753 + if err != nil { 754 + log.Println("failed to reach knotserver", err) 741 755 rp.pages.Error503(w) 742 756 return 743 757 } 744 758 defer resp.Body.Close() 759 + 760 + // forward 304 not modified 761 + if resp.StatusCode == http.StatusNotModified { 762 + w.WriteHeader(http.StatusNotModified) 763 + return 764 + } 745 765 746 766 if resp.StatusCode != http.StatusOK { 747 767 log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode)
+12 -7
knotserver/routes.go
··· 286 286 mimeType = "image/svg+xml" 287 287 } 288 288 289 + contentHash := sha256.Sum256(contents) 290 + eTag := fmt.Sprintf("\"%x\"", contentHash) 291 + 289 292 // allow image, video, and text/plain files to be served directly 290 293 switch { 291 - case strings.HasPrefix(mimeType, "image/"): 292 - // allowed 293 - case strings.HasPrefix(mimeType, "video/"): 294 - // allowed 294 + case strings.HasPrefix(mimeType, "image/"), strings.HasPrefix(mimeType, "video/"): 295 + if clientETag := r.Header.Get("If-None-Match"); clientETag == eTag { 296 + w.WriteHeader(http.StatusNotModified) 297 + return 298 + } 299 + w.Header().Set("ETag", eTag) 300 + 295 301 case strings.HasPrefix(mimeType, "text/plain"): 296 - // allowed 302 + w.Header().Set("Cache-Control", "public, no-cache") 303 + 297 304 default: 298 305 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 299 306 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 300 307 return 301 308 } 302 309 303 - w.Header().Set("Cache-Control", "public, max-age=86400") // cache for 24 hours 304 - w.Header().Set("ETag", fmt.Sprintf("%x", sha256.Sum256(contents))) 305 310 w.Header().Set("Content-Type", mimeType) 306 311 w.Write(contents) 307 312 }