processing_handler_test.go 14 KB

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