rest.CORS refuses "*" together with credentials since go-pkgz/rest#52, so the bump and the option have to land together: the option does not exist in v1.22.0 and the panic fires at construction, inside routes(), which makes it a startup failure rather than a request-time one. The wildcard stays. The comment widget is embedded on arbitrary third-party sites, so the set of origins is not knowable, which is why the escape hatch was asked for upstream instead of accepting the panic. What it costs is unchanged and now written next to the call: any site a signed-in user visits can read authenticated responses, so state-changing requests have to keep being protected by something other than the origin, X-XSRF-Token today. The example module is tidied in the same commit, as it reaches go-pkgz/rest through the replace directive and its indirect graph would otherwise keep the old pin and fail the readonly module check in CI. The bump also carries testify to v1.12.0, which drops go-spew and go-difflib from the module graph.
73 lines
2.7 KiB
Go
73 lines
2.7 KiB
Go
package rest
|
|
|
|
import (
|
|
"crypto/sha1" //nolint not used for cryptography
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// CacheControl is a middleware setting cache expiration. Using url+version for etag
|
|
func CacheControl(expiration time.Duration, version string) func(http.Handler) http.Handler {
|
|
return CacheControlDynamic(expiration, func(*http.Request) string { return version })
|
|
}
|
|
|
|
// CacheControlDynamic is a middleware setting cache expiration. Using url+ func(r) for etag.
|
|
// Conditional requests are handled for GET and HEAD only, answering 304 when If-None-Match carries
|
|
// the current etag. Other methods are passed to the handler, as this middleware doesn't know enough
|
|
// about the resource to enforce their preconditions.
|
|
func CacheControlDynamic(expiration time.Duration, versionFn func(r *http.Request) string) func(http.Handler) http.Handler {
|
|
|
|
etag := func(r *http.Request, version string) string {
|
|
s := fmt.Sprintf("%s:%s", version, r.URL.String())
|
|
return fmt.Sprintf("%x", sha1.Sum([]byte(s))) //nolint
|
|
}
|
|
|
|
return func(h http.Handler) http.Handler {
|
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
e := `"` + etag(r, versionFn(r)) + `"`
|
|
w.Header().Set("Etag", e)
|
|
w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%d, no-cache", int(expiration.Seconds())))
|
|
|
|
// If-Match and If-Unmodified-Since outrank If-None-Match and can call for a 412, which only
|
|
// the handler can decide, so answering 304 over them would hide it. Presence is enough,
|
|
// and it is checked across every field as repeated ones all count
|
|
preconditioned := len(r.Header.Values("If-Match")) > 0 || len(r.Header.Values("If-Unmodified-Since")) > 0
|
|
|
|
safeMethod := r.Method == http.MethodGet || r.Method == http.MethodHead
|
|
|
|
if safeMethod && !preconditioned {
|
|
if etagMatches(r.Header.Values("If-None-Match"), e) {
|
|
w.WriteHeader(http.StatusNotModified)
|
|
return
|
|
}
|
|
}
|
|
h.ServeHTTP(w, r)
|
|
}
|
|
return http.HandlerFunc(fn)
|
|
}
|
|
}
|
|
|
|
// etagMatches reports whether any If-None-Match header field carries the given etag.
|
|
// Repeated fields form a single list, so all of them are examined. Handles comma-separated lists
|
|
// and the W/ weak-validator prefix, comparing with the weak comparison of RFC 9110. The "*"
|
|
// wildcard is deliberately not matched here, as it asks whether any representation exists and the
|
|
// middleware can't answer that before the handler runs.
|
|
func etagMatches(headers []string, etag string) bool {
|
|
etag = strings.TrimPrefix(strings.TrimSpace(etag), "W/")
|
|
for _, header := range headers {
|
|
for tag := range strings.SplitSeq(header, ",") {
|
|
tag = strings.TrimSpace(tag)
|
|
if tag == "" || tag == "*" {
|
|
continue
|
|
}
|
|
if strings.TrimPrefix(tag, "W/") == etag {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|