diff --git a/config.go b/config.go index d76cc013..5d3d50e0 100644 --- a/config.go +++ b/config.go @@ -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 } diff --git a/go.mod b/go.mod index 440e42ec..ca729608 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 4104fbd9..abd0df7d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/handlers/processing/handler.go b/handlers/processing/handler.go index 8cbb29d3..e7482d7c 100644 --- a/handlers/processing/handler.go +++ b/handlers/processing/handler.go @@ -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)) } diff --git a/imgproxy.go b/imgproxy.go index c66eeaa1..cb366dbb 100644 --- a/imgproxy.go +++ b/imgproxy.go @@ -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 +} diff --git a/init.go b/init.go index c5687782..26c4fce6 100644 --- a/init.go +++ b/init.go @@ -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 } diff --git a/integration/processing_handler_test.go b/integration/processing_handler_test.go index 8fa49ea3..d2774967 100644 --- a/integration/processing_handler_test.go +++ b/integration/processing_handler_test.go @@ -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) diff --git a/options/config.go b/options/config.go new file mode 100644 index 00000000..737d1d15 --- /dev/null +++ b/options/config.go @@ -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 +} diff --git a/options/factory.go b/options/factory.go new file mode 100644 index 00000000..2ec505e7 --- /dev/null +++ b/options/factory.go @@ -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() +} diff --git a/options/presets.go b/options/presets.go index 8def8863..ba584bb7 100644 --- a/options/presets.go +++ b/options/presets.go @@ -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) } } diff --git a/options/presets_test.go b/options/presets_test.go index 4eae3386..7376d6f5 100644 --- a/options/presets_test.go +++ b/options/presets_test.go @@ -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) } diff --git a/options/processing_options.go b/options/processing_options.go index ccbb7b0f..2f45f05a 100644 --- a/options/processing_options.go +++ b/options/processing_options.go @@ -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 +} diff --git a/options/processing_options_test.go b/options/processing_options_test.go index 6d38f6ec..c1f1f1c1 100644 --- a/options/processing_options_test.go +++ b/options/processing_options_test.go @@ -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)) } diff --git a/options/url.go b/options/url.go index 6c01ee92..2de4b1af 100644 --- a/options/url.go +++ b/options/url.go @@ -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) } diff --git a/options/url_options.go b/options/url_options.go index 53787ae7..e0f2b593 100644 --- a/options/url_options.go +++ b/options/url_options.go @@ -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 diff --git a/processing/processing_test.go b/processing/processing_test.go index 3e017350..14d4b06c 100644 --- a/processing/processing_test.go +++ b/processing/processing_test.go @@ -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") diff --git a/processing/watermark.go b/processing/watermark.go index f4c2e2c4..b0cb0c2a 100644 --- a/processing/watermark.go +++ b/processing/watermark.go @@ -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 } diff --git a/testutil/equal_but_not_same.go b/testutil/equal_but_not_same.go new file mode 100644 index 00000000..2ee19e98 --- /dev/null +++ b/testutil/equal_but_not_same.go @@ -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() + } +}