123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526 |
- package integration
- import (
- "fmt"
- "net/http"
- "net/http/httptest"
- "regexp"
- "testing"
- "time"
- "github.com/imgproxy/imgproxy/v3/config/configurators"
- "github.com/imgproxy/imgproxy/v3/fetcher"
- "github.com/imgproxy/imgproxy/v3/httpheaders"
- "github.com/imgproxy/imgproxy/v3/imagedata"
- "github.com/imgproxy/imgproxy/v3/imagetype"
- "github.com/imgproxy/imgproxy/v3/options"
- "github.com/imgproxy/imgproxy/v3/processing/svg"
- "github.com/imgproxy/imgproxy/v3/testutil"
- "github.com/imgproxy/imgproxy/v3/vips"
- "github.com/stretchr/testify/suite"
- )
- // ProcessingHandlerTestSuite is a test suite for testing image processing handler
- type ProcessingHandlerTestSuite struct {
- Suite
- }
- func (s *ProcessingHandlerTestSuite) SetupTest() {
- s.Config().Fetcher.Transport.HTTP.AllowLoopbackSourceAddresses = true
- }
- func (s *ProcessingHandlerTestSuite) SetupSubTest() {
- // We use t.Run() a lot, so we need to reset lazy objects at the beginning of each subtest
- s.ResetLazyObjects()
- }
- func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
- tt := []struct {
- name string
- url string
- statusCode int
- }{
- {
- name: "NoSignature",
- url: "/unsafe/rs:fill:4:4/plain/local:///test1.png",
- statusCode: http.StatusForbidden,
- },
- {
- name: "BadSignature",
- url: "/bad-signature/rs:fill:4:4/plain/local:///test1.png",
- statusCode: http.StatusForbidden,
- },
- {
- name: "ValidSignature",
- url: "/My9d3xq_PYpVHsPrCyww0Kh1w5KZeZhIlWhsa4az1TI/rs:fill:4:4/plain/local:///test1.png",
- statusCode: http.StatusOK,
- },
- }
- for _, tc := range tt {
- s.Run(tc.name, func() {
- s.Config().Security.Keys = [][]byte{[]byte("test-key")}
- s.Config().Security.Salts = [][]byte{[]byte("test-salt")}
- res := s.GET(tc.url)
- s.Require().Equal(tc.statusCode, res.StatusCode)
- })
- }
- }
- func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
- imagedata.RedirectAllRequestsTo("local:///test1.png")
- defer imagedata.StopRedirectingRequests()
- tt := []struct {
- name string
- allowedSources []string
- requestPath string
- expectedError bool
- }{
- {
- name: "match http URL without wildcard",
- allowedSources: []string{"local://", "http://images.dev/"},
- requestPath: "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
- },
- {
- name: "match http URL with wildcard in hostname single level",
- allowedSources: []string{"local://", "http://*.mycdn.dev/"},
- requestPath: "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
- },
- {
- name: "match http URL with wildcard in hostname multiple levels",
- allowedSources: []string{"local://", "http://*.mycdn.dev/"},
- requestPath: "/unsafe/plain/http://a-1.b-2.mycdn.dev/lorem/ipsum.jpg",
- },
- {
- name: "no match s3 URL with allowed local and http URLs",
- allowedSources: []string{"local://", "http://images.dev/"},
- requestPath: "/unsafe/plain/s3://images/lorem/ipsum.jpg",
- expectedError: true,
- },
- {
- name: "no match http URL with wildcard in hostname including slash",
- allowedSources: []string{"local://", "http://*.mycdn.dev/"},
- requestPath: "/unsafe/plain/http://other.dev/.mycdn.dev/lorem/ipsum.jpg",
- expectedError: true,
- },
- }
- for _, tc := range tt {
- s.Run(tc.name, func() {
- s.Config().Security.AllowedSources = make([]*regexp.Regexp, len(tc.allowedSources))
- for i, pattern := range tc.allowedSources {
- s.Config().Security.AllowedSources[i] = configurators.RegexpFromPattern(pattern)
- }
- res := s.GET(tc.requestPath)
- if tc.expectedError {
- s.Require().Equal(http.StatusNotFound, res.StatusCode)
- } else {
- s.Require().Equal(http.StatusOK, res.StatusCode)
- }
- })
- }
- }
- func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
- data := s.TestData.Read("test1.png")
- server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- rw.WriteHeader(200)
- rw.Write(data)
- }))
- defer server.Close()
- url := fmt.Sprintf("/unsafe/rs:fill:4:4/plain/%s/test1.png", server.URL)
- // We wrap this in a subtest to reset s.router()
- s.Run("AllowLoopbackSourceAddressesTrue", func() {
- s.Config().Fetcher.Transport.HTTP.AllowLoopbackSourceAddresses = true
- res := s.GET(url)
- s.Require().Equal(http.StatusOK, res.StatusCode)
- })
- s.Run("AllowLoopbackSourceAddressesFalse", func() {
- s.Config().Fetcher.Transport.HTTP.AllowLoopbackSourceAddresses = false
- res := s.GET(url)
- s.Require().Equal(http.StatusNotFound, res.StatusCode)
- })
- }
- func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
- vips.DisableLoadSupport(imagetype.PNG)
- defer vips.ResetLoadSupport()
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
- s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
- vips.DisableSaveSupport(imagetype.PNG)
- defer vips.ResetSaveSupport()
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
- s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
- s.Config().Processing.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
- s.Require().Equal(http.StatusOK, res.StatusCode)
- s.Require().True(s.TestData.FileEqualsToReader("test1.png", res.Body))
- }
- func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
- res := s.GET("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
- s.Require().Equal(http.StatusOK, res.StatusCode)
- s.Require().True(s.TestData.FileEqualsToReader("test1.png", res.Body))
- }
- func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
- s.Config().Processing.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
- s.Require().Equal(http.StatusOK, res.StatusCode)
- s.Require().True(s.TestData.FileEqualsToReader("test1.png", res.Body))
- }
- func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
- s.Config().Processing.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
- s.Require().Equal(http.StatusOK, res.StatusCode)
- s.Require().False(s.TestData.FileEqualsToReader("test1.png", res.Body))
- }
- func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
- s.Require().Equal(http.StatusOK, res.StatusCode)
- c := fetcher.NewDefaultConfig()
- f, err := fetcher.New(&c)
- s.Require().NoError(err)
- idf := imagedata.NewFactory(f)
- data, err := idf.NewFromBytes(s.TestData.Read("test1.svg"))
- s.Require().NoError(err)
- cfg := svg.NewDefaultConfig()
- svg := svg.New(&cfg)
- expected, err := svg.Process(&options.Options{}, data)
- s.Require().NoError(err)
- s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
- }
- func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
- s.Require().Equal(http.StatusOK, res.StatusCode)
- s.Require().False(s.TestData.FileEqualsToReader("test1.svg", res.Body))
- }
- func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
- s.Require().Equal(http.StatusUnprocessableEntity, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughCacheControl() {
- s.Config().Server.ResponseWriter.CacheControlPassthrough = true
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- rw.Header().Set(httpheaders.CacheControl, "max-age=1234, public")
- rw.Header().Set(httpheaders.Expires, time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
- rw.WriteHeader(200)
- rw.Write(s.TestData.Read("test1.png"))
- }))
- defer ts.Close()
- res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
- s.Require().Equal(http.StatusOK, res.StatusCode)
- s.Require().Equal("max-age=1234, public", res.Header.Get(httpheaders.CacheControl))
- s.Require().Empty(res.Header.Get(httpheaders.Expires))
- }
- func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughExpires() {
- s.Config().Server.ResponseWriter.CacheControlPassthrough = true
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- rw.Header().Set(httpheaders.Expires, time.Now().Add(1239*time.Second).UTC().Format(http.TimeFormat))
- rw.WriteHeader(200)
- rw.Write(s.TestData.Read("test1.png"))
- }))
- defer ts.Close()
- res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
- // Use regex to allow some delay
- s.Require().Regexp("max-age=123[0-9], public", res.Header.Get(httpheaders.CacheControl))
- s.Require().Empty(res.Header.Get(httpheaders.Expires))
- }
- func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
- s.Config().Server.ResponseWriter.CacheControlPassthrough = false
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- rw.Header().Set(httpheaders.CacheControl, "max-age=1234, public")
- rw.Header().Set(httpheaders.Expires, time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
- rw.WriteHeader(200)
- rw.Write(s.TestData.Read("test1.png"))
- }))
- defer ts.Close()
- res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
- s.Require().NotEqual("max-age=1234, public", res.Header.Get(httpheaders.CacheControl))
- s.Require().Empty(res.Header.Get(httpheaders.Expires))
- }
- func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
- s.Config().Handlers.Processing.ETagEnabled = false
- res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
- s.Require().Equal(200, res.StatusCode)
- s.Require().Empty(res.Header.Get(httpheaders.Etag))
- }
- func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
- s.Config().Handlers.Processing.ETagEnabled = true
- etag := `"loremipsumdolor"`
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- s.NotEmpty(r.Header.Get(httpheaders.IfNoneMatch))
- rw.Header().Set(httpheaders.Etag, etag)
- rw.WriteHeader(http.StatusNotModified)
- }))
- defer ts.Close()
- header := make(http.Header)
- header.Set(httpheaders.IfNoneMatch, etag)
- res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
- s.Require().Equal(304, res.StatusCode)
- s.Require().Equal(etag, res.Header.Get(httpheaders.Etag))
- }
- func (s *ProcessingHandlerTestSuite) TestLastModifiedEnabled() {
- s.Config().Handlers.Processing.LastModifiedEnabled = true
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- rw.Header().Set(httpheaders.LastModified, "Wed, 21 Oct 2015 07:28:00 GMT")
- rw.WriteHeader(200)
- rw.Write(s.TestData.Read("test1.png"))
- }))
- defer ts.Close()
- res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
- s.Require().Equal("Wed, 21 Oct 2015 07:28:00 GMT", res.Header.Get(httpheaders.LastModified))
- }
- func (s *ProcessingHandlerTestSuite) TestLastModifiedDisabled() {
- s.Config().Handlers.Processing.LastModifiedEnabled = false
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- rw.Header().Set(httpheaders.LastModified, "Wed, 21 Oct 2015 07:28:00 GMT")
- rw.WriteHeader(200)
- rw.Write(s.TestData.Read("test1.png"))
- }))
- defer ts.Close()
- res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
- s.Require().Empty(res.Header.Get(httpheaders.LastModified))
- }
- func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedDisabled() {
- s.Config().Handlers.Processing.LastModifiedEnabled = false
- data := s.TestData.Read("test1.png")
- lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
- s.Empty(modifiedSince)
- rw.WriteHeader(200)
- rw.Write(data)
- }))
- defer ts.Close()
- header := make(http.Header)
- header.Set(httpheaders.IfModifiedSince, lastModified)
- res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
- s.Require().Equal(200, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedEnabled() {
- s.Config().Handlers.Processing.LastModifiedEnabled = true
- lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
- s.Equal(lastModified, modifiedSince)
- rw.WriteHeader(304)
- }))
- defer ts.Close()
- header := make(http.Header)
- header.Set(httpheaders.IfModifiedSince, lastModified)
- res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
- s.Require().Equal(304, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedDisabled() {
- data := s.TestData.Read("test1.png")
- s.Config().Handlers.Processing.LastModifiedEnabled = false
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
- s.Empty(modifiedSince)
- rw.WriteHeader(200)
- rw.Write(data)
- }))
- defer ts.Close()
- recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
- header := make(http.Header)
- header.Set(httpheaders.IfModifiedSince, recentTimestamp)
- res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
- s.Require().Equal(200, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedEnabled() {
- s.Config().Handlers.Processing.LastModifiedEnabled = true
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- fileLastModified, _ := time.Parse(http.TimeFormat, "Wed, 21 Oct 2015 07:28:00 GMT")
- modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
- parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
- s.NoError(err)
- s.True(fileLastModified.Before(parsedModifiedSince))
- rw.WriteHeader(304)
- }))
- defer ts.Close()
- recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
- header := make(http.Header)
- header.Set(httpheaders.IfModifiedSince, recentTimestamp)
- res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
- s.Require().Equal(304, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedDisabled() {
- s.Config().Handlers.Processing.LastModifiedEnabled = false
- data := s.TestData.Read("test1.png")
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
- s.Empty(modifiedSince)
- rw.WriteHeader(200)
- rw.Write(data)
- }))
- defer ts.Close()
- oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
- header := make(http.Header)
- header.Set(httpheaders.IfModifiedSince, oldTimestamp)
- res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
- s.Require().Equal(200, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedEnabled() {
- s.Config().Handlers.Processing.LastModifiedEnabled = true
- data := s.TestData.Read("test1.png")
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- fileLastModified, _ := time.Parse(http.TimeFormat, "Wed, 21 Oct 2015 07:28:00 GMT")
- modifiedSince := r.Header.Get(httpheaders.IfModifiedSince)
- parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
- s.NoError(err)
- s.True(fileLastModified.After(parsedModifiedSince))
- rw.WriteHeader(200)
- rw.Write(data)
- }))
- defer ts.Close()
- oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
- header := make(http.Header)
- header.Set(httpheaders.IfModifiedSince, oldTimestamp)
- res := s.GET(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
- s.Require().Equal(200, res.StatusCode)
- }
- func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
- s.Config().Processing.AlwaysRasterizeSvg = true
- res := s.GET("/unsafe/rs:fill:40:40/plain/local:///test1.svg")
- s.Require().Equal(200, res.StatusCode)
- s.Require().Equal("image/png", res.Header.Get(httpheaders.ContentType))
- }
- func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
- s.Config().Processing.AlwaysRasterizeSvg = true
- s.Config().OptionsParser.EnforceWebp = true
- res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
- s.Require().Equal(200, res.StatusCode)
- s.Require().Equal("image/webp", res.Header.Get(httpheaders.ContentType))
- }
- func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
- s.Config().Processing.AlwaysRasterizeSvg = false
- s.Config().OptionsParser.EnforceWebp = true
- res := s.GET("/unsafe/plain/local:///test1.svg")
- s.Require().Equal(200, res.StatusCode)
- s.Require().Equal("image/svg+xml", res.Header.Get(httpheaders.ContentType))
- }
- func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
- s.Config().Processing.AlwaysRasterizeSvg = true
- s.Config().Processing.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
- res := s.GET("/unsafe/plain/local:///test1.svg@svg")
- s.Require().Equal(200, res.StatusCode)
- s.Require().Equal("image/svg+xml", res.Header.Get(httpheaders.ContentType))
- }
- func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
- s.Config().Security.DefaultOptions.MaxSrcFileSize = 1
- ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- rw.WriteHeader(200)
- rw.Write(s.TestData.Read("test1.png"))
- }))
- defer ts.Close()
- res := s.GET("/unsafe/rs:fill:4:4/plain/" + ts.URL)
- s.Require().Equal(422, res.StatusCode)
- }
- func TestProcessingHandler(t *testing.T) {
- suite.Run(t, new(ProcessingHandlerTestSuite))
- }
|