forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

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 if !rp.config.Core.Dev { 735 protocol = "https" 736 } 737 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 if err != nil { 740 - log.Println("failed to reach knotserver:", err) 741 rp.pages.Error503(w) 742 return 743 } 744 defer resp.Body.Close() 745 746 if resp.StatusCode != http.StatusOK { 747 log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode)
··· 734 if !rp.config.Core.Dev { 735 protocol = "https" 736 } 737 + 738 blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) 739 + 740 + req, err := http.NewRequest("GET", blobURL, nil) 741 if err != nil { 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) 755 rp.pages.Error503(w) 756 return 757 } 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 + } 765 766 if resp.StatusCode != http.StatusOK { 767 log.Printf("knotserver returned non-OK status for raw blob %s: %d", blobURL, resp.StatusCode)
+12 -7
knotserver/routes.go
··· 286 mimeType = "image/svg+xml" 287 } 288 289 // allow image, video, and text/plain files to be served directly 290 switch { 291 - case strings.HasPrefix(mimeType, "image/"): 292 - // allowed 293 - case strings.HasPrefix(mimeType, "video/"): 294 - // allowed 295 case strings.HasPrefix(mimeType, "text/plain"): 296 - // allowed 297 default: 298 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 299 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 300 return 301 } 302 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 w.Header().Set("Content-Type", mimeType) 306 w.Write(contents) 307 }
··· 286 mimeType = "image/svg+xml" 287 } 288 289 + contentHash := sha256.Sum256(contents) 290 + eTag := fmt.Sprintf("\"%x\"", contentHash) 291 + 292 // allow image, video, and text/plain files to be served directly 293 switch { 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 + 301 case strings.HasPrefix(mimeType, "text/plain"): 302 + w.Header().Set("Cache-Control", "public, no-cache") 303 + 304 default: 305 l.Error("attempted to serve disallowed file type", "mimetype", mimeType) 306 writeError(w, "only image, video, and text files can be accessed directly", http.StatusForbidden) 307 return 308 } 309 310 w.Header().Set("Content-Type", mimeType) 311 w.Write(contents) 312 }