mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-04 18:33:36 +02:00
processing_handler.go -> handlers/processing
This commit is contained in:
committed by
Sergei Aleksandrovich
parent
7aec46f146
commit
8bc70491fb
59
handlers/processing/config.go
Normal file
59
handlers/processing/config.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/config"
|
||||
)
|
||||
|
||||
// Config represents handler config
|
||||
type Config struct {
|
||||
PathPrefix string // Route path prefix
|
||||
CookiePassthrough bool // Whether to passthrough cookies
|
||||
ReportDownloadingErrors bool // Whether to report downloading errors
|
||||
LastModifiedEnabled bool // Whether to enable Last-Modified
|
||||
ETagEnabled bool // Whether to enable ETag
|
||||
ReportIOErrors bool // Whether to report IO errors
|
||||
FallbackImageHTTPCode int // Fallback image HTTP status code
|
||||
EnableDebugHeaders bool // Whether to enable debug headers
|
||||
FallbackImageData string // Fallback image data (base64)
|
||||
FallbackImagePath string // Fallback image path (local file system)
|
||||
FallbackImageURL string // Fallback image URL (remote)
|
||||
}
|
||||
|
||||
// NewDefaultConfig creates a new configuration with defaults
|
||||
func NewDefaultConfig() *Config {
|
||||
return &Config{
|
||||
PathPrefix: "",
|
||||
CookiePassthrough: false,
|
||||
ReportDownloadingErrors: true,
|
||||
LastModifiedEnabled: true,
|
||||
ETagEnabled: true,
|
||||
ReportIOErrors: false,
|
||||
FallbackImageHTTPCode: http.StatusOK,
|
||||
EnableDebugHeaders: false,
|
||||
}
|
||||
}
|
||||
|
||||
// LoadFromEnv loads config from environment variables
|
||||
func LoadFromEnv(c *Config) (*Config, error) {
|
||||
c.PathPrefix = config.PathPrefix
|
||||
c.CookiePassthrough = config.CookiePassthrough
|
||||
c.ReportDownloadingErrors = config.ReportDownloadingErrors
|
||||
c.LastModifiedEnabled = config.LastModifiedEnabled
|
||||
c.ETagEnabled = config.ETagEnabled
|
||||
c.ReportIOErrors = config.ReportIOErrors
|
||||
c.FallbackImageHTTPCode = config.FallbackImageHTTPCode
|
||||
c.EnableDebugHeaders = config.EnableDebugHeaders
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Validate checks configuration values
|
||||
func (c *Config) Validate() error {
|
||||
if c.FallbackImageHTTPCode != 0 && (c.FallbackImageHTTPCode < 100 || c.FallbackImageHTTPCode > 599) {
|
||||
return errors.New("fallback image HTTP code should be between 100 and 599")
|
||||
}
|
||||
return nil
|
||||
}
|
71
handlers/processing/errors.go
Normal file
71
handlers/processing/errors.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||
)
|
||||
|
||||
// Monitoring error categories
|
||||
const (
|
||||
categoryTimeout = "timeout"
|
||||
categoryImageDataSize = "image_data_size"
|
||||
categoryPathParsing = "path_parsing"
|
||||
categorySecurity = "security"
|
||||
categoryQueue = "queue"
|
||||
categoryDownload = "download"
|
||||
categoryProcessing = "processing"
|
||||
categoryIO = "IO"
|
||||
categoryConfig = "config(tmp)" // NOTE: THIS IS TEMPORARY
|
||||
)
|
||||
|
||||
type (
|
||||
ResponseWriteError struct{ error }
|
||||
InvalidURLError string
|
||||
)
|
||||
|
||||
func newResponseWriteError(cause error) *ierrors.Error {
|
||||
return ierrors.Wrap(
|
||||
ResponseWriteError{cause},
|
||||
1,
|
||||
ierrors.WithPublicMessage("Failed to write response"),
|
||||
)
|
||||
}
|
||||
|
||||
func (e ResponseWriteError) Error() string {
|
||||
return fmt.Sprintf("Failed to write response: %s", e.error)
|
||||
}
|
||||
|
||||
func (e ResponseWriteError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func newInvalidURLErrorf(status int, format string, args ...interface{}) error {
|
||||
return ierrors.Wrap(
|
||||
InvalidURLError(fmt.Sprintf(format, args...)),
|
||||
1,
|
||||
ierrors.WithStatusCode(status),
|
||||
ierrors.WithPublicMessage("Invalid URL"),
|
||||
ierrors.WithShouldReport(false),
|
||||
)
|
||||
}
|
||||
|
||||
func (e InvalidURLError) Error() string { return string(e) }
|
||||
|
||||
// newCantSaveError creates "resulting image not supported" error
|
||||
func newCantSaveError(format imagetype.Type) error {
|
||||
return ierrors.Wrap(newInvalidURLErrorf(
|
||||
http.StatusUnprocessableEntity,
|
||||
"Resulting image format is not supported: %s", format,
|
||||
), 1, ierrors.WithCategory(categoryPathParsing))
|
||||
}
|
||||
|
||||
// newCantLoadError creates "source image not supported" error
|
||||
func newCantLoadError(format imagetype.Type) error {
|
||||
return ierrors.Wrap(newInvalidURLErrorf(
|
||||
http.StatusUnprocessableEntity,
|
||||
"Source image format is not supported: %s", format,
|
||||
), 1, ierrors.WithCategory(categoryProcessing))
|
||||
}
|
143
handlers/processing/handler.go
Normal file
143
handlers/processing/handler.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
||||
"github.com/imgproxy/imgproxy/v3/errorreport"
|
||||
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/imgproxy/imgproxy/v3/monitoring"
|
||||
"github.com/imgproxy/imgproxy/v3/monitoring/stats"
|
||||
"github.com/imgproxy/imgproxy/v3/options"
|
||||
"github.com/imgproxy/imgproxy/v3/security"
|
||||
"github.com/imgproxy/imgproxy/v3/semaphores"
|
||||
)
|
||||
|
||||
// Handler handles image processing requests
|
||||
type Handler struct {
|
||||
hw *headerwriter.Writer // Configured HeaderWriter instance
|
||||
stream *stream.Handler // Stream handler for raw image streaming
|
||||
config *Config // Handler configuration
|
||||
semaphores *semaphores.Semaphores
|
||||
fallbackImage auximageprovider.Provider
|
||||
}
|
||||
|
||||
// New creates new handler object
|
||||
func New(
|
||||
stream *stream.Handler,
|
||||
hw *headerwriter.Writer,
|
||||
semaphores *semaphores.Semaphores,
|
||||
fi auximageprovider.Provider,
|
||||
config *Config,
|
||||
) (*Handler, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Handler{
|
||||
hw: hw,
|
||||
config: config,
|
||||
stream: stream,
|
||||
semaphores: semaphores,
|
||||
fallbackImage: fi,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Execute handles the image processing request
|
||||
func (h *Handler) Execute(
|
||||
reqID string,
|
||||
rw http.ResponseWriter,
|
||||
imageRequest *http.Request,
|
||||
) error {
|
||||
// Increment the number of requests in progress
|
||||
stats.IncRequestsInProgress()
|
||||
defer stats.DecRequestsInProgress()
|
||||
|
||||
ctx := imageRequest.Context()
|
||||
|
||||
// Verify URL signature and extract image url and processing options
|
||||
imageURL, po, mm, err := h.newRequest(ctx, imageRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// if processing options indicate raw image streaming, stream it and return
|
||||
if po.Raw {
|
||||
return h.stream.Execute(ctx, imageRequest, imageURL, reqID, po, rw)
|
||||
}
|
||||
|
||||
req := &request{
|
||||
handler: h,
|
||||
imageRequest: imageRequest,
|
||||
reqID: reqID,
|
||||
rw: rw,
|
||||
config: h.config,
|
||||
po: po,
|
||||
imageURL: imageURL,
|
||||
monitoringMeta: mm,
|
||||
semaphores: h.semaphores,
|
||||
hwr: h.hw.NewRequest(),
|
||||
}
|
||||
|
||||
return req.execute(ctx)
|
||||
}
|
||||
|
||||
// newRequest extracts image url and processing options from request URL and verifies them
|
||||
func (h *Handler) newRequest(
|
||||
ctx context.Context,
|
||||
imageRequest *http.Request,
|
||||
) (string, *options.ProcessingOptions, monitoring.Meta, error) {
|
||||
// let's extract signature and valid request path from a request
|
||||
path, signature, err := splitPathSignature(imageRequest, h.config)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
// verify the signature (if any)
|
||||
if err = security.VerifySignature(signature, path); err != nil {
|
||||
return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
|
||||
}
|
||||
|
||||
// parse image url and processing options
|
||||
po, imageURL, err := options.ParsePath(path, imageRequest.Header)
|
||||
if err != nil {
|
||||
return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(categoryPathParsing))
|
||||
}
|
||||
|
||||
// get image origin and create monitoring meta object
|
||||
imageOrigin := imageOrigin(imageURL)
|
||||
|
||||
mm := monitoring.Meta{
|
||||
monitoring.MetaSourceImageURL: imageURL,
|
||||
monitoring.MetaSourceImageOrigin: imageOrigin,
|
||||
monitoring.MetaProcessingOptions: po.Diff().Flatten(),
|
||||
}
|
||||
|
||||
// set error reporting and monitoring context
|
||||
errorreport.SetMetadata(imageRequest, "Source Image URL", imageURL)
|
||||
errorreport.SetMetadata(imageRequest, "Source Image Origin", imageOrigin)
|
||||
errorreport.SetMetadata(imageRequest, "Processing Options", po)
|
||||
|
||||
monitoring.SetMetadata(ctx, mm)
|
||||
|
||||
// verify that image URL came from the valid source
|
||||
err = security.VerifySourceURL(imageURL)
|
||||
if err != nil {
|
||||
return "", nil, mm, ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
|
||||
}
|
||||
|
||||
return imageURL, po, mm, nil
|
||||
}
|
||||
|
||||
// imageOrigin extracts image origin from URL
|
||||
func imageOrigin(imageURL string) string {
|
||||
if u, uerr := url.Parse(imageURL); uerr == nil {
|
||||
return u.Scheme + "://" + u.Host
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
56
handlers/processing/path.go
Normal file
56
handlers/processing/path.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
)
|
||||
|
||||
// fixPathRe is used in path re-denormalization
|
||||
var fixPathRe = regexp.MustCompile(`/plain/(\S+)\:/([^/])`)
|
||||
|
||||
// splitPathSignature splits signature and path components from the request URI
|
||||
func splitPathSignature(r *http.Request, config *Config) (string, string, error) {
|
||||
uri := r.RequestURI
|
||||
|
||||
// cut query params
|
||||
uri, _, _ = strings.Cut(uri, "?")
|
||||
|
||||
// cut path prefix
|
||||
if len(config.PathPrefix) > 0 {
|
||||
uri = strings.TrimPrefix(uri, config.PathPrefix)
|
||||
}
|
||||
|
||||
// cut leading slash
|
||||
uri = strings.TrimPrefix(uri, "/")
|
||||
|
||||
signature, path, _ := strings.Cut(uri, "/")
|
||||
if len(signature) == 0 || len(path) == 0 {
|
||||
return "", "", ierrors.Wrap(
|
||||
newInvalidURLErrorf(http.StatusNotFound, "Invalid path: %s", path), 0,
|
||||
ierrors.WithCategory(categoryPathParsing),
|
||||
)
|
||||
}
|
||||
|
||||
// restore broken slashes in the path
|
||||
path = redenormalizePath(path)
|
||||
|
||||
return path, signature, nil
|
||||
}
|
||||
|
||||
// redenormalizePath undoes path normalization done by some browsers and revers proxies
|
||||
func redenormalizePath(path string) string {
|
||||
for _, match := range fixPathRe.FindAllStringSubmatch(path, -1) {
|
||||
repl := fmt.Sprintf("/plain/%s://", match[1])
|
||||
if match[1] == "local" {
|
||||
repl += "/"
|
||||
}
|
||||
repl += match[2]
|
||||
path = strings.Replace(path, match[0], repl, 1)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
180
handlers/processing/path_test.go
Normal file
180
handlers/processing/path_test.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type PathTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestPathTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(PathTestSuite))
|
||||
}
|
||||
|
||||
func (s *PathTestSuite) createRequest(path string) *http.Request {
|
||||
return httptest.NewRequest("GET", path, nil)
|
||||
}
|
||||
|
||||
func (s *PathTestSuite) TestParsePath() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
pathPrefix string
|
||||
requestPath string
|
||||
expectedPath string
|
||||
expectedSig string
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "BasicPath",
|
||||
requestPath: "/dummy_signature/rs:fill:300:200/plain/http://example.com/image.jpg",
|
||||
expectedPath: "rs:fill:300:200/plain/http://example.com/image.jpg",
|
||||
expectedSig: "dummy_signature",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "PathWithQueryParams",
|
||||
requestPath: "/dummy_signature/rs:fill:300:200/plain/http://example.com/image.jpg?param1=value1¶m2=value2",
|
||||
expectedPath: "rs:fill:300:200/plain/http://example.com/image.jpg",
|
||||
expectedSig: "dummy_signature",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "PathWithPrefix",
|
||||
pathPrefix: "/imgproxy",
|
||||
requestPath: "/imgproxy/dummy_signature/rs:fill:300:200/plain/http://example.com/image.jpg",
|
||||
expectedPath: "rs:fill:300:200/plain/http://example.com/image.jpg",
|
||||
expectedSig: "dummy_signature",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "PathWithRedenormalization",
|
||||
requestPath: "/dummy_signature/rs:fill:300:200/plain/https:/example.com/path/to/image.jpg",
|
||||
expectedPath: "rs:fill:300:200/plain/https://example.com/path/to/image.jpg",
|
||||
expectedSig: "dummy_signature",
|
||||
expectedError: false,
|
||||
},
|
||||
{
|
||||
name: "NoSignatureSeparator",
|
||||
requestPath: "/invalid_path_without_slash",
|
||||
expectedPath: "",
|
||||
expectedSig: "",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "EmptyPath",
|
||||
requestPath: "/",
|
||||
expectedPath: "",
|
||||
expectedSig: "",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "OnlySignature",
|
||||
requestPath: "/signature_only",
|
||||
expectedPath: "",
|
||||
expectedSig: "",
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
config := &Config{
|
||||
PathPrefix: tc.pathPrefix,
|
||||
}
|
||||
|
||||
req := s.createRequest(tc.requestPath)
|
||||
path, signature, err := splitPathSignature(req, config)
|
||||
|
||||
if tc.expectedError {
|
||||
var ierr *ierrors.Error
|
||||
|
||||
s.Require().Error(err)
|
||||
s.Require().ErrorAs(err, &ierr)
|
||||
s.Require().Equal(categoryPathParsing, ierr.Category())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
s.Require().NoError(err)
|
||||
s.Require().Equal(tc.expectedPath, path)
|
||||
s.Require().Equal(tc.expectedSig, signature)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PathTestSuite) TestRedenormalizePathHTTPProtocol() {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "HTTP",
|
||||
input: "/plain/http:/example.com/image.jpg",
|
||||
expected: "/plain/http://example.com/image.jpg",
|
||||
},
|
||||
{
|
||||
name: "HTTPS",
|
||||
input: "/plain/https:/example.com/image.jpg",
|
||||
expected: "/plain/https://example.com/image.jpg",
|
||||
},
|
||||
{
|
||||
name: "Local",
|
||||
input: "/plain/local:/image.jpg",
|
||||
expected: "/plain/local:///image.jpg",
|
||||
},
|
||||
{
|
||||
name: "NormalizedPath",
|
||||
input: "/plain/http://example.com/image.jpg",
|
||||
expected: "/plain/http://example.com/image.jpg",
|
||||
},
|
||||
{
|
||||
name: "ProtocolMissing",
|
||||
input: "/rs:fill:300:200/plain/example.com/image.jpg",
|
||||
expected: "/rs:fill:300:200/plain/example.com/image.jpg",
|
||||
},
|
||||
{
|
||||
name: "EmptyString",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "SingleSlash",
|
||||
input: "/",
|
||||
expected: "/",
|
||||
},
|
||||
{
|
||||
name: "NoPlainPrefix",
|
||||
input: "/http:/example.com/image.jpg",
|
||||
expected: "/http:/example.com/image.jpg",
|
||||
},
|
||||
{
|
||||
name: "NoProtocol",
|
||||
input: "/plain/example.com/image.jpg",
|
||||
expected: "/plain/example.com/image.jpg",
|
||||
},
|
||||
{
|
||||
name: "EndsWithProtocol",
|
||||
input: "/plain/http:",
|
||||
expected: "/plain/http:",
|
||||
},
|
||||
{
|
||||
name: "OnlyProtocol",
|
||||
input: "/plain/http:/test",
|
||||
expected: "/plain/http://test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
s.Run(tc.name, func() {
|
||||
result := redenormalizePath(tc.input)
|
||||
s.Equal(tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
140
handlers/processing/request.go
Normal file
140
handlers/processing/request.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package processing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||
"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/semaphores"
|
||||
"github.com/imgproxy/imgproxy/v3/server"
|
||||
"github.com/imgproxy/imgproxy/v3/vips"
|
||||
)
|
||||
|
||||
// request holds the parameters and state for a single request request
|
||||
type request struct {
|
||||
handler *Handler
|
||||
imageRequest *http.Request
|
||||
reqID string
|
||||
rw http.ResponseWriter
|
||||
config *Config
|
||||
po *options.ProcessingOptions
|
||||
imageURL string
|
||||
monitoringMeta monitoring.Meta
|
||||
semaphores *semaphores.Semaphores
|
||||
hwr *headerwriter.Request
|
||||
}
|
||||
|
||||
// execute handles the actual processing logic
|
||||
func (r *request) execute(ctx context.Context) error {
|
||||
// Check if we can save the resulting image
|
||||
canSave := vips.SupportsSave(r.po.Format) ||
|
||||
r.po.Format == imagetype.Unknown ||
|
||||
r.po.Format == imagetype.SVG
|
||||
|
||||
if !canSave {
|
||||
return newCantSaveError(r.po.Format)
|
||||
}
|
||||
|
||||
// Acquire queue semaphore (if enabled)
|
||||
releaseQueueSem, err := r.semaphores.AcquireQueue()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer releaseQueueSem()
|
||||
|
||||
// Acquire processing semaphore
|
||||
releaseProcessingSem, err := r.acquireProcessingSem(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer releaseProcessingSem()
|
||||
|
||||
// Deal with processing image counter
|
||||
stats.IncImagesInProgress()
|
||||
defer stats.DecImagesInProgress()
|
||||
|
||||
// Response status code is OK by default
|
||||
statusCode := http.StatusOK
|
||||
|
||||
// Request headers
|
||||
imgRequestHeaders := r.makeImageRequestHeaders()
|
||||
|
||||
// create download options
|
||||
do := r.makeDownloadOptions(ctx, imgRequestHeaders)
|
||||
|
||||
// Fetch image actual
|
||||
originData, originHeaders, err := r.fetchImage(ctx, do)
|
||||
if err == nil {
|
||||
defer originData.Close() // if any originData has been opened, we need to close it
|
||||
}
|
||||
|
||||
// Check that image detection didn't take too long
|
||||
if terr := server.CheckTimeout(ctx); terr != nil {
|
||||
return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
|
||||
}
|
||||
|
||||
// Respond with NotModified if image was not modified
|
||||
var nmErr imagefetcher.NotModifiedError
|
||||
|
||||
if errors.As(err, &nmErr) {
|
||||
r.hwr.SetOriginHeaders(nmErr.Headers())
|
||||
|
||||
return r.respondWithNotModified()
|
||||
}
|
||||
|
||||
// Prepare to write image response headers
|
||||
r.hwr.SetOriginHeaders(originHeaders)
|
||||
|
||||
// If error is not related to NotModified, respond with fallback image and replace image data
|
||||
if err != nil {
|
||||
originData, statusCode, err = r.handleDownloadError(ctx, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if image supports load from origin format
|
||||
if !vips.SupportsLoad(originData.Format()) {
|
||||
return newCantLoadError(originData.Format())
|
||||
}
|
||||
|
||||
// Actually process the image
|
||||
result, err := r.processImage(ctx, originData)
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// Write debug headers. It seems unlogical to move they to headerwriter since they're
|
||||
// not used anywhere else.
|
||||
err = r.writeDebugHeaders(result, originData)
|
||||
if err != nil {
|
||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
||||
}
|
||||
|
||||
// Responde with actual image
|
||||
err = r.respondWithImage(statusCode, result.OutData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
278
handlers/processing/request_methods.go
Normal file
278
handlers/processing/request_methods.go
Normal file
@@ -0,0 +1,278 @@
|
||||
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 imagedata.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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
Reference in New Issue
Block a user