mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-09 11:42:48 +02:00
headerwriter in processing_handler.go (#1507)
* headerwriter in processing_handler.go * Remove not required etag tests * ETagEnabled, LastModifiedEnabled true by default * Changed Passthrough signature * Removed etag package * Merge writeDebugHeaders*
This commit is contained in:
14
CHANGELOG.v4.md
Normal file
14
CHANGELOG.v4.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 📑 Changelog (version/4 dev)
|
||||||
|
|
||||||
|
## ✨ 2025-08-27
|
||||||
|
|
||||||
|
### 🔄 Changed
|
||||||
|
|
||||||
|
- `If-None-Match` is passed through to server request, `Etag` passed through from server response
|
||||||
|
if `IMGPROXY_USE_ETAG` is true.
|
||||||
|
- `IMGPROXY_USE_ETAG` is now true by default.
|
||||||
|
- `IMGPROXY_USE_LAST_MODIFIED` is now true by default.
|
||||||
|
|
||||||
|
### ❌ Removed
|
||||||
|
|
||||||
|
- `Etag` calculations on the imgproxy side
|
@@ -351,10 +351,10 @@ func Reset() {
|
|||||||
SwiftConnectTimeoutSeconds = 10
|
SwiftConnectTimeoutSeconds = 10
|
||||||
SwiftTimeoutSeconds = 60
|
SwiftTimeoutSeconds = 60
|
||||||
|
|
||||||
ETagEnabled = false
|
ETagEnabled = true
|
||||||
ETagBuster = ""
|
ETagBuster = ""
|
||||||
|
|
||||||
LastModifiedEnabled = false
|
LastModifiedEnabled = true
|
||||||
|
|
||||||
BaseURL = ""
|
BaseURL = ""
|
||||||
URLReplacements = make([]URLReplacement, 0)
|
URLReplacements = make([]URLReplacement, 0)
|
||||||
|
160
etag/etag.go
160
etag/etag.go
@@ -1,160 +0,0 @@
|
|||||||
package etag
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"hash"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/textproto"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/options"
|
|
||||||
)
|
|
||||||
|
|
||||||
type eTagCalc struct {
|
|
||||||
hash hash.Hash
|
|
||||||
enc *json.Encoder
|
|
||||||
}
|
|
||||||
|
|
||||||
var eTagCalcPool = sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
h := sha256.New()
|
|
||||||
|
|
||||||
enc := json.NewEncoder(h)
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
enc.SetIndent("", "")
|
|
||||||
|
|
||||||
return &eTagCalc{h, enc}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handler struct {
|
|
||||||
poHashActual, poHashExpected string
|
|
||||||
|
|
||||||
imgEtagActual, imgEtagExpected string
|
|
||||||
imgHashActual, imgHashExpected string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ParseExpectedETag(etag string) {
|
|
||||||
// We suuport only a single ETag value
|
|
||||||
if i := strings.IndexByte(etag, ','); i >= 0 {
|
|
||||||
etag = textproto.TrimString(etag[:i])
|
|
||||||
}
|
|
||||||
|
|
||||||
etagLen := len(etag)
|
|
||||||
|
|
||||||
// ETag is empty or invalid
|
|
||||||
if etagLen < 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// We support strong ETags only
|
|
||||||
if etag[0] != '"' || etag[etagLen-1] != '"' {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove quotes
|
|
||||||
etag = etag[1 : etagLen-1]
|
|
||||||
|
|
||||||
i := strings.Index(etag, "/")
|
|
||||||
if i < 0 || i > etagLen-3 {
|
|
||||||
// Doesn't look like imgproxy ETag
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
poPart, imgPartMark, imgPart := etag[:i], etag[i+1], etag[i+2:]
|
|
||||||
|
|
||||||
switch imgPartMark {
|
|
||||||
case 'R':
|
|
||||||
imgPartDec, err := base64.RawStdEncoding.DecodeString(imgPart)
|
|
||||||
if err == nil {
|
|
||||||
h.imgEtagExpected = string(imgPartDec)
|
|
||||||
}
|
|
||||||
case 'D':
|
|
||||||
h.imgHashExpected = imgPart
|
|
||||||
default:
|
|
||||||
// Unknown image part mark
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.poHashExpected = poPart
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ProcessingOptionsMatch() bool {
|
|
||||||
return h.poHashActual == h.poHashExpected
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) SetActualProcessingOptions(po *options.ProcessingOptions) bool {
|
|
||||||
c := eTagCalcPool.Get().(*eTagCalc)
|
|
||||||
defer eTagCalcPool.Put(c)
|
|
||||||
|
|
||||||
c.hash.Reset()
|
|
||||||
c.hash.Write([]byte(config.ETagBuster))
|
|
||||||
c.enc.Encode(po)
|
|
||||||
|
|
||||||
h.poHashActual = base64.RawURLEncoding.EncodeToString(c.hash.Sum(nil))
|
|
||||||
|
|
||||||
return h.ProcessingOptionsMatch()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) ImageEtagExpected() string {
|
|
||||||
return h.imgEtagExpected
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) SetActualImageData(imgdata imagedata.ImageData, headers http.Header) (bool, error) {
|
|
||||||
var haveActualImgETag bool
|
|
||||||
h.imgEtagActual = headers.Get(httpheaders.Etag)
|
|
||||||
haveActualImgETag = len(h.imgEtagActual) > 0
|
|
||||||
|
|
||||||
// Just in case server didn't check ETag properly and returned the same one
|
|
||||||
// as we expected
|
|
||||||
if haveActualImgETag && h.imgEtagExpected == h.imgEtagActual {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
haveExpectedImgHash := len(h.imgHashExpected) != 0
|
|
||||||
|
|
||||||
if !haveActualImgETag || haveExpectedImgHash {
|
|
||||||
c := eTagCalcPool.Get().(*eTagCalc)
|
|
||||||
defer eTagCalcPool.Put(c)
|
|
||||||
|
|
||||||
c.hash.Reset()
|
|
||||||
|
|
||||||
_, err := io.Copy(c.hash, imgdata.Reader())
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
h.imgHashActual = base64.RawURLEncoding.EncodeToString(c.hash.Sum(nil))
|
|
||||||
|
|
||||||
return haveExpectedImgHash && h.imgHashActual == h.imgHashExpected, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) GenerateActualETag() string {
|
|
||||||
return h.generate(h.poHashActual, h.imgEtagActual, h.imgHashActual)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) GenerateExpectedETag() string {
|
|
||||||
return h.generate(h.poHashExpected, h.imgEtagExpected, h.imgHashExpected)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) generate(poHash, imgEtag, imgHash string) string {
|
|
||||||
imgPartMark := 'D'
|
|
||||||
imgPart := imgHash
|
|
||||||
if len(imgEtag) != 0 {
|
|
||||||
imgPartMark = 'R'
|
|
||||||
imgPart = base64.RawURLEncoding.EncodeToString([]byte(imgEtag))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(`"%s/%c%s"`, poHash, imgPartMark, imgPart)
|
|
||||||
}
|
|
@@ -1,159 +0,0 @@
|
|||||||
package etag
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/stretchr/testify/suite"
|
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/options"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
etagReq = `"yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y/RImxvcmVtaXBzdW1kb2xvciI"`
|
|
||||||
etagData = `"yj0WO6sFU4GCciYUBWjzvvfqrBh869doeOC2Pp5EI1Y/D3t8wWhX4piqDCV4ZMEZsKvOaIO6onhKjbf9f-ZfYUV0"`
|
|
||||||
)
|
|
||||||
|
|
||||||
type EtagTestSuite struct {
|
|
||||||
suite.Suite
|
|
||||||
|
|
||||||
po *options.ProcessingOptions
|
|
||||||
imgWithETag imagedata.ImageData
|
|
||||||
imgWithEtagHeaders http.Header
|
|
||||||
imgWithoutETag imagedata.ImageData
|
|
||||||
imgWithoutEtagHeaders http.Header
|
|
||||||
|
|
||||||
h Handler
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) SetupSuite() {
|
|
||||||
logrus.SetOutput(io.Discard)
|
|
||||||
s.po = options.NewProcessingOptions()
|
|
||||||
|
|
||||||
d, err := os.ReadFile("../testdata/test1.jpg")
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
imgWithETag, err := imagedata.NewFromBytes(d)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
s.imgWithEtagHeaders = make(http.Header)
|
|
||||||
s.imgWithEtagHeaders.Add(httpheaders.Etag, `"loremipsumdolor"`)
|
|
||||||
|
|
||||||
imgWithoutETag, err := imagedata.NewFromBytes(d)
|
|
||||||
s.imgWithoutEtagHeaders = make(http.Header)
|
|
||||||
s.Require().NoError(err)
|
|
||||||
|
|
||||||
s.imgWithETag = imgWithETag
|
|
||||||
s.imgWithoutETag = imgWithoutETag
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TeardownSuite() {
|
|
||||||
logrus.SetOutput(os.Stdout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) SetupTest() {
|
|
||||||
s.h = Handler{}
|
|
||||||
config.Reset()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestGenerateActualReq() {
|
|
||||||
s.h.SetActualProcessingOptions(s.po)
|
|
||||||
s.h.SetActualImageData(s.imgWithETag, s.imgWithEtagHeaders)
|
|
||||||
|
|
||||||
s.Require().Equal(etagReq, s.h.GenerateActualETag())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestGenerateActualData() {
|
|
||||||
s.h.SetActualProcessingOptions(s.po)
|
|
||||||
s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders)
|
|
||||||
|
|
||||||
s.Require().Equal(etagData, s.h.GenerateActualETag())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestGenerateExpectedReq() {
|
|
||||||
s.h.ParseExpectedETag(etagReq)
|
|
||||||
s.Require().Equal(etagReq, s.h.GenerateExpectedETag())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestGenerateExpectedData() {
|
|
||||||
s.h.ParseExpectedETag(etagData)
|
|
||||||
s.Require().Equal(etagData, s.h.GenerateExpectedETag())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestProcessingOptionsCheckSuccess() {
|
|
||||||
s.h.ParseExpectedETag(etagReq)
|
|
||||||
|
|
||||||
s.Require().True(s.h.SetActualProcessingOptions(s.po))
|
|
||||||
s.Require().True(s.h.ProcessingOptionsMatch())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestProcessingOptionsCheckFailure() {
|
|
||||||
i := strings.Index(etagReq, "/")
|
|
||||||
wrongEtag := `"wrongpohash` + etagReq[i:]
|
|
||||||
|
|
||||||
s.h.ParseExpectedETag(wrongEtag)
|
|
||||||
|
|
||||||
s.Require().False(s.h.SetActualProcessingOptions(s.po))
|
|
||||||
s.Require().False(s.h.ProcessingOptionsMatch())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageETagExpectedPresent() {
|
|
||||||
s.h.ParseExpectedETag(etagReq)
|
|
||||||
|
|
||||||
//nolint:testifylint // False-positive expected-actual
|
|
||||||
s.Require().Equal(s.imgWithEtagHeaders.Get(httpheaders.Etag), s.h.ImageEtagExpected())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageETagExpectedBlank() {
|
|
||||||
s.h.ParseExpectedETag(etagData)
|
|
||||||
|
|
||||||
s.Require().Empty(s.h.ImageEtagExpected())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckDataToDataSuccess() {
|
|
||||||
s.h.ParseExpectedETag(etagData)
|
|
||||||
s.Require().True(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
|
|
||||||
i := strings.Index(etagData, "/")
|
|
||||||
wrongEtag := etagData[:i] + `/Dwrongimghash"`
|
|
||||||
|
|
||||||
s.h.ParseExpectedETag(wrongEtag)
|
|
||||||
s.Require().False(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckDataToReqSuccess() {
|
|
||||||
s.h.ParseExpectedETag(etagData)
|
|
||||||
s.Require().True(s.h.SetActualImageData(s.imgWithETag, s.imgWithEtagHeaders))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
|
|
||||||
i := strings.Index(etagData, "/")
|
|
||||||
wrongEtag := etagData[:i] + `/Dwrongimghash"`
|
|
||||||
|
|
||||||
s.h.ParseExpectedETag(wrongEtag)
|
|
||||||
s.Require().False(s.h.SetActualImageData(s.imgWithETag, s.imgWithEtagHeaders))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestImageDataCheckReqToDataFailure() {
|
|
||||||
s.h.ParseExpectedETag(etagReq)
|
|
||||||
s.Require().False(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *EtagTestSuite) TestETagBusterFailure() {
|
|
||||||
config.ETagBuster = "busted"
|
|
||||||
|
|
||||||
s.h.ParseExpectedETag(etagReq)
|
|
||||||
s.Require().False(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEtag(t *testing.T) {
|
|
||||||
suite.Run(t, new(EtagTestSuite))
|
|
||||||
}
|
|
@@ -118,14 +118,16 @@ func (s *request) execute(ctx context.Context) error {
|
|||||||
// Output streaming response headers
|
// Output streaming response headers
|
||||||
hw := s.handler.hw.NewRequest(res.Header, s.imageURL)
|
hw := s.handler.hw.NewRequest(res.Header, s.imageURL)
|
||||||
|
|
||||||
hw.Passthrough(s.handler.config.PassthroughResponseHeaders) // NOTE: priority? This is lowest as it was
|
hw.Passthrough(s.handler.config.PassthroughResponseHeaders...) // NOTE: priority? This is lowest as it was
|
||||||
hw.SetContentLength(int(res.ContentLength))
|
hw.SetContentLength(int(res.ContentLength))
|
||||||
hw.SetCanonical()
|
hw.SetCanonical()
|
||||||
hw.SetExpires(s.po.Expires)
|
hw.SetExpires(s.po.Expires)
|
||||||
hw.Write(s.rw)
|
|
||||||
|
|
||||||
// Write Content-Disposition header
|
// Set the Content-Disposition header
|
||||||
s.writeContentDisposition(r.URL().Path, res)
|
s.setContentDisposition(r.URL().Path, res, hw)
|
||||||
|
|
||||||
|
// Write headers from writer
|
||||||
|
hw.Write(s.rw)
|
||||||
|
|
||||||
// Copy the status code from the original response
|
// Copy the status code from the original response
|
||||||
s.rw.WriteHeader(res.StatusCode)
|
s.rw.WriteHeader(res.StatusCode)
|
||||||
@@ -154,8 +156,8 @@ func (s *request) getImageRequestHeaders() http.Header {
|
|||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeContentDisposition writes the headers to the response writer
|
// setContentDisposition writes the headers to the response writer
|
||||||
func (s *request) writeContentDisposition(imagePath string, serverResponse *http.Response) {
|
func (s *request) setContentDisposition(imagePath string, serverResponse *http.Response, hw *headerwriter.Request) {
|
||||||
// Try to set correct Content-Disposition file name and extension
|
// Try to set correct Content-Disposition file name and extension
|
||||||
if serverResponse.StatusCode < 200 || serverResponse.StatusCode >= 300 {
|
if serverResponse.StatusCode < 200 || serverResponse.StatusCode >= 300 {
|
||||||
return
|
return
|
||||||
@@ -163,17 +165,13 @@ func (s *request) writeContentDisposition(imagePath string, serverResponse *http
|
|||||||
|
|
||||||
ct := serverResponse.Header.Get(httpheaders.ContentType)
|
ct := serverResponse.Header.Get(httpheaders.ContentType)
|
||||||
|
|
||||||
// Try to best guess the file name and extension
|
hw.SetContentDisposition(
|
||||||
cd := httpheaders.ContentDispositionValue(
|
|
||||||
imagePath,
|
imagePath,
|
||||||
s.po.Filename,
|
s.po.Filename,
|
||||||
"",
|
"",
|
||||||
ct,
|
ct,
|
||||||
s.po.ReturnAttachment,
|
s.po.ReturnAttachment,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Write the Content-Disposition header
|
|
||||||
s.rw.Header().Set(httpheaders.ContentDisposition, cd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// streamData copies the image data from the response body to the response writer
|
// streamData copies the image data from the response body to the response writer
|
||||||
|
@@ -12,7 +12,6 @@ type Config struct {
|
|||||||
DefaultTTL int // Default Cache-Control max-age= value for cached images
|
DefaultTTL int // Default Cache-Control max-age= value for cached images
|
||||||
FallbackImageTTL int // TTL for images served as fallbacks
|
FallbackImageTTL int // TTL for images served as fallbacks
|
||||||
CacheControlPassthrough bool // Passthrough the Cache-Control from the original response
|
CacheControlPassthrough bool // Passthrough the Cache-Control from the original response
|
||||||
LastModifiedEnabled bool // Set the Last-Modified header
|
|
||||||
EnableClientHints bool // Enable Vary header
|
EnableClientHints bool // Enable Vary header
|
||||||
SetVaryAccept bool // Whether to include Accept in Vary header
|
SetVaryAccept bool // Whether to include Accept in Vary header
|
||||||
}
|
}
|
||||||
@@ -23,7 +22,6 @@ func NewDefaultConfig() *Config {
|
|||||||
SetCanonicalHeader: false,
|
SetCanonicalHeader: false,
|
||||||
DefaultTTL: 31536000,
|
DefaultTTL: 31536000,
|
||||||
FallbackImageTTL: 0,
|
FallbackImageTTL: 0,
|
||||||
LastModifiedEnabled: false,
|
|
||||||
CacheControlPassthrough: false,
|
CacheControlPassthrough: false,
|
||||||
EnableClientHints: false,
|
EnableClientHints: false,
|
||||||
SetVaryAccept: false,
|
SetVaryAccept: false,
|
||||||
@@ -35,7 +33,6 @@ func (c *Config) LoadFromEnv() (*Config, error) {
|
|||||||
c.SetCanonicalHeader = config.SetCanonicalHeader
|
c.SetCanonicalHeader = config.SetCanonicalHeader
|
||||||
c.DefaultTTL = config.TTL
|
c.DefaultTTL = config.TTL
|
||||||
c.FallbackImageTTL = config.FallbackImageTTL
|
c.FallbackImageTTL = config.FallbackImageTTL
|
||||||
c.LastModifiedEnabled = config.LastModifiedEnabled
|
|
||||||
c.CacheControlPassthrough = config.CacheControlPassthrough
|
c.CacheControlPassthrough = config.CacheControlPassthrough
|
||||||
c.EnableClientHints = config.EnableClientHints
|
c.EnableClientHints = config.EnableClientHints
|
||||||
c.SetVaryAccept = config.AutoWebp ||
|
c.SetVaryAccept = config.AutoWebp ||
|
||||||
|
@@ -17,8 +17,8 @@ type Writer struct {
|
|||||||
varyValue string
|
varyValue string
|
||||||
}
|
}
|
||||||
|
|
||||||
// writer is a private struct that builds HTTP response headers for a specific request.
|
// Request is a private struct that builds HTTP response headers for a specific request.
|
||||||
type writer struct {
|
type Request struct {
|
||||||
writer *Writer
|
writer *Writer
|
||||||
originalResponseHeaders http.Header // Original response headers
|
originalResponseHeaders http.Header // Original response headers
|
||||||
result http.Header // Headers to be written to the response
|
result http.Header // Headers to be written to the response
|
||||||
@@ -51,8 +51,8 @@ func New(config *Config) (*Writer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewRequest creates a new header writer instance for a specific request with the provided origin headers and URL.
|
// NewRequest creates a new header writer instance for a specific request with the provided origin headers and URL.
|
||||||
func (w *Writer) NewRequest(originalResponseHeaders http.Header, url string) *writer {
|
func (w *Writer) NewRequest(originalResponseHeaders http.Header, url string) *Request {
|
||||||
return &writer{
|
return &Request{
|
||||||
writer: w,
|
writer: w,
|
||||||
originalResponseHeaders: originalResponseHeaders,
|
originalResponseHeaders: originalResponseHeaders,
|
||||||
url: url,
|
url: url,
|
||||||
@@ -63,123 +63,124 @@ func (w *Writer) NewRequest(originalResponseHeaders http.Header, url string) *wr
|
|||||||
|
|
||||||
// SetIsFallbackImage sets the Fallback-Image header to
|
// SetIsFallbackImage sets the Fallback-Image header to
|
||||||
// indicate that the fallback image was used.
|
// indicate that the fallback image was used.
|
||||||
func (w *writer) SetIsFallbackImage() {
|
func (r *Request) SetIsFallbackImage() {
|
||||||
// We set maxAge to FallbackImageTTL if it's explicitly passed
|
// We set maxAge to FallbackImageTTL if it's explicitly passed
|
||||||
if w.writer.config.FallbackImageTTL < 0 {
|
if r.writer.config.FallbackImageTTL < 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// However, we should not overwrite existing value if set (or greater than ours)
|
// However, we should not overwrite existing value if set (or greater than ours)
|
||||||
if w.maxAge < 0 || w.maxAge > w.writer.config.FallbackImageTTL {
|
if r.maxAge < 0 || r.maxAge > r.writer.config.FallbackImageTTL {
|
||||||
w.maxAge = w.writer.config.FallbackImageTTL
|
r.maxAge = r.writer.config.FallbackImageTTL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetExpires sets the TTL from time
|
// SetExpires sets the TTL from time
|
||||||
func (w *writer) SetExpires(expires *time.Time) {
|
func (r *Request) SetExpires(expires *time.Time) {
|
||||||
if expires == nil {
|
if expires == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert current maxAge to time
|
// Convert current maxAge to time
|
||||||
currentMaxAgeTime := time.Now().Add(time.Duration(w.maxAge) * time.Second)
|
currentMaxAgeTime := time.Now().Add(time.Duration(r.maxAge) * time.Second)
|
||||||
|
|
||||||
// If maxAge outlives expires or was not set, we'll use expires as maxAge.
|
// If maxAge outlives expires or was not set, we'll use expires as maxAge.
|
||||||
if w.maxAge < 0 || expires.Before(currentMaxAgeTime) {
|
if r.maxAge < 0 || expires.Before(currentMaxAgeTime) {
|
||||||
w.maxAge = min(w.writer.config.DefaultTTL, max(0, int(time.Until(*expires).Seconds())))
|
r.maxAge = min(r.writer.config.DefaultTTL, max(0, int(time.Until(*expires).Seconds())))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLastModified sets the Last-Modified header from request
|
|
||||||
func (w *writer) SetLastModified() {
|
|
||||||
if !w.writer.config.LastModifiedEnabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val := w.originalResponseHeaders.Get(httpheaders.LastModified)
|
|
||||||
if len(val) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.result.Set(httpheaders.LastModified, val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetVary sets the Vary header
|
// SetVary sets the Vary header
|
||||||
func (w *writer) SetVary() {
|
func (r *Request) SetVary() {
|
||||||
if len(w.writer.varyValue) > 0 {
|
if len(r.writer.varyValue) > 0 {
|
||||||
w.result.Set(httpheaders.Vary, w.writer.varyValue)
|
r.result.Set(httpheaders.Vary, r.writer.varyValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentDisposition sets the Content-Disposition header, passthrough to ContentDispositionValue
|
||||||
|
func (r *Request) SetContentDisposition(originURL, filename, ext, contentType string, returnAttachment bool) {
|
||||||
|
value := httpheaders.ContentDispositionValue(
|
||||||
|
originURL,
|
||||||
|
filename,
|
||||||
|
ext,
|
||||||
|
contentType,
|
||||||
|
returnAttachment,
|
||||||
|
)
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
r.result.Set(httpheaders.ContentDisposition, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Passthrough copies specified headers from the original response headers to the response headers.
|
// Passthrough copies specified headers from the original response headers to the response headers.
|
||||||
func (w *writer) Passthrough(only []string) {
|
func (r *Request) Passthrough(only ...string) {
|
||||||
httpheaders.Copy(w.originalResponseHeaders, w.result, only)
|
httpheaders.Copy(r.originalResponseHeaders, r.result, only)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CopyFrom copies specified headers from the headers object. Please note that
|
// CopyFrom copies specified headers from the headers object. Please note that
|
||||||
// all the past operations may overwrite those values.
|
// all the past operations may overwrite those values.
|
||||||
func (w *writer) CopyFrom(headers http.Header, only []string) {
|
func (r *Request) CopyFrom(headers http.Header, only []string) {
|
||||||
httpheaders.Copy(headers, w.result, only)
|
httpheaders.Copy(headers, r.result, only)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContentLength sets the Content-Length header
|
// SetContentLength sets the Content-Length header
|
||||||
func (w *writer) SetContentLength(contentLength int) {
|
func (r *Request) SetContentLength(contentLength int) {
|
||||||
if contentLength < 0 {
|
if contentLength < 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.result.Set(httpheaders.ContentLength, strconv.Itoa(contentLength))
|
r.result.Set(httpheaders.ContentLength, strconv.Itoa(contentLength))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetContentType sets the Content-Type header
|
// SetContentType sets the Content-Type header
|
||||||
func (w *writer) SetContentType(mime string) {
|
func (r *Request) SetContentType(mime string) {
|
||||||
w.result.Set(httpheaders.ContentType, mime)
|
r.result.Set(httpheaders.ContentType, mime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeCanonical sets the Link header with the canonical URL.
|
// writeCanonical sets the Link header with the canonical URL.
|
||||||
// It is mandatory for any response if enabled in the configuration.
|
// It is mandatory for any response if enabled in the configuration.
|
||||||
func (w *writer) SetCanonical() {
|
func (r *Request) SetCanonical() {
|
||||||
if !w.writer.config.SetCanonicalHeader {
|
if !r.writer.config.SetCanonicalHeader {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(w.url, "https://") || strings.HasPrefix(w.url, "http://") {
|
if strings.HasPrefix(r.url, "https://") || strings.HasPrefix(r.url, "http://") {
|
||||||
value := fmt.Sprintf(`<%s>; rel="canonical"`, w.url)
|
value := fmt.Sprintf(`<%s>; rel="canonical"`, r.url)
|
||||||
w.result.Set(httpheaders.Link, value)
|
r.result.Set(httpheaders.Link, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setCacheControl sets the Cache-Control header with the specified value.
|
// setCacheControl sets the Cache-Control header with the specified value.
|
||||||
func (w *writer) setCacheControl(value int) bool {
|
func (r *Request) setCacheControl(value int) bool {
|
||||||
if value <= 0 {
|
if value <= 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
w.result.Set(httpheaders.CacheControl, fmt.Sprintf("max-age=%d, public", value))
|
r.result.Set(httpheaders.CacheControl, fmt.Sprintf("max-age=%d, public", value))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// setCacheControlNoCache sets the Cache-Control header to no-cache (default).
|
// setCacheControlNoCache sets the Cache-Control header to no-cache (default).
|
||||||
func (w *writer) setCacheControlNoCache() {
|
func (r *Request) setCacheControlNoCache() {
|
||||||
w.result.Set(httpheaders.CacheControl, "no-cache")
|
r.result.Set(httpheaders.CacheControl, "no-cache")
|
||||||
}
|
}
|
||||||
|
|
||||||
// setCacheControlPassthrough sets the Cache-Control header from the request
|
// setCacheControlPassthrough sets the Cache-Control header from the request
|
||||||
// if passthrough is enabled in the configuration.
|
// if passthrough is enabled in the configuration.
|
||||||
func (w *writer) setCacheControlPassthrough() bool {
|
func (r *Request) setCacheControlPassthrough() bool {
|
||||||
if !w.writer.config.CacheControlPassthrough || w.maxAge > 0 {
|
if !r.writer.config.CacheControlPassthrough || r.maxAge > 0 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if val := w.originalResponseHeaders.Get(httpheaders.CacheControl); val != "" {
|
if val := r.originalResponseHeaders.Get(httpheaders.CacheControl); val != "" {
|
||||||
w.result.Set(httpheaders.CacheControl, val)
|
r.result.Set(httpheaders.CacheControl, val)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if val := w.originalResponseHeaders.Get(httpheaders.Expires); val != "" {
|
if val := r.originalResponseHeaders.Get(httpheaders.Expires); val != "" {
|
||||||
if t, err := time.Parse(http.TimeFormat, val); err == nil {
|
if t, err := time.Parse(http.TimeFormat, val); err == nil {
|
||||||
maxAge := max(0, int(time.Until(t).Seconds()))
|
maxAge := max(0, int(time.Until(t).Seconds()))
|
||||||
return w.setCacheControl(maxAge)
|
return r.setCacheControl(maxAge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,24 +188,24 @@ func (w *writer) setCacheControlPassthrough() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setCSP sets the Content-Security-Policy header to prevent script execution.
|
// setCSP sets the Content-Security-Policy header to prevent script execution.
|
||||||
func (w *writer) setCSP() {
|
func (r *Request) setCSP() {
|
||||||
w.result.Set(httpheaders.ContentSecurityPolicy, "script-src 'none'")
|
r.result.Set(httpheaders.ContentSecurityPolicy, "script-src 'none'")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write writes the headers to the response writer. It does not overwrite
|
// Write writes the headers to the response writer. It does not overwrite
|
||||||
// target headers, which were set outside the header writer.
|
// target headers, which were set outside the header writer.
|
||||||
func (w *writer) Write(rw http.ResponseWriter) {
|
func (r *Request) Write(rw http.ResponseWriter) {
|
||||||
// Then, let's try to set Cache-Control using priority order
|
// Then, let's try to set Cache-Control using priority order
|
||||||
switch {
|
switch {
|
||||||
case w.setCacheControl(w.maxAge): // First, try set explicit
|
case r.setCacheControl(r.maxAge): // First, try set explicit
|
||||||
case w.setCacheControlPassthrough(): // Try to pick up from request headers
|
case r.setCacheControlPassthrough(): // Try to pick up from request headers
|
||||||
case w.setCacheControl(w.writer.config.DefaultTTL): // Fallback to default value
|
case r.setCacheControl(r.writer.config.DefaultTTL): // Fallback to default value
|
||||||
default:
|
default:
|
||||||
w.setCacheControlNoCache() // By default we use no-cache
|
r.setCacheControlNoCache() // By default we use no-cache
|
||||||
}
|
}
|
||||||
|
|
||||||
w.setCSP()
|
r.setCSP()
|
||||||
|
|
||||||
// Copy all headers to the response without overwriting existing ones
|
// Copy all headers to the response without overwriting existing ones
|
||||||
httpheaders.CopyAll(w.result, rw.Header(), false)
|
httpheaders.CopyAll(r.result, rw.Header(), false)
|
||||||
}
|
}
|
||||||
|
@@ -23,7 +23,7 @@ type writerTestCase struct {
|
|||||||
req http.Header
|
req http.Header
|
||||||
res http.Header
|
res http.Header
|
||||||
config Config
|
config Config
|
||||||
fn func(*writer)
|
fn func(*Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HeaderWriterSuite) TestHeaderCases() {
|
func (s *HeaderWriterSuite) TestHeaderCases() {
|
||||||
@@ -45,7 +45,6 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
SetCanonicalHeader: false,
|
SetCanonicalHeader: false,
|
||||||
DefaultTTL: 0,
|
DefaultTTL: 0,
|
||||||
CacheControlPassthrough: false,
|
CacheControlPassthrough: false,
|
||||||
LastModifiedEnabled: false,
|
|
||||||
EnableClientHints: false,
|
EnableClientHints: false,
|
||||||
SetVaryAccept: false,
|
SetVaryAccept: false,
|
||||||
},
|
},
|
||||||
@@ -105,7 +104,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
SetCanonicalHeader: true,
|
SetCanonicalHeader: true,
|
||||||
DefaultTTL: 3600,
|
DefaultTTL: 3600,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetCanonical()
|
w.SetCanonical()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -134,28 +133,10 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
SetCanonicalHeader: false,
|
SetCanonicalHeader: false,
|
||||||
DefaultTTL: 3600,
|
DefaultTTL: 3600,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetCanonical()
|
w.SetCanonical()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "LastModified",
|
|
||||||
req: http.Header{
|
|
||||||
httpheaders.LastModified: []string{expires.Format(http.TimeFormat)},
|
|
||||||
},
|
|
||||||
res: http.Header{
|
|
||||||
httpheaders.LastModified: []string{expires.Format(http.TimeFormat)},
|
|
||||||
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
|
||||||
httpheaders.CacheControl: []string{"max-age=3600, public"},
|
|
||||||
},
|
|
||||||
config: Config{
|
|
||||||
LastModifiedEnabled: true,
|
|
||||||
DefaultTTL: 3600,
|
|
||||||
},
|
|
||||||
fn: func(w *writer) {
|
|
||||||
w.SetLastModified()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "SetMaxAgeTTL",
|
name: "SetMaxAgeTTL",
|
||||||
req: http.Header{},
|
req: http.Header{},
|
||||||
@@ -167,7 +148,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
DefaultTTL: 3600,
|
DefaultTTL: 3600,
|
||||||
FallbackImageTTL: 1,
|
FallbackImageTTL: 1,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetIsFallbackImage()
|
w.SetIsFallbackImage()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -181,7 +162,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
config: Config{
|
config: Config{
|
||||||
DefaultTTL: math.MaxInt32,
|
DefaultTTL: math.MaxInt32,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetExpires(&expires)
|
w.SetExpires(&expires)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -196,7 +177,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
DefaultTTL: math.MaxInt32,
|
DefaultTTL: math.MaxInt32,
|
||||||
FallbackImageTTL: 600,
|
FallbackImageTTL: 600,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetIsFallbackImage()
|
w.SetIsFallbackImage()
|
||||||
w.SetExpires(&shortExpires)
|
w.SetExpires(&shortExpires)
|
||||||
},
|
},
|
||||||
@@ -213,7 +194,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
EnableClientHints: true,
|
EnableClientHints: true,
|
||||||
SetVaryAccept: true,
|
SetVaryAccept: true,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetVary()
|
w.SetVary()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -228,8 +209,8 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
||||||
},
|
},
|
||||||
config: Config{},
|
config: Config{},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.Passthrough([]string{"X-Test"})
|
w.Passthrough("X-Test")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -241,7 +222,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
||||||
},
|
},
|
||||||
config: Config{},
|
config: Config{},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
h := http.Header{}
|
h := http.Header{}
|
||||||
h.Set("X-From", "baz")
|
h.Set("X-From", "baz")
|
||||||
w.CopyFrom(h, []string{"X-From"})
|
w.CopyFrom(h, []string{"X-From"})
|
||||||
@@ -256,7 +237,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
||||||
},
|
},
|
||||||
config: Config{},
|
config: Config{},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetContentLength(123)
|
w.SetContentLength(123)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -269,7 +250,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
httpheaders.ContentSecurityPolicy: []string{"script-src 'none'"},
|
||||||
},
|
},
|
||||||
config: Config{},
|
config: Config{},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetContentType("image/png")
|
w.SetContentType("image/png")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -283,7 +264,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
config: Config{
|
config: Config{
|
||||||
DefaultTTL: 3600,
|
DefaultTTL: 3600,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetExpires(nil)
|
w.SetExpires(nil)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -298,7 +279,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
config: Config{
|
config: Config{
|
||||||
SetVaryAccept: true,
|
SetVaryAccept: true,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetVary()
|
w.SetVary()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -313,7 +294,7 @@ func (s *HeaderWriterSuite) TestHeaderCases() {
|
|||||||
config: Config{
|
config: Config{
|
||||||
EnableClientHints: true,
|
EnableClientHints: true,
|
||||||
},
|
},
|
||||||
fn: func(w *writer) {
|
fn: func(w *Request) {
|
||||||
w.SetVary()
|
w.SetVary()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
package httpheaders
|
package httpheaders
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
// Copy copies specified headers from one header to another.
|
// Copy copies specified headers from one header to another.
|
||||||
func Copy(from, to http.Header, only []string) {
|
func Copy(from, to http.Header, only []string) {
|
||||||
|
2
main.go
2
main.go
@@ -45,7 +45,7 @@ func buildRouter(r *server.Router) *server.Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.GET(
|
r.GET(
|
||||||
"/*", handleProcessing,
|
"/*", callHandleProcessing,
|
||||||
r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
|
r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -2,13 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/sync/semaphore"
|
"golang.org/x/sync/semaphore"
|
||||||
@@ -16,7 +14,6 @@ import (
|
|||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
"github.com/imgproxy/imgproxy/v3/cookies"
|
"github.com/imgproxy/imgproxy/v3/cookies"
|
||||||
"github.com/imgproxy/imgproxy/v3/errorreport"
|
"github.com/imgproxy/imgproxy/v3/errorreport"
|
||||||
"github.com/imgproxy/imgproxy/v3/etag"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
||||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
||||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||||
@@ -36,8 +33,6 @@ import (
|
|||||||
var (
|
var (
|
||||||
queueSem *semaphore.Weighted
|
queueSem *semaphore.Weighted
|
||||||
processingSem *semaphore.Weighted
|
processingSem *semaphore.Weighted
|
||||||
|
|
||||||
headerVaryValue string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func initProcessingHandler() {
|
func initProcessingHandler() {
|
||||||
@@ -46,88 +41,22 @@ func initProcessingHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processingSem = semaphore.NewWeighted(int64(config.Workers))
|
processingSem = semaphore.NewWeighted(int64(config.Workers))
|
||||||
|
|
||||||
vary := make([]string, 0)
|
|
||||||
|
|
||||||
if config.AutoWebp ||
|
|
||||||
config.EnforceWebp ||
|
|
||||||
config.AutoAvif ||
|
|
||||||
config.EnforceAvif ||
|
|
||||||
config.AutoJxl ||
|
|
||||||
config.EnforceJxl {
|
|
||||||
vary = append(vary, "Accept")
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.EnableClientHints {
|
|
||||||
vary = append(vary, "Sec-CH-DPR", "DPR", "Sec-CH-Width", "Width")
|
|
||||||
}
|
|
||||||
|
|
||||||
headerVaryValue = strings.Join(vary, ", ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setCacheControl(rw http.ResponseWriter, force *time.Time, originHeaders http.Header) {
|
// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
|
||||||
ttl := -1
|
func writeDebugHeaders(rw http.ResponseWriter, result *processing.Result, originData imagedata.ImageData) error {
|
||||||
|
|
||||||
if _, ok := originHeaders["Fallback-Image"]; ok && config.FallbackImageTTL > 0 {
|
|
||||||
ttl = config.FallbackImageTTL
|
|
||||||
}
|
|
||||||
|
|
||||||
if force != nil && (ttl < 0 || force.Before(time.Now().Add(time.Duration(ttl)*time.Second))) {
|
|
||||||
ttl = min(config.TTL, max(0, int(time.Until(*force).Seconds())))
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.CacheControlPassthrough && ttl < 0 && originHeaders != nil {
|
|
||||||
if val := originHeaders.Get(httpheaders.CacheControl); len(val) > 0 {
|
|
||||||
rw.Header().Set(httpheaders.CacheControl, val)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if val := originHeaders.Get(httpheaders.Expires); len(val) > 0 {
|
|
||||||
if t, err := time.Parse(http.TimeFormat, val); err == nil {
|
|
||||||
ttl = max(0, int(time.Until(t).Seconds()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ttl < 0 {
|
|
||||||
ttl = config.TTL
|
|
||||||
}
|
|
||||||
|
|
||||||
if ttl > 0 {
|
|
||||||
rw.Header().Set(httpheaders.CacheControl, fmt.Sprintf("max-age=%d, public", ttl))
|
|
||||||
} else {
|
|
||||||
rw.Header().Set(httpheaders.CacheControl, "no-cache")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setLastModified(rw http.ResponseWriter, originHeaders http.Header) {
|
|
||||||
if config.LastModifiedEnabled {
|
|
||||||
if val := originHeaders.Get(httpheaders.LastModified); len(val) != 0 {
|
|
||||||
rw.Header().Set(httpheaders.LastModified, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setVary(rw http.ResponseWriter) {
|
|
||||||
if len(headerVaryValue) > 0 {
|
|
||||||
rw.Header().Set(httpheaders.Vary, headerVaryValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setCanonical(rw http.ResponseWriter, originURL string) {
|
|
||||||
if config.SetCanonicalHeader {
|
|
||||||
if strings.HasPrefix(originURL, "https://") || strings.HasPrefix(originURL, "http://") {
|
|
||||||
linkHeader := fmt.Sprintf(`<%s>; rel="canonical"`, originURL)
|
|
||||||
rw.Header().Set("Link", linkHeader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeOriginContentLengthDebugHeader(rw http.ResponseWriter, originData imagedata.ImageData) error {
|
|
||||||
if !config.EnableDebugHeaders {
|
if !config.EnableDebugHeaders {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if result != nil {
|
||||||
|
rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
|
||||||
|
rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
|
||||||
|
rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
|
||||||
|
rw.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read origin image size
|
||||||
size, err := originData.Size()
|
size, err := originData.Size()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
||||||
@@ -138,18 +67,16 @@ func writeOriginContentLengthDebugHeader(rw http.ResponseWriter, originData imag
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeDebugHeaders(rw http.ResponseWriter, result *processing.Result) {
|
func respondWithImage(
|
||||||
if !config.EnableDebugHeaders || result == nil {
|
reqID string,
|
||||||
return
|
r *http.Request,
|
||||||
}
|
rw http.ResponseWriter,
|
||||||
|
statusCode int,
|
||||||
rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
|
resultData imagedata.ImageData,
|
||||||
rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
|
po *options.ProcessingOptions,
|
||||||
rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
|
originURL string,
|
||||||
rw.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
|
hw *headerwriter.Request,
|
||||||
}
|
) error {
|
||||||
|
|
||||||
func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData imagedata.ImageData, originHeaders http.Header) error {
|
|
||||||
// We read the size of the image data here, so we can set Content-Length header.
|
// We read the size of the image data here, so we can set Content-Length header.
|
||||||
// This indireclty ensures that the image data is fully read from the source, no
|
// This indireclty ensures that the image data is fully read from the source, no
|
||||||
// errors happened.
|
// errors happened.
|
||||||
@@ -158,25 +85,29 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
|
|||||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
contentDisposition := httpheaders.ContentDispositionValue(
|
hw.SetContentType(resultData.Format().Mime())
|
||||||
|
hw.SetContentLength(resultSize)
|
||||||
|
hw.SetContentDisposition(
|
||||||
originURL,
|
originURL,
|
||||||
po.Filename,
|
po.Filename,
|
||||||
resultData.Format().Ext(),
|
resultData.Format().Ext(),
|
||||||
"",
|
"",
|
||||||
po.ReturnAttachment,
|
po.ReturnAttachment,
|
||||||
)
|
)
|
||||||
|
hw.SetExpires(po.Expires)
|
||||||
|
hw.SetVary()
|
||||||
|
hw.SetCanonical()
|
||||||
|
|
||||||
rw.Header().Set(httpheaders.ContentType, resultData.Format().Mime())
|
if config.LastModifiedEnabled {
|
||||||
rw.Header().Set(httpheaders.ContentDisposition, contentDisposition)
|
hw.Passthrough(httpheaders.LastModified)
|
||||||
|
}
|
||||||
|
|
||||||
setCacheControl(rw, po.Expires, originHeaders)
|
if config.ETagEnabled {
|
||||||
setLastModified(rw, originHeaders)
|
hw.Passthrough(httpheaders.Etag)
|
||||||
setVary(rw)
|
}
|
||||||
setCanonical(rw, originURL)
|
|
||||||
|
|
||||||
rw.Header().Set(httpheaders.ContentSecurityPolicy, "script-src 'none'")
|
hw.Write(rw)
|
||||||
|
|
||||||
rw.Header().Set(httpheaders.ContentLength, strconv.Itoa(resultSize))
|
|
||||||
rw.WriteHeader(statusCode)
|
rw.WriteHeader(statusCode)
|
||||||
|
|
||||||
_, err = io.Copy(rw, resultData.Reader())
|
_, err = io.Copy(rw, resultData.Reader())
|
||||||
@@ -201,13 +132,20 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, originURL string, originHeaders http.Header) {
|
func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, originURL string, hw *headerwriter.Request) {
|
||||||
setCacheControl(rw, po.Expires, originHeaders)
|
hw.SetExpires(po.Expires)
|
||||||
setVary(rw)
|
hw.SetVary()
|
||||||
|
|
||||||
|
if config.ETagEnabled {
|
||||||
|
hw.Passthrough(httpheaders.Etag)
|
||||||
|
}
|
||||||
|
|
||||||
|
hw.Write(rw)
|
||||||
|
|
||||||
|
rw.WriteHeader(http.StatusNotModified)
|
||||||
|
|
||||||
rw.WriteHeader(304)
|
|
||||||
server.LogResponse(
|
server.LogResponse(
|
||||||
reqID, r, 304, nil,
|
reqID, r, http.StatusNotModified, nil,
|
||||||
log.Fields{
|
log.Fields{
|
||||||
"image_url": originURL,
|
"image_url": originURL,
|
||||||
"processing_options": po,
|
"processing_options": po,
|
||||||
@@ -215,7 +153,32 @@ func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWrite
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) error {
|
func callHandleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) error {
|
||||||
|
// NOTE: This is temporary, will be moved level up at once
|
||||||
|
hwc, err := headerwriter.NewDefaultConfig().LoadFromEnv()
|
||||||
|
if err != nil {
|
||||||
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
hw, err := headerwriter.New(hwc)
|
||||||
|
if err != nil {
|
||||||
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
sc, err := stream.NewDefaultConfig().LoadFromEnv()
|
||||||
|
if err != nil {
|
||||||
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := stream.New(sc, hw, imagedata.Fetcher)
|
||||||
|
if err != nil {
|
||||||
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
||||||
|
}
|
||||||
|
|
||||||
|
return handleProcessing(reqID, rw, r, hw, stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request, hw *headerwriter.Writer, stream *stream.Handler) error {
|
||||||
stats.IncRequestsInProgress()
|
stats.IncRequestsInProgress()
|
||||||
defer stats.DecRequestsInProgress()
|
defer stats.DecRequestsInProgress()
|
||||||
|
|
||||||
@@ -277,30 +240,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
if po.Raw {
|
if po.Raw {
|
||||||
// NOTE: This is temporary, there would be no categoryConfig once we
|
return stream.Execute(ctx, r, imageURL, reqID, po, rw)
|
||||||
// finish with refactoring.
|
|
||||||
// TODO: Move this up
|
|
||||||
cfg, cerr := stream.NewDefaultConfig().LoadFromEnv()
|
|
||||||
if cerr != nil {
|
|
||||||
return ierrors.Wrap(cerr, 0, ierrors.WithCategory(categoryConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
hwc, cerr := headerwriter.NewDefaultConfig().LoadFromEnv()
|
|
||||||
if cerr != nil {
|
|
||||||
return ierrors.Wrap(cerr, 0, ierrors.WithCategory(categoryConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
hw, cerr := headerwriter.New(hwc)
|
|
||||||
if cerr != nil {
|
|
||||||
return ierrors.Wrap(cerr, 0, ierrors.WithCategory(categoryConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
handler, cerr := stream.New(cfg, hw, imagedata.Fetcher)
|
|
||||||
if cerr != nil {
|
|
||||||
return ierrors.Wrap(cerr, 0, ierrors.WithCategory(categoryConfig))
|
|
||||||
}
|
|
||||||
|
|
||||||
return handler.Execute(ctx, r, imageURL, reqID, po, rw)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SVG is a special case. Though saving to svg is not supported, SVG->SVG is.
|
// SVG is a special case. Though saving to svg is not supported, SVG->SVG is.
|
||||||
@@ -313,22 +253,14 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) err
|
|||||||
|
|
||||||
imgRequestHeader := make(http.Header)
|
imgRequestHeader := make(http.Header)
|
||||||
|
|
||||||
var etagHandler etag.Handler
|
// If ETag is enabled, we forward If-None-Match header
|
||||||
|
|
||||||
if config.ETagEnabled {
|
if config.ETagEnabled {
|
||||||
etagHandler.ParseExpectedETag(r.Header.Get("If-None-Match"))
|
imgRequestHeader.Set(httpheaders.IfNoneMatch, r.Header.Get(httpheaders.IfNoneMatch))
|
||||||
|
|
||||||
if etagHandler.SetActualProcessingOptions(po) {
|
|
||||||
if imgEtag := etagHandler.ImageEtagExpected(); len(imgEtag) != 0 {
|
|
||||||
imgRequestHeader.Set("If-None-Match", imgEtag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If LastModified is enabled, we forward If-Modified-Since header
|
||||||
if config.LastModifiedEnabled {
|
if config.LastModifiedEnabled {
|
||||||
if modifiedSince := r.Header.Get("If-Modified-Since"); len(modifiedSince) != 0 {
|
imgRequestHeader.Set(httpheaders.IfModifiedSince, r.Header.Get(httpheaders.IfModifiedSince))
|
||||||
imgRequestHeader.Set("If-Modified-Since", modifiedSince)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if queueSem != nil {
|
if queueSem != nil {
|
||||||
@@ -393,27 +325,28 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) err
|
|||||||
return imagedata.DownloadAsync(ctx, imageURL, "source image", downloadOpts)
|
return imagedata.DownloadAsync(ctx, imageURL, "source image", downloadOpts)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Close originData if no error occurred
|
||||||
|
if err == nil {
|
||||||
|
defer originData.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that image detection didn't take too long
|
||||||
|
if terr := server.CheckTimeout(ctx); terr != nil {
|
||||||
|
return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
var nmErr imagefetcher.NotModifiedError
|
var nmErr imagefetcher.NotModifiedError
|
||||||
|
|
||||||
switch {
|
// Respond with NotModified if image was not modified
|
||||||
case err == nil:
|
if errors.As(err, &nmErr) {
|
||||||
defer originData.Close()
|
hwr := hw.NewRequest(nmErr.Headers(), imageURL)
|
||||||
|
|
||||||
case errors.As(err, &nmErr):
|
respondWithNotModified(reqID, r, rw, po, imageURL, hwr)
|
||||||
if config.ETagEnabled && len(etagHandler.ImageEtagExpected()) != 0 {
|
|
||||||
rw.Header().Set(httpheaders.Etag, etagHandler.GenerateExpectedETag())
|
|
||||||
}
|
|
||||||
|
|
||||||
respondWithNotModified(reqID, r, rw, po, imageURL, nmErr.Headers())
|
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
// If error is not related to NotModified, respond with fallback image
|
||||||
// This may be a request timeout error or a request cancelled error.
|
if err != nil {
|
||||||
// Check it before moving further
|
|
||||||
if terr := server.CheckTimeout(ctx); terr != nil {
|
|
||||||
return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
ierr := ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
|
ierr := ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
|
||||||
if config.ReportDownloadingErrors {
|
if config.ReportDownloadingErrors {
|
||||||
ierr = ierrors.Wrap(ierr, 0, ierrors.WithShouldReport(true))
|
ierr = ierrors.Wrap(ierr, 0, ierrors.WithShouldReport(true))
|
||||||
@@ -447,28 +380,6 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) err
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if terr := server.CheckTimeout(ctx); terr != nil {
|
|
||||||
return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.ETagEnabled && statusCode == http.StatusOK {
|
|
||||||
imgDataMatch, eerr := etagHandler.SetActualImageData(originData, originHeaders)
|
|
||||||
if eerr != nil && config.ReportIOErrors {
|
|
||||||
return ierrors.Wrap(eerr, 0, ierrors.WithCategory(categoryIO))
|
|
||||||
}
|
|
||||||
|
|
||||||
rw.Header().Set("ETag", etagHandler.GenerateActualETag())
|
|
||||||
|
|
||||||
if imgDataMatch && etagHandler.ProcessingOptionsMatch() {
|
|
||||||
respondWithNotModified(reqID, r, rw, po, imageURL, originHeaders)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if terr := server.CheckTimeout(ctx); terr != nil {
|
|
||||||
return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
if !vips.SupportsLoad(originData.Format()) {
|
if !vips.SupportsLoad(originData.Format()) {
|
||||||
return ierrors.Wrap(newInvalidURLErrorf(
|
return ierrors.Wrap(newInvalidURLErrorf(
|
||||||
http.StatusUnprocessableEntity,
|
http.StatusUnprocessableEntity,
|
||||||
@@ -496,18 +407,16 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) err
|
|||||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryProcessing))
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryProcessing))
|
||||||
}
|
}
|
||||||
|
|
||||||
if terr := server.CheckTimeout(ctx); terr != nil {
|
hwr := hw.NewRequest(originHeaders, imageURL)
|
||||||
return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
|
|
||||||
}
|
|
||||||
|
|
||||||
writeDebugHeaders(rw, result)
|
// Write debug headers. It seems unlogical to move they to headerwriter since they're
|
||||||
|
// not used anywhere else.
|
||||||
err = writeOriginContentLengthDebugHeader(rw, originData)
|
err = writeDebugHeaders(rw, result, originData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
err = respondWithImage(reqID, r, rw, statusCode, result.OutData, po, imageURL, originData, originHeaders)
|
err = respondWithImage(reqID, r, rw, statusCode, result.OutData, po, imageURL, hwr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -8,7 +8,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,11 +16,9 @@ import (
|
|||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/config"
|
"github.com/imgproxy/imgproxy/v3/config"
|
||||||
"github.com/imgproxy/imgproxy/v3/config/configurators"
|
"github.com/imgproxy/imgproxy/v3/config/configurators"
|
||||||
"github.com/imgproxy/imgproxy/v3/etag"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||||
"github.com/imgproxy/imgproxy/v3/options"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/server"
|
"github.com/imgproxy/imgproxy/v3/server"
|
||||||
"github.com/imgproxy/imgproxy/v3/svg"
|
"github.com/imgproxy/imgproxy/v3/svg"
|
||||||
"github.com/imgproxy/imgproxy/v3/testutil"
|
"github.com/imgproxy/imgproxy/v3/testutil"
|
||||||
@@ -103,34 +100,6 @@ func (s *ProcessingHandlerTestSuite) readTestImageData(name string) imagedata.Im
|
|||||||
return imgdata
|
return imgdata
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) readImageData(imgdata imagedata.ImageData) []byte {
|
|
||||||
data, err := io.ReadAll(imgdata.Reader())
|
|
||||||
s.Require().NoError(err)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, imagedata.ImageData, http.Header, string) {
|
|
||||||
poStr := "rs:fill:4:4"
|
|
||||||
|
|
||||||
po := options.NewProcessingOptions()
|
|
||||||
po.ResizingType = options.ResizeFill
|
|
||||||
po.Width = 4
|
|
||||||
po.Height = 4
|
|
||||||
|
|
||||||
imgdata := s.readTestImageData("test1.png")
|
|
||||||
headers := make(http.Header)
|
|
||||||
|
|
||||||
if len(imgETag) != 0 {
|
|
||||||
headers.Set(httpheaders.Etag, imgETag)
|
|
||||||
}
|
|
||||||
|
|
||||||
var h etag.Handler
|
|
||||||
|
|
||||||
h.SetActualProcessingOptions(po)
|
|
||||||
h.SetActualImageData(imgdata, headers)
|
|
||||||
return poStr, imgdata, headers, h.GenerateActualETag()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestRequest() {
|
func (s *ProcessingHandlerTestSuite) TestRequest() {
|
||||||
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
|
||||||
res := rw.Result()
|
res := rw.Result()
|
||||||
@@ -411,166 +380,27 @@ func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
|
|||||||
s.Require().Empty(res.Header.Get("ETag"))
|
s.Require().Empty(res.Header.Get("ETag"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
|
|
||||||
config.ETagEnabled = true
|
|
||||||
|
|
||||||
poStr, _, headers, etag := s.sampleETagData("loremipsumdolor")
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
s.Empty(r.Header.Get("If-None-Match"))
|
|
||||||
|
|
||||||
rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
|
|
||||||
rw.WriteHeader(200)
|
|
||||||
rw.Write(s.readTestFile("test1.png"))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL))
|
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
|
||||||
s.Require().Equal(etag, res.Header.Get("ETag"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
|
|
||||||
config.ETagEnabled = true
|
|
||||||
|
|
||||||
poStr, imgdata, _, etag := s.sampleETagData("")
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
s.Empty(r.Header.Get("If-None-Match"))
|
|
||||||
|
|
||||||
rw.WriteHeader(200)
|
|
||||||
rw.Write(s.readImageData(imgdata))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL))
|
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
|
||||||
s.Require().Equal(etag, res.Header.Get("ETag"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
|
|
||||||
config.ETagEnabled = true
|
|
||||||
|
|
||||||
poStr, _, headers, etag := s.sampleETagData(`"loremipsumdolor"`)
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
s.Equal(headers.Get(httpheaders.Etag), r.Header.Get(httpheaders.IfNoneMatch))
|
|
||||||
|
|
||||||
rw.WriteHeader(304)
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
header := make(http.Header)
|
|
||||||
header.Set("If-None-Match", etag)
|
|
||||||
|
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
|
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(304, res.StatusCode)
|
|
||||||
s.Require().Equal(etag, res.Header.Get("ETag"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
|
||||||
config.ETagEnabled = true
|
config.ETagEnabled = true
|
||||||
|
|
||||||
poStr, imgdata, _, etag := s.sampleETagData("")
|
etag := `"loremipsumdolor"`
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||||
s.Empty(r.Header.Get("If-None-Match"))
|
s.NotEmpty(r.Header.Get(httpheaders.IfNoneMatch))
|
||||||
|
|
||||||
rw.WriteHeader(200)
|
rw.Header().Set(httpheaders.Etag, etag)
|
||||||
rw.Write(s.readImageData(imgdata))
|
rw.WriteHeader(http.StatusNotModified)
|
||||||
}))
|
}))
|
||||||
defer ts.Close()
|
defer ts.Close()
|
||||||
|
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set("If-None-Match", etag)
|
header.Set(httpheaders.IfNoneMatch, etag)
|
||||||
|
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
|
rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
|
||||||
res := rw.Result()
|
res := rw.Result()
|
||||||
|
|
||||||
s.Require().Equal(304, res.StatusCode)
|
s.Require().Equal(304, res.StatusCode)
|
||||||
s.Require().Equal(etag, res.Header.Get("ETag"))
|
s.Require().Equal(etag, res.Header.Get(httpheaders.Etag))
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
|
|
||||||
config.ETagEnabled = true
|
|
||||||
|
|
||||||
poStr, imgdata, headers, actualETag := s.sampleETagData(`"loremipsumdolor"`)
|
|
||||||
_, _, _, expectedETag := s.sampleETagData(`"loremipsum"`)
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
s.Equal(`"loremipsum"`, r.Header.Get("If-None-Match"))
|
|
||||||
|
|
||||||
rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
|
|
||||||
rw.WriteHeader(200)
|
|
||||||
rw.Write(s.readImageData(imgdata))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
header := make(http.Header)
|
|
||||||
header.Set("If-None-Match", expectedETag)
|
|
||||||
|
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
|
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
|
||||||
s.Require().Equal(actualETag, res.Header.Get("ETag"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
|
|
||||||
config.ETagEnabled = true
|
|
||||||
|
|
||||||
poStr, imgdata, _, actualETag := s.sampleETagData("")
|
|
||||||
// Change the data hash
|
|
||||||
expectedETag := actualETag[:strings.IndexByte(actualETag, '/')] + "/Dasdbefj"
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
s.Empty(r.Header.Get("If-None-Match"))
|
|
||||||
|
|
||||||
rw.WriteHeader(200)
|
|
||||||
rw.Write(s.readImageData(imgdata))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
header := make(http.Header)
|
|
||||||
header.Set("If-None-Match", expectedETag)
|
|
||||||
|
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
|
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
|
||||||
s.Require().Equal(actualETag, res.Header.Get("ETag"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestETagProcessingOptionsNotMatch() {
|
|
||||||
config.ETagEnabled = true
|
|
||||||
|
|
||||||
poStr, imgdata, headers, actualETag := s.sampleETagData("")
|
|
||||||
// Change the processing options hash
|
|
||||||
expectedETag := "abcdefj" + actualETag[strings.IndexByte(actualETag, '/'):]
|
|
||||||
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
s.Empty(r.Header.Get("If-None-Match"))
|
|
||||||
|
|
||||||
rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
|
|
||||||
rw.WriteHeader(200)
|
|
||||||
rw.Write(s.readImageData(imgdata))
|
|
||||||
}))
|
|
||||||
defer ts.Close()
|
|
||||||
|
|
||||||
header := make(http.Header)
|
|
||||||
header.Set("If-None-Match", expectedETag)
|
|
||||||
|
|
||||||
rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
|
|
||||||
res := rw.Result()
|
|
||||||
|
|
||||||
s.Require().Equal(200, res.StatusCode)
|
|
||||||
s.Require().Equal(actualETag, res.Header.Get("ETag"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ProcessingHandlerTestSuite) TestLastModifiedEnabled() {
|
func (s *ProcessingHandlerTestSuite) TestLastModifiedEnabled() {
|
||||||
|
Reference in New Issue
Block a user