You've already forked imgproxy
mirror of
https://github.com/imgproxy/imgproxy.git
synced 2026-04-08 02:22:48 +02:00
173 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|