processing_handler.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. package main
  2. import (
  3. "errors"
  4. "io"
  5. "net/http"
  6. "net/url"
  7. "strconv"
  8. "strings"
  9. log "github.com/sirupsen/logrus"
  10. "golang.org/x/sync/semaphore"
  11. "github.com/imgproxy/imgproxy/v3/config"
  12. "github.com/imgproxy/imgproxy/v3/cookies"
  13. "github.com/imgproxy/imgproxy/v3/errorreport"
  14. "github.com/imgproxy/imgproxy/v3/handlers/stream"
  15. "github.com/imgproxy/imgproxy/v3/headerwriter"
  16. "github.com/imgproxy/imgproxy/v3/httpheaders"
  17. "github.com/imgproxy/imgproxy/v3/ierrors"
  18. "github.com/imgproxy/imgproxy/v3/imagedata"
  19. "github.com/imgproxy/imgproxy/v3/imagefetcher"
  20. "github.com/imgproxy/imgproxy/v3/imagetype"
  21. "github.com/imgproxy/imgproxy/v3/monitoring"
  22. "github.com/imgproxy/imgproxy/v3/monitoring/stats"
  23. "github.com/imgproxy/imgproxy/v3/options"
  24. "github.com/imgproxy/imgproxy/v3/processing"
  25. "github.com/imgproxy/imgproxy/v3/security"
  26. "github.com/imgproxy/imgproxy/v3/server"
  27. "github.com/imgproxy/imgproxy/v3/vips"
  28. )
  29. var (
  30. queueSem *semaphore.Weighted
  31. processingSem *semaphore.Weighted
  32. )
  33. func initProcessingHandler() {
  34. if config.RequestsQueueSize > 0 {
  35. queueSem = semaphore.NewWeighted(int64(config.RequestsQueueSize + config.Workers))
  36. }
  37. processingSem = semaphore.NewWeighted(int64(config.Workers))
  38. }
  39. // writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
  40. func writeDebugHeaders(rw http.ResponseWriter, result *processing.Result, originData imagedata.ImageData) error {
  41. if !config.EnableDebugHeaders {
  42. return nil
  43. }
  44. if result != nil {
  45. rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
  46. rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
  47. rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
  48. rw.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
  49. }
  50. // Try to read origin image size
  51. size, err := originData.Size()
  52. if err != nil {
  53. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
  54. }
  55. rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
  56. return nil
  57. }
  58. func respondWithImage(
  59. reqID string,
  60. r *http.Request,
  61. rw http.ResponseWriter,
  62. statusCode int,
  63. resultData imagedata.ImageData,
  64. po *options.ProcessingOptions,
  65. originURL string,
  66. hw *headerwriter.Request,
  67. ) error {
  68. // We read the size of the image data here, so we can set Content-Length header.
  69. // This indireclty ensures that the image data is fully read from the source, no
  70. // errors happened.
  71. resultSize, err := resultData.Size()
  72. if err != nil {
  73. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
  74. }
  75. hw.SetContentType(resultData.Format().Mime())
  76. hw.SetContentLength(resultSize)
  77. hw.SetContentDisposition(
  78. originURL,
  79. po.Filename,
  80. resultData.Format().Ext(),
  81. "",
  82. po.ReturnAttachment,
  83. )
  84. hw.SetExpires(po.Expires)
  85. hw.SetVary()
  86. hw.SetCanonical()
  87. if config.LastModifiedEnabled {
  88. hw.Passthrough(httpheaders.LastModified)
  89. }
  90. if config.ETagEnabled {
  91. hw.Passthrough(httpheaders.Etag)
  92. }
  93. hw.Write(rw)
  94. rw.WriteHeader(statusCode)
  95. _, err = io.Copy(rw, resultData.Reader())
  96. var ierr *ierrors.Error
  97. if err != nil {
  98. ierr = newResponseWriteError(err)
  99. if config.ReportIOErrors {
  100. return ierrors.Wrap(ierr, 0, ierrors.WithCategory(categoryIO), ierrors.WithShouldReport(true))
  101. }
  102. }
  103. server.LogResponse(
  104. reqID, r, statusCode, ierr,
  105. log.Fields{
  106. "image_url": originURL,
  107. "processing_options": po,
  108. },
  109. )
  110. return nil
  111. }
  112. func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, originURL string, hw *headerwriter.Request) {
  113. hw.SetExpires(po.Expires)
  114. hw.SetVary()
  115. if config.ETagEnabled {
  116. hw.Passthrough(httpheaders.Etag)
  117. }
  118. hw.Write(rw)
  119. rw.WriteHeader(http.StatusNotModified)
  120. server.LogResponse(
  121. reqID, r, http.StatusNotModified, nil,
  122. log.Fields{
  123. "image_url": originURL,
  124. "processing_options": po,
  125. },
  126. )
  127. }
  128. func callHandleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) error {
  129. // NOTE: This is temporary, will be moved level up at once
  130. hwc, err := headerwriter.NewDefaultConfig().LoadFromEnv()
  131. if err != nil {
  132. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
  133. }
  134. hw, err := headerwriter.New(hwc)
  135. if err != nil {
  136. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
  137. }
  138. sc, err := stream.NewDefaultConfig().LoadFromEnv()
  139. if err != nil {
  140. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
  141. }
  142. stream, err := stream.New(sc, hw, imagedata.Fetcher)
  143. if err != nil {
  144. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
  145. }
  146. return handleProcessing(reqID, rw, r, hw, stream)
  147. }
  148. func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request, hw *headerwriter.Writer, stream *stream.Handler) error {
  149. stats.IncRequestsInProgress()
  150. defer stats.DecRequestsInProgress()
  151. ctx := r.Context()
  152. path := r.RequestURI
  153. if queryStart := strings.IndexByte(path, '?'); queryStart >= 0 {
  154. path = path[:queryStart]
  155. }
  156. if len(config.PathPrefix) > 0 {
  157. path = strings.TrimPrefix(path, config.PathPrefix)
  158. }
  159. path = strings.TrimPrefix(path, "/")
  160. signature := ""
  161. if signatureEnd := strings.IndexByte(path, '/'); signatureEnd > 0 {
  162. signature = path[:signatureEnd]
  163. path = path[signatureEnd:]
  164. } else {
  165. return ierrors.Wrap(
  166. newInvalidURLErrorf(http.StatusNotFound, "Invalid path: %s", path), 0,
  167. ierrors.WithCategory(categoryPathParsing),
  168. )
  169. }
  170. path = fixPath(path)
  171. if err := security.VerifySignature(signature, path); err != nil {
  172. return ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
  173. }
  174. po, imageURL, err := options.ParsePath(path, r.Header)
  175. if err != nil {
  176. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryPathParsing))
  177. }
  178. var imageOrigin any
  179. if u, uerr := url.Parse(imageURL); uerr == nil {
  180. imageOrigin = u.Scheme + "://" + u.Host
  181. }
  182. errorreport.SetMetadata(r, "Source Image URL", imageURL)
  183. errorreport.SetMetadata(r, "Source Image Origin", imageOrigin)
  184. errorreport.SetMetadata(r, "Processing Options", po)
  185. monitoringMeta := monitoring.Meta{
  186. monitoring.MetaSourceImageURL: imageURL,
  187. monitoring.MetaSourceImageOrigin: imageOrigin,
  188. monitoring.MetaProcessingOptions: po.Diff().Flatten(),
  189. }
  190. monitoring.SetMetadata(ctx, monitoringMeta)
  191. err = security.VerifySourceURL(imageURL)
  192. if err != nil {
  193. return ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
  194. }
  195. if po.Raw {
  196. return stream.Execute(ctx, r, imageURL, reqID, po, rw)
  197. }
  198. // SVG is a special case. Though saving to svg is not supported, SVG->SVG is.
  199. if !vips.SupportsSave(po.Format) && po.Format != imagetype.Unknown && po.Format != imagetype.SVG {
  200. return ierrors.Wrap(newInvalidURLErrorf(
  201. http.StatusUnprocessableEntity,
  202. "Resulting image format is not supported: %s", po.Format,
  203. ), 0, ierrors.WithCategory(categoryPathParsing))
  204. }
  205. imgRequestHeader := make(http.Header)
  206. // If ETag is enabled, we forward If-None-Match header
  207. if config.ETagEnabled {
  208. imgRequestHeader.Set(httpheaders.IfNoneMatch, r.Header.Get(httpheaders.IfNoneMatch))
  209. }
  210. // If LastModified is enabled, we forward If-Modified-Since header
  211. if config.LastModifiedEnabled {
  212. imgRequestHeader.Set(httpheaders.IfModifiedSince, r.Header.Get(httpheaders.IfModifiedSince))
  213. }
  214. if queueSem != nil {
  215. acquired := queueSem.TryAcquire(1)
  216. if !acquired {
  217. panic(newTooManyRequestsError())
  218. }
  219. defer queueSem.Release(1)
  220. }
  221. // The heavy part starts here, so we need to restrict worker number
  222. err = func() error {
  223. defer monitoring.StartQueueSegment(ctx)()
  224. err = processingSem.Acquire(ctx, 1)
  225. if err != nil {
  226. // We don't actually need to check timeout here,
  227. // but it's an easy way to check if this is an actual timeout
  228. // or the request was canceled
  229. if terr := server.CheckTimeout(ctx); terr != nil {
  230. return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
  231. }
  232. // We should never reach this line as err could be only ctx.Err()
  233. // and we've already checked for it. But beter safe than sorry
  234. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryQueue))
  235. }
  236. return nil
  237. }()
  238. if err != nil {
  239. return err
  240. }
  241. defer processingSem.Release(1)
  242. stats.IncImagesInProgress()
  243. defer stats.DecImagesInProgress()
  244. statusCode := http.StatusOK
  245. originData, originHeaders, err := func() (imagedata.ImageData, http.Header, error) {
  246. downloadFinished := monitoring.StartDownloadingSegment(ctx, monitoringMeta.Filter(
  247. monitoring.MetaSourceImageURL,
  248. monitoring.MetaSourceImageOrigin,
  249. ))
  250. downloadOpts := imagedata.DownloadOptions{
  251. Header: imgRequestHeader,
  252. CookieJar: nil,
  253. MaxSrcFileSize: po.SecurityOptions.MaxSrcFileSize,
  254. DownloadFinished: downloadFinished,
  255. }
  256. if config.CookiePassthrough {
  257. downloadOpts.CookieJar, err = cookies.JarFromRequest(r)
  258. if err != nil {
  259. return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
  260. }
  261. }
  262. return imagedata.DownloadAsync(ctx, imageURL, "source image", downloadOpts)
  263. }()
  264. // Close originData if no error occurred
  265. if err == nil {
  266. defer originData.Close()
  267. }
  268. // Check that image detection didn't take too long
  269. if terr := server.CheckTimeout(ctx); terr != nil {
  270. return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
  271. }
  272. var nmErr imagefetcher.NotModifiedError
  273. // Respond with NotModified if image was not modified
  274. if errors.As(err, &nmErr) {
  275. hwr := hw.NewRequest(nmErr.Headers(), imageURL)
  276. respondWithNotModified(reqID, r, rw, po, imageURL, hwr)
  277. return nil
  278. }
  279. // If error is not related to NotModified, respond with fallback image
  280. if err != nil {
  281. ierr := ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
  282. if config.ReportDownloadingErrors {
  283. ierr = ierrors.Wrap(ierr, 0, ierrors.WithShouldReport(true))
  284. }
  285. if imagedata.FallbackImage == nil {
  286. return ierr
  287. }
  288. // Just send error
  289. monitoring.SendError(ctx, categoryDownload, ierr)
  290. // We didn't return, so we have to report error
  291. if ierr.ShouldReport() {
  292. errorreport.Report(ierr, r)
  293. }
  294. log.WithField("request_id", reqID).Warningf("Could not load image %s. Using fallback image. %s", imageURL, ierr.Error())
  295. if config.FallbackImageHTTPCode > 0 {
  296. statusCode = config.FallbackImageHTTPCode
  297. } else {
  298. statusCode = ierr.StatusCode()
  299. }
  300. originData = imagedata.FallbackImage
  301. originHeaders = imagedata.FallbackImageHeaders.Clone()
  302. if config.FallbackImageTTL > 0 {
  303. originHeaders.Set("Fallback-Image", "1")
  304. }
  305. }
  306. if !vips.SupportsLoad(originData.Format()) {
  307. return ierrors.Wrap(newInvalidURLErrorf(
  308. http.StatusUnprocessableEntity,
  309. "Source image format is not supported: %s", originData.Format(),
  310. ), 0, ierrors.WithCategory(categoryProcessing))
  311. }
  312. result, err := func() (*processing.Result, error) {
  313. defer monitoring.StartProcessingSegment(ctx, monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
  314. return processing.ProcessImage(ctx, originData, po)
  315. }()
  316. // Let's close resulting image data only if it differs from the source image data
  317. if result != nil && result.OutData != nil && result.OutData != originData {
  318. defer result.OutData.Close()
  319. }
  320. // First, check if the processing error wasn't caused by an image data error
  321. if derr := originData.Error(); derr != nil {
  322. return ierrors.Wrap(derr, 0, ierrors.WithCategory(categoryDownload))
  323. }
  324. // If it wasn't, than it was a processing error
  325. if err != nil {
  326. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryProcessing))
  327. }
  328. hwr := hw.NewRequest(originHeaders, imageURL)
  329. // Write debug headers. It seems unlogical to move they to headerwriter since they're
  330. // not used anywhere else.
  331. err = writeDebugHeaders(rw, result, originData)
  332. if err != nil {
  333. return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
  334. }
  335. err = respondWithImage(reqID, r, rw, statusCode, result.OutData, po, imageURL, hwr)
  336. if err != nil {
  337. return err
  338. }
  339. return nil
  340. }