IMG-13: separate http.Headers from ImageData (#1475)

* Separate headers from ImageData

* processing.Result
This commit is contained in:
Victor Sokolov
2025-08-06 16:46:35 +02:00
committed by GitHub
parent 52ecfeaf8b
commit f7a13c99de
15 changed files with 198 additions and 187 deletions

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"hash"
"io"
"net/http"
"net/textproto"
"strings"
"sync"
@@ -107,9 +108,9 @@ func (h *Handler) ImageEtagExpected() string {
return h.imgEtagExpected
}
func (h *Handler) SetActualImageData(imgdata imagedata.ImageData) (bool, error) {
func (h *Handler) SetActualImageData(imgdata imagedata.ImageData, headers http.Header) (bool, error) {
var haveActualImgETag bool
h.imgEtagActual = imgdata.Headers().Get(httpheaders.Etag)
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

View File

@@ -2,6 +2,7 @@ package etag
import (
"io"
"net/http"
"os"
"strings"
"testing"
@@ -23,9 +24,11 @@ const (
type EtagTestSuite struct {
suite.Suite
po *options.ProcessingOptions
imgWithETag imagedata.ImageData
imgWithoutETag imagedata.ImageData
po *options.ProcessingOptions
imgWithETag imagedata.ImageData
imgWithEtagHeaders http.Header
imgWithoutETag imagedata.ImageData
imgWithoutEtagHeaders http.Header
h Handler
}
@@ -39,9 +42,11 @@ func (s *EtagTestSuite) SetupSuite() {
imgWithETag, err := imagedata.NewFromBytes(d)
s.Require().NoError(err)
imgWithETag.Headers().Add(httpheaders.Etag, `"loremipsumdolor"`)
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
@@ -59,14 +64,14 @@ func (s *EtagTestSuite) SetupTest() {
func (s *EtagTestSuite) TestGenerateActualReq() {
s.h.SetActualProcessingOptions(s.po)
s.h.SetActualImageData(s.imgWithETag)
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.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders)
s.Require().Equal(etagData, s.h.GenerateActualETag())
}
@@ -102,7 +107,7 @@ func (s *EtagTestSuite) TestImageETagExpectedPresent() {
s.h.ParseExpectedETag(etagReq)
//nolint:testifylint // False-positive expected-actual
s.Require().Equal(s.imgWithETag.Headers().Get(httpheaders.Etag), s.h.ImageEtagExpected())
s.Require().Equal(s.imgWithEtagHeaders.Get(httpheaders.Etag), s.h.ImageEtagExpected())
}
func (s *EtagTestSuite) TestImageETagExpectedBlank() {
@@ -113,7 +118,7 @@ func (s *EtagTestSuite) TestImageETagExpectedBlank() {
func (s *EtagTestSuite) TestImageDataCheckDataToDataSuccess() {
s.h.ParseExpectedETag(etagData)
s.Require().True(s.h.SetActualImageData(s.imgWithoutETag))
s.Require().True(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
}
func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
@@ -121,12 +126,12 @@ func (s *EtagTestSuite) TestImageDataCheckDataToDataFailure() {
wrongEtag := etagData[:i] + `/Dwrongimghash"`
s.h.ParseExpectedETag(wrongEtag)
s.Require().False(s.h.SetActualImageData(s.imgWithoutETag))
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.Require().True(s.h.SetActualImageData(s.imgWithETag, s.imgWithEtagHeaders))
}
func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
@@ -134,19 +139,19 @@ func (s *EtagTestSuite) TestImageDataCheckDataToReqFailure() {
wrongEtag := etagData[:i] + `/Dwrongimghash"`
s.h.ParseExpectedETag(wrongEtag)
s.Require().False(s.h.SetActualImageData(s.imgWithETag))
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.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.Require().False(s.h.SetActualImageData(s.imgWithoutETag, s.imgWithoutEtagHeaders))
}
func TestEtag(t *testing.T) {

View File

@@ -61,4 +61,9 @@ const (
XForwardedHost = "X-Forwarded-Host"
XForwardedProto = "X-Forwarded-Proto"
XFrameOptions = "X-Frame-Options"
XOriginWidth = "X-Origin-Width"
XOriginHeight = "X-Origin-Height"
XResultWidth = "X-Result-Width"
XResultHeight = "X-Result-Height"
XOriginContentLength = "X-Origin-Content-Length"
)

View File

@@ -37,7 +37,9 @@ func initDownloading() error {
return nil
}
func download(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (ImageData, error) {
func download(ctx context.Context, imageURL string, opts DownloadOptions, secopts security.Options) (ImageData, http.Header, error) {
h := make(http.Header)
// We use this for testing
if len(redirectAllRequestsTo) > 0 {
imageURL = redirectAllRequestsTo
@@ -45,16 +47,19 @@ func download(ctx context.Context, imageURL string, opts DownloadOptions, secopt
req, err := Fetcher.BuildRequest(ctx, imageURL, opts.Header, opts.CookieJar)
if err != nil {
return nil, err
return nil, h, err
}
defer req.Cancel()
res, err := req.FetchImage()
if res != nil {
h = res.Header.Clone()
}
if err != nil {
if res != nil {
res.Body.Close()
}
return nil, err
return nil, h, err
}
res, err = security.LimitResponseSize(res, secopts)
@@ -62,22 +67,15 @@ func download(ctx context.Context, imageURL string, opts DownloadOptions, secopt
defer res.Body.Close()
}
if err != nil {
return nil, err
return nil, h, err
}
imgdata, err := readAndCheckImage(res.Body, int(res.ContentLength), secopts)
if err != nil {
return nil, ierrors.Wrap(err, 0)
return nil, h, ierrors.Wrap(err, 0)
}
// NOTE: This will be removed in the future in favor of headers/image data separation
for k, v := range res.Header {
for _, v := range v {
imgdata.Headers().Add(k, v)
}
}
return imgdata, nil
return imgdata, h, nil
}
func RedirectAllRequestsTo(u string) {

View File

@@ -4,7 +4,6 @@ import (
"bytes"
"encoding/base64"
"io"
"net/http"
"os"
"github.com/imgproxy/imgproxy/v3/imagemeta"
@@ -12,22 +11,13 @@ import (
"github.com/imgproxy/imgproxy/v3/security"
)
// NewFromBytesWithFormat creates a new ImageData instance from the provided format,
// http headers and byte slice.
func NewFromBytesWithFormat(format imagetype.Type, b []byte, headers http.Header) ImageData {
var h http.Header
if headers == nil {
h = make(http.Header)
} else {
h = headers.Clone()
}
// NewFromBytesWithFormat creates a new ImageData instance from the provided format
// and byte slice.
func NewFromBytesWithFormat(format imagetype.Type, b []byte) ImageData {
return &imageDataBytes{
data: b,
format: format,
headers: h,
cancel: nil,
data: b,
format: format,
cancel: nil,
}
}
@@ -40,7 +30,7 @@ func NewFromBytes(b []byte) (ImageData, error) {
return nil, err
}
return NewFromBytesWithFormat(meta.Format(), b, nil), nil
return NewFromBytesWithFormat(meta.Format(), b), nil
}
// NewFromPath creates a new ImageData from an os.File

View File

@@ -15,8 +15,9 @@ import (
)
var (
Watermark ImageData
FallbackImage ImageData
Watermark ImageData
FallbackImage ImageData
FallbackImageHeaders http.Header // Headers for the fallback image
)
type ImageData interface {
@@ -25,17 +26,12 @@ type ImageData interface {
Format() imagetype.Type // Format returns the image format from the metadata (shortcut)
Size() (int, error) // Size returns the size of the image data in bytes
AddCancel(context.CancelFunc) // AddCancel attaches a cancel function to the image data
// This will be removed in the future
Headers() http.Header // Headers returns the HTTP headers of the image data, will be removed in the future
}
// imageDataBytes represents image data stored in a byte slice in memory
type imageDataBytes struct {
format imagetype.Type
data []byte
headers http.Header
format imagetype.Type
data []byte
cancel []context.CancelFunc
cancelOnce sync.Once
}
@@ -66,10 +62,6 @@ func (d *imageDataBytes) Size() (int, error) {
return len(d.data), nil
}
func (d *imageDataBytes) Headers() http.Header {
return d.headers
}
func (d *imageDataBytes) AddCancel(cancel context.CancelFunc) {
d.cancel = append(d.cancel, cancel)
}
@@ -113,7 +105,7 @@ func loadWatermark() error {
}
case len(config.WatermarkURL) > 0:
Watermark, err = Download(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
Watermark, _, err = Download(context.Background(), config.WatermarkURL, "watermark", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
if err != nil {
return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
}
@@ -140,7 +132,7 @@ func loadFallbackImage() (err error) {
}
case len(config.FallbackImageURL) > 0:
FallbackImage, err = Download(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
FallbackImage, FallbackImageHeaders, err = Download(context.Background(), config.FallbackImageURL, "fallback image", DownloadOptions{Header: nil, CookieJar: nil}, security.DefaultOptions())
if err != nil {
return ierrors.Wrap(err, 0, ierrors.WithPrefix("can't download from URL"))
}
@@ -149,21 +141,17 @@ func loadFallbackImage() (err error) {
FallbackImage = nil
}
if FallbackImage != nil && err == nil && config.FallbackImageTTL > 0 {
FallbackImage.Headers().Set("Fallback-Image", "1")
}
return err
}
func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (ImageData, error) {
imgdata, err := download(ctx, imageURL, opts, secopts)
func Download(ctx context.Context, imageURL, desc string, opts DownloadOptions, secopts security.Options) (ImageData, http.Header, error) {
imgdata, h, err := download(ctx, imageURL, opts, secopts)
if err != nil {
return nil, ierrors.Wrap(
return nil, h, ierrors.Wrap(
err, 0,
ierrors.WithPrefix(fmt.Sprintf("Can't download %s", desc)),
)
}
return imgdata, nil
return imgdata, h, nil
}

View File

@@ -91,7 +91,7 @@ func (s *ImageDataTestSuite) SetupTest() {
}
func (s *ImageDataTestSuite) TestDownloadStatusOK() {
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().NoError(err)
s.Require().NotNil(imgdata)
@@ -158,7 +158,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusPartialContent() {
s.Run(tc.name, func() {
s.header.Set("Content-Range", tc.contentRange)
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
if tc.expectErr {
s.Require().Error(err)
@@ -178,7 +178,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusNotFound() {
s.data = []byte("Not Found")
s.header.Set("Content-Type", "text/plain")
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
@@ -190,7 +190,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusForbidden() {
s.data = []byte("Forbidden")
s.header.Set("Content-Type", "text/plain")
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
@@ -202,7 +202,7 @@ func (s *ImageDataTestSuite) TestDownloadStatusInternalServerError() {
s.data = []byte("Internal Server Error")
s.header.Set("Content-Type", "text/plain")
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode())
@@ -216,7 +216,7 @@ func (s *ImageDataTestSuite) TestDownloadUnreachable() {
serverURL := fmt.Sprintf("http://%s", l.Addr().String())
imgdata, err := Download(context.Background(), serverURL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), serverURL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(500, ierrors.Wrap(err, 0).StatusCode())
@@ -226,7 +226,7 @@ func (s *ImageDataTestSuite) TestDownloadUnreachable() {
func (s *ImageDataTestSuite) TestDownloadInvalidImage() {
s.data = []byte("invalid")
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
@@ -236,7 +236,7 @@ func (s *ImageDataTestSuite) TestDownloadInvalidImage() {
func (s *ImageDataTestSuite) TestDownloadSourceAddressNotAllowed() {
config.AllowLoopbackSourceAddresses = false
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(404, ierrors.Wrap(err, 0).StatusCode())
@@ -246,7 +246,7 @@ func (s *ImageDataTestSuite) TestDownloadSourceAddressNotAllowed() {
func (s *ImageDataTestSuite) TestDownloadImageTooLarge() {
config.MaxSrcResolution = 1
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
@@ -256,7 +256,7 @@ func (s *ImageDataTestSuite) TestDownloadImageTooLarge() {
func (s *ImageDataTestSuite) TestDownloadImageFileTooLarge() {
config.MaxSrcFileSize = 1
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().Error(err)
s.Require().Equal(422, ierrors.Wrap(err, 0).StatusCode())
@@ -275,7 +275,7 @@ func (s *ImageDataTestSuite) TestDownloadGzip() {
s.data = buf.Bytes()
s.header.Set("Content-Encoding", "gzip")
imgdata, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
imgdata, _, err := Download(context.Background(), s.server.URL, "Test image", DownloadOptions{}, security.DefaultOptions())
s.Require().NoError(err)
s.Require().NotNil(imgdata)

View File

@@ -49,7 +49,7 @@ func readAndCheckImage(r io.Reader, contentLength int, secopts security.Options)
return nil, imagefetcher.WrapError(err)
}
i := NewFromBytesWithFormat(meta.Format(), buf.Bytes(), nil)
i := NewFromBytesWithFormat(meta.Format(), buf.Bytes())
i.AddCancel(cancel)
return i, nil
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"runtime"
"strconv"
log "github.com/sirupsen/logrus"
@@ -79,7 +78,7 @@ func ValidatePreferredFormats() error {
}
if len(filtered) == 0 {
return errors.New("No supported preferred formats specified")
return errors.New("no supported preferred formats specified")
}
config.PreferredFormats = filtered
@@ -248,7 +247,15 @@ func saveImageToFitBytes(ctx context.Context, po *options.ProcessingOptions, img
}
}
func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.ProcessingOptions) (imagedata.ImageData, error) {
type Result struct {
OutData imagedata.ImageData
OriginWidth int
OriginHeight int
ResultWidth int
ResultHeight int
}
func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.ProcessingOptions) (*Result, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
@@ -358,12 +365,15 @@ func ProcessImage(ctx context.Context, imgdata imagedata.ImageData, po *options.
outData, err = img.Save(po.Format, po.GetQuality())
}
if err == nil {
outData.Headers().Set("X-Origin-Width", strconv.Itoa(originWidth))
outData.Headers().Set("X-Origin-Height", strconv.Itoa(originHeight))
outData.Headers().Set("X-Result-Width", strconv.Itoa(img.Width()))
outData.Headers().Set("X-Result-Height", strconv.Itoa(img.Height()))
if err != nil {
return nil, err
}
return outData, err
return &Result{
OutData: outData,
OriginWidth: originWidth,
OriginHeight: originHeight,
ResultWidth: img.Width(),
ResultHeight: img.Height(),
}, nil
}

View File

@@ -49,14 +49,10 @@ func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
return imagedata
}
func (s *ProcessingTestSuite) checkSize(imgdata imagedata.ImageData, width, height int) {
img := new(vips.Image)
err := img.Load(imgdata, 1, 1, 1)
s.Require().NoError(err)
defer img.Clear()
s.Require().Equal(width, img.Width(), "Width mismatch")
s.Require().Equal(height, img.Height(), "Height mismatch")
func (s *ProcessingTestSuite) checkSize(r *Result, width, height int) {
s.Require().NotNil(r)
s.Require().Equal(width, r.ResultWidth, "Width mismatch")
s.Require().Equal(height, r.ResultHeight, "Height mismatch")
}
func (s *ProcessingTestSuite) TestResizeToFit() {
@@ -88,11 +84,11 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -127,11 +123,11 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -171,11 +167,11 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -215,11 +211,11 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -253,11 +249,11 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -292,11 +288,11 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -338,11 +334,11 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -384,11 +380,11 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -422,11 +418,11 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -461,11 +457,11 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -507,11 +503,11 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -551,11 +547,11 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
po.Width = tc.width
po.Height = tc.height
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
result, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
s.Require().NotNil(result)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}
@@ -980,11 +976,12 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
po.Rotate = tc.rotate
po.Padding = tc.padding
outImgdata, err := ProcessImage(context.Background(), imgdata, po)
s.Require().NoError(err)
s.Require().NotNil(outImgdata)
result, err := ProcessImage(context.Background(), imgdata, po)
s.checkSize(outImgdata, tc.outWidth, tc.outHeight)
s.Require().NoError(err)
s.Require().NotNil(result)
s.checkSize(result, tc.outWidth, tc.outHeight)
})
}
}

View File

@@ -125,7 +125,31 @@ func setCanonical(rw http.ResponseWriter, originURL string) {
}
}
func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData imagedata.ImageData) {
func writeOriginContentLengthDebugHeader(ctx context.Context, rw http.ResponseWriter, originData imagedata.ImageData) {
if !config.EnableDebugHeaders {
return
}
size, err := originData.Size()
if err != nil {
checkErr(ctx, "image_data_size", err)
}
rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
}
func writeDebugHeaders(rw http.ResponseWriter, result *processing.Result) {
if !config.EnableDebugHeaders || result == nil {
return
}
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))
}
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) {
var contentDisposition string
if len(po.Filename) > 0 {
contentDisposition = resultData.Format().ContentDisposition(po.Filename, po.ReturnAttachment)
@@ -133,35 +157,22 @@ func respondWithImage(reqID string, r *http.Request, rw http.ResponseWriter, sta
contentDisposition = resultData.Format().ContentDispositionFromURL(originURL, po.ReturnAttachment)
}
rw.Header().Set("Content-Type", resultData.Format().Mime())
rw.Header().Set("Content-Disposition", contentDisposition)
rw.Header().Set(httpheaders.ContentType, resultData.Format().Mime())
rw.Header().Set(httpheaders.ContentDisposition, contentDisposition)
setCacheControl(rw, po.Expires, originData.Headers())
setLastModified(rw, originData.Headers())
setCacheControl(rw, po.Expires, originHeaders)
setLastModified(rw, originHeaders)
setVary(rw)
setCanonical(rw, originURL)
if config.EnableDebugHeaders {
originSize, err := originData.Size()
if err != nil {
checkErr(r.Context(), "image_data_size", err)
}
rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(originSize))
rw.Header().Set("X-Origin-Width", resultData.Headers().Get("X-Origin-Width"))
rw.Header().Set("X-Origin-Height", resultData.Headers().Get("X-Origin-Height"))
rw.Header().Set("X-Result-Width", resultData.Headers().Get("X-Result-Width"))
rw.Header().Set("X-Result-Height", resultData.Headers().Get("X-Result-Height"))
}
rw.Header().Set("Content-Security-Policy", "script-src 'none'")
rw.Header().Set(httpheaders.ContentSecurityPolicy, "script-src 'none'")
resultSize, err := resultData.Size()
if err != nil {
checkErr(r.Context(), "image_data_size", err)
}
rw.Header().Set("Content-Length", strconv.Itoa(resultSize))
rw.Header().Set(httpheaders.ContentLength, strconv.Itoa(resultSize))
rw.WriteHeader(statusCode)
_, err = io.Copy(rw, resultData.Reader())
@@ -348,7 +359,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
statusCode := http.StatusOK
originData, err := func() (imagedata.ImageData, error) {
originData, originHeaders, err := func() (imagedata.ImageData, http.Header, error) {
defer metrics.StartDownloadingSegment(ctx, metrics.Meta{
metrics.MetaSourceImageURL: metricsMeta[metrics.MetaSourceImageURL],
metrics.MetaSourceImageOrigin: metricsMeta[metrics.MetaSourceImageOrigin],
@@ -412,17 +423,22 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
}
originData = imagedata.FallbackImage
originHeaders = imagedata.FallbackImageHeaders.Clone()
if config.FallbackImageTTL > 0 {
originHeaders.Set("Fallback-Image", "1")
}
}
checkErr(ctx, "timeout", router.CheckTimeout(ctx))
if config.ETagEnabled && statusCode == http.StatusOK {
imgDataMatch, terr := etagHandler.SetActualImageData(originData)
imgDataMatch, terr := etagHandler.SetActualImageData(originData, originHeaders)
if terr == nil {
rw.Header().Set("ETag", etagHandler.GenerateActualETag())
if imgDataMatch && etagHandler.ProcessingOptionsMatch() {
respondWithNotModified(reqID, r, rw, po, imageURL, originData.Headers())
respondWithNotModified(reqID, r, rw, po, imageURL, originHeaders)
return
}
}
@@ -444,11 +460,13 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
defer sanitized.Close()
respondWithImage(reqID, r, rw, statusCode, sanitized, po, imageURL, originData)
writeOriginContentLengthDebugHeader(ctx, rw, originData)
respondWithImage(reqID, r, rw, statusCode, sanitized, po, imageURL, originData, originHeaders)
return
}
respondWithImage(reqID, r, rw, statusCode, originData, po, imageURL, originData)
writeOriginContentLengthDebugHeader(ctx, rw, originData)
respondWithImage(reqID, r, rw, statusCode, originData, po, imageURL, originData, originHeaders)
return
}
@@ -467,7 +485,7 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
))
}
resultData, err := func() (imagedata.ImageData, error) {
result, err := func() (*processing.Result, error) {
defer metrics.StartProcessingSegment(ctx, metrics.Meta{
metrics.MetaProcessingOptions: metricsMeta[metrics.MetaProcessingOptions],
})()
@@ -475,9 +493,12 @@ func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
}()
checkErr(ctx, "processing", err)
defer resultData.Close()
defer result.OutData.Close()
checkErr(ctx, "timeout", router.CheckTimeout(ctx))
respondWithImage(reqID, r, rw, statusCode, resultData, po, imageURL, originData)
writeDebugHeaders(rw, result)
writeOriginContentLengthDebugHeader(ctx, rw, originData)
respondWithImage(reqID, r, rw, statusCode, result.OutData, po, imageURL, originData, originHeaders)
}

View File

@@ -106,7 +106,7 @@ func (s *ProcessingHandlerTestSuite) readImageData(imgdata imagedata.ImageData)
return data
}
func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, imagedata.ImageData, string) {
func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, imagedata.ImageData, http.Header, string) {
poStr := "rs:fill:4:4"
po := options.NewProcessingOptions()
@@ -115,16 +115,17 @@ func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, ima
po.Height = 4
imgdata := s.readTestImageData("test1.png")
headers := make(http.Header)
if len(imgETag) != 0 {
imgdata.Headers().Set(httpheaders.Etag, imgETag)
headers.Set(httpheaders.Etag, imgETag)
}
var h etag.Handler
h.SetActualProcessingOptions(po)
h.SetActualImageData(imgdata)
return poStr, imgdata, h.GenerateActualETag()
h.SetActualImageData(imgdata, headers)
return poStr, imgdata, headers, h.GenerateActualETag()
}
func (s *ProcessingHandlerTestSuite) TestRequest() {
@@ -412,12 +413,12 @@ func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
config.ETagEnabled = true
poStr, imgdata, etag := s.sampleETagData("loremipsumdolor")
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", imgdata.Headers().Get(httpheaders.Etag))
rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
rw.WriteHeader(200)
rw.Write(s.readTestFile("test1.png"))
}))
@@ -433,7 +434,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
config.ETagEnabled = true
poStr, imgdata, etag := s.sampleETagData("")
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"))
@@ -453,10 +454,10 @@ func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
config.ETagEnabled = true
poStr, imgdata, etag := s.sampleETagData(`"loremipsumdolor"`)
poStr, _, headers, etag := s.sampleETagData(`"loremipsumdolor"`)
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
s.Equal(imgdata.Headers().Get(httpheaders.Etag), r.Header.Get(httpheaders.IfNoneMatch))
s.Equal(headers.Get(httpheaders.Etag), r.Header.Get(httpheaders.IfNoneMatch))
rw.WriteHeader(304)
}))
@@ -475,7 +476,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
config.ETagEnabled = true
poStr, imgdata, etag := s.sampleETagData("")
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"))
@@ -498,13 +499,13 @@ func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
config.ETagEnabled = true
poStr, imgdata, actualETag := s.sampleETagData(`"loremipsumdolor"`)
_, _, expectedETag := s.sampleETagData(`"loremipsum"`)
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", imgdata.Headers().Get(httpheaders.Etag))
rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
rw.WriteHeader(200)
rw.Write(s.readImageData(imgdata))
}))
@@ -523,7 +524,7 @@ func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
config.ETagEnabled = true
poStr, imgdata, actualETag := s.sampleETagData("")
poStr, imgdata, _, actualETag := s.sampleETagData("")
// Change the data hash
expectedETag := actualETag[:strings.IndexByte(actualETag, '/')] + "/Dasdbefj"
@@ -548,14 +549,14 @@ func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
func (s *ProcessingHandlerTestSuite) TestETagProcessingOptionsNotMatch() {
config.ETagEnabled = true
poStr, imgdata, actualETag := s.sampleETagData("")
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", imgdata.Headers().Get(httpheaders.Etag))
rw.Header().Set("ETag", headers.Get(httpheaders.Etag))
rw.WriteHeader(200)
rw.Write(s.readImageData(imgdata))
}))

View File

@@ -48,7 +48,6 @@ func Sanitize(data imagedata.ImageData) (imagedata.ImageData, error) {
newData := imagedata.NewFromBytesWithFormat(
imagetype.SVG,
buf.Bytes(),
data.Headers(),
)
newData.AddCancel(cancel)

View File

@@ -8,7 +8,6 @@ import (
"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/testutil"
)
@@ -32,8 +31,6 @@ func (s *SvgTestSuite) readTestFile(name string) imagedata.ImageData {
s.Require().NoError(err)
d, err := imagedata.NewFromBytes(data)
d.Headers().Set(httpheaders.ContentType, "image/svg+xml")
d.Headers().Set(httpheaders.CacheControl, "public, max-age=12345")
s.Require().NoError(err)
return d
@@ -46,7 +43,6 @@ func (s *SvgTestSuite) TestSanitize() {
s.Require().NoError(err)
s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), actual.Reader()))
s.Require().Equal(origin.Headers(), actual.Headers())
}
func TestSvg(t *testing.T) {

View File

@@ -470,7 +470,7 @@ func (img *Image) Save(imgtype imagetype.Type, quality int) (imagedata.ImageData
b := ptrToBytes(ptr, int(imgsize))
i := imagedata.NewFromBytesWithFormat(imgtype, b, nil)
i := imagedata.NewFromBytesWithFormat(imgtype, b)
i.AddCancel(cancel)
return i, nil