mirror of
https://github.com/imgproxy/imgproxy.git
synced 2025-10-08 19:30:35 +02:00
Another attempt to DRY
This commit is contained in:
@@ -4,7 +4,7 @@ import (
|
|||||||
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
||||||
"github.com/imgproxy/imgproxy/v3/ensure"
|
"github.com/imgproxy/imgproxy/v3/ensure"
|
||||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||||
processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
|
"github.com/imgproxy/imgproxy/v3/handlers"
|
||||||
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
||||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
||||||
"github.com/imgproxy/imgproxy/v3/semaphores"
|
"github.com/imgproxy/imgproxy/v3/semaphores"
|
||||||
@@ -20,7 +20,7 @@ type Config struct {
|
|||||||
WatermarkImage auximageprovider.StaticConfig
|
WatermarkImage auximageprovider.StaticConfig
|
||||||
Transport transport.Config
|
Transport transport.Config
|
||||||
Fetcher fetcher.Config
|
Fetcher fetcher.Config
|
||||||
ProcessingHandler processinghandler.Config
|
ProcessingHandler handlers.Config
|
||||||
StreamHandler stream.Config
|
StreamHandler stream.Config
|
||||||
Server server.Config
|
Server server.Config
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ func NewDefaultConfig() Config {
|
|||||||
WatermarkImage: auximageprovider.NewDefaultStaticConfig(),
|
WatermarkImage: auximageprovider.NewDefaultStaticConfig(),
|
||||||
Transport: transport.NewDefaultConfig(),
|
Transport: transport.NewDefaultConfig(),
|
||||||
Fetcher: fetcher.NewDefaultConfig(),
|
Fetcher: fetcher.NewDefaultConfig(),
|
||||||
ProcessingHandler: processinghandler.NewDefaultConfig(),
|
ProcessingHandler: handlers.NewDefaultConfig(),
|
||||||
StreamHandler: stream.NewDefaultConfig(),
|
StreamHandler: stream.NewDefaultConfig(),
|
||||||
Server: server.NewDefaultConfig(),
|
Server: server.NewDefaultConfig(),
|
||||||
}
|
}
|
||||||
@@ -74,7 +74,7 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = processinghandler.LoadConfigFromEnv(&c.ProcessingHandler); err != nil {
|
if _, err = handlers.LoadConfigFromEnv(&c.ProcessingHandler); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
package processing
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
24
handlers/context.go
Normal file
24
handlers/context.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/semaphores"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Context defines the input interface handler needs to operate.
|
||||||
|
// In a nutshell, this interface strips ImgProxy definition from implementation.
|
||||||
|
// All the dependent components could share the same global interface.
|
||||||
|
//
|
||||||
|
// It might as well be implemented on the Handler struct itself, no matter.
|
||||||
|
// However, in this case, we'we got to implement it on every Handler struct.
|
||||||
|
type Context interface {
|
||||||
|
HeaderWriter() *headerwriter.Writer
|
||||||
|
Fetcher() *fetcher.Fetcher
|
||||||
|
Semaphores() *semaphores.Semaphores
|
||||||
|
FallbackImage() auximageprovider.Provider
|
||||||
|
WatermarkImage() auximageprovider.Provider
|
||||||
|
ImageDataFactory() *imagedata.Factory
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package processing
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -10,15 +10,15 @@ import (
|
|||||||
|
|
||||||
// Monitoring error categories
|
// Monitoring error categories
|
||||||
const (
|
const (
|
||||||
categoryTimeout = "timeout"
|
CategoryTimeout = "timeout"
|
||||||
categoryImageDataSize = "image_data_size"
|
CategoryImageDataSize = "image_data_size"
|
||||||
categoryPathParsing = "path_parsing"
|
CategoryPathParsing = "path_parsing"
|
||||||
categorySecurity = "security"
|
CategorySecurity = "security"
|
||||||
categoryQueue = "queue"
|
CategoryQueue = "queue"
|
||||||
categoryDownload = "download"
|
CategoryDownload = "download"
|
||||||
categoryProcessing = "processing"
|
CategoryProcessing = "processing"
|
||||||
categoryIO = "IO"
|
CategoryIO = "IO"
|
||||||
categoryConfig = "config(tmp)" // NOTE: THIS IS TEMPORARY
|
CategoryConfig = "config(tmp)" // NOTE: THIS IS TEMPORARY
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -26,7 +26,7 @@ type (
|
|||||||
InvalidURLError string
|
InvalidURLError string
|
||||||
)
|
)
|
||||||
|
|
||||||
func newResponseWriteError(cause error) *ierrors.Error {
|
func NewResponseWriteError(cause error) *ierrors.Error {
|
||||||
return ierrors.Wrap(
|
return ierrors.Wrap(
|
||||||
ResponseWriteError{cause},
|
ResponseWriteError{cause},
|
||||||
1,
|
1,
|
||||||
@@ -42,7 +42,7 @@ func (e ResponseWriteError) Unwrap() error {
|
|||||||
return e.error
|
return e.error
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInvalidURLErrorf(status int, format string, args ...interface{}) error {
|
func NewInvalidURLErrorf(status int, format string, args ...interface{}) error {
|
||||||
return ierrors.Wrap(
|
return ierrors.Wrap(
|
||||||
InvalidURLError(fmt.Sprintf(format, args...)),
|
InvalidURLError(fmt.Sprintf(format, args...)),
|
||||||
1,
|
1,
|
||||||
@@ -55,17 +55,17 @@ func newInvalidURLErrorf(status int, format string, args ...interface{}) error {
|
|||||||
func (e InvalidURLError) Error() string { return string(e) }
|
func (e InvalidURLError) Error() string { return string(e) }
|
||||||
|
|
||||||
// newCantSaveError creates "resulting image not supported" error
|
// newCantSaveError creates "resulting image not supported" error
|
||||||
func newCantSaveError(format imagetype.Type) error {
|
func NewCantSaveError(format imagetype.Type) error {
|
||||||
return ierrors.Wrap(newInvalidURLErrorf(
|
return ierrors.Wrap(NewInvalidURLErrorf(
|
||||||
http.StatusUnprocessableEntity,
|
http.StatusUnprocessableEntity,
|
||||||
"Resulting image format is not supported: %s", format,
|
"Resulting image format is not supported: %s", format,
|
||||||
), 1, ierrors.WithCategory(categoryPathParsing))
|
), 1, ierrors.WithCategory(CategoryPathParsing))
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCantLoadError creates "source image not supported" error
|
// newCantLoadError creates "source image not supported" error
|
||||||
func newCantLoadError(format imagetype.Type) error {
|
func NewCantLoadError(format imagetype.Type) error {
|
||||||
return ierrors.Wrap(newInvalidURLErrorf(
|
return ierrors.Wrap(NewInvalidURLErrorf(
|
||||||
http.StatusUnprocessableEntity,
|
http.StatusUnprocessableEntity,
|
||||||
"Source image format is not supported: %s", format,
|
"Source image format is not supported: %s", format,
|
||||||
), 1, ierrors.WithCategory(categoryProcessing))
|
), 1, ierrors.WithCategory(CategoryProcessing))
|
||||||
}
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package processing
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -30,8 +30,8 @@ func splitPathSignature(r *http.Request, config *Config) (string, string, error)
|
|||||||
signature, path, _ := strings.Cut(uri, "/")
|
signature, path, _ := strings.Cut(uri, "/")
|
||||||
if len(signature) == 0 || len(path) == 0 {
|
if len(signature) == 0 || len(path) == 0 {
|
||||||
return "", "", ierrors.Wrap(
|
return "", "", ierrors.Wrap(
|
||||||
newInvalidURLErrorf(http.StatusNotFound, "Invalid path: %s", path), 0,
|
NewInvalidURLErrorf(http.StatusNotFound, "Invalid path: %s", path), 0,
|
||||||
ierrors.WithCategory(categoryPathParsing),
|
ierrors.WithCategory(CategoryPathParsing),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package processing
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -96,7 +96,7 @@ func (s *PathTestSuite) TestParsePath() {
|
|||||||
|
|
||||||
s.Require().Error(err)
|
s.Require().Error(err)
|
||||||
s.Require().ErrorAs(err, &ierr)
|
s.Require().ErrorAs(err, &ierr)
|
||||||
s.Require().Equal(categoryPathParsing, ierr.Category())
|
s.Require().Equal(CategoryPathParsing, ierr.Category())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
@@ -3,54 +3,40 @@ package processing
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/auximageprovider"
|
"github.com/imgproxy/imgproxy/v3/handlers"
|
||||||
"github.com/imgproxy/imgproxy/v3/errorreport"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
"github.com/imgproxy/imgproxy/v3/handlers/stream"
|
||||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/monitoring"
|
"github.com/imgproxy/imgproxy/v3/monitoring"
|
||||||
"github.com/imgproxy/imgproxy/v3/monitoring/stats"
|
"github.com/imgproxy/imgproxy/v3/monitoring/stats"
|
||||||
"github.com/imgproxy/imgproxy/v3/options"
|
"github.com/imgproxy/imgproxy/v3/options"
|
||||||
"github.com/imgproxy/imgproxy/v3/security"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/semaphores"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler handles image processing requests
|
// Handler handles image processing requests
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
hw *headerwriter.Writer // Configured HeaderWriter instance
|
hCtx handlers.Context // Input context interface
|
||||||
stream *stream.Handler // Stream handler for raw image streaming
|
stream *stream.Handler // Stream handler for raw image streaming
|
||||||
config *Config // Handler configuration
|
config *handlers.Config // Handler configuration
|
||||||
semaphores *semaphores.Semaphores
|
}
|
||||||
fallbackImage auximageprovider.Provider
|
|
||||||
watermarkImage auximageprovider.Provider
|
type request struct {
|
||||||
idf *imagedata.Factory
|
*handlers.Request
|
||||||
|
Options *options.ProcessingOptions // Processing options extracted from URL
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates new handler object
|
// New creates new handler object
|
||||||
func New(
|
func New(
|
||||||
|
context handlers.Context,
|
||||||
stream *stream.Handler,
|
stream *stream.Handler,
|
||||||
hw *headerwriter.Writer,
|
config *handlers.Config,
|
||||||
semaphores *semaphores.Semaphores,
|
|
||||||
fi auximageprovider.Provider,
|
|
||||||
wi auximageprovider.Provider,
|
|
||||||
idf *imagedata.Factory,
|
|
||||||
config *Config,
|
|
||||||
) (*Handler, error) {
|
) (*Handler, error) {
|
||||||
if err := config.Validate(); err != nil {
|
if err := config.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Handler{
|
return &Handler{
|
||||||
hw: hw,
|
hCtx: context,
|
||||||
config: config,
|
config: config,
|
||||||
stream: stream,
|
stream: stream,
|
||||||
semaphores: semaphores,
|
|
||||||
fallbackImage: fi,
|
|
||||||
watermarkImage: wi,
|
|
||||||
idf: idf,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,57 +52,29 @@ func (h *Handler) Execute(
|
|||||||
|
|
||||||
ctx := imageRequest.Context()
|
ctx := imageRequest.Context()
|
||||||
|
|
||||||
// Verify URL signature and extract image url and processing options
|
r, po, err := handlers.NewRequest(h.hCtx, h, imageRequest, h.config, reqID, rw)
|
||||||
imageURL, po, mm, err := h.newRequest(ctx, imageRequest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// if processing options indicate raw image streaming, stream it and return
|
// if processing options indicate raw image streaming, stream it and return
|
||||||
if po.Raw {
|
if po.Raw {
|
||||||
return h.stream.Execute(ctx, imageRequest, imageURL, reqID, po, rw)
|
return h.stream.Execute(ctx, imageRequest, r.ImageURL, reqID, po, rw)
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &request{
|
req := &request{
|
||||||
handler: h,
|
Request: r,
|
||||||
imageRequest: imageRequest,
|
Options: po,
|
||||||
reqID: reqID,
|
|
||||||
rw: rw,
|
|
||||||
config: h.config,
|
|
||||||
po: po,
|
|
||||||
imageURL: imageURL,
|
|
||||||
monitoringMeta: mm,
|
|
||||||
semaphores: h.semaphores,
|
|
||||||
hwr: h.hw.NewRequest(),
|
|
||||||
idf: h.idf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return req.execute(ctx)
|
return execute(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newRequest extracts image url and processing options from request URL and verifies them
|
func (h *Handler) ParsePath(path string, headers http.Header) (*options.ProcessingOptions, string, error) {
|
||||||
func (h *Handler) newRequest(
|
return options.ParsePath(path, headers)
|
||||||
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)
|
func (h *Handler) CreateMeta(ctx context.Context, imageURL string, po *options.ProcessingOptions) monitoring.Meta {
|
||||||
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)
|
imageOrigin := imageOrigin(imageURL)
|
||||||
|
|
||||||
mm := monitoring.Meta{
|
mm := monitoring.Meta{
|
||||||
@@ -125,27 +83,13 @@ func (h *Handler) newRequest(
|
|||||||
monitoring.MetaProcessingOptions: po.Diff().Flatten(),
|
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)
|
monitoring.SetMetadata(ctx, mm)
|
||||||
|
|
||||||
// verify that image URL came from the valid source
|
// NOTE: errorreport needs to be patched (just not in the context of this PR)
|
||||||
err = security.VerifySourceURL(imageURL)
|
// set error reporting and monitoring context
|
||||||
if err != nil {
|
// errorreport.SetMetadata(ctx, "Source Image URL", imageURL)
|
||||||
return "", nil, mm, ierrors.Wrap(err, 0, ierrors.WithCategory(categorySecurity))
|
// errorreport.SetMetadata(ctx, "Source Image Origin", imageOrigin)
|
||||||
}
|
// errorreport.SetMetadata(ctx, "Processing Options", po)
|
||||||
|
|
||||||
return imageURL, po, mm, nil
|
return mm
|
||||||
}
|
|
||||||
|
|
||||||
// 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 ""
|
|
||||||
}
|
}
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
package processing
|
|
@@ -4,55 +4,29 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/fetcher"
|
"github.com/imgproxy/imgproxy/v3/fetcher"
|
||||||
"github.com/imgproxy/imgproxy/v3/headerwriter"
|
"github.com/imgproxy/imgproxy/v3/handlers"
|
||||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/imagetype"
|
"github.com/imgproxy/imgproxy/v3/imagetype"
|
||||||
"github.com/imgproxy/imgproxy/v3/monitoring"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/monitoring/stats"
|
"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/server"
|
||||||
"github.com/imgproxy/imgproxy/v3/vips"
|
"github.com/imgproxy/imgproxy/v3/vips"
|
||||||
)
|
)
|
||||||
|
|
||||||
// request holds the parameters and state for a single request request
|
func execute(ctx context.Context, r *request) error {
|
||||||
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
|
|
||||||
idf *imagedata.Factory
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute handles the actual processing logic
|
|
||||||
func (r *request) execute(ctx context.Context) error {
|
|
||||||
// Check if we can save the resulting image
|
// Check if we can save the resulting image
|
||||||
canSave := vips.SupportsSave(r.po.Format) ||
|
canSave := vips.SupportsSave(r.Options.Format) ||
|
||||||
r.po.Format == imagetype.Unknown ||
|
r.Options.Format == imagetype.Unknown ||
|
||||||
r.po.Format == imagetype.SVG
|
r.Options.Format == imagetype.SVG
|
||||||
|
|
||||||
if !canSave {
|
if !canSave {
|
||||||
return newCantSaveError(r.po.Format)
|
return handlers.NewCantSaveError(r.Options.Format)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire queue semaphore (if enabled)
|
|
||||||
releaseQueueSem, err := r.semaphores.AcquireQueue()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer releaseQueueSem()
|
|
||||||
|
|
||||||
// Acquire processing semaphore
|
// Acquire processing semaphore
|
||||||
releaseProcessingSem, err := r.acquireProcessingSem(ctx)
|
releaseProcessingSem, err := r.AcquireProcessingSem(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -66,37 +40,37 @@ func (r *request) execute(ctx context.Context) error {
|
|||||||
statusCode := http.StatusOK
|
statusCode := http.StatusOK
|
||||||
|
|
||||||
// Request headers
|
// Request headers
|
||||||
imgRequestHeaders := r.makeImageRequestHeaders()
|
imgRequestHeaders := r.MakeImageRequestHeaders()
|
||||||
|
|
||||||
// create download options
|
// create download options
|
||||||
do := r.makeDownloadOptions(ctx, imgRequestHeaders)
|
do := r.MakeDownloadOptions(imgRequestHeaders, r.Options.SecurityOptions)
|
||||||
|
|
||||||
// Fetch image actual
|
// Fetch image actual
|
||||||
originData, originHeaders, err := r.fetchImage(ctx, do)
|
originData, originHeaders, err := r.FetchImage(ctx, do)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer originData.Close() // if any originData has been opened, we need to close it
|
defer originData.Close() // if any originData has been opened, we need to close it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that image detection didn't take too long
|
// Check that image detection didn't take too long
|
||||||
if terr := server.CheckTimeout(ctx); terr != nil {
|
if terr := server.CheckTimeout(ctx); terr != nil {
|
||||||
return ierrors.Wrap(terr, 0, ierrors.WithCategory(categoryTimeout))
|
return ierrors.Wrap(terr, 0, ierrors.WithCategory(handlers.CategoryTimeout))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Respond with NotModified if image was not modified
|
// Respond with NotModified if image was not modified
|
||||||
var nmErr fetcher.NotModifiedError
|
var nmErr fetcher.NotModifiedError
|
||||||
|
|
||||||
if errors.As(err, &nmErr) {
|
if errors.As(err, &nmErr) {
|
||||||
r.hwr.SetOriginHeaders(nmErr.Headers())
|
r.HeaderWriter.SetOriginHeaders(nmErr.Headers())
|
||||||
|
|
||||||
return r.respondWithNotModified()
|
return respondWithNotModified(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare to write image response headers
|
// Prepare to write image response headers
|
||||||
r.hwr.SetOriginHeaders(originHeaders)
|
r.HeaderWriter.SetOriginHeaders(originHeaders)
|
||||||
|
|
||||||
// If error is not related to NotModified, respond with fallback image and replace image data
|
// If error is not related to NotModified, respond with fallback image and replace image data
|
||||||
if err != nil {
|
if err != nil {
|
||||||
originData, statusCode, err = r.handleDownloadError(ctx, err)
|
originData, statusCode, err = handleDownloadError(ctx, r, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -104,11 +78,11 @@ func (r *request) execute(ctx context.Context) error {
|
|||||||
|
|
||||||
// Check if image supports load from origin format
|
// Check if image supports load from origin format
|
||||||
if !vips.SupportsLoad(originData.Format()) {
|
if !vips.SupportsLoad(originData.Format()) {
|
||||||
return newCantLoadError(originData.Format())
|
return handlers.NewCantLoadError(originData.Format())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actually process the image
|
// Actually process the image
|
||||||
result, err := r.processImage(ctx, originData)
|
result, err := processImage(ctx, r, originData)
|
||||||
|
|
||||||
// Let's close resulting image data only if it differs from the source image data
|
// Let's close resulting image data only if it differs from the source image data
|
||||||
if result != nil && result.OutData != nil && result.OutData != originData {
|
if result != nil && result.OutData != nil && result.OutData != originData {
|
||||||
@@ -117,26 +91,35 @@ func (r *request) execute(ctx context.Context) error {
|
|||||||
|
|
||||||
// First, check if the processing error wasn't caused by an image data error
|
// First, check if the processing error wasn't caused by an image data error
|
||||||
if derr := originData.Error(); derr != nil {
|
if derr := originData.Error(); derr != nil {
|
||||||
return ierrors.Wrap(derr, 0, ierrors.WithCategory(categoryDownload))
|
return ierrors.Wrap(derr, 0, ierrors.WithCategory(handlers.CategoryDownload))
|
||||||
}
|
}
|
||||||
|
|
||||||
// If it wasn't, than it was a processing error
|
// If it wasn't, than it was a processing error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryProcessing))
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryProcessing))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write debug headers. It seems unlogical to move they to headerwriter since they're
|
// Write debug headers. It seems unlogical to move they to headerwriter since they're
|
||||||
// not used anywhere else.
|
// not used anywhere else.
|
||||||
err = r.writeDebugHeaders(result, originData)
|
err = writeDebugHeaders(r, result, originData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryImageDataSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responde with actual image
|
// Responde with actual image
|
||||||
err = r.respondWithImage(statusCode, result.OutData)
|
err = respondWithImage(r, statusCode, result.OutData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return 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 ""
|
||||||
|
}
|
||||||
|
@@ -6,114 +6,48 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/imgproxy/imgproxy/v3/cookies"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/errorreport"
|
"github.com/imgproxy/imgproxy/v3/errorreport"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/handlers"
|
||||||
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
"github.com/imgproxy/imgproxy/v3/httpheaders"
|
||||||
"github.com/imgproxy/imgproxy/v3/ierrors"
|
"github.com/imgproxy/imgproxy/v3/ierrors"
|
||||||
"github.com/imgproxy/imgproxy/v3/imagedata"
|
"github.com/imgproxy/imgproxy/v3/imagedata"
|
||||||
"github.com/imgproxy/imgproxy/v3/monitoring"
|
"github.com/imgproxy/imgproxy/v3/monitoring"
|
||||||
"github.com/imgproxy/imgproxy/v3/options"
|
|
||||||
"github.com/imgproxy/imgproxy/v3/processing"
|
"github.com/imgproxy/imgproxy/v3/processing"
|
||||||
"github.com/imgproxy/imgproxy/v3/server"
|
"github.com/imgproxy/imgproxy/v3/server"
|
||||||
log "github.com/sirupsen/logrus"
|
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
|
// handleDownloadError replaces the image data with fallback image if needed
|
||||||
func (r *request) handleDownloadError(
|
func handleDownloadError(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
r *request,
|
||||||
originalErr error,
|
originalErr error,
|
||||||
) (imagedata.ImageData, int, error) {
|
) (imagedata.ImageData, int, error) {
|
||||||
err := r.wrapDownloadingErr(originalErr)
|
err := r.WrapDownloadingErr(originalErr)
|
||||||
|
|
||||||
// If there is no fallback image configured, just return the error
|
// If there is no fallback image configured, just return the error
|
||||||
data, headers := r.getFallbackImage(ctx, r.po)
|
data, headers := getFallbackImage(ctx, r)
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Just send error
|
// Just send error
|
||||||
monitoring.SendError(ctx, categoryDownload, err)
|
monitoring.SendError(ctx, handlers.CategoryDownload, err)
|
||||||
|
|
||||||
// We didn't return, so we have to report error
|
// We didn't return, so we have to report error
|
||||||
if err.ShouldReport() {
|
if err.ShouldReport() {
|
||||||
errorreport.Report(err, r.imageRequest)
|
errorreport.Report(err, r.Req)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.
|
log.
|
||||||
WithField("request_id", r.reqID).
|
WithField("request_id", r.ID).
|
||||||
Warningf("Could not load image %s. Using fallback image. %s", r.imageURL, err.Error())
|
Warningf("Could not load image %s. Using fallback image. %s", r.ImageURL, err.Error())
|
||||||
|
|
||||||
var statusCode int
|
var statusCode int
|
||||||
|
|
||||||
// Set status code if needed
|
// Set status code if needed
|
||||||
if r.config.FallbackImageHTTPCode > 0 {
|
if r.Config.FallbackImageHTTPCode > 0 {
|
||||||
statusCode = r.config.FallbackImageHTTPCode
|
statusCode = r.Config.FallbackImageHTTPCode
|
||||||
} else {
|
} else {
|
||||||
statusCode = err.StatusCode()
|
statusCode = err.StatusCode()
|
||||||
}
|
}
|
||||||
@@ -122,27 +56,27 @@ func (r *request) handleDownloadError(
|
|||||||
headers.Del(httpheaders.Expires)
|
headers.Del(httpheaders.Expires)
|
||||||
headers.Del(httpheaders.LastModified)
|
headers.Del(httpheaders.LastModified)
|
||||||
|
|
||||||
r.hwr.SetOriginHeaders(headers)
|
r.HeaderWriter.SetOriginHeaders(headers)
|
||||||
r.hwr.SetIsFallbackImage()
|
r.HeaderWriter.SetIsFallbackImage()
|
||||||
|
|
||||||
return data, statusCode, nil
|
return data, statusCode, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFallbackImage returns fallback image if any
|
// getFallbackImage returns fallback image if any
|
||||||
func (r *request) getFallbackImage(
|
func getFallbackImage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
po *options.ProcessingOptions,
|
r *request,
|
||||||
) (imagedata.ImageData, http.Header) {
|
) (imagedata.ImageData, http.Header) {
|
||||||
if r.handler.fallbackImage == nil {
|
if r.Context.FallbackImage() == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, h, err := r.handler.fallbackImage.Get(ctx, po)
|
data, h, err := r.Context.FallbackImage().Get(ctx, r.Options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warning(err.Error())
|
log.Warning(err.Error())
|
||||||
|
|
||||||
if ierr := r.wrapDownloadingErr(err); ierr.ShouldReport() {
|
if ierr := r.WrapDownloadingErr(err); ierr.ShouldReport() {
|
||||||
errorreport.Report(ierr, r.imageRequest)
|
errorreport.Report(ierr, r.Req)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -152,127 +86,130 @@ func (r *request) getFallbackImage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// processImage calls actual image processing
|
// processImage calls actual image processing
|
||||||
func (r *request) processImage(ctx context.Context, originData imagedata.ImageData) (*processing.Result, error) {
|
func processImage(
|
||||||
defer monitoring.StartProcessingSegment(ctx, r.monitoringMeta.Filter(monitoring.MetaProcessingOptions))()
|
ctx context.Context,
|
||||||
return processing.ProcessImage(ctx, originData, r.po, r.handler.watermarkImage, r.handler.idf)
|
r *request,
|
||||||
|
originData imagedata.ImageData,
|
||||||
|
) (*processing.Result, error) {
|
||||||
|
defer monitoring.StartProcessingSegment(
|
||||||
|
ctx,
|
||||||
|
r.MonitoringMeta.Filter(monitoring.MetaProcessingOptions),
|
||||||
|
)()
|
||||||
|
return processing.ProcessImage(ctx, originData, r.Options, r.Context.WatermarkImage(), r.Context.ImageDataFactory())
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
|
// writeDebugHeaders writes debug headers (X-Origin-*, X-Result-*) to the response
|
||||||
func (r *request) writeDebugHeaders(result *processing.Result, originData imagedata.ImageData) error {
|
func writeDebugHeaders(
|
||||||
if !r.config.EnableDebugHeaders {
|
r *request,
|
||||||
|
result *processing.Result,
|
||||||
|
originData imagedata.ImageData,
|
||||||
|
) error {
|
||||||
|
if !r.Config.EnableDebugHeaders {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if result != nil {
|
if result != nil {
|
||||||
r.rw.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
|
r.ResponseWriter.Header().Set(httpheaders.XOriginWidth, strconv.Itoa(result.OriginWidth))
|
||||||
r.rw.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
|
r.ResponseWriter.Header().Set(httpheaders.XOriginHeight, strconv.Itoa(result.OriginHeight))
|
||||||
r.rw.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
|
r.ResponseWriter.Header().Set(httpheaders.XResultWidth, strconv.Itoa(result.ResultWidth))
|
||||||
r.rw.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
|
r.ResponseWriter.Header().Set(httpheaders.XResultHeight, strconv.Itoa(result.ResultHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to read origin image size
|
// Try to read origin image size
|
||||||
size, err := originData.Size()
|
size, err := originData.Size()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryImageDataSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
r.rw.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
|
r.ResponseWriter.Header().Set(httpheaders.XOriginContentLength, strconv.Itoa(size))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// respondWithNotModified writes not-modified response
|
// respondWithNotModified writes not-modified response
|
||||||
func (r *request) respondWithNotModified() error {
|
func respondWithNotModified(r *request) error {
|
||||||
r.hwr.SetExpires(r.po.Expires)
|
r.HeaderWriter.SetExpires(r.Options.Expires)
|
||||||
r.hwr.SetVary()
|
r.HeaderWriter.SetVary()
|
||||||
|
|
||||||
if r.config.LastModifiedEnabled {
|
if r.Config.LastModifiedEnabled {
|
||||||
r.hwr.Passthrough(httpheaders.LastModified)
|
r.HeaderWriter.Passthrough(httpheaders.LastModified)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.config.ETagEnabled {
|
if r.Config.ETagEnabled {
|
||||||
r.hwr.Passthrough(httpheaders.Etag)
|
r.HeaderWriter.Passthrough(httpheaders.Etag)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.hwr.Write(r.rw)
|
r.HeaderWriter.Write(r.ResponseWriter)
|
||||||
|
|
||||||
r.rw.WriteHeader(http.StatusNotModified)
|
r.ResponseWriter.WriteHeader(http.StatusNotModified)
|
||||||
|
|
||||||
server.LogResponse(
|
server.LogResponse(
|
||||||
r.reqID, r.imageRequest, http.StatusNotModified, nil,
|
r.ID, r.Req, http.StatusNotModified, nil,
|
||||||
log.Fields{
|
log.Fields{
|
||||||
"image_url": r.imageURL,
|
"image_url": r.ImageURL,
|
||||||
"processing_options": r.po,
|
"processing_options": r.Options,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *request) respondWithImage(statusCode int, resultData imagedata.ImageData) error {
|
func respondWithImage(r *request, statusCode int, resultData imagedata.ImageData) error {
|
||||||
// We read the size of the image data here, so we can set Content-Length header.
|
// 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
|
// This indireclty ensures that the image data is fully read from the source, no
|
||||||
// errors happened.
|
// errors happened.
|
||||||
resultSize, err := resultData.Size()
|
resultSize, err := resultData.Size()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ierrors.Wrap(err, 0, ierrors.WithCategory(categoryImageDataSize))
|
return ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryImageDataSize))
|
||||||
}
|
}
|
||||||
|
|
||||||
r.hwr.SetContentType(resultData.Format().Mime())
|
r.HeaderWriter.SetContentType(resultData.Format().Mime())
|
||||||
r.hwr.SetContentLength(resultSize)
|
r.HeaderWriter.SetContentLength(resultSize)
|
||||||
r.hwr.SetContentDisposition(
|
r.HeaderWriter.SetContentDisposition(
|
||||||
r.imageURL,
|
r.ImageURL,
|
||||||
r.po.Filename,
|
r.Options.Filename,
|
||||||
resultData.Format().Ext(),
|
resultData.Format().Ext(),
|
||||||
"",
|
"",
|
||||||
r.po.ReturnAttachment,
|
r.Options.ReturnAttachment,
|
||||||
)
|
)
|
||||||
r.hwr.SetExpires(r.po.Expires)
|
r.HeaderWriter.SetExpires(r.Options.Expires)
|
||||||
r.hwr.SetVary()
|
r.HeaderWriter.SetVary()
|
||||||
r.hwr.SetCanonical(r.imageURL)
|
r.HeaderWriter.SetCanonical(r.ImageURL)
|
||||||
|
|
||||||
if r.config.LastModifiedEnabled {
|
if r.Config.LastModifiedEnabled {
|
||||||
r.hwr.Passthrough(httpheaders.LastModified)
|
r.HeaderWriter.Passthrough(httpheaders.LastModified)
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.config.ETagEnabled {
|
if r.Config.ETagEnabled {
|
||||||
r.hwr.Passthrough(httpheaders.Etag)
|
r.HeaderWriter.Passthrough(httpheaders.Etag)
|
||||||
}
|
}
|
||||||
|
|
||||||
r.hwr.Write(r.rw)
|
r.HeaderWriter.Write(r.ResponseWriter)
|
||||||
|
|
||||||
r.rw.WriteHeader(statusCode)
|
r.ResponseWriter.WriteHeader(statusCode)
|
||||||
|
|
||||||
_, err = io.Copy(r.rw, resultData.Reader())
|
_, err = io.Copy(r.ResponseWriter, resultData.Reader())
|
||||||
|
|
||||||
var ierr *ierrors.Error
|
var ierr *ierrors.Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ierr = newResponseWriteError(err)
|
ierr = handlers.NewResponseWriteError(err)
|
||||||
|
|
||||||
if r.config.ReportIOErrors {
|
if r.Config.ReportIOErrors {
|
||||||
return ierrors.Wrap(ierr, 0, ierrors.WithCategory(categoryIO), ierrors.WithShouldReport(true))
|
return ierrors.Wrap(
|
||||||
|
ierr, 0,
|
||||||
|
ierrors.WithCategory(handlers.CategoryIO),
|
||||||
|
ierrors.WithShouldReport(true),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
server.LogResponse(
|
server.LogResponse(
|
||||||
r.reqID, r.imageRequest, statusCode, ierr,
|
r.ID, r.Req, statusCode, ierr,
|
||||||
log.Fields{
|
log.Fields{
|
||||||
"image_url": r.imageURL,
|
"image_url": r.ImageURL,
|
||||||
"processing_options": r.po,
|
"processing_options": r.Options,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
178
handlers/request.go
Normal file
178
handlers/request.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/imgproxy/imgproxy/v3/cookies"
|
||||||
|
"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/monitoring"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/security"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/server"
|
||||||
|
"github.com/imgproxy/imgproxy/v3/structdiff"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options is an object of URL options extracted from the URL
|
||||||
|
type Options = structdiff.Diffable
|
||||||
|
|
||||||
|
// PathPaser is an interface for URL path parser: it extracts processing options and image path
|
||||||
|
type Constructor[O Options] interface {
|
||||||
|
ParsePath(path string, headers http.Header) (O, string, error)
|
||||||
|
CreateMeta(ctx context.Context, imageURL string, po O) monitoring.Meta
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Context Context // Input context interface
|
||||||
|
Config *Config // Handler configuration
|
||||||
|
ID string // Request ID
|
||||||
|
Req *http.Request // Original HTTP request
|
||||||
|
ResponseWriter http.ResponseWriter // HTTP response writer
|
||||||
|
HeaderWriter *headerwriter.Request // Header writer request
|
||||||
|
ImageURL string // Image URL to process
|
||||||
|
MonitoringMeta monitoring.Meta // Monitoring metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrepareRequest extracts image url and processing options from request URL and verifies them
|
||||||
|
func NewRequest[P Constructor[O], O Options](
|
||||||
|
handler Context, // or, essentially, instance
|
||||||
|
constructor P,
|
||||||
|
imageRequest *http.Request,
|
||||||
|
config *Config,
|
||||||
|
reqID string,
|
||||||
|
rw http.ResponseWriter,
|
||||||
|
) (*Request, O, error) {
|
||||||
|
// let's extract signature and valid request path from a request
|
||||||
|
path, signature, err := splitPathSignature(imageRequest, config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, *new(O), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify the signature (if any)
|
||||||
|
if err = security.VerifySignature(signature, path); err != nil {
|
||||||
|
return nil, *new(O), ierrors.Wrap(err, 0, ierrors.WithCategory(CategorySecurity))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse image url and processing options
|
||||||
|
po, imageURL, err := constructor.ParsePath(path, imageRequest.Header)
|
||||||
|
if err != nil {
|
||||||
|
return nil, *new(O), ierrors.Wrap(err, 0, ierrors.WithCategory(CategoryPathParsing))
|
||||||
|
}
|
||||||
|
|
||||||
|
mm := constructor.CreateMeta(imageRequest.Context(), imageURL, po)
|
||||||
|
|
||||||
|
// verify that image URL came from the valid source
|
||||||
|
err = security.VerifySourceURL(imageURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, *new(O), ierrors.Wrap(err, 0, ierrors.WithCategory(CategorySecurity))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Request{
|
||||||
|
Context: handler,
|
||||||
|
Config: config,
|
||||||
|
ID: reqID,
|
||||||
|
Req: imageRequest,
|
||||||
|
ResponseWriter: rw,
|
||||||
|
HeaderWriter: handler.HeaderWriter().NewRequest(),
|
||||||
|
ImageURL: imageURL,
|
||||||
|
MonitoringMeta: mm,
|
||||||
|
}, po, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeDownloadOptions creates [imagedata.DownloadOptions]
|
||||||
|
// from image request headers and security options.
|
||||||
|
func (r *Request) MakeDownloadOptions(
|
||||||
|
h http.Header,
|
||||||
|
secops security.Options,
|
||||||
|
) imagedata.DownloadOptions {
|
||||||
|
return imagedata.DownloadOptions{
|
||||||
|
Header: h,
|
||||||
|
MaxSrcFileSize: secops.MaxSrcFileSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireProcessingSem acquires the processing semaphore.
|
||||||
|
// It allows as many concurrent processing requests as workers are configured.
|
||||||
|
func (r *Request) AcquireProcessingSem(ctx context.Context) (context.CancelFunc, error) {
|
||||||
|
defer monitoring.StartQueueSegment(ctx)()
|
||||||
|
|
||||||
|
sem := r.Context.Semaphores()
|
||||||
|
|
||||||
|
// Acquire queue semaphore (if enabled)
|
||||||
|
releaseQueueSem, err := sem.AcquireQueue()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Defer releasing the queue semaphore since we'll exit the queue on return
|
||||||
|
defer releaseQueueSem()
|
||||||
|
|
||||||
|
// Acquire processing semaphore
|
||||||
|
releaseProcessingSem, err := sem.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 releaseProcessingSem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Req.Header.Get(httpheaders.IfNoneMatch))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If LastModified is enabled, we forward If-Modified-Since header
|
||||||
|
if r.Config.LastModifiedEnabled {
|
||||||
|
h.Set(httpheaders.IfModifiedSince, r.Req.Header.Get(httpheaders.IfModifiedSince))
|
||||||
|
}
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchImage downloads the source image asynchronously
|
||||||
|
func (r *Request) FetchImage(
|
||||||
|
ctx context.Context,
|
||||||
|
do imagedata.DownloadOptions,
|
||||||
|
) (imagedata.ImageData, http.Header, error) {
|
||||||
|
do.DownloadFinished = monitoring.StartDownloadingSegment(ctx, r.MonitoringMeta.Filter(
|
||||||
|
monitoring.MetaSourceImageURL,
|
||||||
|
monitoring.MetaSourceImageOrigin,
|
||||||
|
))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if r.Config.CookiePassthrough {
|
||||||
|
do.CookieJar, err = cookies.JarFromRequest(r.Req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(CategoryDownload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Context.ImageDataFactory().DownloadAsync(ctx, r.ImageURL, "source image", do)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
80
imgproxy.go
80
imgproxy.go
@@ -31,19 +31,21 @@ const (
|
|||||||
|
|
||||||
// ImgProxy holds all the components needed for imgproxy to function
|
// ImgProxy holds all the components needed for imgproxy to function
|
||||||
type ImgProxy struct {
|
type ImgProxy struct {
|
||||||
HeaderWriter *headerwriter.Writer
|
headerWriter *headerwriter.Writer
|
||||||
Semaphores *semaphores.Semaphores
|
semaphores *semaphores.Semaphores
|
||||||
FallbackImage auximageprovider.Provider
|
fallbackImage auximageprovider.Provider
|
||||||
WatermarkImage auximageprovider.Provider
|
watermarkImage auximageprovider.Provider
|
||||||
Fetcher *fetcher.Fetcher
|
fetcher *fetcher.Fetcher
|
||||||
ProcessingHandler *processinghandler.Handler
|
processingHandler *processinghandler.Handler
|
||||||
StreamHandler *stream.Handler
|
streamHandler *stream.Handler
|
||||||
ImageDataFactory *imagedata.Factory
|
imageDataFactory *imagedata.Factory
|
||||||
Config *Config
|
config *Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new imgproxy instance
|
// New creates a new imgproxy instance
|
||||||
func New(ctx context.Context, config *Config) (*ImgProxy, error) {
|
func New(ctx context.Context, config *Config) (*ImgProxy, error) {
|
||||||
|
i := &ImgProxy{}
|
||||||
|
|
||||||
headerWriter, err := headerwriter.New(&config.HeaderWriter)
|
headerWriter, err := headerwriter.New(&config.HeaderWriter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -82,7 +84,7 @@ func New(ctx context.Context, config *Config) (*ImgProxy, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ph, err := processinghandler.New(
|
ph, err := processinghandler.New(
|
||||||
streamHandler, headerWriter, semaphores, fallbackImage, watermarkImage, idf, &config.ProcessingHandler,
|
i, streamHandler, &config.ProcessingHandler,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -103,22 +105,22 @@ func New(ctx context.Context, config *Config) (*ImgProxy, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ImgProxy{
|
i.headerWriter = headerWriter
|
||||||
HeaderWriter: headerWriter,
|
i.semaphores = semaphores
|
||||||
Semaphores: semaphores,
|
i.fallbackImage = fallbackImage
|
||||||
FallbackImage: fallbackImage,
|
i.watermarkImage = watermarkImage
|
||||||
WatermarkImage: watermarkImage,
|
i.fetcher = fetcher
|
||||||
Fetcher: fetcher,
|
i.processingHandler = ph
|
||||||
StreamHandler: streamHandler,
|
i.streamHandler = streamHandler
|
||||||
ProcessingHandler: ph,
|
i.imageDataFactory = idf
|
||||||
ImageDataFactory: idf,
|
i.config = config
|
||||||
Config: config,
|
|
||||||
}, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildRouter sets up the HTTP routes and middleware
|
// BuildRouter sets up the HTTP routes and middleware
|
||||||
func (i *ImgProxy) BuildRouter() (*server.Router, error) {
|
func (i *ImgProxy) BuildRouter() (*server.Router, error) {
|
||||||
r, err := server.NewRouter(&i.Config.Server)
|
r, err := server.NewRouter(&i.config.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -128,12 +130,12 @@ func (i *ImgProxy) BuildRouter() (*server.Router, error) {
|
|||||||
|
|
||||||
r.GET(faviconPath, r.NotFoundHandler).Silent()
|
r.GET(faviconPath, r.NotFoundHandler).Silent()
|
||||||
r.GET(healthPath, handlers.HealthHandler).Silent()
|
r.GET(healthPath, handlers.HealthHandler).Silent()
|
||||||
if i.Config.Server.HealthCheckPath != "" {
|
if i.config.Server.HealthCheckPath != "" {
|
||||||
r.GET(i.Config.Server.HealthCheckPath, handlers.HealthHandler).Silent()
|
r.GET(i.config.Server.HealthCheckPath, handlers.HealthHandler).Silent()
|
||||||
}
|
}
|
||||||
|
|
||||||
r.GET(
|
r.GET(
|
||||||
"/*", i.ProcessingHandler.Execute,
|
"/*", i.processingHandler.Execute,
|
||||||
r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
|
r.WithSecret, r.WithCORS, r.WithPanic, r.WithReportError, r.WithMonitoring,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -171,7 +173,7 @@ func (i *ImgProxy) StartServer(ctx context.Context) error {
|
|||||||
|
|
||||||
// startMemoryTicker starts a ticker that periodically frees memory and optionally logs memory stats
|
// startMemoryTicker starts a ticker that periodically frees memory and optionally logs memory stats
|
||||||
func (i *ImgProxy) startMemoryTicker(ctx context.Context) {
|
func (i *ImgProxy) startMemoryTicker(ctx context.Context) {
|
||||||
ticker := time.NewTicker(i.Config.Server.FreeMemoryInterval)
|
ticker := time.NewTicker(i.config.Server.FreeMemoryInterval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -181,9 +183,33 @@ func (i *ImgProxy) startMemoryTicker(ctx context.Context) {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
memory.Free()
|
memory.Free()
|
||||||
|
|
||||||
if i.Config.Server.LogMemStats {
|
if i.config.Server.LogMemStats {
|
||||||
memory.LogStats()
|
memory.LogStats()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *ImgProxy) HeaderWriter() *headerwriter.Writer {
|
||||||
|
return i.headerWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ImgProxy) Semaphores() *semaphores.Semaphores {
|
||||||
|
return i.semaphores
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ImgProxy) FallbackImage() auximageprovider.Provider {
|
||||||
|
return i.fallbackImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ImgProxy) WatermarkImage() auximageprovider.Provider {
|
||||||
|
return i.watermarkImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ImgProxy) Fetcher() *fetcher.Fetcher {
|
||||||
|
return i.fetcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ImgProxy) ImageDataFactory() *imagedata.Factory {
|
||||||
|
return i.imageDataFactory
|
||||||
|
}
|
||||||
|
@@ -267,7 +267,7 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingSVG() {
|
|||||||
|
|
||||||
s.Require().Equal(http.StatusOK, res.StatusCode)
|
s.Require().Equal(http.StatusOK, res.StatusCode)
|
||||||
|
|
||||||
data, err := s.imgproxy().ImageDataFactory.NewFromBytes(s.testData.Read("test1.svg"))
|
data, err := s.imgproxy().ImageDataFactory().NewFromBytes(s.testData.Read("test1.svg"))
|
||||||
s.Require().NoError(err)
|
s.Require().NoError(err)
|
||||||
|
|
||||||
expected, err := svg.Sanitize(data)
|
expected, err := svg.Sanitize(data)
|
||||||
|
Reference in New Issue
Block a user