123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- package processing
- import (
- "context"
- "io"
- "net/http"
- "strconv"
- "github.com/imgproxy/imgproxy/v3/cookies"
- "github.com/imgproxy/imgproxy/v3/errorreport"
- "github.com/imgproxy/imgproxy/v3/httpheaders"
- "github.com/imgproxy/imgproxy/v3/ierrors"
- "github.com/imgproxy/imgproxy/v3/imagedata"
- "github.com/imgproxy/imgproxy/v3/monitoring"
- "github.com/imgproxy/imgproxy/v3/options"
- "github.com/imgproxy/imgproxy/v3/processing"
- "github.com/imgproxy/imgproxy/v3/server"
- log "github.com/sirupsen/logrus"
- )
- // makeImageRequestHeaders creates headers for the image request
- func (r *request) makeImageRequestHeaders() http.Header {
- h := make(http.Header)
- // If ETag is enabled, we forward If-None-Match header
- if r.config.ETagEnabled {
- h.Set(httpheaders.IfNoneMatch, r.imageRequest.Header.Get(httpheaders.IfNoneMatch))
- }
- // If LastModified is enabled, we forward If-Modified-Since header
- if r.config.LastModifiedEnabled {
- h.Set(httpheaders.IfModifiedSince, r.imageRequest.Header.Get(httpheaders.IfModifiedSince))
- }
- return h
- }
- // acquireProcessingSem acquires the processing semaphore
- func (r *request) acquireProcessingSem(ctx context.Context) (context.CancelFunc, error) {
- defer monitoring.StartQueueSegment(ctx)()
- fn, err := r.semaphores.AcquireProcessing(ctx)
- if err != nil {
- // We don't actually need to check timeout here,
- // but it's an easy way to check if this is an actual timeout
- // or the request was canceled
- if terr := server.CheckTimeout(ctx); terr != nil {
- return nil, ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
- }
- // We should never reach this line as err could be only ctx.Err()
- // and we've already checked for it. But beter safe than sorry
- return nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryQueue))
- }
- return fn, nil
- }
- // makeDownloadOptions creates a new default download options
- func (r *request) makeDownloadOptions(ctx context.Context, h http.Header) imagedata.DownloadOptions {
- downloadFinished := monitoring.StartDownloadingSegment(ctx, r.monitoringMeta.Filter(
- monitoring.MetaSourceImageURL,
- monitoring.MetaSourceImageOrigin,
- ))
- return imagedata.DownloadOptions{
- Header: h,
- MaxSrcFileSize: r.po.SecurityOptions.MaxSrcFileSize,
- DownloadFinished: downloadFinished,
- }
- }
- // fetchImage downloads the source image asynchronously
- func (r *request) fetchImage(ctx context.Context, do imagedata.DownloadOptions) (imagedata.ImageData, http.Header, error) {
- var err error
- if r.config.CookiePassthrough {
- do.CookieJar, err = cookies.JarFromRequest(r.imageRequest)
- if err != nil {
- return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
- }
- }
- return r.idf.DownloadAsync(ctx, r.imageURL, "source image", do)
- }
- // handleDownloadError replaces the image data with fallback image if needed
- func (r *request) handleDownloadError(
- ctx context.Context,
- originalErr error,
- ) (imagedata.ImageData, int, error) {
- err := r.wrapDownloadingErr(originalErr)
- // If there is no fallback image configured, just return the error
- data, headers := r.getFallbackImage(ctx, r.po)
- if data == nil {
- return nil, 0, err
- }
- // Just send error
- monitoring.SendError(ctx, categoryDownload, err)
- // We didn't return, so we have to report error
- if err.ShouldReport() {
- errorreport.Report(err, r.imageRequest)
- }
- log.
- WithField("request_id", r.reqID).
- Warningf("Could not load image %s. Using fallback image. %s", r.imageURL, err.Error())
- var statusCode int
- // Set status code if needed
- if r.config.FallbackImageHTTPCode > 0 {
- statusCode = r.config.FallbackImageHTTPCode
- } else {
- statusCode = err.StatusCode()
- }
- // Fallback image should have exact FallbackImageTTL lifetime
- headers.Del(httpheaders.Expires)
- headers.Del(httpheaders.LastModified)
- r.hwr.SetOriginHeaders(headers)
- r.hwr.SetIsFallbackImage()
- return data, statusCode, nil
- }
- // getFallbackImage returns fallback image if any
- func (r *request) getFallbackImage(
- ctx context.Context,
- po *options.ProcessingOptions,
- ) (imagedata.ImageData, http.Header) {
- if r.handler.fallbackImage == nil {
- return nil, nil
- }
- data, h, err := r.handler.fallbackImage.Get(ctx, po)
- if err != nil {
- log.Warning(err.Error())
- if ierr := r.wrapDownloadingErr(err); ierr.ShouldReport() {
- errorreport.Report(ierr, r.imageRequest)
- }
- return nil, nil
- }
- return data, h
- }
- // processImage calls actual image processing
- func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
- defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
- return processing.ProcessImage(ctx, originData, r.po, r.handler.watermarkImage, r.handler.imageData)
- }
- // writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
- func (r *request) writeDebugHeaders(result *processing.Result, originData imagedata.ImageData) error {
- if !r.config.EnableDebugHeaders {
- return nil
- }
- if result != nil {
- r.rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
- r.rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
- r.rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
- r.rw.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
- }
- // Try to read origin image size
- size, err := originData.Size()
- if err != nil {
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
- }
- r.rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
- return nil
- }
- // respondWithNotModified writes not-modified response
- func (r *request) respondWithNotModified() error {
- r.hwr.SetExpires(r.po.Expires)
- r.hwr.SetVary()
- if r.config.LastModifiedEnabled {
- r.hwr.Passthrough(httpheaders.LastModified)
- }
- if r.config.ETagEnabled {
- r.hwr.Passthrough(httpheaders.Etag)
- }
- r.hwr.Write(r.rw)
- r.rw.WriteHeader(http.StatusNotModified)
- server.LogResponse(
- r.reqID, r.imageRequest, http.StatusNotModified, nil,
- log.Fields{
- "image_url": r.imageURL,
- "processing_options": r.po,
- },
- )
- return nil
- }
- func (r *request) respondWithImage(statusCode int, resultData imagedata.ImageData) error {
- // We read the size of the image data here, so we can set Content-Length header.
- // This indireclty ensures that the image data is fully read from the source, no
- // errors happened.
- resultSize, err := resultData.Size()
- if err != nil {
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
- }
- r.hwr.SetContentType(resultData.Format().Mime())
- r.hwr.SetContentLength(resultSize)
- r.hwr.SetContentDisposition(
- r.imageURL,
- r.po.Filename,
- resultData.Format().Ext(),
- "",
- r.po.ReturnAttachment,
- )
- r.hwr.SetExpires(r.po.Expires)
- r.hwr.SetVary()
- r.hwr.SetCanonical(r.imageURL)
- if r.config.LastModifiedEnabled {
- r.hwr.Passthrough(httpheaders.LastModified)
- }
- if r.config.ETagEnabled {
- r.hwr.Passthrough(httpheaders.Etag)
- }
- r.hwr.Write(r.rw)
- r.rw.WriteHeader(statusCode)
- _, err = io.Copy(r.rw, resultData.Reader())
- var ierr *ierrors.Error
- if err != nil {
- ierr = newResponseWriteError(err)
- if r.config.ReportIOErrors {
- return ierrors.Wrap(ierr, 0, ierrors.WithCategory(categoryIO), ierrors.WithShouldReport(true))
- }
- }
- server.LogResponse(
- r.reqID, r.imageRequest, statusCode, ierr,
- log.Fields{
- "image_url": r.imageURL,
- "processing_options": r.po,
- },
- )
- return nil
- }
- // wrapDownloadingErr wraps original error to download error
- func (r *request) wrapDownloadingErr(originalErr error) *ierrors.Error {
- err := ierrors.Wrap(originalErr, 0, ierrors.WithCategory(categoryDownload))
- // we report this error only if enabled
- if r.config.ReportDownloadingErrors {
- err = ierrors.Wrap(err, 0, ierrors.WithShouldReport(true))
- }
- return err
- }
|