IMG-57: introduce processing options factory (#1526)

* Intoduced options.Factory

* ProcessingOptionsFactory in processing and watermark

* Clone with testutil.Helper
This commit is contained in:
Victor Sokolov
2025-09-16 17:04:21 +02:00
committed by GitHub
parent 355e3c506e
commit 4b05e87274
18 changed files with 739 additions and 399 deletions

View File

@@ -6,6 +6,7 @@ import (
"github.com/imgproxy/imgproxy/v3/fetcher"
processinghandler "github.com/imgproxy/imgproxy/v3/handlers/processing"
streamhandler "github.com/imgproxy/imgproxy/v3/handlers/stream"
"github.com/imgproxy/imgproxy/v3/options"
"github.com/imgproxy/imgproxy/v3/security"
"github.com/imgproxy/imgproxy/v3/server"
"github.com/imgproxy/imgproxy/v3/workers"
@@ -26,6 +27,7 @@ type Config struct {
Handlers HandlerConfigs
Server server.Config
Security security.Config
Options options.Config
}
// NewDefaultConfig creates a new default configuration
@@ -41,6 +43,7 @@ func NewDefaultConfig() Config {
},
Server: server.NewDefaultConfig(),
Security: security.NewDefaultConfig(),
Options: options.NewDefaultConfig(),
}
}
@@ -78,7 +81,11 @@ func LoadConfigFromEnv(c *Config) (*Config, error) {
return nil, err
}
if _, err := security.LoadConfigFromEnv(&c.Security); err != nil {
if _, err = security.LoadConfigFromEnv(&c.Security); err != nil {
return nil, err
}
if _, err = options.LoadConfigFromEnv(&c.Options); err != nil {
return nil, err
}

7
go.mod
View File

@@ -11,6 +11,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1
github.com/DarthSim/godotenv v1.3.1
github.com/DataDog/datadog-agent/pkg/trace v0.67.0
github.com/DataDog/datadog-go/v5 v5.6.0
github.com/DataDog/dd-trace-go/v2 v2.0.1
github.com/airbrake/gobrake/v5 v5.6.2
@@ -42,6 +43,7 @@ require (
github.com/stretchr/testify v1.10.0
github.com/tdewolff/parse/v2 v2.8.1
github.com/trimmer-io/go-xmp v1.0.0
github.com/urfave/cli/v3 v3.4.1
go.opentelemetry.io/contrib/detectors/aws/ec2 v1.37.0
go.opentelemetry.io/contrib/detectors/aws/ecs v1.37.0
go.opentelemetry.io/contrib/detectors/aws/eks v1.37.0
@@ -58,7 +60,6 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
go.uber.org/automaxprocs v1.6.0
golang.org/x/image v0.28.0
golang.org/x/net v0.41.0
golang.org/x/sync v0.15.0
golang.org/x/sys v0.33.0
@@ -82,7 +83,6 @@ require (
github.com/DataDog/datadog-agent/pkg/obfuscate v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/proto v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/trace v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/util/log v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/util/scrubber v0.67.0 // indirect
github.com/DataDog/datadog-agent/pkg/version v0.67.0 // indirect
@@ -119,7 +119,6 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575 // indirect
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eapache/queue/v2 v2.0.0-20230407133247-75960ed334e4 // indirect
@@ -175,7 +174,6 @@ require (
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect
github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect
@@ -184,7 +182,6 @@ require (
github.com/tinylib/msgp v1.3.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/urfave/cli/v3 v3.4.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/errs v1.4.0 // indirect

9
go.sum
View File

@@ -44,7 +44,6 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/DarthSim/godotenv v1.3.1 h1:NMWdswlRx2M9uPY4Ux8p/Q/rDs7A97OG89fECiQ/Tz0=
github.com/DarthSim/godotenv v1.3.1/go.mod h1:B3ySe1HYTUFFR6+TPyHyxPWjUdh48il0Blebg9p1cCc=
github.com/DataDog/appsec-internal-go v1.13.0 h1:aO6DmHYsAU8BNFuvYJByhMKGgcQT3WAbj9J/sgAJxtA=
@@ -173,8 +172,6 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI=
github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -397,8 +394,6 @@ github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3 h1:4+LEVO
github.com/richardartoul/molecule v1.0.1-0.20240531184615-7ca0df43c0b3/go.mod h1:vl5+MqJ1nBINuSsUI2mGgH79UweUT/B5Fy8857PqyyI=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a8tTFrMLUcfWwyC0pnifVo2ClaLq+hP8=
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/secure-systems-lab/go-securesystemslib v0.9.0 h1:rf1HIbL64nUpEIZnjLZ3mcNEL9NBPB0iuVjyxvq3LZc=
@@ -446,8 +441,6 @@ github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfj
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/trimmer-io/go-xmp v1.0.0 h1:zY8bolSga5kOjBAaHS6hrdxLgEoYuT875xTy0QDwZWs=
github.com/trimmer-io/go-xmp v1.0.0/go.mod h1:Aaptr9sp1lLv7UnCAdQ+gSHZyY2miYaKmcNVj7HRBwA=
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM=
github.com/urfave/cli/v3 v3.4.1/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/vmihailenco/msgpack/v4 v4.3.13 h1:A2wsiTbvp63ilDaWmsk2wjx6xZdxQOvpiNlKBGKKXKI=
@@ -582,8 +575,6 @@ golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
golang.org/x/image v0.28.0/go.mod h1:GUJYXtnGKEUgggyzh+Vxt+AviiCcyiwpsl8iQ8MvwGY=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=

View File

@@ -26,6 +26,7 @@ type HandlerContext interface {
WatermarkImage() auximageprovider.Provider
ImageDataFactory() *imagedata.Factory
Security() *security.Checker
OptionsFactory() *options.Factory
}
// Handler handles image processing requests
@@ -108,7 +109,7 @@ func (h *Handler) newRequest(
}
// parse image url and processing options
po, imageURL, err := options.ParsePath(path, req.Header)
po, imageURL, err := h.OptionsFactory().ParsePath(path, req.Header)
if err != nil {
return "", nil, nil, ierrors.Wrap(err, 0, ierrors.WithCategory(handlers.CategoryPathParsing))
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/imgproxy/imgproxy/v3/imagedata"
"github.com/imgproxy/imgproxy/v3/memory"
"github.com/imgproxy/imgproxy/v3/monitoring/prometheus"
"github.com/imgproxy/imgproxy/v3/options"
"github.com/imgproxy/imgproxy/v3/security"
"github.com/imgproxy/imgproxy/v3/server"
"github.com/imgproxy/imgproxy/v3/workers"
@@ -41,6 +42,7 @@ type Imgproxy struct {
imageDataFactory *imagedata.Factory
handlers ImgproxyHandlers
security *security.Checker
optionsFactory *options.Factory
config *Config
}
@@ -73,6 +75,11 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
return nil, err
}
processingOptionsFactory, err := options.NewFactory(&config.Options, security)
if err != nil {
return nil, err
}
imgproxy := &Imgproxy{
workers: workers,
fallbackImage: fallbackImage,
@@ -81,6 +88,7 @@ func New(ctx context.Context, config *Config) (*Imgproxy, error) {
imageDataFactory: idf,
config: config,
security: security,
optionsFactory: processingOptionsFactory,
}
imgproxy.handlers.Health = healthhandler.New()
@@ -199,3 +207,7 @@ func (i *Imgproxy) ImageDataFactory() *imagedata.Factory {
func (i *Imgproxy) Security() *security.Checker {
return i.security
}
func (i *Imgproxy) OptionsFactory() *options.Factory {
return i.optionsFactory
}

11
init.go
View File

@@ -10,7 +10,6 @@ import (
"github.com/imgproxy/imgproxy/v3/gliblog"
"github.com/imgproxy/imgproxy/v3/logger"
"github.com/imgproxy/imgproxy/v3/monitoring"
"github.com/imgproxy/imgproxy/v3/options"
"github.com/imgproxy/imgproxy/v3/processing"
"github.com/imgproxy/imgproxy/v3/vips"
"go.uber.org/automaxprocs/maxprocs"
@@ -53,16 +52,6 @@ func Init() error {
return err
}
if err := options.ParsePresets(config.Presets); err != nil {
vips.Shutdown()
return err
}
if err := options.ValidatePresets(); err != nil {
vips.Shutdown()
return err
}
return nil
}

View File

@@ -168,7 +168,7 @@ func (s *ProcessingHandlerTestSuite) TestResultingFormatNotSupported() {
}
func (s *ProcessingHandlerTestSuite) TestSkipProcessingConfig() {
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png")
@@ -184,7 +184,7 @@ func (s *ProcessingHandlerTestSuite) TestSkipProcessingPO() {
}
func (s *ProcessingHandlerTestSuite) TestSkipProcessingSameFormat() {
config.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.PNG}
res := s.GET("/unsafe/rs:fill:4:4/plain/local:///test1.png@png")
@@ -477,7 +477,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvg() {
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
config.AlwaysRasterizeSvg = true
config.EnforceWebp = true
s.Config().Options.EnforceWebp = true
res := s.GET("/unsafe/plain/local:///test1.svg", http.Header{"Accept": []string{"image/webp"}})
@@ -487,7 +487,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithEnforceAvif() {
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
config.AlwaysRasterizeSvg = false
config.EnforceWebp = true
s.Config().Options.EnforceWebp = true
res := s.GET("/unsafe/plain/local:///test1.svg")
@@ -497,7 +497,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgDisabled() {
func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
config.AlwaysRasterizeSvg = true
config.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
s.Config().Options.SkipProcessingFormats = []imagetype.Type{imagetype.SVG}
res := s.GET("/unsafe/plain/local:///test1.svg@svg")
@@ -506,7 +506,7 @@ func (s *ProcessingHandlerTestSuite) TestAlwaysRasterizeSvgWithFormat() {
}
func (s *ProcessingHandlerTestSuite) TestMaxSrcFileSizeGlobal() {
config.MaxSrcFileSize = 1
s.Config().Security.DefaultOptions.MaxSrcFileSize = 1
ts := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(200)

168
options/config.go Normal file
View File

@@ -0,0 +1,168 @@
package options
import (
"errors"
"fmt"
"maps"
"slices"
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ensure"
"github.com/imgproxy/imgproxy/v3/imagetype"
)
// URLReplacement represents a URL replacement configuration
type URLReplacement = config.URLReplacement
// Config represents the configuration for options processing
type Config struct {
// Processing behavior defaults
StripMetadata bool // Whether to strip metadata by default
KeepCopyright bool // Whether to keep copyright information when stripping metadata
StripColorProfile bool // Whether to strip color profile by default
AutoRotate bool // Whether to auto-rotate images by default
EnforceThumbnail bool // Whether to enforce thumbnail extraction by default
ReturnAttachment bool // Whether to return images as attachments by default
// Image processing formats
SkipProcessingFormats []imagetype.Type // List of formats to skip processing for
// Presets configuration
Presets []string // Available presets
OnlyPresets bool // Whether to allow only presets
// Quality settings
Quality int // Default quality for image processing
FormatQuality map[imagetype.Type]int // Quality settings per image format
// Security and validation
AllowedProcessingOptions []string // List of allowed processing options
// Format preference and enforcement
AutoWebp bool // Whether to automatically serve WebP when supported
EnforceWebp bool // Whether to enforce WebP format
AutoAvif bool // Whether to automatically serve AVIF when supported
EnforceAvif bool // Whether to enforce AVIF format
AutoJxl bool // Whether to automatically serve JXL when supported
EnforceJxl bool // Whether to enforce JXL format
// Client hints
EnableClientHints bool // Whether to enable client hints support
// URL processing
ArgumentsSeparator string // Separator for URL arguments
BaseURL string // Base URL for relative URLs
URLReplacements []URLReplacement // URL replacement rules
Base64URLIncludesFilename bool // Whether base64 URLs include filename
}
// NewDefaultConfig creates a new default configuration for options processing
func NewDefaultConfig() Config {
return Config{
// Processing behavior defaults (copied from global config defaults)
StripMetadata: true,
KeepCopyright: true,
StripColorProfile: true,
AutoRotate: true,
EnforceThumbnail: false,
ReturnAttachment: false,
OnlyPresets: false,
// Quality settings (copied from global config defaults)
Quality: 80,
FormatQuality: map[imagetype.Type]int{
imagetype.WEBP: 79,
imagetype.AVIF: 63,
imagetype.JXL: 77,
},
// Format preference and enforcement (copied from global config defaults)
AutoWebp: false,
EnforceWebp: false,
AutoAvif: false,
EnforceAvif: false,
AutoJxl: false,
EnforceJxl: false,
// Client hints
EnableClientHints: false,
// URL processing (copied from global config defaults)
ArgumentsSeparator: ":",
BaseURL: "",
Base64URLIncludesFilename: false,
}
}
// LoadConfigFromEnv loads configuration from global config variables
func LoadConfigFromEnv(c *Config) (*Config, error) {
c = ensure.Ensure(c, NewDefaultConfig)
// Copy from global config variables
c.StripMetadata = config.StripMetadata
c.KeepCopyright = config.KeepCopyright
c.StripColorProfile = config.StripColorProfile
c.AutoRotate = config.AutoRotate
c.EnforceThumbnail = config.EnforceThumbnail
c.ReturnAttachment = config.ReturnAttachment
// Image processing formats
c.SkipProcessingFormats = slices.Clone(config.SkipProcessingFormats)
// Presets configuration
c.Presets = slices.Clone(config.Presets)
c.OnlyPresets = config.OnlyPresets
// Quality settings
c.Quality = config.Quality
c.FormatQuality = maps.Clone(config.FormatQuality)
// Security and validation
c.AllowedProcessingOptions = slices.Clone(config.AllowedProcessingOptions)
// Format preference and enforcement
c.AutoWebp = config.AutoWebp
c.EnforceWebp = config.EnforceWebp
c.AutoAvif = config.AutoAvif
c.EnforceAvif = config.EnforceAvif
c.AutoJxl = config.AutoJxl
c.EnforceJxl = config.EnforceJxl
// Client hints
c.EnableClientHints = config.EnableClientHints
// URL processing
c.ArgumentsSeparator = config.ArgumentsSeparator
c.BaseURL = config.BaseURL
c.URLReplacements = slices.Clone(config.URLReplacements)
c.Base64URLIncludesFilename = config.Base64URLIncludesFilename
return c, nil
}
// Validate validates the configuration values
func (c *Config) Validate() error {
// Quality validation (copied from global config validation)
if c.Quality <= 0 {
return fmt.Errorf("quality should be greater than 0, now - %d", c.Quality)
} else if c.Quality > 100 {
return fmt.Errorf("quality can't be greater than 100, now - %d", c.Quality)
}
// Format quality validation
for format, quality := range c.FormatQuality {
if quality <= 0 {
return fmt.Errorf("format quality for %s should be greater than 0, now - %d", format, quality)
} else if quality > 100 {
return fmt.Errorf("format quality for %s can't be greater than 100, now - %d", format, quality)
}
}
// Arguments separator validation
if c.ArgumentsSeparator == "" {
return errors.New("arguments separator cannot be empty")
}
return nil
}

45
options/factory.go Normal file
View File

@@ -0,0 +1,45 @@
package options
import (
"github.com/imgproxy/imgproxy/v3/security"
)
// Presets is a map of preset names to their corresponding urlOptions
type Presets = map[string]urlOptions
// Factory creates ProcessingOptions instances
type Factory struct {
config *Config // Factory configuration
security *security.Checker // Security checker for generating security options
presets Presets // Parsed presets
defaultPO *ProcessingOptions // Default processing options
}
// NewFactory creates new Factory instance
func NewFactory(config *Config, security *security.Checker) (*Factory, error) {
if err := config.Validate(); err != nil {
return nil, err
}
f := &Factory{
config: config,
security: security,
presets: make(map[string]urlOptions),
defaultPO: newDefaultProcessingOptions(config, security),
}
if err := f.parsePresets(); err != nil {
return nil, err
}
if err := f.validatePresets(); err != nil {
return nil, err
}
return f, nil
}
// NewProcessingOptions creates new ProcessingOptions instance
func (f *Factory) NewProcessingOptions() *ProcessingOptions {
return f.defaultPO.clone()
}

View File

@@ -5,11 +5,10 @@ import (
"strings"
)
var presets map[string]urlOptions
func ParsePresets(presetStrs []string) error {
for _, presetStr := range presetStrs {
if err := parsePreset(presetStr); err != nil {
// parsePresets parses presets from the config and fills the presets map
func (f *Factory) parsePresets() error {
for _, presetStr := range f.config.Presets {
if err := f.parsePreset(presetStr); err != nil {
return err
}
}
@@ -17,7 +16,8 @@ func ParsePresets(presetStrs []string) error {
return nil
}
func parsePreset(presetStr string) error {
// parsePreset parses a preset string and returns the name and options
func (f *Factory) parsePreset(presetStr string) error {
presetStr = strings.Trim(presetStr, " ")
if len(presetStr) == 0 || strings.HasPrefix(presetStr, "#") {
@@ -27,39 +27,41 @@ func parsePreset(presetStr string) error {
parts := strings.Split(presetStr, "=")
if len(parts) != 2 {
return fmt.Errorf("Invalid preset string: %s", presetStr)
return fmt.Errorf("invalid preset string: %s", presetStr)
}
name := strings.Trim(parts[0], " ")
if len(name) == 0 {
return fmt.Errorf("Empty preset name: %s", presetStr)
return fmt.Errorf("empty preset name: %s", presetStr)
}
value := strings.Trim(parts[1], " ")
if len(value) == 0 {
return fmt.Errorf("Empty preset value: %s", presetStr)
return fmt.Errorf("empty preset value: %s", presetStr)
}
optsStr := strings.Split(value, "/")
opts, rest := parseURLOptions(optsStr)
opts, rest := f.parseURLOptions(optsStr)
if len(rest) > 0 {
return fmt.Errorf("Invalid preset value: %s", presetStr)
return fmt.Errorf("invalid preset value: %s", presetStr)
}
if presets == nil {
presets = make(map[string]urlOptions)
if f.presets == nil {
f.presets = make(Presets)
}
presets[name] = opts
f.presets[name] = opts
return nil
}
func ValidatePresets() error {
for name, opts := range presets {
po := NewProcessingOptions()
if err := applyURLOptions(po, opts, true, name); err != nil {
// validatePresets validates all presets by applying them to a new ProcessingOptions instance
func (f *Factory) validatePresets() error {
for name, opts := range f.presets {
po := f.NewProcessingOptions()
if err := f.applyURLOptions(po, opts, true, name); err != nil {
return fmt.Errorf("Error in preset `%s`: %s", name, err)
}
}

View File

@@ -1,101 +1,93 @@
package options
import (
"fmt"
"testing"
"github.com/imgproxy/imgproxy/v3/security"
"github.com/imgproxy/imgproxy/v3/testutil"
"github.com/stretchr/testify/suite"
"github.com/imgproxy/imgproxy/v3/config"
)
type PresetsTestSuite struct{ suite.Suite }
type PresetsTestSuite struct {
testutil.LazySuite
func (s *PresetsTestSuite) SetupTest() {
config.Reset()
// Reset presets
presets = make(map[string]urlOptions)
security *security.Checker
}
func (s *PresetsTestSuite) SetupSuite() {
c := security.NewDefaultConfig()
security, err := security.New(&c)
s.Require().NoError(err)
s.security = security
}
func (s *PresetsTestSuite) newFactory(presets ...string) (*Factory, error) {
c := NewDefaultConfig()
c.Presets = presets
return NewFactory(&c, s.security)
}
func (s *PresetsTestSuite) TestParsePreset() {
err := parsePreset("test=resize:fit:100:200/sharpen:2")
f, err := s.newFactory("test=resize:fit:100:200/sharpen:2")
s.Require().NoError(err)
s.Require().Equal(urlOptions{
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
urlOption{Name: "sharpen", Args: []string{"2"}},
}, presets["test"])
}, f.presets["test"])
}
func (s *PresetsTestSuite) TestParsePresetInvalidString() {
presetStr := "resize:fit:100:200/sharpen:2"
err := parsePreset(presetStr)
_, err := s.newFactory(presetStr)
s.Require().Equal(fmt.Errorf("Invalid preset string: %s", presetStr), err)
s.Require().Empty(presets)
s.Require().Error(err, "invalid preset string: %s", presetStr)
}
func (s *PresetsTestSuite) TestParsePresetEmptyName() {
presetStr := "=resize:fit:100:200/sharpen:2"
err := parsePreset(presetStr)
_, err := s.newFactory(presetStr)
s.Require().Equal(fmt.Errorf("Empty preset name: %s", presetStr), err)
s.Require().Empty(presets)
s.Require().Error(err, "empty preset name: %s", presetStr)
}
func (s *PresetsTestSuite) TestParsePresetEmptyValue() {
presetStr := "test="
err := parsePreset(presetStr)
_, err := s.newFactory(presetStr)
s.Require().Equal(fmt.Errorf("Empty preset value: %s", presetStr), err)
s.Require().Empty(presets)
s.Require().Error(err, "empty preset value: %s", presetStr)
}
func (s *PresetsTestSuite) TestParsePresetInvalidValue() {
presetStr := "test=resize:fit:100:200/sharpen:2/blur"
err := parsePreset(presetStr)
_, err := s.newFactory(presetStr)
s.Require().Equal(fmt.Errorf("Invalid preset value: %s", presetStr), err)
s.Require().Empty(presets)
s.Require().Error(err, "invalid preset value: %s", presetStr)
}
func (s *PresetsTestSuite) TestParsePresetEmptyString() {
err := parsePreset(" ")
f, err := s.newFactory(" ")
s.Require().NoError(err)
s.Require().Empty(presets)
s.Require().Empty(f.presets)
}
func (s *PresetsTestSuite) TestParsePresetComment() {
err := parsePreset("# test=resize:fit:100:200/sharpen:2")
f, err := s.newFactory("# test=resize:fit:100:200/sharpen:2")
s.Require().NoError(err)
s.Require().Empty(presets)
s.Require().Empty(f.presets)
}
func (s *PresetsTestSuite) TestValidatePresets() {
presets = map[string]urlOptions{
"test": {
urlOption{Name: "resize", Args: []string{"fit", "100", "200"}},
urlOption{Name: "sharpen", Args: []string{"2"}},
},
}
err := ValidatePresets()
f, err := s.newFactory("test=resize:fit:100:200/sharpen:2")
s.Require().NoError(err)
s.Require().NotEmpty(f.presets)
}
func (s *PresetsTestSuite) TestValidatePresetsInvalid() {
presets = map[string]urlOptions{
"test": {
urlOption{Name: "resize", Args: []string{"fit", "-1", "-2"}},
urlOption{Name: "sharpen", Args: []string{"2"}},
},
}
err := ValidatePresets()
_, err := s.newFactory("test=resize:fit:-1:-2/sharpen:2")
s.Require().Error(err)
}

View File

@@ -2,7 +2,7 @@ package options
import (
"encoding/base64"
"fmt"
"maps"
"net/http"
"slices"
"strconv"
@@ -11,7 +11,6 @@ import (
log "github.com/sirupsen/logrus"
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/imagetype"
"github.com/imgproxy/imgproxy/v3/imath"
@@ -118,34 +117,10 @@ type ProcessingOptions struct {
SecurityOptions security.Options
defaultQuality int
defaultOptions *ProcessingOptions
}
func NewProcessingOptions() *ProcessingOptions {
// NOTE: This is temporary hack until ProcessingOptions does not have Factory
securityCfg, err := security.LoadConfigFromEnv(nil)
if err != nil {
fmt.Println(err)
}
// NOTE: This is a temporary workaround for logrus bug that deadlocks
// if log is used within another log (issue 1448)
if len(securityCfg.Salts) == 0 {
securityCfg.Salts = [][]byte{[]byte("logrusbugworkaround")}
}
if len(securityCfg.Keys) == 0 {
securityCfg.Keys = [][]byte{[]byte("logrusbugworkaround")}
}
// END OF WORKAROUND
security, err := security.New(securityCfg)
if err != nil {
fmt.Println(err)
}
securityOptions := security.NewOptions()
// NOTE: This is temporary hack until ProcessingOptions does not have Factory
func newDefaultProcessingOptions(config *Config, security *security.Checker) *ProcessingOptions {
po := ProcessingOptions{
ResizingType: ResizeFit,
Width: 0,
@@ -160,6 +135,7 @@ func NewProcessingOptions() *ProcessingOptions {
Trim: TrimOptions{Enabled: false, Threshold: 10, Smart: true},
Rotate: 0,
Quality: 0,
FormatQuality: maps.Clone(config.FormatQuality),
MaxBytes: 0,
Format: imagetype.Unknown,
Background: vips.Color{R: 255, G: 255, B: 255},
@@ -174,20 +150,13 @@ func NewProcessingOptions() *ProcessingOptions {
EnforceThumbnail: config.EnforceThumbnail,
ReturnAttachment: config.ReturnAttachment,
SkipProcessingFormats: append([]imagetype.Type(nil), config.SkipProcessingFormats...),
UsedPresets: make([]string, 0, len(config.Presets)),
SkipProcessingFormats: slices.Clone(config.SkipProcessingFormats),
SecurityOptions: securityOptions,
SecurityOptions: security.NewOptions(),
// Basically, we need this to update ETag when `IMGPROXY_QUALITY` is changed
defaultQuality: config.Quality,
}
po.FormatQuality = make(map[imagetype.Type]int, len(config.FormatQuality))
for k, v := range config.FormatQuality {
po.FormatQuality[k] = v
}
return &po
}
@@ -206,7 +175,7 @@ func (po *ProcessingOptions) GetQuality() int {
}
func (po *ProcessingOptions) Diff() structdiff.Entries {
return structdiff.Diff(NewProcessingOptions(), po)
return structdiff.Diff(po.defaultOptions, po)
}
func (po *ProcessingOptions) String() string {
@@ -217,6 +186,34 @@ func (po *ProcessingOptions) MarshalJSON() ([]byte, error) {
return po.Diff().MarshalJSON()
}
// Default returns the ProcessingOptions instance with defaults set
func (po *ProcessingOptions) Default() *ProcessingOptions {
return po.defaultOptions.clone()
}
// clone clones ProcessingOptions struct and its slices and maps
func (po *ProcessingOptions) clone() *ProcessingOptions {
clone := *po
clone.FormatQuality = maps.Clone(po.FormatQuality)
clone.SkipProcessingFormats = slices.Clone(po.SkipProcessingFormats)
clone.UsedPresets = slices.Clone(po.UsedPresets)
if po.Expires != nil {
poExipres := *po.Expires
clone.Expires = &poExipres
}
// Copy the pointer to the default options struct from parent.
// Nil means that we have just cloned the default options struct itself
// so we set it as default options.
if clone.defaultOptions == nil {
clone.defaultOptions = po
}
return &clone
}
func parseDimension(d *int, name, arg string) error {
if v, err := strconv.Atoi(arg); err == nil && v >= 0 {
*d = v
@@ -707,9 +704,9 @@ func applyPixelateOption(po *ProcessingOptions, args []string) error {
return nil
}
func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...string) error {
func (f *Factory) applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...string) error {
for _, preset := range args {
if p, ok := presets[preset]; ok {
if p, ok := f.presets[preset]; ok {
if slices.Contains(usedPresets, preset) {
log.Warningf("Recursive preset usage is detected: %s", preset)
continue
@@ -717,7 +714,7 @@ func applyPresetOption(po *ProcessingOptions, args []string, usedPresets ...stri
po.UsedPresets = append(po.UsedPresets, preset)
if err := applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
if err := f.applyURLOptions(po, p, true, append(usedPresets, preset)...); err != nil {
return err
}
} else {
@@ -1010,7 +1007,7 @@ func applyMaxResultDimensionOption(po *ProcessingOptions, args []string) error {
return nil
}
func applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
func (f *Factory) applyURLOption(po *ProcessingOptions, name string, args []string, usedPresets ...string) error {
switch name {
case "resize", "rs":
return applyResizeOption(po, args)
@@ -1090,7 +1087,7 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
return applyReturnAttachmentOption(po, args)
// Presets
case "preset", "pr":
return applyPresetOption(po, args, usedPresets...)
return f.applyPresetOption(po, args, usedPresets...)
// Security
case "max_src_resolution", "msr":
return applyMaxSrcResolutionOption(po, args)
@@ -1107,15 +1104,15 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
return newUnknownOptionError("processing", name)
}
func applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
allowAll = allowAll || len(config.AllowedProcessingOptions) == 0
func (f *Factory) applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, usedPresets ...string) error {
allowAll = allowAll || len(f.config.AllowedProcessingOptions) == 0
for _, opt := range options {
if !allowAll && !slices.Contains(config.AllowedProcessingOptions, opt.Name) {
if !allowAll && !slices.Contains(f.config.AllowedProcessingOptions, opt.Name) {
return newForbiddenOptionError("processing", opt.Name)
}
if err := applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
if err := f.applyURLOption(po, opt.Name, opt.Args, usedPresets...); err != nil {
return err
}
}
@@ -1123,27 +1120,27 @@ func applyURLOptions(po *ProcessingOptions, options urlOptions, allowAll bool, u
return nil
}
func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
po := NewProcessingOptions()
func (f *Factory) defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
po := f.NewProcessingOptions()
headerAccept := headers.Get("Accept")
if strings.Contains(headerAccept, "image/webp") {
po.PreferWebP = config.AutoWebp || config.EnforceWebp
po.EnforceWebP = config.EnforceWebp
po.PreferWebP = f.config.AutoWebp || f.config.EnforceWebp
po.EnforceWebP = f.config.EnforceWebp
}
if strings.Contains(headerAccept, "image/avif") {
po.PreferAvif = config.AutoAvif || config.EnforceAvif
po.EnforceAvif = config.EnforceAvif
po.PreferAvif = f.config.AutoAvif || f.config.EnforceAvif
po.EnforceAvif = f.config.EnforceAvif
}
if strings.Contains(headerAccept, "image/jxl") {
po.PreferJxl = config.AutoJxl || config.EnforceJxl
po.EnforceJxl = config.EnforceJxl
po.PreferJxl = f.config.AutoJxl || f.config.EnforceJxl
po.EnforceJxl = f.config.EnforceJxl
}
if config.EnableClientHints {
if f.config.EnableClientHints {
headerDPR := headers.Get("Sec-CH-DPR")
if len(headerDPR) == 0 {
headerDPR = headers.Get("DPR")
@@ -1165,8 +1162,8 @@ func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
}
}
if _, ok := presets["default"]; ok {
if err := applyPresetOption(po, []string{"default"}); err != nil {
if _, ok := f.presets["default"]; ok {
if err := f.applyPresetOption(po, []string{"default"}); err != nil {
return po, err
}
}
@@ -1174,80 +1171,21 @@ func defaultProcessingOptions(headers http.Header) (*ProcessingOptions, error) {
return po, nil
}
func parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
if _, ok := resizeTypes[parts[0]]; ok {
return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
}
po, err := defaultProcessingOptions(headers)
if err != nil {
return nil, "", err
}
options, urlParts := parseURLOptions(parts)
if err = applyURLOptions(po, options, false); err != nil {
return nil, "", err
}
url, extension, err := DecodeURL(urlParts)
if err != nil {
return nil, "", err
}
if !po.Raw && len(extension) > 0 {
if err = applyFormatOption(po, []string{extension}); err != nil {
return nil, "", err
}
}
return po, url, nil
}
func parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
po, err := defaultProcessingOptions(headers)
if err != nil {
return nil, "", err
}
presets := strings.Split(parts[0], config.ArgumentsSeparator)
urlParts := parts[1:]
if err = applyPresetOption(po, presets); err != nil {
return nil, "", err
}
url, extension, err := DecodeURL(urlParts)
if err != nil {
return nil, "", err
}
if !po.Raw && len(extension) > 0 {
if err = applyFormatOption(po, []string{extension}); err != nil {
return nil, "", err
}
}
return po, url, nil
}
func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, error) {
// ParsePath parses the given request path and returns the processing options and image URL
func (f *Factory) ParsePath(
path string,
headers http.Header,
) (po *ProcessingOptions, imageURL string, err error) {
if path == "" || path == "/" {
return nil, "", newInvalidURLError("Invalid path: %s", path)
return nil, "", newInvalidURLError("invalid path: %s", path)
}
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
var (
imageURL string
po *ProcessingOptions
err error
)
if config.OnlyPresets {
po, imageURL, err = parsePathPresets(parts, headers)
if f.config.OnlyPresets {
po, imageURL, err = f.parsePathPresets(parts, headers)
} else {
po, imageURL, err = parsePathOptions(parts, headers)
po, imageURL, err = f.parsePathOptions(parts, headers)
}
if err != nil {
@@ -1256,3 +1194,62 @@ func ParsePath(path string, headers http.Header) (*ProcessingOptions, string, er
return po, imageURL, nil
}
// parsePathOptions parses processing options from the URL path
func (f *Factory) parsePathOptions(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
if _, ok := resizeTypes[parts[0]]; ok {
return nil, "", newInvalidURLError("It looks like you're using the deprecated basic URL format")
}
po, err := f.defaultProcessingOptions(headers)
if err != nil {
return nil, "", err
}
options, urlParts := f.parseURLOptions(parts)
if err = f.applyURLOptions(po, options, false); err != nil {
return nil, "", err
}
url, extension, err := f.DecodeURL(urlParts)
if err != nil {
return nil, "", err
}
if !po.Raw && len(extension) > 0 {
if err = applyFormatOption(po, []string{extension}); err != nil {
return nil, "", err
}
}
return po, url, nil
}
// parsePathPresets parses presets from the URL path
func (f *Factory) parsePathPresets(parts []string, headers http.Header) (*ProcessingOptions, string, error) {
po, err := f.defaultProcessingOptions(headers)
if err != nil {
return nil, "", err
}
presets := strings.Split(parts[0], f.config.ArgumentsSeparator)
urlParts := parts[1:]
if err = f.applyPresetOption(po, presets); err != nil {
return nil, "", err
}
url, extension, err := f.DecodeURL(urlParts)
if err != nil {
return nil, "", err
}
if !po.Raw && len(extension) > 0 {
if err = applyFormatOption(po, []string{extension}); err != nil {
return nil, "", err
}
}
return po, url, nil
}

View File

@@ -8,25 +8,65 @@ import (
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/suite"
"time"
"github.com/imgproxy/imgproxy/v3/config"
"github.com/imgproxy/imgproxy/v3/imagetype"
"github.com/imgproxy/imgproxy/v3/security"
"github.com/imgproxy/imgproxy/v3/testutil"
"github.com/stretchr/testify/suite"
)
type ProcessingOptionsTestSuite struct{ suite.Suite }
type ProcessingOptionsTestSuite struct {
testutil.LazySuite
func (s *ProcessingOptionsTestSuite) SetupTest() {
config.Reset()
// Reset presets
presets = make(map[string]urlOptions)
securityCfg testutil.LazyObj[*security.Config]
security testutil.LazyObj[*security.Checker]
config testutil.LazyObj[*Config]
factory testutil.LazyObj[*Factory]
}
func (s *ProcessingOptionsTestSuite) SetupSuite() {
s.config, _ = testutil.NewLazySuiteObj(
s,
func() (*Config, error) {
c := NewDefaultConfig()
return &c, nil
},
)
s.securityCfg, _ = testutil.NewLazySuiteObj(
s,
func() (*security.Config, error) {
c := security.NewDefaultConfig()
return &c, nil
},
)
s.security, _ = testutil.NewLazySuiteObj(
s,
func() (*security.Checker, error) {
return security.New(s.securityCfg())
},
)
s.factory, _ = testutil.NewLazySuiteObj(
s,
func() (*Factory, error) {
return NewFactory(s.config(), s.security())
},
)
}
func (s *ProcessingOptionsTestSuite) SetupSubTest() {
s.ResetLazyObjects()
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(originURL, imageURL)
@@ -34,11 +74,11 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URL() {
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithFilename() {
config.Base64URLIncludesFilename = true
s.config().Base64URLIncludesFilename = true
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/%s.png/puppy.jpg", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(originURL, imageURL)
@@ -48,7 +88,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithFilename() {
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/%s", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(originURL, imageURL)
@@ -56,26 +96,26 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithoutExtension() {
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithBase() {
config.BaseURL = "http://images.dev/"
s.config().BaseURL = "http://images.dev/"
originURL := "lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
s.Require().Equal(imagetype.PNG, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() {
config.URLReplacements = []config.URLReplacement{
s.config().URLReplacements = []config.URLReplacement{
{Regexp: regexp.MustCompile("^test://([^/]*)/"), Replacement: "test2://images.dev/${1}/dolor/"},
{Regexp: regexp.MustCompile("^test2://"), Replacement: "http://"},
}
originURL := "test://lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg?param=value", imageURL)
@@ -85,7 +125,7 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLWithReplacement() {
func (s *ProcessingOptionsTestSuite) TestParsePlainURL() {
originURL := "http://images.dev/lorem/ipsum.jpg"
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(originURL, imageURL)
@@ -96,7 +136,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
originURL := "http://images.dev/lorem/ipsum.jpg"
path := fmt.Sprintf("/size:100:100/plain/%s", originURL)
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(originURL, imageURL)
@@ -105,7 +145,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithoutExtension() {
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(originURL, imageURL)
@@ -113,26 +153,26 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscaped() {
}
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithBase() {
config.BaseURL = "http://images.dev/"
s.config().BaseURL = "http://images.dev/"
originURL := "lorem/ipsum.jpg"
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
s.Require().Equal(imagetype.PNG, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() {
config.URLReplacements = []config.URLReplacement{
s.config().URLReplacements = []config.URLReplacement{
{Regexp: regexp.MustCompile("^test://([^/]*)/"), Replacement: "test2://images.dev/${1}/dolor/"},
{Regexp: regexp.MustCompile("^test2://"), Replacement: "http://"},
}
originURL := "test://lorem/ipsum.jpg"
path := fmt.Sprintf("/size:100:100/plain/%s@png", originURL)
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal("http://images.dev/lorem/dolor/ipsum.jpg", imageURL)
@@ -140,22 +180,22 @@ func (s *ProcessingOptionsTestSuite) TestParsePlainURLWithReplacement() {
}
func (s *ProcessingOptionsTestSuite) TestParsePlainURLEscapedWithBase() {
config.BaseURL = "http://images.dev/"
s.config().BaseURL = "http://images.dev/"
originURL := "lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/size:100:100/plain/%s@png", url.PathEscape(originURL))
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
s.Require().Equal(fmt.Sprintf("%s%s", config.BaseURL, originURL), imageURL)
s.Require().Equal(fmt.Sprintf("%s%s", s.config().BaseURL, originURL), imageURL)
s.Require().Equal(imagetype.PNG, po.Format)
}
func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() {
config.ArgumentsSeparator = ","
s.config().ArgumentsSeparator = ","
path := "/size,100,100,1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -164,27 +204,9 @@ func (s *ProcessingOptionsTestSuite) TestParseWithArgumentsSeparator() {
s.Require().True(po.Enlarge)
}
// func (s *ProcessingOptionsTestSuite) TestParseURLAllowedSource() {
// config.AllowedSources = []string{"local://", "http://images.dev/"}
// path := "/plain/http://images.dev/lorem/ipsum.jpg"
// _, _, err := ParsePath(path, make(http.Header))
// s.Require().NoError(err)
// }
// func (s *ProcessingOptionsTestSuite) TestParseURLNotAllowedSource() {
// config.AllowedSources = []string{"local://", "http://images.dev/"}
// path := "/plain/s3://images/lorem/ipsum.jpg"
// _, _, err := ParsePath(path, make(http.Header))
// s.Require().Error(err)
// }
func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
path := "/format:webp/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -193,7 +215,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathFormat() {
func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
path := "/resize:fill:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -205,7 +227,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathResize() {
func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
path := "/resizing_type:fill/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -214,7 +236,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathResizingType() {
func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
path := "/size:100:200:1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -225,7 +247,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSize() {
func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
path := "/width:100/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -234,7 +256,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidth() {
func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
path := "/height:100/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -243,7 +265,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathHeight() {
func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
path := "/enlarge:1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -252,7 +274,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathEnlarge() {
func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
path := "/extend:1:so:10:20/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -264,21 +286,21 @@ func (s *ProcessingOptionsTestSuite) TestParsePathExtend() {
func (s *ProcessingOptionsTestSuite) TestParsePathExtendSmartGravity() {
path := "/extend:1:sm/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
_, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().Error(err)
}
func (s *ProcessingOptionsTestSuite) TestParsePathExtendReplicateGravity() {
path := "/extend:1:re/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
_, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().Error(err)
}
func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
path := "/gravity:soea/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -287,7 +309,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathGravity() {
func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() {
path := "/gravity:fp:0.5:0.75/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -298,14 +320,14 @@ func (s *ProcessingOptionsTestSuite) TestParsePathGravityFocusPoint() {
func (s *ProcessingOptionsTestSuite) TestParsePathGravityReplicate() {
path := "/gravity:re/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
_, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().Error(err)
}
func (s *ProcessingOptionsTestSuite) TestParsePathCrop() {
path := "/crop:100:200/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -318,7 +340,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCrop() {
func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() {
path := "/crop:100:200:nowe:10:20/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -331,14 +353,14 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCropGravity() {
func (s *ProcessingOptionsTestSuite) TestParsePathCropGravityReplicate() {
path := "/crop:100:200:re/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
_, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().Error(err)
}
func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
path := "/quality:55/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -347,7 +369,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathQuality() {
func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
path := "/background:128:129:130/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -359,7 +381,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackground() {
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
path := "/background:ffddee/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -371,7 +393,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundHex() {
func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
path := "/background:fff/background:/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -380,7 +402,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBackgroundDisable() {
func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
path := "/blur:0.2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -389,7 +411,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathBlur() {
func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
path := "/sharpen:0.2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -397,7 +419,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathSharpen() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
path := "/dpr:2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -405,7 +427,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDpr() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
path := "/watermark:0.5:soea:10:20:0.6/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -417,17 +439,13 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWatermark() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
presets["test1"] = urlOptions{
urlOption{Name: "resizing_type", Args: []string{"fill"}},
}
presets["test2"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
urlOption{Name: "quality", Args: []string{"50"}},
s.config().Presets = []string{
"test1=resizing_type:fill",
"test2=blur:0.2/quality:50",
}
path := "/preset:test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -437,14 +455,12 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPreset() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
presets["default"] = urlOptions{
urlOption{Name: "resizing_type", Args: []string{"fill"}},
urlOption{Name: "blur", Args: []string{"0.2"}},
urlOption{Name: "quality", Args: []string{"50"}},
s.config().Presets = []string{
"default=resizing_type:fill/blur:0.2/quality:50",
}
path := "/quality:70/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -454,18 +470,13 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetDefault() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
presets["test1"] = urlOptions{
urlOption{Name: "resizing_type", Args: []string{"fill"}},
urlOption{Name: "preset", Args: []string{"test2"}},
}
presets["test2"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
urlOption{Name: "preset", Args: []string{"test1"}},
s.config().Presets = []string{
"test1=resizing_type:fill/preset:test2",
"test2=blur:0.2/preset:test1",
}
path := "/preset:test1/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -474,7 +485,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathPresetLoopDetection() {
func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
path := "/cachebuster:123/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -483,7 +494,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathCachebuster() {
func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
path := "/strip_metadata:true/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -491,11 +502,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathStripMetadata() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
config.AutoWebp = true
s.config().AutoWebp = true
path := "/plain/http://images.dev/lorem/ipsum.jpg"
headers := http.Header{"Accept": []string{"image/webp"}}
po, _, err := ParsePath(path, headers)
po, _, err := s.factory().ParsePath(path, headers)
s.Require().NoError(err)
@@ -504,11 +515,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWebpDetection() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
config.EnforceWebp = true
s.config().EnforceWebp = true
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Accept": []string{"image/webp"}}
po, _, err := ParsePath(path, headers)
po, _, err := s.factory().ParsePath(path, headers)
s.Require().NoError(err)
@@ -517,11 +528,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWebpEnforce() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
config.EnableClientHints = true
s.config().EnableClientHints = true
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
po, _, err := s.factory().ParsePath(path, headers)
s.Require().NoError(err)
@@ -531,7 +542,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeader() {
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
po, _, err := s.factory().ParsePath(path, headers)
s.Require().NoError(err)
@@ -539,11 +550,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderDisabled() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
config.EnableClientHints = true
s.config().EnableClientHints = true
path := "/width:150/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Width": []string{"100"}}
po, _, err := ParsePath(path, headers)
po, _, err := s.factory().ParsePath(path, headers)
s.Require().NoError(err)
@@ -551,11 +562,11 @@ func (s *ProcessingOptionsTestSuite) TestParsePathWidthHeaderRedefine() {
}
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
config.EnableClientHints = true
s.config().EnableClientHints = true
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Dpr": []string{"2"}}
po, _, err := ParsePath(path, headers)
po, _, err := s.factory().ParsePath(path, headers)
s.Require().NoError(err)
@@ -565,7 +576,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDprHeader() {
func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
path := "/plain/http://images.dev/lorem/ipsum.jpg@png"
headers := http.Header{"Dpr": []string{"2"}}
po, _, err := ParsePath(path, headers)
po, _, err := s.factory().ParsePath(path, headers)
s.Require().NoError(err)
@@ -575,7 +586,7 @@ func (s *ProcessingOptionsTestSuite) TestParsePathDprHeaderDisabled() {
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
path := "/skp:jpg:png/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -585,7 +596,7 @@ func (s *ProcessingOptionsTestSuite) TestParseSkipProcessing() {
func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
path := "/skp:jpg:png:bad_format/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
_, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().Error(err)
s.Require().Equal("Invalid image format in skip processing: bad_format", err.Error())
@@ -593,30 +604,28 @@ func (s *ProcessingOptionsTestSuite) TestParseSkipProcessingInvalid() {
func (s *ProcessingOptionsTestSuite) TestParseExpires() {
path := "/exp:32503669200/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
_, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
}
func (s *ProcessingOptionsTestSuite) TestParseExpiresExpired() {
path := "/exp:1609448400/plain/http://images.dev/lorem/ipsum.jpg"
_, _, err := ParsePath(path, make(http.Header))
_, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().Error(err, "Expired URL")
}
func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
config.OnlyPresets = true
presets["test1"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
}
presets["test2"] = urlOptions{
urlOption{Name: "quality", Args: []string{"50"}},
s.config().OnlyPresets = true
s.config().Presets = []string{
"test1=blur:0.2",
"test2=quality:50",
}
path := "/test1:test2/plain/http://images.dev/lorem/ipsum.jpg"
po, _, err := ParsePath(path, make(http.Header))
po, _, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -625,18 +634,16 @@ func (s *ProcessingOptionsTestSuite) TestParsePathOnlyPresets() {
}
func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
config.OnlyPresets = true
presets["test1"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
}
presets["test2"] = urlOptions{
urlOption{Name: "quality", Args: []string{"50"}},
s.config().OnlyPresets = true
s.config().Presets = []string{
"test1=blur:0.2",
"test2=quality:50",
}
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
path := fmt.Sprintf("/test1:test2/%s.png", base64.RawURLEncoding.EncodeToString([]byte(originURL)))
po, imageURL, err := ParsePath(path, make(http.Header))
po, imageURL, err := s.factory().ParsePath(path, make(http.Header))
s.Require().NoError(err)
@@ -646,12 +653,6 @@ func (s *ProcessingOptionsTestSuite) TestParseBase64URLOnlyPresets() {
}
func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
config.AllowedProcessingOptions = []string{"w", "h", "pr"}
presets["test1"] = urlOptions{
urlOption{Name: "blur", Args: []string{"0.2"}},
}
originURL := "http://images.dev/lorem/ipsum.jpg?param=value"
testCases := []struct {
@@ -666,8 +667,13 @@ func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
for _, tc := range testCases {
s.Run(strings.ReplaceAll(tc.options, "/", "_"), func() {
s.config().AllowedProcessingOptions = []string{"w", "h", "pr"}
s.config().Presets = []string{
"test1=blur:0.2",
}
path := fmt.Sprintf("/%s/%s.png", tc.options, base64.RawURLEncoding.EncodeToString([]byte(originURL)))
_, _, err := ParsePath(path, make(http.Header))
_, _, err := s.factory().ParsePath(path, make(http.Header))
if len(tc.expectedError) > 0 {
s.Require().Error(err)
@@ -679,6 +685,23 @@ func (s *ProcessingOptionsTestSuite) TestParseAllowedOptions() {
}
}
func (s *ProcessingOptionsTestSuite) TestProcessingOptionsClone() {
now := time.Now()
// Create ProcessingOptions using factory
original := s.factory().NewProcessingOptions()
original.SkipProcessingFormats = []imagetype.Type{
imagetype.PNG, imagetype.JPEG,
}
original.UsedPresets = []string{"preset1", "preset2"}
original.Expires = &now
// Clone the original
cloned := original.clone()
testutil.EqualButNotSame(s.T(), original, cloned)
}
func TestProcessingOptions(t *testing.T) {
suite.Run(t, new(ProcessingOptionsTestSuite))
}

View File

@@ -5,28 +5,26 @@ import (
"fmt"
"net/url"
"strings"
"github.com/imgproxy/imgproxy/v3/config"
)
const urlTokenPlain = "plain"
func preprocessURL(u string) string {
for _, repl := range config.URLReplacements {
func (f *Factory) preprocessURL(u string) string {
for _, repl := range f.config.URLReplacements {
u = repl.Regexp.ReplaceAllString(u, repl.Replacement)
}
if len(config.BaseURL) == 0 || strings.HasPrefix(u, config.BaseURL) {
if len(f.config.BaseURL) == 0 || strings.HasPrefix(u, f.config.BaseURL) {
return u
}
return fmt.Sprintf("%s%s", config.BaseURL, u)
return fmt.Sprintf("%s%s", f.config.BaseURL, u)
}
func decodeBase64URL(parts []string) (string, string, error) {
func (f *Factory) decodeBase64URL(parts []string) (string, string, error) {
var format string
if len(parts) > 1 && config.Base64URLIncludesFilename {
if len(parts) > 1 && f.config.Base64URLIncludesFilename {
parts = parts[:len(parts)-1]
}
@@ -50,10 +48,10 @@ func decodeBase64URL(parts []string) (string, string, error) {
return "", "", newInvalidURLError("Invalid url encoding: %s", encoded)
}
return preprocessURL(string(imageURL)), format, nil
return f.preprocessURL(string(imageURL)), format, nil
}
func decodePlainURL(parts []string) (string, string, error) {
func (f *Factory) decodePlainURL(parts []string) (string, string, error) {
var format string
encoded := strings.Join(parts, "/")
@@ -76,17 +74,17 @@ func decodePlainURL(parts []string) (string, string, error) {
return "", "", newInvalidURLError("Invalid url encoding: %s", encoded)
}
return preprocessURL(unescaped), format, nil
return f.preprocessURL(unescaped), format, nil
}
func DecodeURL(parts []string) (string, string, error) {
func (f *Factory) DecodeURL(parts []string) (string, string, error) {
if len(parts) == 0 {
return "", "", newInvalidURLError("Image URL is empty")
}
if parts[0] == urlTokenPlain && len(parts) > 1 {
return decodePlainURL(parts[1:])
return f.decodePlainURL(parts[1:])
}
return decodeBase64URL(parts)
return f.decodeBase64URL(parts)
}

View File

@@ -2,8 +2,6 @@ package options
import (
"strings"
"github.com/imgproxy/imgproxy/v3/config"
)
type urlOption struct {
@@ -13,12 +11,12 @@ type urlOption struct {
type urlOptions []urlOption
func parseURLOptions(opts []string) (urlOptions, []string) {
func (f *Factory) parseURLOptions(opts []string) (urlOptions, []string) {
parsed := make(urlOptions, 0, len(opts))
urlStart := len(opts) + 1
for i, opt := range opts {
args := strings.Split(opt, config.ArgumentsSeparator)
args := strings.Split(opt, f.config.ArgumentsSeparator)
if len(args) == 1 {
urlStart = i

View File

@@ -16,12 +16,14 @@ import (
"github.com/imgproxy/imgproxy/v3/ierrors"
"github.com/imgproxy/imgproxy/v3/imagedata"
"github.com/imgproxy/imgproxy/v3/options"
"github.com/imgproxy/imgproxy/v3/security"
"github.com/imgproxy/imgproxy/v3/vips"
)
type ProcessingTestSuite struct {
suite.Suite
idf *imagedata.Factory
pof *options.Factory
}
func (s *ProcessingTestSuite) SetupSuite() {
@@ -41,6 +43,18 @@ func (s *ProcessingTestSuite) SetupSuite() {
s.Require().NoError(err)
s.idf = imagedata.NewFactory(f)
scfg, err := security.LoadConfigFromEnv(nil)
s.Require().NoError(err)
security, err := security.New(scfg)
s.Require().NoError(err)
cfg, err := options.LoadConfigFromEnv(nil)
s.Require().NoError(err)
s.pof, err = options.NewFactory(cfg, security)
s.Require().NoError(err)
}
func (s *ProcessingTestSuite) openFile(name string) imagedata.ImageData {
@@ -63,7 +77,7 @@ func (s *ProcessingTestSuite) checkSize(r *Result, width, height int) {
func (s *ProcessingTestSuite) TestResizeToFit() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFit
testCases := []struct {
@@ -101,7 +115,7 @@ func (s *ProcessingTestSuite) TestResizeToFit() {
func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFit
po.Enlarge = true
@@ -140,7 +154,7 @@ func (s *ProcessingTestSuite) TestResizeToFitEnlarge() {
func (s *ProcessingTestSuite) TestResizeToFitExtend() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFit
po.Extend = options.ExtendOptions{
Enabled: true,
@@ -184,7 +198,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtend() {
func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFit
po.ExtendAspectRatio = options.ExtendOptions{
Enabled: true,
@@ -228,7 +242,7 @@ func (s *ProcessingTestSuite) TestResizeToFitExtendAR() {
func (s *ProcessingTestSuite) TestResizeToFill() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFill
testCases := []struct {
@@ -266,7 +280,7 @@ func (s *ProcessingTestSuite) TestResizeToFill() {
func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFill
po.Enlarge = true
@@ -305,7 +319,7 @@ func (s *ProcessingTestSuite) TestResizeToFillEnlarge() {
func (s *ProcessingTestSuite) TestResizeToFillExtend() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFill
po.Extend = options.ExtendOptions{
Enabled: true,
@@ -351,7 +365,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtend() {
func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFill
po.ExtendAspectRatio = options.ExtendOptions{
Enabled: true,
@@ -397,7 +411,7 @@ func (s *ProcessingTestSuite) TestResizeToFillExtendAR() {
func (s *ProcessingTestSuite) TestResizeToFillDown() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFillDown
testCases := []struct {
@@ -435,7 +449,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDown() {
func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFillDown
po.Enlarge = true
@@ -474,7 +488,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownEnlarge() {
func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFillDown
po.Extend = options.ExtendOptions{
Enabled: true,
@@ -520,7 +534,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtend() {
func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.ResizingType = options.ResizeFillDown
po.ExtendAspectRatio = options.ExtendOptions{
Enabled: true,
@@ -564,7 +578,7 @@ func (s *ProcessingTestSuite) TestResizeToFillDownExtendAR() {
func (s *ProcessingTestSuite) TestResultSizeLimit() {
imgdata := s.openFile("test2.jpg")
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
testCases := []struct {
limit int
@@ -992,7 +1006,7 @@ func (s *ProcessingTestSuite) TestResultSizeLimit() {
}
func (s *ProcessingTestSuite) TestImageResolutionTooLarge() {
po := options.NewProcessingOptions()
po := s.pof.NewProcessingOptions()
po.SecurityOptions.MaxSrcResolution = 1
imgdata := s.openFile("test2.jpg")

View File

@@ -22,20 +22,22 @@ var watermarkPipeline = pipeline{
padding,
}
func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.WatermarkOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, po *options.ProcessingOptions, imgWidth, imgHeight int, offsetScale float64, framesCount int) error {
if err := wm.Load(wmData, 1, 1.0, 1); err != nil {
return err
}
po := options.NewProcessingOptions()
po.ResizingType = options.ResizeFit
po.Dpr = 1
po.Enlarge = true
po.Format = wmData.Format()
opts := po.Watermark
wmPo := po.Default()
wmPo.ResizingType = options.ResizeFit
wmPo.Dpr = 1
wmPo.Enlarge = true
wmPo.Format = wmData.Format()
if opts.Scale > 0 {
po.Width = max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
po.Height = max(imath.ScaleToEven(imgHeight, opts.Scale), 1)
wmPo.Width = max(imath.ScaleToEven(imgWidth, opts.Scale), 1)
wmPo.Height = max(imath.ScaleToEven(imgHeight, opts.Scale), 1)
}
if opts.ShouldReplicate() {
@@ -53,14 +55,14 @@ func prepareWatermark(wm *vips.Image, wmData imagedata.ImageData, opts *options.
offY = imath.ScaleToEven(imgHeight, opts.Position.Y)
}
po.Padding.Enabled = true
po.Padding.Left = offX / 2
po.Padding.Right = offX - po.Padding.Left
po.Padding.Top = offY / 2
po.Padding.Bottom = offY - po.Padding.Top
wmPo.Padding.Enabled = true
wmPo.Padding.Left = offX / 2
wmPo.Padding.Right = offX - wmPo.Padding.Left
wmPo.Padding.Top = offY / 2
wmPo.Padding.Bottom = offY - wmPo.Padding.Top
}
if err := watermarkPipeline.Run(context.Background(), wm, po, wmData, nil); err != nil {
if err := watermarkPipeline.Run(context.Background(), wm, wmPo, wmData, nil); err != nil {
return err
}
@@ -110,7 +112,7 @@ func applyWatermark(
height := img.Height()
frameHeight := height / framesCount
if err := prepareWatermark(wm, wmData, &opts, width, frameHeight, offsetScale, framesCount); err != nil {
if err := prepareWatermark(wm, wmData, po, width, frameHeight, offsetScale, framesCount); err != nil {
return err
}

View File

@@ -0,0 +1,104 @@
package testutil
import (
"reflect"
"testing"
"github.com/stretchr/testify/require"
)
// EqualButNotSame asserts that expected and actual objects are not the same.
// It recursively checks all fields to ensure that no pointers are shared.
// If a pointer, slice or map are nil in either object, the test fails.
func EqualButNotSame(t *testing.T, expected, actual any) {
t.Helper()
expectedVal := reflect.ValueOf(expected)
actualVal := reflect.ValueOf(actual)
deepEqual(t, expectedVal, actualVal, "")
}
// deepEqual recursively verifies that all values are equal but pointers are different
// except for the Expires field which is explicitly allowed to be shared
func deepEqual(t *testing.T, left, right reflect.Value, fieldPath string) {
require.True(t, left.IsValid() && right.IsValid(), "invalid value at %s", fieldPath)
require.Equal(t, left.Type(), right.Type(), "types are not equal at %s", fieldPath)
switch left.Kind() {
case reflect.Ptr:
// Pointers should not be nil and must point to different objects
require.False(t, left.IsNil(), "nil pointer at %s (left)", fieldPath)
require.False(t, right.IsNil(), "nil pointer at %s (right)", fieldPath)
require.NotSame(t, left.Interface(), right.Interface(), "shared pointer at %s", fieldPath)
deepEqual(t, left.Elem(), right.Elem(), fieldPath)
case reflect.Slice:
// Slices should contain some elements and must not share the same underlying array
require.Equal(t, left.Len(), right.Len(), "slice length mismatch at %s", fieldPath)
require.NotEmpty(t, left.Len(), "slice must not be empty %s (left)", fieldPath)
require.NotEmpty(t, right.Len(), "slice must not be empty %s (right)", fieldPath)
require.NotEqual(t, left.Pointer(), right.Pointer(), "shared slices at %s", fieldPath)
// Recursively verify slice elements
for i := 0; i < left.Len(); i++ {
elemPath := buildPath(fieldPath, "[", anyToString(i), "]")
deepEqual(t, left.Index(i), right.Index(i), elemPath)
}
case reflect.Map:
// Maps should contain some elements and must not share the same underlying map
require.Equal(t, left.Len(), right.Len(), "map length mismatch at %s", fieldPath)
require.NotEmpty(t, left.Len(), "map must not be empty %s (left)", fieldPath)
require.NotEmpty(t, right.Len(), "map must not be empty %s (right)", fieldPath)
require.NotEqual(t, left.Pointer(), right.Pointer(), "shared maps at %s", fieldPath)
// Recursively verify map values
for _, key := range left.MapKeys() {
keyStr := anyToString(key.Interface())
keyPath := buildPath(fieldPath, "[", keyStr, "]")
originalMapVal := left.MapIndex(key)
clonedMapVal := right.MapIndex(key)
deepEqual(t, originalMapVal, clonedMapVal, keyPath)
}
case reflect.Struct:
require.Equal(t, left.Interface(), right.Interface(), "structs are not equal at %s", fieldPath)
// Fallback to recursive field-by-field comparison
for i := 0; i < left.NumField(); i++ {
field := left.Type().Field(i)
if !field.IsExported() {
continue // Skip unexported fields
}
nestedPath := buildPath(fieldPath, ".", field.Name, "")
originalFieldVal := left.Field(i)
clonedFieldVal := right.Field(i)
deepEqual(t, originalFieldVal, clonedFieldVal, nestedPath)
}
default:
// For primitive types, just verify equality
require.Equal(t, left.Interface(), right.Interface(), "values not equal at %s", fieldPath)
}
}
// buildPath builds a field path for error messages
func buildPath(basePath, separator, element, suffix string) string {
if basePath == "" {
return element + suffix
}
return basePath + separator + element + suffix
}
// anyToString converts an any to a string for path building
func anyToString(v any) string {
switch val := v.(type) {
case string:
return val
default:
return reflect.ValueOf(val).String()
}
}