processing_handler_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529
  1. package main
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io/ioutil"
  6. "net/http"
  7. "net/http/httptest"
  8. "os"
  9. "path/filepath"
  10. "regexp"
  11. "strings"
  12. "testing"
  13. "github.com/imgproxy/imgproxy/v3/config"
  14. "github.com/imgproxy/imgproxy/v3/config/configurators"
  15. "github.com/imgproxy/imgproxy/v3/etag"
  16. "github.com/imgproxy/imgproxy/v3/imagedata"
  17. "github.com/imgproxy/imgproxy/v3/imagemeta"
  18. "github.com/imgproxy/imgproxy/v3/imagetype"
  19. "github.com/imgproxy/imgproxy/v3/options"
  20. "github.com/imgproxy/imgproxy/v3/router"
  21. "github.com/imgproxy/imgproxy/v3/svg"
  22. "github.com/imgproxy/imgproxy/v3/vips"
  23. "github.com/sirupsen/logrus"
  24. "github.com/stretchr/testify/assert"
  25. "github.com/stretchr/testify/suite"
  26. )
  27. type ProcessingHandlerTestSuite struct {
  28. suite.Suite
  29. router *router.Router
  30. }
  31. func (s *ProcessingHandlerTestSuite) SetupSuite() {
  32. config.Reset()
  33. wd, err := os.Getwd()
  34. assert.Nil(s.T(), err)
  35. config.LocalFileSystemRoot = filepath.Join(wd, "/testdata")
  36. initialize()
  37. logrus.SetOutput(ioutil.Discard)
  38. s.router = buildRouter()
  39. }
  40. func (s *ProcessingHandlerTestSuite) TeardownSuite() {
  41. shutdown()
  42. logrus.SetOutput(os.Stdout)
  43. }
  44. func (s *ProcessingHandlerTestSuite) SetupTest() {
  45. // We don't need config.LocalFileSystemRoot anymore as it is used
  46. // only during initialization
  47. config.Reset()
  48. }
  49. func (s *ProcessingHandlerTestSuite) send(path string, header ...http.Header) *httptest.ResponseRecorder {
  50. req := httptest.NewRequest(http.MethodGet, path, nil)
  51. rw := httptest.NewRecorder()
  52. if len(header) > 0 {
  53. req.Header = header[0]
  54. }
  55. s.router.ServeHTTP(rw, req)
  56. return rw
  57. }
  58. func (s *ProcessingHandlerTestSuite) readTestFile(name string) []byte {
  59. wd, err := os.Getwd()
  60. assert.Nil(s.T(), err)
  61. data, err := ioutil.ReadFile(filepath.Join(wd, "testdata", name))
  62. assert.Nil(s.T(), err)
  63. return data
  64. }
  65. func (s *ProcessingHandlerTestSuite) readBody(res *http.Response) []byte {
  66. data, err := ioutil.ReadAll(res.Body)
  67. assert.Nil(s.T(), err)
  68. return data
  69. }
  70. func (s *ProcessingHandlerTestSuite) sampleETagData(imgETag string) (string, *imagedata.ImageData, string) {
  71. poStr := "rs:fill:4:4"
  72. po := options.NewProcessingOptions()
  73. po.ResizingType = options.ResizeFill
  74. po.Width = 4
  75. po.Height = 4
  76. imgdata := imagedata.ImageData{
  77. Type: imagetype.PNG,
  78. Data: s.readTestFile("test1.png"),
  79. }
  80. if len(imgETag) != 0 {
  81. imgdata.Headers = map[string]string{"ETag": imgETag}
  82. }
  83. var h etag.Handler
  84. h.SetActualProcessingOptions(po)
  85. h.SetActualImageData(&imgdata)
  86. return poStr, &imgdata, h.GenerateActualETag()
  87. }
  88. func (s *ProcessingHandlerTestSuite) TestRequest() {
  89. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  90. res := rw.Result()
  91. assert.Equal(s.T(), 200, res.StatusCode)
  92. assert.Equal(s.T(), "image/png", res.Header.Get("Content-Type"))
  93. meta, err := imagemeta.DecodeMeta(res.Body)
  94. assert.Nil(s.T(), err)
  95. assert.Equal(s.T(), imagetype.PNG, meta.Format())
  96. assert.Equal(s.T(), 4, meta.Width())
  97. assert.Equal(s.T(), 4, meta.Height())
  98. }
  99. func (s *ProcessingHandlerTestSuite) TestSignatureValidationFailure() {
  100. config.Keys = [][]byte{[]byte("test-key")}
  101. config.Salts = [][]byte{[]byte("test-salt")}
  102. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  103. res := rw.Result()
  104. assert.Equal(s.T(), 403, res.StatusCode)
  105. }
  106. func (s *ProcessingHandlerTestSuite) TestSignatureValidationSuccess() {
  107. config.Keys = [][]byte{[]byte("test-key")}
  108. config.Salts = [][]byte{[]byte("test-salt")}
  109. rw := s.send("/My9d3xq_PYpVHsPrCyww0Kh1w5KZeZhIlWhsa4az1TI/rs:fill:4:4/plain/local:///test1.png")
  110. res := rw.Result()
  111. assert.Equal(s.T(), 200, res.StatusCode)
  112. }
  113. func (s *ProcessingHandlerTestSuite) TestSourceValidation() {
  114. imagedata.RedirectAllRequestsTo("local:///test1.png")
  115. defer imagedata.StopRedirectingRequests()
  116. tt := []struct {
  117. name string
  118. allowedSources []string
  119. requestPath string
  120. expectedError bool
  121. }{
  122. {
  123. name: "match http URL without wildcard",
  124. allowedSources: []string{"local://", "http://images.dev/"},
  125. requestPath: "/unsafe/plain/http://images.dev/lorem/ipsum.jpg",
  126. expectedError: false,
  127. },
  128. {
  129. name: "match http URL with wildcard in hostname single level",
  130. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  131. requestPath: "/unsafe/plain/http://a-1.mycdn.dev/lorem/ipsum.jpg",
  132. expectedError: false,
  133. },
  134. {
  135. name: "match http URL with wildcard in hostname multiple levels",
  136. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  137. requestPath: "/unsafe/plain/http://a-1.b-2.mycdn.dev/lorem/ipsum.jpg",
  138. expectedError: false,
  139. },
  140. {
  141. name: "no match s3 URL with allowed local and http URLs",
  142. allowedSources: []string{"local://", "http://images.dev/"},
  143. requestPath: "/unsafe/plain/s3://images/lorem/ipsum.jpg",
  144. expectedError: true,
  145. },
  146. {
  147. name: "no match http URL with wildcard in hostname including slash",
  148. allowedSources: []string{"local://", "http://*.mycdn.dev/"},
  149. requestPath: "/unsafe/plain/http://other.dev/.mycdn.dev/lorem/ipsum.jpg",
  150. expectedError: true,
  151. },
  152. }
  153. for _, tc := range tt {
  154. s.T().Run(tc.name, func(t *testing.T) {
  155. exps := make([]*regexp.Regexp, len(tc.allowedSources))
  156. for i, pattern := range tc.allowedSources {
  157. exps[i] = configurators.RegexpFromPattern(pattern)
  158. }
  159. config.AllowedSources = exps
  160. rw := s.send(tc.requestPath)
  161. res := rw.Result()
  162. if tc.expectedError {
  163. assert.Equal(s.T(), 404, res.StatusCode)
  164. } else {
  165. assert.Equal(s.T(), 200, res.StatusCode)
  166. }
  167. })
  168. }
  169. }
  170. func (s *ProcessingHandlerTestSuite) TestSourceFormatNotSupported() {
  171. vips.DisableLoadSupport(imagetype.PNG)
  172. defer vips.ResetLoadSupport()
  173. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  174. res := rw.Result()
  175. assert.Equal(s.T(), 422, res.StatusCode)
  176. }
  177. func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
  178. vips.DisableSaveSupport(imagetype.PNG)
  179. defer vips.ResetSaveSupport()
  180. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
  181. res := rw.Result()
  182. assert.Equal(s.T(), 422, res.StatusCode)
  183. }
  184. func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
  185. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  186. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  187. res := rw.Result()
  188. assert.Equal(s.T(), 200, res.StatusCode)
  189. actual := s.readBody(res)
  190. expected := s.readTestFile("test1.png")
  191. assert.True(s.T(), bytes.Equal(expected, actual))
  192. }
  193. func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
  194. rw := s.send("/unsafe/rs:fill:4:4/skp:png/plain/local:///test1.png")
  195. res := rw.Result()
  196. assert.Equal(s.T(), 200, res.StatusCode)
  197. actual := s.readBody(res)
  198. expected := s.readTestFile("test1.png")
  199. assert.True(s.T(), bytes.Equal(expected, actual))
  200. }
  201. func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
  202. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  203. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
  204. res := rw.Result()
  205. assert.Equal(s.T(), 200, res.StatusCode)
  206. actual := s.readBody(res)
  207. expected := s.readTestFile("test1.png")
  208. assert.True(s.T(), bytes.Equal(expected, actual))
  209. }
  210. func (s *ProcessingHandlerTestSuite) TestSkipProcessingDifferentFormat() {
  211. config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
  212. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@jpg")
  213. res := rw.Result()
  214. assert.Equal(s.T(), 200, res.StatusCode)
  215. actual := s.readBody(res)
  216. expected := s.readTestFile("test1.png")
  217. assert.False(s.T(), bytes.Equal(expected, actual))
  218. }
  219. func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
  220. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg")
  221. res := rw.Result()
  222. assert.Equal(s.T(), 200, res.StatusCode)
  223. actual := s.readBody(res)
  224. expected, err := svg.Satitize(s.readTestFile("test1.svg"))
  225. assert.Nil(s.T(), err)
  226. assert.True(s.T(), bytes.Equal(expected, actual))
  227. }
  228. func (s *ProcessingHandlerTestSuite) TestNotSkipProcessingSVGToJPG() {
  229. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.svg@jpg")
  230. res := rw.Result()
  231. assert.Equal(s.T(), 200, res.StatusCode)
  232. actual := s.readBody(res)
  233. expected := s.readTestFile("test1.svg")
  234. assert.False(s.T(), bytes.Equal(expected, actual))
  235. }
  236. func (s *ProcessingHandlerTestSuite) TestErrorSavingToSVG() {
  237. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png@svg")
  238. res := rw.Result()
  239. assert.Equal(s.T(), 422, res.StatusCode)
  240. }
  241. func (s *ProcessingHandlerTestSuite) TestCacheControlPassthrough() {
  242. config.CacheControlPassthrough = true
  243. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  244. rw.Header().Set("Cache-Control", "fake-cache-control")
  245. rw.Header().Set("Expires", "fake-expires")
  246. rw.WriteHeader(200)
  247. rw.Write(s.readTestFile("test1.png"))
  248. }))
  249. defer ts.Close()
  250. rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  251. res := rw.Result()
  252. assert.Equal(s.T(), "fake-cache-control", res.Header.Get("Cache-Control"))
  253. assert.Equal(s.T(), "fake-expires", res.Header.Get("Expires"))
  254. }
  255. func (s *ProcessingHandlerTestSuite) TestCacheControlPassthroughDisabled() {
  256. config.CacheControlPassthrough = false
  257. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  258. rw.Header().Set("Cache-Control", "fake-cache-control")
  259. rw.Header().Set("Expires", "fake-expires")
  260. rw.WriteHeader(200)
  261. rw.Write(s.readTestFile("test1.png"))
  262. }))
  263. defer ts.Close()
  264. rw := s.send("/unsafe/rs:fill:4:4/plain/" + ts.URL)
  265. res := rw.Result()
  266. assert.NotEqual(s.T(), "fake-cache-control", res.Header.Get("Cache-Control"))
  267. assert.NotEqual(s.T(), "fake-expires", res.Header.Get("Expires"))
  268. }
  269. func (s *ProcessingHandlerTestSuite) TestETagDisabled() {
  270. config.ETagEnabled = false
  271. rw := s.send("/unsafe/rs:fill:4:4/plain/local:///test1.png")
  272. res := rw.Result()
  273. assert.Equal(s.T(), 200, res.StatusCode)
  274. assert.Empty(s.T(), res.Header.Get("ETag"))
  275. }
  276. func (s *ProcessingHandlerTestSuite) TestETagReqNoIfNotModified() {
  277. config.ETagEnabled = true
  278. poStr, imgdata, etag := s.sampleETagData("loremipsumdolor")
  279. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  280. assert.Empty(s.T(), r.Header.Get("If-None-Match"))
  281. rw.Header().Set("ETag", imgdata.Headers["ETag"])
  282. rw.WriteHeader(200)
  283. rw.Write(s.readTestFile("test1.png"))
  284. }))
  285. defer ts.Close()
  286. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL))
  287. res := rw.Result()
  288. assert.Equal(s.T(), 200, res.StatusCode)
  289. assert.Equal(s.T(), etag, res.Header.Get("ETag"))
  290. }
  291. func (s *ProcessingHandlerTestSuite) TestETagDataNoIfNotModified() {
  292. config.ETagEnabled = true
  293. poStr, imgdata, etag := s.sampleETagData("")
  294. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  295. assert.Empty(s.T(), r.Header.Get("If-None-Match"))
  296. rw.WriteHeader(200)
  297. rw.Write(imgdata.Data)
  298. }))
  299. defer ts.Close()
  300. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL))
  301. res := rw.Result()
  302. assert.Equal(s.T(), 200, res.StatusCode)
  303. assert.Equal(s.T(), etag, res.Header.Get("ETag"))
  304. }
  305. func (s *ProcessingHandlerTestSuite) TestETagReqMatch() {
  306. config.ETagEnabled = true
  307. poStr, imgdata, etag := s.sampleETagData(`"loremipsumdolor"`)
  308. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  309. assert.Equal(s.T(), imgdata.Headers["ETag"], r.Header.Get("If-None-Match"))
  310. rw.WriteHeader(304)
  311. }))
  312. defer ts.Close()
  313. header := make(http.Header)
  314. header.Set("If-None-Match", etag)
  315. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  316. res := rw.Result()
  317. assert.Equal(s.T(), 304, res.StatusCode)
  318. assert.Equal(s.T(), etag, res.Header.Get("ETag"))
  319. }
  320. func (s *ProcessingHandlerTestSuite) TestETagDataMatch() {
  321. config.ETagEnabled = true
  322. poStr, imgdata, etag := s.sampleETagData("")
  323. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  324. assert.Empty(s.T(), r.Header.Get("If-None-Match"))
  325. rw.WriteHeader(200)
  326. rw.Write(imgdata.Data)
  327. }))
  328. defer ts.Close()
  329. header := make(http.Header)
  330. header.Set("If-None-Match", etag)
  331. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  332. res := rw.Result()
  333. assert.Equal(s.T(), 304, res.StatusCode)
  334. assert.Equal(s.T(), etag, res.Header.Get("ETag"))
  335. }
  336. func (s *ProcessingHandlerTestSuite) TestETagReqNotMatch() {
  337. config.ETagEnabled = true
  338. poStr, imgdata, actualETag := s.sampleETagData(`"loremipsumdolor"`)
  339. _, _, expectedETag := s.sampleETagData(`"loremipsum"`)
  340. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  341. assert.Equal(s.T(), `"loremipsum"`, r.Header.Get("If-None-Match"))
  342. rw.Header().Set("ETag", imgdata.Headers["ETag"])
  343. rw.WriteHeader(200)
  344. rw.Write(imgdata.Data)
  345. }))
  346. defer ts.Close()
  347. header := make(http.Header)
  348. header.Set("If-None-Match", expectedETag)
  349. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  350. res := rw.Result()
  351. assert.Equal(s.T(), 200, res.StatusCode)
  352. assert.Equal(s.T(), actualETag, res.Header.Get("ETag"))
  353. }
  354. func (s *ProcessingHandlerTestSuite) TestETagDataNotMatch() {
  355. config.ETagEnabled = true
  356. poStr, imgdata, actualETag := s.sampleETagData("")
  357. // Change the data hash
  358. expectedETag := actualETag[:strings.IndexByte(actualETag, '/')] + "/Dasdbefj"
  359. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  360. assert.Empty(s.T(), r.Header.Get("If-None-Match"))
  361. rw.WriteHeader(200)
  362. rw.Write(imgdata.Data)
  363. }))
  364. defer ts.Close()
  365. header := make(http.Header)
  366. header.Set("If-None-Match", expectedETag)
  367. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  368. res := rw.Result()
  369. assert.Equal(s.T(), 200, res.StatusCode)
  370. assert.Equal(s.T(), actualETag, res.Header.Get("ETag"))
  371. }
  372. func (s *ProcessingHandlerTestSuite) TestETagProcessingOptionsNotMatch() {
  373. config.ETagEnabled = true
  374. poStr, imgdata, actualETag := s.sampleETagData("")
  375. // Change the processing options hash
  376. expectedETag := "abcdefj" + actualETag[strings.IndexByte(actualETag, '/'):]
  377. ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
  378. assert.Empty(s.T(), r.Header.Get("If-None-Match"))
  379. rw.Header().Set("ETag", imgdata.Headers["ETag"])
  380. rw.WriteHeader(200)
  381. rw.Write(imgdata.Data)
  382. }))
  383. defer ts.Close()
  384. header := make(http.Header)
  385. header.Set("If-None-Match", expectedETag)
  386. rw := s.send(fmt.Sprintf("/unsafe/%s/plain/%s", poStr, ts.URL), header)
  387. res := rw.Result()
  388. assert.Equal(s.T(), 200, res.StatusCode)
  389. assert.Equal(s.T(), actualETag, res.Header.Get("ETag"))
  390. }
  391. func TestProcessingHandler(t *testing.T) {
  392. suite.Run(t, new(ProcessingHandlerTestSuite))
  393. }