package conditionalheaders import ( "encoding/base64" "net/http" "strings" "github.com/imgproxy/imgproxy/v4/httpheaders" ) // Request represents a request with conditional headers information. type Request struct { config *Config ifModifiedSince string // raw value of the If-Modified-Since header from the user request ifNoneMatch string // raw value of the If-None-Match header from the user request originHeaders http.Header } // newFromRequest creates a new ConditionalHeaders instance from the given HTTP request. func newFromRequest(c *Config, req *http.Request) *Request { ifModifiedSince := req.Header.Get(httpheaders.IfModifiedSince) ifNoneMatch := req.Header.Get(httpheaders.IfNoneMatch) return &Request{ config: c, ifModifiedSince: ifModifiedSince, ifNoneMatch: ifNoneMatch, originHeaders: nil, } } // SetOriginHeaders sets the origin headers for the request. func (c *Request) SetOriginHeaders(h http.Header) { c.originHeaders = h } // InjectImageRequestHeaders injects conditional headers into the source // image request if needed. func (c *Request) InjectImageRequestHeaders(imageReqHeaders http.Header) { var abort bool etag, abort := c.computeEtag() if abort { return } ifModifiedSince := c.computeIfModifiedSince() if len(ifModifiedSince) > 0 { imageReqHeaders.Set(httpheaders.IfModifiedSince, ifModifiedSince) } if len(etag) > 0 { imageReqHeaders.Set(httpheaders.IfNoneMatch, etag) } } // InjectUserResponseHeaders injects conditional headers into the user response func (c *Request) InjectUserResponseHeaders(rw http.ResponseWriter) { c.injectLastModifiedHeader(rw) c.injectEtagHeader(rw) } // computeIfModifiedSince determines whether the If-Modified-Since header should // be sent to the source image server. It returns value to be set (if any) func (c *Request) computeIfModifiedSince() string { // If the feature is disabled or no header is present, we shouldn't // send the header, but that should not affect other headers if !c.config.LastModifiedEnabled || len(c.ifModifiedSince) == 0 { return "" } // No buster is set: we should send the header as is if c.config.LastModifiedBuster.IsZero() { return c.ifModifiedSince } // Parse the header ifModifiedSince, err := http.ParseTime(c.ifModifiedSince) // Header has invalid format, or // the buster is set, and header is older than the buster if err != nil || !c.config.LastModifiedBuster.Before(ifModifiedSince) { return "" } // Otherwise no conditional headers should be sent at all return c.ifModifiedSince } // computeEtag determines whether the If-None-Match header should be sent to the // source image server. It returns etag value to be set and boolean indicating // whether the conditional headers should be sent at all. func (c *Request) computeEtag() (string, bool) { // If the feature is disabled or no header is present, // we shouldn't send the header at all, but it should not affect other headers if !c.config.ETagEnabled || len(c.ifNoneMatch) == 0 { return "", false } // If etag buster is not set, we should send the header as is if present if len(c.config.ETagBuster) == 0 { return c.ifNoneMatch, false } // Unquote and remove /W ifNoneMatch := httpheaders.UnquoteEtag(c.ifNoneMatch) // We expect that incoming ETag header has the buster rest, busterFound := strings.CutPrefix(ifNoneMatch, c.config.ETagBuster+"/") if !busterFound { return "", true // do not send any conditional headers otherwise (???) } // Parse the rest of the header as base64-encoded string, if it fails, // we should not send any conditional headers (invalid etag) etag, err := base64.RawURLEncoding.DecodeString(rest) if err != nil { return "", true // do not send any conditional headers otherwise } // Quotes will be encoded into etag return string(etag), false } // injectLastModifiedHeader injects the Last-Modified header into the user response func (c *Request) injectLastModifiedHeader(rw http.ResponseWriter) { // If the feature is disabled, we shouldn't send the header at all if !c.config.LastModifiedEnabled { return } // No header is present: nothing to inject val := c.originHeaders.Get(httpheaders.LastModified) if len(val) == 0 { return } // If the incoming header is older than the buster, we should replace it with the buster lastModified, err := http.ParseTime(val) if err != nil { return // invalid values are not forwarded } if lastModified.Before(c.config.LastModifiedBuster) { val = c.config.LastModifiedBuster.Format(http.TimeFormat) } // Otherwise, we should just pass the header through rw.Header().Set(httpheaders.LastModified, val) } // injectEtagHeader injects the ETag header into the user response func (c *Request) injectEtagHeader(rw http.ResponseWriter) { if !c.config.ETagEnabled { return } etag := c.originHeaders.Get(httpheaders.Etag) if len(etag) == 0 { return } if len(c.config.ETagBuster) > 0 { etag = `"` + c.config.ETagBuster + "/" + base64.RawURLEncoding.EncodeToString([]byte(etag)) + `"` } if len(etag) > 0 { rw.Header().Set(httpheaders.Etag, etag) } }