processing_handler_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  1. package main
  2. import (
  3. "fmt"
  4. "io"
  5. "net/http"
  6. "net/http/httptest"
  7. "os"
  8. "path/filepath"
  9. "regexp"
  10. "strings"
  11. "testing"
  12. "time"
  13. "github.com/sirupsen/logrus"
  14. "github.com/stretchr/testify/suite"
  15. "go.withmatt.com/httpheaders"
  16. "github.com/imgproxy/imgproxy/v3/config"
  17. "github.com/imgproxy/imgproxy/v3/config/configurators"
  18. "github.com/imgproxy/imgproxy/v3/etag"
  19. "github.com/imgproxy/imgproxy/v3/imagedata"
  20. "github.com/imgproxy/imgproxy/v3/imagemeta"
  21. "github.com/imgproxy/imgproxy/v3/imagetype"
  22. "github.com/imgproxy/imgproxy/v3/options"
  23. "github.com/imgproxy/imgproxy/v3/router"
  24. "github.com/imgproxy/imgproxy/v3/svg"
  25. "github.com/imgproxy/imgproxy/v3/testutil"
  26. "github.com/imgproxy/imgproxy/v3/vips"
  27. )
  28. type ProcessingHandlerTestSuite struct {
  29. suite.Suite
  30. router *router.Router
  31. }
  32. func (s *ProcessingHandlerTestSuite) SetupSuite() {
  33. config.Reset()
  34. wd, err := os.Getwd()
  35. s.Require().NoError(err)
  36. s.T().Setenv("IMGPROXY_LOCAL_FILESYSTEM_ROOT", filepath.Join(wd, "/testdata"))
  37. s.T().Setenv("IMGPROXY_CLIENT_KEEP_ALIVE_TIMEOUT", "0")
  38. err = initialize()
  39. s.Require().NoError(err)
  40. logrus.SetOutput(io.Discard)
  41. s.router = buildRouter()
  42. }
  43. func (s *ProcessingHandlerTestSuite) TeardownSuite() {
  44. shutdown()
  45. logrus.SetOutput(os.Stdout)
  46. }
  47. func (s *ProcessingHandlerTestSuite) SetupTest() {
  48. // We don't need config.LocalFileSystemRoot anymore as it is used
  49. // only during initialization
  50. config.Reset()
  51. config.AllowLoopbackSourceAddresses = true
  52. }
  53. func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *httptest.ResponseRecorder {
  54. req := httptest.NewRequest(http.MethodGet, path, nil)
  55. rw := httptest.NewRecorder()
  56. if len(header) > 0 {
  57. req.Header = header[0]
  58. }
  59. s.router.ServeHTTP(rw, req)
  60. return rw
  61. }
  62. func (s *ProcessingHandlerTestSuite) readTestFile(name string) []byte {
  63. wd, err := os.Getwd()
  64. s.Require().NoError(err)
  65. data, err := os.ReadFile(filepath.Join(wd, "testdata", name))
  66. s.Require().NoError(err)
  67. return data
  68. }
  69. func (s *ProcessingHandlerTestSuite) readTestImageData(name string) *imagedata.ImageData {
  70. wd, err := os.Getwd()
  71. s.Require().NoError(err)
  72. data, err := os.ReadFile(filepath.Join(wd, "testdata", name))
  73. s.Require().NoError(err)
  74. imgdata, err := imagedata.NewFromBytes(data, make(http.Header))
  75. s.Require().NoError(err)
  76. return imgdata
  77. }
  78. func (s *ProcessingHandlerTestSuite) readImageData(imgdata *imagedata.ImageData) []byte {
  79. data, err := io.ReadAll(imgdata.Reader())
  80. s.Require().NoError(err)
  81. return data
  82. }
  83. func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, *imagedata.ImageData, string) {
  84. poStr := "rs:fill:4:4"
  85. po := options.NewProcessingOptions()
  86. po.ResizingType = options.ResizeFill
  87. po.Width = 4
  88. po.Height = 4
  89. imgdata := s.readTestImageData("test1.png")
  90. if len(imgETag) != 0 {
  91. imgdata.Headers().Set(httpheaders.Etag, imgETag)
  92. }
  93. var h etag.Handler
  94. h.SetActualProcessingOptions(po)
  95. h.SetActualImageData(imgdata)
  96. return poStr, imgdata, h.GenerateActualETag()
  97. }
  98. func (s *ProcessingHandlerTestSuite) TestRequest() {
  99. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  100. res := rw.Result()
  101. s.Require().Equal(200, res.StatusCode)
  102. s.Require().Equal("image/png", res.Header.Get("Content-Type"))
  103. meta, err := imagemeta.DecodeMeta(res.Body)
  104. s.Require().NoError(err)
  105. s.Require().Equal(imagetype.PNG, meta.Format())
  106. s.Require().Equal(4, meta.Width())
  107. s.Require().Equal(4, meta.Height())
  108. }
  109. func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
  110. config.Keys = [][]byte{[]byte("test-key")}
  111. config.Salts = [][]byte{[]byte("test-salt")}
  112. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  113. res := rw.Result()
  114. s.Require().Equal(403, res.StatusCode)
  115. }
  116. func (s *ProcessingHandlerTestSuite) TestSignatureValidationSuccess() {
  117. config.Keys = [][]byte{[]byte("test-key")}
  118. config.Salts = [][]byte{[]byte("test-salt")}
  119. rw := s.send("/My9d3xq_PYpVHsPrCyww0Kh1w5KZeZhIlWhsa4az1TI/rs:fill:4:4/plain/local:///test1.png")
  120. res := rw.Result()
  121. s.Require().Equal(200, res.StatusCode)
  122. }
  123. func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
  124. imagedata.RedirectAllRequestsTo("local:///test1.png")
  125. defer imagedata.StopRedirectingRequests()
  126. tt := []struct {
  127. name string
  128. allowedSources []string
  129. requestPath string
  130. expectedError bool
  131. }{
  132. {
  133. name: "match http URL without wildcard",
  134. allowedSources: []string{"local://", "http://images.dev/"},
  135. requestPath: "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
  136. expectedError: false,
  137. },
  138. {
  139. name: "match http URL with wildcard in hostname single level",
  140. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  141. requestPath: "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
  142. expectedError: false,
  143. },
  144. {
  145. name: "match http URL with wildcard in hostname multiple levels",
  146. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  147. requestPath: "/unsafe/plain/http://a-1.b-2.mycdn.dev/lorem/ipsum.jpg",
  148. expectedError: false,
  149. },
  150. {
  151. name: "no match s3 URL with allowed local and http URLs",
  152. allowedSources: []string{"local://", "http://images.dev/"},
  153. requestPath: "/unsafe/plain/s3://images/lorem/ipsum.jpg",
  154. expectedError: true,
  155. },
  156. {
  157. name: "no match http URL with wildcard in hostname including slash",
  158. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  159. requestPath: "/unsafe/plain/http://other.dev/.mycdn.dev/lorem/ipsum.jpg",
  160. expectedError: true,
  161. },
  162. }
  163. for _, tc := range tt {
  164. s.Run(tc.name, func() {
  165. exps := make([]*regexp.Regexp, len(tc.allowedSources))
  166. for i, pattern := range tc.allowedSources {
  167. exps[i] = configurators.RegexpFromPattern(pattern)
  168. }
  169. config.AllowedSources = exps
  170. rw := s.send(tc.requestPath)
  171. res := rw.Result()
  172. if tc.expectedError {
  173. s.Require().Equal(404, res.StatusCode)
  174. } else {
  175. s.Require().Equal(200, res.StatusCode)
  176. }
  177. })
  178. }
  179. }
  180. func (s *ProcessingHandlerTestSuite) TestSourceNetworkValidation() {
  181. data := s.readTestFile("test1.png")
  182. server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  183. rw.WriteHeader(200)
  184. rw.Write(data)
  185. }))
  186. defer server.Close()
  187. var rw *httptest.ResponseRecorder
  188. u := fmt.Sprintf("/unsafe/rs:fill:4:4/plain/%s/test1.png", server.URL)
  189. rw = s.send(u)
  190. s.Require().Equal(200, rw.Result().StatusCode)
  191. config.AllowLoopbackSourceAddresses = false
  192. rw = s.send(u)
  193. s.Require().Equal(404, rw.Result().StatusCode)
  194. }
  195. func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
  196. vips.DisableLoadSupport(imagetype.PNG)
  197. defer vips.ResetLoadSupport()
  198. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  199. res := rw.Result()
  200. s.Require().Equal(422, res.StatusCode)
  201. }
  202. func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
  203. vips.DisableSaveSupport(imagetype.PNG)
  204. defer vips.ResetSaveSupport()
  205. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
  206. res := rw.Result()
  207. s.Require().Equal(422, res.StatusCode)
  208. }
  209. func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
  210. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  211. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  212. res := rw.Result()
  213. s.Require().Equal(200, res.StatusCode)
  214. expected := s.readTestImageData("test1.png")
  215. s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
  216. }
  217. func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
  218. rw := s.send("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
  219. res := rw.Result()
  220. s.Require().Equal(200, res.StatusCode)
  221. expected := s.readTestImageData("test1.png")
  222. s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
  223. }
  224. func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
  225. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  226. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
  227. res := rw.Result()
  228. s.Require().Equal(200, res.StatusCode)
  229. expected := s.readTestImageData("test1.png")
  230. s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
  231. }
  232. func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
  233. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  234. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
  235. res := rw.Result()
  236. s.Require().Equal(200, res.StatusCode)
  237. expected := s.readTestImageData("test1.png")
  238. s.Require().False(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
  239. }
  240. func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
  241. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
  242. res := rw.Result()
  243. s.Require().Equal(200, res.StatusCode)
  244. expected, err := svg.Sanitize(s.readTestImageData("test1.svg"))
  245. s.Require().NoError(err)
  246. s.Require().True(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
  247. }
  248. func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
  249. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
  250. res := rw.Result()
  251. s.Require().Equal(200, res.StatusCode)
  252. expected := s.readTestImageData("test1.svg")
  253. s.Require().False(testutil.ReadersEqual(s.T(), expected.Reader(), res.Body))
  254. }
  255. func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
  256. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
  257. res := rw.Result()
  258. s.Require().Equal(422, res.StatusCode)
  259. }
  260. func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughCacheControl() {
  261. config.CacheControlPassthrough = true
  262. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  263. rw.Header().Set("Cache-Control", "max-age=1234, public")
  264. rw.Header().Set("Expires", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
  265. rw.WriteHeader(200)
  266. rw.Write(s.readTestFile("test1.png"))
  267. }))
  268. defer ts.Close()
  269. rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  270. res := rw.Result()
  271. s.Require().Equal("max-age=1234, public", res.Header.Get("Cache-Control"))
  272. s.Require().Empty(res.Header.Get("Expires"))
  273. }
  274. func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughExpires() {
  275. config.CacheControlPassthrough = true
  276. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  277. rw.Header().Set("Expires", time.Now().Add(1239*time.Second).UTC().Format(http.TimeFormat))
  278. rw.WriteHeader(200)
  279. rw.Write(s.readTestFile("test1.png"))
  280. }))
  281. defer ts.Close()
  282. rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  283. res := rw.Result()
  284. // Use regex to allow some delay
  285. s.Require().Regexp("max-age=123[0-9], public", res.Header.Get("Cache-Control"))
  286. s.Require().Empty(res.Header.Get("Expires"))
  287. }
  288. func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
  289. config.CacheControlPassthrough = false
  290. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  291. rw.Header().Set("Cache-Control", "max-age=1234, public")
  292. rw.Header().Set("Expires", time.Now().Add(time.Hour).UTC().Format(http.TimeFormat))
  293. rw.WriteHeader(200)
  294. rw.Write(s.readTestFile("test1.png"))
  295. }))
  296. defer ts.Close()
  297. rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  298. res := rw.Result()
  299. s.Require().NotEqual("max-age=1234, public", res.Header.Get("Cache-Control"))
  300. s.Require().Empty(res.Header.Get("Expires"))
  301. }
  302. func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
  303. config.ETagEnabled = false
  304. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  305. res := rw.Result()
  306. s.Require().Equal(200, res.StatusCode)
  307. s.Require().Empty(res.Header.Get("ETag"))
  308. }
  309. func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
  310. config.ETagEnabled = true
  311. poStr, imgdata, etag := s.sampleETagData("loremipsumdolor")
  312. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  313. s.Empty(r.Header.Get("If-None-Match"))
  314. rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
  315. rw.WriteHeader(200)
  316. rw.Write(s.readTestFile("test1.png"))
  317. }))
  318. defer ts.Close()
  319. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL))
  320. res := rw.Result()
  321. s.Require().Equal(200, res.StatusCode)
  322. s.Require().Equal(etag, res.Header.Get("ETag"))
  323. }
  324. func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
  325. config.ETagEnabled = true
  326. poStr, imgdata, etag := s.sampleETagData("")
  327. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  328. s.Empty(r.Header.Get("If-None-Match"))
  329. rw.WriteHeader(200)
  330. rw.Write(s.readImageData(imgdata))
  331. }))
  332. defer ts.Close()
  333. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL))
  334. res := rw.Result()
  335. s.Require().Equal(200, res.StatusCode)
  336. s.Require().Equal(etag, res.Header.Get("ETag"))
  337. }
  338. func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
  339. config.ETagEnabled = true
  340. poStr, imgdata, etag := s.sampleETagData(`"loremipsumdolor"`)
  341. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  342. s.Equal(imgdata.Headers().Get(httpheaders.Etag), r.Header.Get(httpheaders.IfNoneMatch))
  343. rw.WriteHeader(304)
  344. }))
  345. defer ts.Close()
  346. header := make(http.Header)
  347. header.Set("If-None-Match", etag)
  348. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  349. res := rw.Result()
  350. s.Require().Equal(304, res.StatusCode)
  351. s.Require().Equal(etag, res.Header.Get("ETag"))
  352. }
  353. func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
  354. config.ETagEnabled = true
  355. poStr, imgdata, etag := s.sampleETagData("")
  356. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  357. s.Empty(r.Header.Get("If-None-Match"))
  358. rw.WriteHeader(200)
  359. rw.Write(s.readImageData(imgdata))
  360. }))
  361. defer ts.Close()
  362. header := make(http.Header)
  363. header.Set("If-None-Match", etag)
  364. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  365. res := rw.Result()
  366. s.Require().Equal(304, res.StatusCode)
  367. s.Require().Equal(etag, res.Header.Get("ETag"))
  368. }
  369. func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
  370. config.ETagEnabled = true
  371. poStr, imgdata, actualETag := s.sampleETagData(`"loremipsumdolor"`)
  372. _, _, expectedETag := s.sampleETagData(`"loremipsum"`)
  373. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  374. s.Equal(`"loremipsum"`, r.Header.Get("If-None-Match"))
  375. rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
  376. rw.WriteHeader(200)
  377. rw.Write(s.readImageData(imgdata))
  378. }))
  379. defer ts.Close()
  380. header := make(http.Header)
  381. header.Set("If-None-Match", expectedETag)
  382. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  383. res := rw.Result()
  384. s.Require().Equal(200, res.StatusCode)
  385. s.Require().Equal(actualETag, res.Header.Get("ETag"))
  386. }
  387. func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
  388. config.ETagEnabled = true
  389. poStr, imgdata, actualETag := s.sampleETagData("")
  390. // Change the data hash
  391. expectedETag := actualETag[:strings.IndexByte(actualETag, '/')] + "/Dasdbefj"
  392. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  393. s.Empty(r.Header.Get("If-None-Match"))
  394. rw.WriteHeader(200)
  395. rw.Write(s.readImageData(imgdata))
  396. }))
  397. defer ts.Close()
  398. header := make(http.Header)
  399. header.Set("If-None-Match", expectedETag)
  400. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  401. res := rw.Result()
  402. s.Require().Equal(200, res.StatusCode)
  403. s.Require().Equal(actualETag, res.Header.Get("ETag"))
  404. }
  405. func (s *ProcessingHandlerTestSuite) TestETagProcessingOptionsNotMatch() {
  406. config.ETagEnabled = true
  407. poStr, imgdata, actualETag := s.sampleETagData("")
  408. // Change the processing options hash
  409. expectedETag := "abcdefj" + actualETag[strings.IndexByte(actualETag, '/'):]
  410. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  411. s.Empty(r.Header.Get("If-None-Match"))
  412. rw.Header().Set("ETag", imgdata.Headers().Get(httpheaders.Etag))
  413. rw.WriteHeader(200)
  414. rw.Write(s.readImageData(imgdata))
  415. }))
  416. defer ts.Close()
  417. header := make(http.Header)
  418. header.Set("If-None-Match", expectedETag)
  419. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  420. res := rw.Result()
  421. s.Require().Equal(200, res.StatusCode)
  422. s.Require().Equal(actualETag, res.Header.Get("ETag"))
  423. }
  424. func (s *ProcessingHandlerTestSuite) TestLastModifiedEnabled() {
  425. config.LastModifiedEnabled = true
  426. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  427. rw.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
  428. rw.WriteHeader(200)
  429. rw.Write(s.readTestFile("test1.png"))
  430. }))
  431. defer ts.Close()
  432. rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  433. res := rw.Result()
  434. s.Require().Equal("Wed, 21 Oct 2015 07:28:00 GMT", res.Header.Get("Last-Modified"))
  435. }
  436. func (s *ProcessingHandlerTestSuite) TestLastModifiedDisabled() {
  437. config.LastModifiedEnabled = false
  438. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  439. rw.Header().Set("Last-Modified", "Wed, 21 Oct 2015 07:28:00 GMT")
  440. rw.WriteHeader(200)
  441. rw.Write(s.readTestFile("test1.png"))
  442. }))
  443. defer ts.Close()
  444. rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  445. res := rw.Result()
  446. s.Require().Empty(res.Header.Get("Last-Modified"))
  447. }
  448. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedDisabled() {
  449. config.LastModifiedEnabled = false
  450. data := s.readTestFile("test1.png")
  451. lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
  452. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  453. modifiedSince := r.Header.Get("If-Modified-Since")
  454. s.Empty(modifiedSince)
  455. rw.WriteHeader(200)
  456. rw.Write(data)
  457. }))
  458. defer ts.Close()
  459. header := make(http.Header)
  460. header.Set("If-Modified-Since", lastModified)
  461. rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  462. res := rw.Result()
  463. s.Require().Equal(200, res.StatusCode)
  464. }
  465. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqExactMatchLastModifiedEnabled() {
  466. config.LastModifiedEnabled = true
  467. lastModified := "Wed, 21 Oct 2015 07:28:00 GMT"
  468. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  469. modifiedSince := r.Header.Get("If-Modified-Since")
  470. s.Equal(lastModified, modifiedSince)
  471. rw.WriteHeader(304)
  472. }))
  473. defer ts.Close()
  474. header := make(http.Header)
  475. header.Set("If-Modified-Since", lastModified)
  476. rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  477. res := rw.Result()
  478. s.Require().Equal(304, res.StatusCode)
  479. }
  480. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedDisabled() {
  481. data := s.readTestFile("test1.png")
  482. config.LastModifiedEnabled = false
  483. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  484. modifiedSince := r.Header.Get("If-Modified-Since")
  485. s.Empty(modifiedSince)
  486. rw.WriteHeader(200)
  487. rw.Write(data)
  488. }))
  489. defer ts.Close()
  490. recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
  491. header := make(http.Header)
  492. header.Set("If-Modified-Since", recentTimestamp)
  493. rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  494. res := rw.Result()
  495. s.Require().Equal(200, res.StatusCode)
  496. }
  497. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareMoreRecentLastModifiedEnabled() {
  498. config.LastModifiedEnabled = true
  499. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  500. fileLastModified, _ := time.Parse(http.TimeFormat, "Wed, 21 Oct 2015 07:28:00 GMT")
  501. modifiedSince := r.Header.Get("If-Modified-Since")
  502. parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
  503. s.NoError(err)
  504. s.True(fileLastModified.Before(parsedModifiedSince))
  505. rw.WriteHeader(304)
  506. }))
  507. defer ts.Close()
  508. recentTimestamp := "Thu, 25 Feb 2021 01:45:00 GMT"
  509. header := make(http.Header)
  510. header.Set("If-Modified-Since", recentTimestamp)
  511. rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  512. res := rw.Result()
  513. s.Require().Equal(304, res.StatusCode)
  514. }
  515. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedDisabled() {
  516. config.LastModifiedEnabled = false
  517. data := s.readTestFile("test1.png")
  518. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  519. modifiedSince := r.Header.Get("If-Modified-Since")
  520. s.Empty(modifiedSince)
  521. rw.WriteHeader(200)
  522. rw.Write(data)
  523. }))
  524. defer ts.Close()
  525. oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
  526. header := make(http.Header)
  527. header.Set("If-Modified-Since", oldTimestamp)
  528. rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  529. res := rw.Result()
  530. s.Require().Equal(200, res.StatusCode)
  531. }
  532. func (s *ProcessingHandlerTestSuite) TestModifiedSinceReqCompareTooOldLastModifiedEnabled() {
  533. config.LastModifiedEnabled = true
  534. data := s.readTestFile("test1.png")
  535. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  536. fileLastModified, _ := time.Parse(http.TimeFormat, "Wed, 21 Oct 2015 07:28:00 GMT")
  537. modifiedSince := r.Header.Get("If-Modified-Since")
  538. parsedModifiedSince, err := time.Parse(http.TimeFormat, modifiedSince)
  539. s.NoError(err)
  540. s.True(fileLastModified.After(parsedModifiedSince))
  541. rw.WriteHeader(200)
  542. rw.Write(data)
  543. }))
  544. defer ts.Close()
  545. oldTimestamp := "Tue, 01 Oct 2013 17:31:00 GMT"
  546. header := make(http.Header)
  547. header.Set("If-Modified-Since", oldTimestamp)
  548. rw := s.send(fmt.Sprintf("/unsafe/plain/%s", ts.URL), header)
  549. res := rw.Result()
  550. s.Require().Equal(200, res.StatusCode)
  551. }
  552. func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
  553. config.AlwaysRasterizeSvg = true
  554. rw := s.send("/unsafe/rs:fill:40:40/plain/local:///test1.svg")
  555. res := rw.Result()
  556. s.Require().Equal(200, res.StatusCode)
  557. s.Require().Equal("image/png", res.Header.Get("Content-Type"))
  558. }
  559. func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
  560. config.AlwaysRasterizeSvg = true
  561. config.EnforceWebp = true
  562. rw := s.send("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
  563. res := rw.Result()
  564. s.Require().Equal(200, res.StatusCode)
  565. s.Require().Equal("image/webp", res.Header.Get("Content-Type"))
  566. }
  567. func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
  568. config.AlwaysRasterizeSvg = false
  569. config.EnforceWebp = true
  570. rw := s.send("/unsafe/plain/local:///test1.svg")
  571. res := rw.Result()
  572. s.Require().Equal(200, res.StatusCode)
  573. s.Require().Equal("image/svg+xml", res.Header.Get("Content-Type"))
  574. }
  575. func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
  576. config.AlwaysRasterizeSvg = true
  577. config.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
  578. rw := s.send("/unsafe/plain/local:///test1.svg@svg")
  579. res := rw.Result()
  580. s.Require().Equal(200, res.StatusCode)
  581. s.Require().Equal("image/svg+xml", res.Header.Get("Content-Type"))
  582. }
  583. func TestProcessingHandler(t *testing.T) {
  584. suite.Run(t, new(ProcessingHandlerTestSuite))
  585. }