processing_handler.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. package main
  2. import (
  3. "errors"
  4. "net/http"
  5. "net/url"
  6. "slices"
  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/etag"
  15. "github.com/imgproxy/imgproxy/v3/handlererr"
  16. "github.com/imgproxy/imgproxy/v3/headerwriter"
  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/imagestreamer"
  21. "github.com/imgproxy/imgproxy/v3/imagetype"
  22. "github.com/imgproxy/imgproxy/v3/metrics"
  23. "github.com/imgproxy/imgproxy/v3/metrics/stats"
  24. "github.com/imgproxy/imgproxy/v3/options"
  25. "github.com/imgproxy/imgproxy/v3/processing"
  26. "github.com/imgproxy/imgproxy/v3/router"
  27. "github.com/imgproxy/imgproxy/v3/security"
  28. "github.com/imgproxy/imgproxy/v3/stemext"
  29. "github.com/imgproxy/imgproxy/v3/svg"
  30. "github.com/imgproxy/imgproxy/v3/vips"
  31. )
  32. var (
  33. queueSem *semaphore.Weighted
  34. processingSem *semaphore.Weighted
  35. )
  36. func initProcessingHandler() {
  37. if config.RequestsQueueSize > 0 {
  38. queueSem = semaphore.NewWeighted(int64(config.RequestsQueueSize + config.Workers))
  39. }
  40. processingSem = semaphore.NewWeighted(int64(config.Workers))
  41. }
  42. func respondWithImage(hw *headerwriter.Writer, reqID string, r *http.Request, rw http.ResponseWriter, statusCode int, resultData *imagedata.ImageData, po *options.ProcessingOptions, originURL string, originData *imagedata.ImageData) {
  43. url, err := url.Parse(originURL)
  44. handlererr.Check(r.Context(), handlererr.ErrTypePathParsing, err)
  45. stem, ext := stemext.FromURL(url).
  46. OverrideStem(po.Filename).
  47. OverrideExt(resultData.Type.Ext()).
  48. StemExtWithFallback()
  49. hw.SetMaxAgeFromExpires(po.Expires)
  50. hw.SetContentDisposition(stem, ext, po.ReturnAttachment)
  51. hw.SetContentType(resultData.Type.Mime())
  52. hw.SetLastModified()
  53. hw.SetVary()
  54. // TODO: think about moving this to the headerwriter
  55. if config.EnableDebugHeaders {
  56. rw.Header().Set("X-Origin-Content-Length", strconv.Itoa(len(originData.Data)))
  57. rw.Header().Set("X-Origin-Width", resultData.Headers["X-Origin-Width"])
  58. rw.Header().Set("X-Origin-Height", resultData.Headers["X-Origin-Height"])
  59. rw.Header().Set("X-Result-Width", resultData.Headers["X-Result-Width"])
  60. rw.Header().Set("X-Result-Height", resultData.Headers["X-Result-Height"])
  61. }
  62. hw.SetContentLength(len(resultData.Data))
  63. hw.SetCanonical()
  64. hw.Write(rw)
  65. rw.WriteHeader(statusCode)
  66. _, err = rw.Write(resultData.Data)
  67. var ierr *ierrors.Error
  68. if err != nil {
  69. ierr = newResponseWriteError(err)
  70. if config.ReportIOErrors {
  71. handlererr.Send(r.Context(), handlererr.ErrTypeIO, ierr)
  72. errorreport.Report(ierr, r)
  73. }
  74. }
  75. router.LogResponse(
  76. reqID, r, statusCode, ierr,
  77. log.Fields{
  78. "image_url": originURL,
  79. "processing_options": po,
  80. },
  81. )
  82. }
  83. func respondWithNotModified(hw *headerwriter.Writer, reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, originURL string, originHeaders map[string]string) {
  84. hw.SetMaxAgeFromExpires(po.Expires)
  85. hw.SetVary()
  86. hw.Write(rw)
  87. rw.WriteHeader(304)
  88. router.LogResponse(
  89. reqID, r, 304, nil,
  90. log.Fields{
  91. "image_url": originURL,
  92. "processing_options": po,
  93. },
  94. )
  95. }
  96. func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) {
  97. stats.IncRequestsInProgress()
  98. defer stats.DecRequestsInProgress()
  99. ctx := r.Context()
  100. path := r.RequestURI
  101. if queryStart := strings.IndexByte(path, '?'); queryStart >= 0 {
  102. path = path[:queryStart]
  103. }
  104. if len(config.PathPrefix) > 0 {
  105. path = strings.TrimPrefix(path, config.PathPrefix)
  106. }
  107. path = strings.TrimPrefix(path, "/")
  108. signature := ""
  109. if signatureEnd := strings.IndexByte(path, '/'); signatureEnd > 0 {
  110. signature = path[:signatureEnd]
  111. path = path[signatureEnd:]
  112. } else {
  113. handlererr.SendAndPanic(ctx, handlererr.ErrTypePathParsing, newInvalidURLErrorf(
  114. http.StatusNotFound, "Invalid path: %s", path),
  115. )
  116. }
  117. path = fixPath(path)
  118. if err := security.VerifySignature(signature, path); err != nil {
  119. handlererr.SendAndPanic(ctx, handlererr.ErrTypeSecurity, err)
  120. }
  121. po, imageURL, err := options.ParsePath(path, r.Header)
  122. handlererr.Check(ctx, handlererr.ErrTypePathParsing, err)
  123. var imageOrigin any
  124. if u, uerr := url.Parse(imageURL); uerr == nil {
  125. imageOrigin = u.Scheme + "://" + u.Host
  126. }
  127. errorreport.SetMetadata(r, "Source Image URL", imageURL)
  128. errorreport.SetMetadata(r, "Source Image Origin", imageOrigin)
  129. errorreport.SetMetadata(r, "Processing Options", po)
  130. metricsMeta := metrics.Meta{
  131. metrics.MetaSourceImageURL: imageURL,
  132. metrics.MetaSourceImageOrigin: imageOrigin,
  133. metrics.MetaProcessingOptions: po.Diff().Flatten(),
  134. }
  135. metrics.SetMetadata(ctx, metricsMeta)
  136. err = security.VerifySourceURL(imageURL)
  137. handlererr.Check(ctx, handlererr.ErrTypeSecurity, err)
  138. if po.Raw {
  139. sf := imagestreamer.NewService(
  140. imagestreamer.NewConfigFromEnv(),
  141. imagedata.Fetcher,
  142. headerwriter.NewFactory(headerwriter.NewConfigFromEnv()),
  143. )
  144. p := imagestreamer.Request{
  145. UserRequest: r,
  146. ImageURL: imageURL,
  147. ReqID: reqID,
  148. ProcessingOptions: po,
  149. }
  150. sf.Stream(ctx, &p, rw)
  151. return
  152. }
  153. // SVG is a special case. Though saving to svg is not supported, SVG->SVG is.
  154. if !vips.SupportsSave(po.Format) && po.Format != imagetype.Unknown && po.Format != imagetype.SVG {
  155. handlererr.SendAndPanic(ctx, handlererr.ErrTypePathParsing, newInvalidURLErrorf(
  156. http.StatusUnprocessableEntity,
  157. "Resulting image format is not supported: %s", po.Format,
  158. ))
  159. }
  160. imgRequestHeader := make(http.Header)
  161. var etagHandler etag.Handler
  162. if config.ETagEnabled {
  163. etagHandler.ParseExpectedETag(r.Header.Get("If-None-Match"))
  164. if etagHandler.SetActualProcessingOptions(po) {
  165. if imgEtag := etagHandler.ImageEtagExpected(); len(imgEtag) != 0 {
  166. imgRequestHeader.Set("If-None-Match", imgEtag)
  167. }
  168. }
  169. }
  170. // ???
  171. if config.LastModifiedEnabled {
  172. if modifiedSince := r.Header.Get("If-Modified-Since"); len(modifiedSince) != 0 {
  173. imgRequestHeader.Set("If-Modified-Since", modifiedSince)
  174. }
  175. }
  176. if queueSem != nil {
  177. acquired := queueSem.TryAcquire(1)
  178. if !acquired {
  179. panic(newTooManyRequestsError())
  180. }
  181. defer queueSem.Release(1)
  182. }
  183. // The heavy part starts here, so we need to restrict worker number
  184. func() {
  185. defer metrics.StartQueueSegment(ctx)()
  186. err = processingSem.Acquire(ctx, 1)
  187. if err != nil {
  188. // We don't actually need to check timeout here,
  189. // but it's an easy way to check if this is an actual timeout
  190. // or the request was canceled
  191. handlererr.Check(ctx, handlererr.ErrTypeQueue, router.CheckTimeout(ctx))
  192. // We should never reach this line as err could be only ctx.Err()
  193. // and we've already checked for it. But beter safe than sorry
  194. handlererr.SendAndPanic(ctx, handlererr.ErrTypeQueue, err)
  195. }
  196. }()
  197. defer processingSem.Release(1)
  198. stats.IncImagesInProgress()
  199. defer stats.DecImagesInProgress()
  200. statusCode := http.StatusOK
  201. originData, originResponseHeaders, err := func() (*imagedata.ImageData, http.Header, error) {
  202. defer metrics.StartDownloadingSegment(ctx, metrics.Meta{
  203. metrics.MetaSourceImageURL: metricsMeta[metrics.MetaSourceImageURL],
  204. metrics.MetaSourceImageOrigin: metricsMeta[metrics.MetaSourceImageOrigin],
  205. })()
  206. downloadOpts := imagedata.DownloadOptions{
  207. Header: imgRequestHeader,
  208. CookieJar: nil,
  209. }
  210. if config.CookiePassthrough {
  211. downloadOpts.CookieJar, err = cookies.JarFromRequest(r)
  212. handlererr.Check(ctx, handlererr.ErrTypeDownload, err)
  213. }
  214. return imagedata.Download(ctx, imageURL, "source image", downloadOpts, po.SecurityOptions)
  215. }()
  216. hwf := headerwriter.NewFactory(headerwriter.NewConfigFromEnv())
  217. hw := hwf.NewHeaderWriter(originResponseHeaders, imageURL)
  218. var nmErr imagefetcher.NotModifiedError
  219. switch {
  220. case err == nil:
  221. defer originData.Close()
  222. case errors.As(err, &nmErr):
  223. if config.ETagEnabled && len(etagHandler.ImageEtagExpected()) != 0 {
  224. rw.Header().Set("ETag", etagHandler.GenerateExpectedETag())
  225. }
  226. h := make(map[string]string)
  227. for k := range nmErr.Headers() {
  228. h[k] = nmErr.Headers().Get(k)
  229. }
  230. respondWithNotModified(hw, reqID, r, rw, po, imageURL, h)
  231. return
  232. default:
  233. // This may be a request timeout error or a request cancelled error.
  234. // Check it before moving further
  235. handlererr.Check(ctx, handlererr.ErrTypeTimeout, router.CheckTimeout(ctx))
  236. ierr := ierrors.Wrap(err, 0)
  237. if config.ReportDownloadingErrors {
  238. ierr = ierrors.Wrap(ierr, 0, ierrors.WithShouldReport(true))
  239. }
  240. handlererr.Send(ctx, handlererr.ErrTypeDownload, ierr)
  241. if imagedata.FallbackImage == nil {
  242. panic(ierr)
  243. }
  244. // We didn't panic, so the error is not reported.
  245. // Report it now
  246. if ierr.ShouldReport() {
  247. errorreport.Report(ierr, r)
  248. }
  249. log.WithField("request_id", reqID).Warningf("Could not load image %s. Using fallback image. %s", imageURL, ierr.Error())
  250. if config.FallbackImageHTTPCode > 0 {
  251. statusCode = config.FallbackImageHTTPCode
  252. } else {
  253. statusCode = ierr.StatusCode()
  254. }
  255. hw.SetMaxAge(config.FallbackImageTTL)
  256. if config.FallbackImageTTL > 0 {
  257. hw.SetIsFallbackImage()
  258. }
  259. originData = imagedata.FallbackImage
  260. }
  261. handlererr.Check(ctx, handlererr.ErrTypeTimeout, router.CheckTimeout(ctx))
  262. if config.ETagEnabled && statusCode == http.StatusOK {
  263. imgDataMatch := etagHandler.SetActualImageData(originData)
  264. rw.Header().Set("ETag", etagHandler.GenerateActualETag())
  265. if imgDataMatch && etagHandler.ProcessingOptionsMatch() {
  266. respondWithNotModified(hw, reqID, r, rw, po, imageURL, originData.Headers)
  267. return
  268. }
  269. }
  270. handlererr.Check(ctx, handlererr.ErrTypeTimeout, router.CheckTimeout(ctx))
  271. // Skip processing svg with unknown or the same destination imageType
  272. // if it's not forced by AlwaysRasterizeSvg option
  273. // Also skip processing if the format is in SkipProcessingFormats
  274. shouldSkipProcessing := (originData.Type == po.Format || po.Format == imagetype.Unknown) &&
  275. (slices.Contains(po.SkipProcessingFormats, originData.Type) ||
  276. originData.Type == imagetype.SVG && !config.AlwaysRasterizeSvg)
  277. if shouldSkipProcessing {
  278. if originData.Type == imagetype.SVG && config.SanitizeSvg {
  279. sanitized, svgErr := svg.Sanitize(originData)
  280. handlererr.Check(ctx, handlererr.ErrTypeSvgProcessing, svgErr)
  281. defer sanitized.Close()
  282. respondWithImage(hw, reqID, r, rw, statusCode, sanitized, po, imageURL, originData)
  283. return
  284. }
  285. respondWithImage(hw, reqID, r, rw, statusCode, originData, po, imageURL, originData)
  286. return
  287. }
  288. if !vips.SupportsLoad(originData.Type) {
  289. handlererr.SendAndPanic(ctx, handlererr.ErrTypeProcessing, newInvalidURLErrorf(
  290. http.StatusUnprocessableEntity,
  291. "Source image format is not supported: %s", originData.Type,
  292. ))
  293. }
  294. // At this point we can't allow requested format to be SVG as we can't save SVGs
  295. if po.Format == imagetype.SVG {
  296. handlererr.SendAndPanic(ctx, handlererr.ErrTypeProcessing, newInvalidURLErrorf(
  297. http.StatusUnprocessableEntity,
  298. "Resulting image format is not supported: svg",
  299. ))
  300. }
  301. resultData, err := func() (*imagedata.ImageData, error) {
  302. defer metrics.StartProcessingSegment(ctx, metrics.Meta{
  303. metrics.MetaProcessingOptions: metricsMeta[metrics.MetaProcessingOptions],
  304. })()
  305. return processing.ProcessImage(ctx, originData, po)
  306. }()
  307. handlererr.Check(ctx, handlererr.ErrTypeProcessing, err)
  308. defer resultData.Close()
  309. handlererr.Check(ctx, handlererr.ErrTypeTimeout, router.CheckTimeout(ctx))
  310. respondWithImage(hw, reqID, r, rw, statusCode, resultData, po, imageURL, originData)
  311. }