1
0
mirror of https://github.com/imgproxy/imgproxy.git synced 2026-04-08 02:22:48 +02:00
Files
imgproxy/httpheaders/conditionalheaders/request.go
2026-02-27 16:57:11 +01:00

173 lines
5.0 KiB
Go

package conditionalheaders
import (
"encoding/base64"
"net/http"
"strings"
"github.com/imgproxy/imgproxy/v3/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)
}
}