processing_handler_test.go 15 KB

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