|
@@ -1,425 +0,0 @@
|
|
|
-package main
|
|
|
-
|
|
|
-import (
|
|
|
- "errors"
|
|
|
- "io"
|
|
|
- "net/http"
|
|
|
- "net/url"
|
|
|
- "strconv"
|
|
|
- "strings"
|
|
|
-
|
|
|
- log "github.com/sirupsen/logrus"
|
|
|
- "golang.org/x/sync/semaphore"
|
|
|
-
|
|
|
- "github.com/imgproxy/imgproxy/v3/config"
|
|
|
- "github.com/imgproxy/imgproxy/v3/cookies"
|
|
|
- "github.com/imgproxy/imgproxy/v3/errorreport"
|
|
|
- "github.com/imgproxy/imgproxy/v3/handlers/stream"
|
|
|
- "github.com/imgproxy/imgproxy/v3/headerwriter"
|
|
|
- "github.com/imgproxy/imgproxy/v3/httpheaders"
|
|
|
- "github.com/imgproxy/imgproxy/v3/ierrors"
|
|
|
- "github.com/imgproxy/imgproxy/v3/imagedata"
|
|
|
- "github.com/imgproxy/imgproxy/v3/imagefetcher"
|
|
|
- "github.com/imgproxy/imgproxy/v3/imagetype"
|
|
|
- "github.com/imgproxy/imgproxy/v3/monitoring"
|
|
|
- "github.com/imgproxy/imgproxy/v3/monitoring/stats"
|
|
|
- "github.com/imgproxy/imgproxy/v3/options"
|
|
|
- "github.com/imgproxy/imgproxy/v3/processing"
|
|
|
- "github.com/imgproxy/imgproxy/v3/security"
|
|
|
- "github.com/imgproxy/imgproxy/v3/server"
|
|
|
- "github.com/imgproxy/imgproxy/v3/vips"
|
|
|
-)
|
|
|
-
|
|
|
-var (
|
|
|
- queueSem *semaphore.Weighted
|
|
|
- processingSem *semaphore.Weighted
|
|
|
-)
|
|
|
-
|
|
|
-func initProcessingHandler() {
|
|
|
- if config.RequestsQueueSize > 0 {
|
|
|
- queueSem = semaphore.NewWeighted(int64(config.RequestsQueueSize + config.Workers))
|
|
|
- }
|
|
|
-
|
|
|
- processingSem = semaphore.NewWeighted(int64(config.Workers))
|
|
|
-}
|
|
|
-
|
|
|
-// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
|
|
|
-func writeDebugHeaders(rw http.ResponseWriter, result *processing.Result, originData imagedata.ImageData) error {
|
|
|
- if !config.EnableDebugHeaders {
|
|
|
- return nil
|
|
|
- }
|
|
|
-
|
|
|
- if result != nil {
|
|
|
- rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
|
|
|
- rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
|
|
|
- rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
|
|
|
- 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))
|
|
|
- }
|
|
|
-
|
|
|
- rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
|
|
|
-
|
|
|
- return nil
|
|
|
-}
|
|
|
-
|
|
|
-func respondWithImage(
|
|
|
- reqID string,
|
|
|
- r *http.Request,
|
|
|
- rw http.ResponseWriter,
|
|
|
- statusCode int,
|
|
|
- resultData imagedata.ImageData,
|
|
|
- po *options.ProcessingOptions,
|
|
|
- originURL string,
|
|
|
- hw *headerwriter.Request,
|
|
|
-) 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))
|
|
|
- }
|
|
|
-
|
|
|
- hw.SetContentType(resultData.Format().Mime())
|
|
|
- hw.SetContentLength(resultSize)
|
|
|
- hw.SetContentDisposition(
|
|
|
- originURL,
|
|
|
- po.Filename,
|
|
|
- resultData.Format().Ext(),
|
|
|
- "",
|
|
|
- po.ReturnAttachment,
|
|
|
- )
|
|
|
- hw.SetExpires(po.Expires)
|
|
|
- hw.SetVary()
|
|
|
- hw.SetCanonical()
|
|
|
-
|
|
|
- if config.LastModifiedEnabled {
|
|
|
- hw.Passthrough(httpheaders.LastModified)
|
|
|
- }
|
|
|
-
|
|
|
- if config.ETagEnabled {
|
|
|
- hw.Passthrough(httpheaders.Etag)
|
|
|
- }
|
|
|
-
|
|
|
- hw.Write(rw)
|
|
|
-
|
|
|
- rw.WriteHeader(statusCode)
|
|
|
-
|
|
|
- _, err = io.Copy(rw, resultData.Reader())
|
|
|
-
|
|
|
- var ierr *ierrors.Error
|
|
|
- if err != nil {
|
|
|
- ierr = newResponseWriteError(err)
|
|
|
-
|
|
|
- if config.ReportIOErrors {
|
|
|
- return ierrors.Wrap(ierr, 0, ierrors.WithCategory(categoryIO), ierrors.WithShouldReport(true))
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- server.LogResponse(
|
|
|
- reqID, r, statusCode, ierr,
|
|
|
- log.Fields{
|
|
|
- "image_url": originURL,
|
|
|
- "processing_options": po,
|
|
|
- },
|
|
|
- )
|
|
|
-
|
|
|
- return nil
|
|
|
-}
|
|
|
-
|
|
|
-func respondWithNotModified(reqID string, r *http.Request, rw http.ResponseWriter, po *options.ProcessingOptions, originURL string, hw *headerwriter.Request) {
|
|
|
- hw.SetExpires(po.Expires)
|
|
|
- hw.SetVary()
|
|
|
-
|
|
|
- if config.ETagEnabled {
|
|
|
- hw.Passthrough(httpheaders.Etag)
|
|
|
- }
|
|
|
-
|
|
|
- hw.Write(rw)
|
|
|
-
|
|
|
- rw.WriteHeader(http.StatusNotModified)
|
|
|
-
|
|
|
- server.LogResponse(
|
|
|
- reqID, r, http.StatusNotModified, nil,
|
|
|
- log.Fields{
|
|
|
- "image_url": originURL,
|
|
|
- "processing_options": po,
|
|
|
- },
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-func callHandleProcessing(reqID string, rw http.ResponseWriter, r *http.Request) error {
|
|
|
- // NOTE: This is temporary, will be moved level up at once
|
|
|
- hwc, err := headerwriter.NewDefaultConfig().LoadFromEnv()
|
|
|
- if err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
|
|
- }
|
|
|
-
|
|
|
- hw, err := headerwriter.New(hwc)
|
|
|
- if err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
|
|
- }
|
|
|
-
|
|
|
- sc, err := stream.NewDefaultConfig().LoadFromEnv()
|
|
|
- if err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
|
|
- }
|
|
|
-
|
|
|
- stream, err := stream.New(sc, hw, imagedata.Fetcher)
|
|
|
- if err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryConfig))
|
|
|
- }
|
|
|
-
|
|
|
- return handleProcessing(reqID, rw, r, hw, stream)
|
|
|
-}
|
|
|
-
|
|
|
-func handleProcessing(reqID string, rw http.ResponseWriter, r *http.Request, hw *headerwriter.Writer, stream *stream.Handler) error {
|
|
|
- stats.IncRequestsInProgress()
|
|
|
- defer stats.DecRequestsInProgress()
|
|
|
-
|
|
|
- ctx := r.Context()
|
|
|
-
|
|
|
- path := r.RequestURI
|
|
|
- if queryStart := strings.IndexByte(path, '?'); queryStart >= 0 {
|
|
|
- path = path[:queryStart]
|
|
|
- }
|
|
|
-
|
|
|
- if len(config.PathPrefix) > 0 {
|
|
|
- path = strings.TrimPrefix(path, config.PathPrefix)
|
|
|
- }
|
|
|
-
|
|
|
- path = strings.TrimPrefix(path, "/")
|
|
|
- signature := ""
|
|
|
-
|
|
|
- if signatureEnd := strings.IndexByte(path, '/'); signatureEnd > 0 {
|
|
|
- signature = path[:signatureEnd]
|
|
|
- path = path[signatureEnd:]
|
|
|
- } else {
|
|
|
- return ierrors.Wrap(
|
|
|
- newInvalidURLErrorf(http.StatusNotFound, "Invalid path: %s", path), 0,
|
|
|
- ierrors.WithCategory(categoryPathParsing),
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- path = fixPath(path)
|
|
|
-
|
|
|
- if err := security.VerifySignature(signature, path); err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
|
|
|
- }
|
|
|
-
|
|
|
- po, imageURL, err := options.ParsePath(path, r.Header)
|
|
|
- if err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryPathParsing))
|
|
|
- }
|
|
|
-
|
|
|
- var imageOrigin any
|
|
|
- if u, uerr := url.Parse(imageURL); uerr == nil {
|
|
|
- imageOrigin = u.Scheme + "://" + u.Host
|
|
|
- }
|
|
|
-
|
|
|
- errorreport.SetMetadata(r, "Source Image URL", imageURL)
|
|
|
- errorreport.SetMetadata(r, "Source Image Origin", imageOrigin)
|
|
|
- errorreport.SetMetadata(r, "Processing Options", po)
|
|
|
-
|
|
|
- monitoringMeta := monitoring.Meta{
|
|
|
- monitoring.MetaSourceImageURL: imageURL,
|
|
|
- monitoring.MetaSourceImageOrigin: imageOrigin,
|
|
|
- monitoring.MetaProcessingOptions: po.Diff().Flatten(),
|
|
|
- }
|
|
|
-
|
|
|
- monitoring.SetMetadata(ctx, monitoringMeta)
|
|
|
-
|
|
|
- err = security.VerifySourceURL(imageURL)
|
|
|
- if err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
|
|
|
- }
|
|
|
-
|
|
|
- if po.Raw {
|
|
|
- return stream.Execute(ctx, r, imageURL, reqID, po, rw)
|
|
|
- }
|
|
|
-
|
|
|
- // SVG is a special case. Though saving to svg is not supported, SVG->SVG is.
|
|
|
- if !vips.SupportsSave(po.Format) && po.Format != imagetype.Unknown && po.Format != imagetype.SVG {
|
|
|
- return ierrors.Wrap(newInvalidURLErrorf(
|
|
|
- http.StatusUnprocessableEntity,
|
|
|
- "Resulting image format is not supported: %s", po.Format,
|
|
|
- ), 0, ierrors.WithCategory(categoryPathParsing))
|
|
|
- }
|
|
|
-
|
|
|
- imgRequestHeader := make(http.Header)
|
|
|
-
|
|
|
- // If ETag is enabled, we forward If-None-Match header
|
|
|
- if config.ETagEnabled {
|
|
|
- imgRequestHeader.Set(httpheaders.IfNoneMatch, r.Header.Get(httpheaders.IfNoneMatch))
|
|
|
- }
|
|
|
-
|
|
|
- // If LastModified is enabled, we forward If-Modified-Since header
|
|
|
- if config.LastModifiedEnabled {
|
|
|
- imgRequestHeader.Set(httpheaders.IfModifiedSince, r.Header.Get(httpheaders.IfModifiedSince))
|
|
|
- }
|
|
|
-
|
|
|
- if queueSem != nil {
|
|
|
- acquired := queueSem.TryAcquire(1)
|
|
|
- if !acquired {
|
|
|
- panic(newTooManyRequestsError())
|
|
|
- }
|
|
|
- defer queueSem.Release(1)
|
|
|
- }
|
|
|
-
|
|
|
- // The heavy part starts here, so we need to restrict worker number
|
|
|
- err = func() error {
|
|
|
- defer monitoring.StartQueueSegment(ctx)()
|
|
|
-
|
|
|
- err = processingSem.Acquire(ctx, 1)
|
|
|
- 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 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 ierrors.Wrap(err, 0, ierrors.WithCategory(categoryQueue))
|
|
|
- }
|
|
|
-
|
|
|
- return nil
|
|
|
- }()
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- defer processingSem.Release(1)
|
|
|
-
|
|
|
- stats.IncImagesInProgress()
|
|
|
- defer stats.DecImagesInProgress()
|
|
|
-
|
|
|
- statusCode := http.StatusOK
|
|
|
-
|
|
|
- originData, originHeaders, err := func() (imagedata.ImageData, http.Header, error) {
|
|
|
- downloadFinished := monitoring.StartDownloadingSegment(ctx, monitoringMeta.Filter(
|
|
|
- monitoring.MetaSourceImageURL,
|
|
|
- monitoring.MetaSourceImageOrigin,
|
|
|
- ))
|
|
|
-
|
|
|
- downloadOpts := imagedata.DownloadOptions{
|
|
|
- Header: imgRequestHeader,
|
|
|
- CookieJar: nil,
|
|
|
- MaxSrcFileSize: po.SecurityOptions.MaxSrcFileSize,
|
|
|
- DownloadFinished: downloadFinished,
|
|
|
- }
|
|
|
-
|
|
|
- if config.CookiePassthrough {
|
|
|
- downloadOpts.CookieJar, err = cookies.JarFromRequest(r)
|
|
|
- if err != nil {
|
|
|
- return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return imagedata.DownloadAsync(ctx, imageURL, "source image", downloadOpts)
|
|
|
- }()
|
|
|
-
|
|
|
- // Close originData if no error occurred
|
|
|
- if err == nil {
|
|
|
- defer originData.Close()
|
|
|
- }
|
|
|
-
|
|
|
- // Check that image detection didn't take too long
|
|
|
- if terr := server.CheckTimeout(ctx); terr != nil {
|
|
|
- return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
|
|
|
- }
|
|
|
-
|
|
|
- var nmErr imagefetcher.NotModifiedError
|
|
|
-
|
|
|
- // Respond with NotModified if image was not modified
|
|
|
- if errors.As(err, &nmErr) {
|
|
|
- hwr := hw.NewRequest(nmErr.Headers(), imageURL)
|
|
|
-
|
|
|
- respondWithNotModified(reqID, r, rw, po, imageURL, hwr)
|
|
|
- return nil
|
|
|
- }
|
|
|
-
|
|
|
- // If error is not related to NotModified, respond with fallback image
|
|
|
- if err != nil {
|
|
|
- ierr := ierrors.Wrap(err, 0, ierrors.WithCategory(categoryDownload))
|
|
|
- if config.ReportDownloadingErrors {
|
|
|
- ierr = ierrors.Wrap(ierr, 0, ierrors.WithShouldReport(true))
|
|
|
- }
|
|
|
-
|
|
|
- if imagedata.FallbackImage == nil {
|
|
|
- return ierr
|
|
|
- }
|
|
|
-
|
|
|
- // Just send error
|
|
|
- monitoring.SendError(ctx, categoryDownload, ierr)
|
|
|
-
|
|
|
- // We didn't return, so we have to report error
|
|
|
- if ierr.ShouldReport() {
|
|
|
- errorreport.Report(ierr, r)
|
|
|
- }
|
|
|
-
|
|
|
- log.WithField("request_id", reqID).Warningf("Could not load image %s. Using fallback image. %s", imageURL, ierr.Error())
|
|
|
-
|
|
|
- if config.FallbackImageHTTPCode > 0 {
|
|
|
- statusCode = config.FallbackImageHTTPCode
|
|
|
- } else {
|
|
|
- statusCode = ierr.StatusCode()
|
|
|
- }
|
|
|
-
|
|
|
- originData = imagedata.FallbackImage
|
|
|
- originHeaders = imagedata.FallbackImageHeaders.Clone()
|
|
|
-
|
|
|
- if config.FallbackImageTTL > 0 {
|
|
|
- originHeaders.Set("Fallback-Image", "1")
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if !vips.SupportsLoad(originData.Format()) {
|
|
|
- return ierrors.Wrap(newInvalidURLErrorf(
|
|
|
- http.StatusUnprocessableEntity,
|
|
|
- "Source image format is not supported: %s", originData.Format(),
|
|
|
- ), 0, ierrors.WithCategory(categoryProcessing))
|
|
|
- }
|
|
|
-
|
|
|
- result, err := func() (*processing.Result, error) {
|
|
|
- defer monitoring.StartProcessingSegment(ctx, monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
|
|
|
- return processing.ProcessImage(ctx, originData, po)
|
|
|
- }()
|
|
|
-
|
|
|
- // Let's close resulting image data only if it differs from the source image data
|
|
|
- if result != nil && result.OutData != nil && result.OutData != originData {
|
|
|
- defer result.OutData.Close()
|
|
|
- }
|
|
|
-
|
|
|
- // First, check if the processing error wasn't caused by an image data error
|
|
|
- if derr := originData.Error(); derr != nil {
|
|
|
- return ierrors.Wrap(derr, 0, ierrors.WithCategory(categoryDownload))
|
|
|
- }
|
|
|
-
|
|
|
- // If it wasn't, than it was a processing error
|
|
|
- if err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryProcessing))
|
|
|
- }
|
|
|
-
|
|
|
- hwr := hw.NewRequest(originHeaders, imageURL)
|
|
|
-
|
|
|
- // Write debug headers. It seems unlogical to move they to headerwriter since they're
|
|
|
- // not used anywhere else.
|
|
|
- err = writeDebugHeaders(rw, result, originData)
|
|
|
- if err != nil {
|
|
|
- return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
|
|
- }
|
|
|
-
|
|
|
- err = respondWithImage(reqID, r, rw, statusCode, result.OutData, po, imageURL, hwr)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
-
|
|
|
- return nil
|
|
|
-}
|